556 lines
17 KiB
Swift
556 lines
17 KiB
Swift
import Foundation
|
|
import SwiftUI
|
|
|
|
enum ClimbType: String, CaseIterable, Codable {
|
|
case rope = "ROPE"
|
|
case boulder = "BOULDER"
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .rope:
|
|
return "Rope"
|
|
case .boulder:
|
|
return "Bouldering"
|
|
}
|
|
}
|
|
}
|
|
|
|
enum DifficultySystem: String, CaseIterable, Codable {
|
|
case vScale = "V_SCALE"
|
|
case font = "FONT"
|
|
case yds = "YDS"
|
|
case custom = "CUSTOM"
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .vScale:
|
|
return "V Scale"
|
|
case .font:
|
|
return "Font Scale"
|
|
case .yds:
|
|
return "YDS (Yosemite)"
|
|
case .custom:
|
|
return "Custom"
|
|
}
|
|
}
|
|
|
|
var isBoulderingSystem: Bool {
|
|
switch self {
|
|
case .vScale, .font:
|
|
return true
|
|
case .yds:
|
|
return false
|
|
case .custom:
|
|
return true
|
|
}
|
|
}
|
|
|
|
var isRopeSystem: Bool {
|
|
switch self {
|
|
case .yds:
|
|
return true
|
|
case .vScale, .font:
|
|
return false
|
|
case .custom:
|
|
return true
|
|
}
|
|
}
|
|
|
|
var availableGrades: [String] {
|
|
switch self {
|
|
case .vScale:
|
|
return [
|
|
"VB", "V0", "V1", "V2", "V3", "V4", "V5", "V6", "V7", "V8", "V9", "V10", "V11",
|
|
"V12", "V13", "V14", "V15", "V16", "V17",
|
|
]
|
|
case .font:
|
|
return [
|
|
"3", "4A", "4B", "4C", "5A", "5B", "5C", "6A", "6A+", "6B", "6B+", "6C", "6C+",
|
|
"7A", "7A+", "7B", "7B+", "7C", "7C+", "8A", "8A+", "8B", "8B+", "8C", "8C+",
|
|
]
|
|
case .yds:
|
|
return [
|
|
"5.0", "5.1", "5.2", "5.3", "5.4", "5.5", "5.6", "5.7", "5.8", "5.9", "5.10a",
|
|
"5.10b", "5.10c", "5.10d", "5.11a", "5.11b", "5.11c", "5.11d", "5.12a", "5.12b",
|
|
"5.12c", "5.12d", "5.13a", "5.13b", "5.13c", "5.13d", "5.14a", "5.14b", "5.14c",
|
|
"5.14d", "5.15a", "5.15b", "5.15c", "5.15d",
|
|
]
|
|
case .custom:
|
|
return []
|
|
}
|
|
}
|
|
|
|
static func systemsForClimbType(_ climbType: ClimbType) -> [DifficultySystem] {
|
|
switch climbType {
|
|
case .boulder:
|
|
return allCases.filter { $0.isBoulderingSystem }
|
|
case .rope:
|
|
return allCases.filter { $0.isRopeSystem }
|
|
}
|
|
}
|
|
}
|
|
|
|
enum AttemptResult: String, CaseIterable, Codable {
|
|
case success = "SUCCESS"
|
|
case fall = "FALL"
|
|
case noProgress = "NO_PROGRESS"
|
|
case flash = "FLASH"
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .success:
|
|
return "Success"
|
|
case .fall:
|
|
return "Fall"
|
|
case .noProgress:
|
|
return "No Progress"
|
|
case .flash:
|
|
return "Flash"
|
|
}
|
|
}
|
|
|
|
var isSuccessful: Bool {
|
|
return self == .success || self == .flash
|
|
}
|
|
}
|
|
|
|
enum SessionStatus: String, CaseIterable, Codable {
|
|
case active = "ACTIVE"
|
|
case completed = "COMPLETED"
|
|
case paused = "PAUSED"
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .active:
|
|
return "Active"
|
|
case .completed:
|
|
return "Completed"
|
|
case .paused:
|
|
return "Paused"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct DifficultyGrade: Codable, Hashable {
|
|
let system: DifficultySystem
|
|
let grade: String
|
|
let numericValue: Int
|
|
|
|
init(system: DifficultySystem, grade: String) {
|
|
self.system = system
|
|
self.grade = grade
|
|
self.numericValue = Self.calculateNumericValue(system: system, grade: grade)
|
|
}
|
|
|
|
private static func calculateNumericValue(system: DifficultySystem, grade: String) -> Int {
|
|
switch system {
|
|
case .vScale:
|
|
if grade == "VB" { return 0 }
|
|
return Int(grade.replacingOccurrences(of: "V", with: "")) ?? 0
|
|
case .font:
|
|
let fontMapping: [String: Int] = [
|
|
"3": 3, "4A": 4, "4B": 5, "4C": 6, "5A": 7, "5B": 8, "5C": 9,
|
|
"6A": 10, "6A+": 11, "6B": 12, "6B+": 13, "6C": 14, "6C+": 15,
|
|
"7A": 16, "7A+": 17, "7B": 18, "7B+": 19, "7C": 20, "7C+": 21,
|
|
"8A": 22, "8A+": 23, "8B": 24, "8B+": 25, "8C": 26, "8C+": 27,
|
|
]
|
|
return fontMapping[grade] ?? 0
|
|
case .yds:
|
|
let ydsMapping: [String: Int] = [
|
|
"5.0": 50, "5.1": 51, "5.2": 52, "5.3": 53, "5.4": 54, "5.5": 55,
|
|
"5.6": 56, "5.7": 57, "5.8": 58, "5.9": 59, "5.10a": 60, "5.10b": 61,
|
|
"5.10c": 62, "5.10d": 63, "5.11a": 64, "5.11b": 65, "5.11c": 66,
|
|
"5.11d": 67, "5.12a": 68, "5.12b": 69, "5.12c": 70, "5.12d": 71,
|
|
"5.13a": 72, "5.13b": 73, "5.13c": 74, "5.13d": 75, "5.14a": 76,
|
|
"5.14b": 77, "5.14c": 78, "5.14d": 79, "5.15a": 80, "5.15b": 81,
|
|
"5.15c": 82, "5.15d": 83,
|
|
]
|
|
return ydsMapping[grade] ?? 0
|
|
case .custom:
|
|
return Int(grade) ?? 0
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Gym: Identifiable, Codable, Hashable {
|
|
let id: UUID
|
|
let name: String
|
|
let location: String?
|
|
let supportedClimbTypes: [ClimbType]
|
|
let difficultySystems: [DifficultySystem]
|
|
let customDifficultyGrades: [String]
|
|
let notes: String?
|
|
let createdAt: Date
|
|
let updatedAt: Date
|
|
|
|
init(
|
|
name: String, location: String? = nil, supportedClimbTypes: [ClimbType],
|
|
difficultySystems: [DifficultySystem], customDifficultyGrades: [String] = [],
|
|
notes: String? = nil
|
|
) {
|
|
self.id = UUID()
|
|
self.name = name
|
|
self.location = location
|
|
self.supportedClimbTypes = supportedClimbTypes
|
|
self.difficultySystems = difficultySystems
|
|
self.customDifficultyGrades = customDifficultyGrades
|
|
self.notes = notes
|
|
let now = Date()
|
|
self.createdAt = now
|
|
self.updatedAt = now
|
|
}
|
|
|
|
func updated(
|
|
name: String? = nil, location: String? = nil, supportedClimbTypes: [ClimbType]? = nil,
|
|
difficultySystems: [DifficultySystem]? = nil, customDifficultyGrades: [String]? = nil,
|
|
notes: String? = nil
|
|
) -> Gym {
|
|
return Gym(
|
|
id: self.id,
|
|
name: name ?? self.name,
|
|
location: location ?? self.location,
|
|
supportedClimbTypes: supportedClimbTypes ?? self.supportedClimbTypes,
|
|
difficultySystems: difficultySystems ?? self.difficultySystems,
|
|
customDifficultyGrades: customDifficultyGrades ?? self.customDifficultyGrades,
|
|
notes: notes ?? self.notes,
|
|
createdAt: self.createdAt,
|
|
updatedAt: Date()
|
|
)
|
|
}
|
|
|
|
private init(
|
|
id: UUID, name: String, location: String?, supportedClimbTypes: [ClimbType],
|
|
difficultySystems: [DifficultySystem], customDifficultyGrades: [String], notes: String?,
|
|
createdAt: Date, updatedAt: Date
|
|
) {
|
|
self.id = id
|
|
self.name = name
|
|
self.location = location
|
|
self.supportedClimbTypes = supportedClimbTypes
|
|
self.difficultySystems = difficultySystems
|
|
self.customDifficultyGrades = customDifficultyGrades
|
|
self.notes = notes
|
|
self.createdAt = createdAt
|
|
self.updatedAt = updatedAt
|
|
}
|
|
|
|
static func fromImport(
|
|
id: UUID, name: String, location: String?, supportedClimbTypes: [ClimbType],
|
|
difficultySystems: [DifficultySystem], customDifficultyGrades: [String], notes: String?,
|
|
createdAt: Date, updatedAt: Date
|
|
) -> Gym {
|
|
return Gym(
|
|
id: id,
|
|
name: name,
|
|
location: location,
|
|
supportedClimbTypes: supportedClimbTypes,
|
|
difficultySystems: difficultySystems,
|
|
customDifficultyGrades: customDifficultyGrades,
|
|
notes: notes,
|
|
createdAt: createdAt,
|
|
updatedAt: updatedAt
|
|
)
|
|
}
|
|
}
|
|
|
|
struct Problem: Identifiable, Codable, Hashable {
|
|
let id: UUID
|
|
let gymId: UUID
|
|
let name: String?
|
|
let description: String?
|
|
let climbType: ClimbType
|
|
let difficulty: DifficultyGrade
|
|
let setter: String?
|
|
let tags: [String]
|
|
let location: String?
|
|
let imagePaths: [String]
|
|
let isActive: Bool
|
|
let dateSet: Date?
|
|
let notes: String?
|
|
let createdAt: Date
|
|
let updatedAt: Date
|
|
|
|
init(
|
|
gymId: UUID, name: String? = nil, description: String? = nil, climbType: ClimbType,
|
|
difficulty: DifficultyGrade, setter: String? = nil, tags: [String] = [],
|
|
location: String? = nil, imagePaths: [String] = [], dateSet: Date? = nil,
|
|
notes: String? = nil
|
|
) {
|
|
self.id = UUID()
|
|
self.gymId = gymId
|
|
self.name = name
|
|
self.description = description
|
|
self.climbType = climbType
|
|
self.difficulty = difficulty
|
|
self.setter = setter
|
|
self.tags = tags
|
|
self.location = location
|
|
self.imagePaths = imagePaths
|
|
self.isActive = true
|
|
self.dateSet = dateSet
|
|
self.notes = notes
|
|
let now = Date()
|
|
self.createdAt = now
|
|
self.updatedAt = now
|
|
}
|
|
|
|
func updated(
|
|
name: String? = nil, description: String? = nil, climbType: ClimbType? = nil,
|
|
difficulty: DifficultyGrade? = nil, setter: String? = nil, tags: [String]? = nil,
|
|
location: String? = nil, imagePaths: [String]? = nil, isActive: Bool? = nil,
|
|
dateSet: Date? = nil, notes: String? = nil
|
|
) -> Problem {
|
|
return Problem(
|
|
id: self.id,
|
|
gymId: self.gymId,
|
|
name: name ?? self.name,
|
|
description: description ?? self.description,
|
|
climbType: climbType ?? self.climbType,
|
|
difficulty: difficulty ?? self.difficulty,
|
|
setter: setter ?? self.setter,
|
|
tags: tags ?? self.tags,
|
|
location: location ?? self.location,
|
|
imagePaths: imagePaths ?? self.imagePaths,
|
|
isActive: isActive ?? self.isActive,
|
|
dateSet: dateSet ?? self.dateSet,
|
|
notes: notes ?? self.notes,
|
|
createdAt: self.createdAt,
|
|
updatedAt: Date()
|
|
)
|
|
}
|
|
|
|
private init(
|
|
id: UUID, gymId: UUID, name: String?, description: String?, climbType: ClimbType,
|
|
difficulty: DifficultyGrade, setter: String?, tags: [String], location: String?,
|
|
imagePaths: [String], isActive: Bool, dateSet: Date?, notes: String?, createdAt: Date,
|
|
updatedAt: Date
|
|
) {
|
|
self.id = id
|
|
self.gymId = gymId
|
|
self.name = name
|
|
self.description = description
|
|
self.climbType = climbType
|
|
self.difficulty = difficulty
|
|
self.setter = setter
|
|
self.tags = tags
|
|
self.location = location
|
|
self.imagePaths = imagePaths
|
|
self.isActive = isActive
|
|
self.dateSet = dateSet
|
|
self.notes = notes
|
|
self.createdAt = createdAt
|
|
self.updatedAt = updatedAt
|
|
}
|
|
|
|
static func fromImport(
|
|
id: UUID, gymId: UUID, name: String?, description: String?, climbType: ClimbType,
|
|
difficulty: DifficultyGrade, setter: String?, tags: [String], location: String?,
|
|
imagePaths: [String], isActive: Bool, dateSet: Date?, notes: String?, createdAt: Date,
|
|
updatedAt: Date
|
|
) -> Problem {
|
|
return Problem(
|
|
id: id,
|
|
gymId: gymId,
|
|
name: name,
|
|
description: description,
|
|
climbType: climbType,
|
|
difficulty: difficulty,
|
|
setter: setter,
|
|
tags: tags,
|
|
location: location,
|
|
imagePaths: imagePaths,
|
|
isActive: isActive,
|
|
dateSet: dateSet,
|
|
notes: notes,
|
|
createdAt: createdAt,
|
|
updatedAt: updatedAt
|
|
)
|
|
}
|
|
}
|
|
|
|
struct ClimbSession: Identifiable, Codable, Hashable {
|
|
let id: UUID
|
|
let gymId: UUID
|
|
let date: Date
|
|
let startTime: Date?
|
|
let endTime: Date?
|
|
let duration: Int? // Duration in minutes
|
|
let status: SessionStatus
|
|
let notes: String?
|
|
let createdAt: Date
|
|
let updatedAt: Date
|
|
|
|
init(gymId: UUID, notes: String? = nil) {
|
|
self.id = UUID()
|
|
self.gymId = gymId
|
|
let now = Date()
|
|
self.date = now
|
|
self.startTime = now
|
|
self.endTime = nil
|
|
self.duration = nil
|
|
self.status = .active
|
|
self.notes = notes
|
|
self.createdAt = now
|
|
self.updatedAt = now
|
|
}
|
|
|
|
func completed() -> ClimbSession {
|
|
let endTime = Date()
|
|
let durationMinutes =
|
|
startTime != nil ? Int(endTime.timeIntervalSince(startTime!) / 60) : nil
|
|
|
|
return ClimbSession(
|
|
id: self.id,
|
|
gymId: self.gymId,
|
|
date: self.date,
|
|
startTime: self.startTime,
|
|
endTime: endTime,
|
|
duration: durationMinutes,
|
|
status: .completed,
|
|
notes: self.notes,
|
|
createdAt: self.createdAt,
|
|
updatedAt: Date()
|
|
)
|
|
}
|
|
|
|
func updated(notes: String? = nil, status: SessionStatus? = nil) -> ClimbSession {
|
|
return ClimbSession(
|
|
id: self.id,
|
|
gymId: self.gymId,
|
|
date: self.date,
|
|
startTime: self.startTime,
|
|
endTime: self.endTime,
|
|
duration: self.duration,
|
|
status: status ?? self.status,
|
|
notes: notes ?? self.notes,
|
|
createdAt: self.createdAt,
|
|
updatedAt: Date()
|
|
)
|
|
}
|
|
|
|
private init(
|
|
id: UUID, gymId: UUID, date: Date, startTime: Date?, endTime: Date?, duration: Int?,
|
|
status: SessionStatus, notes: String?, createdAt: Date, updatedAt: Date
|
|
) {
|
|
self.id = id
|
|
self.gymId = gymId
|
|
self.date = date
|
|
self.startTime = startTime
|
|
self.endTime = endTime
|
|
self.duration = duration
|
|
self.status = status
|
|
self.notes = notes
|
|
self.createdAt = createdAt
|
|
self.updatedAt = updatedAt
|
|
}
|
|
|
|
static func fromImport(
|
|
id: UUID, gymId: UUID, date: Date, startTime: Date?, endTime: Date?, duration: Int?,
|
|
status: SessionStatus, notes: String?, createdAt: Date, updatedAt: Date
|
|
) -> ClimbSession {
|
|
return ClimbSession(
|
|
id: id,
|
|
gymId: gymId,
|
|
date: date,
|
|
startTime: startTime,
|
|
endTime: endTime,
|
|
duration: duration,
|
|
status: status,
|
|
notes: notes,
|
|
createdAt: createdAt,
|
|
updatedAt: updatedAt
|
|
)
|
|
}
|
|
}
|
|
|
|
struct Attempt: Identifiable, Codable, Hashable {
|
|
let id: UUID
|
|
let sessionId: UUID
|
|
let problemId: UUID
|
|
let result: AttemptResult
|
|
let highestHold: String?
|
|
let notes: String?
|
|
let duration: Int?
|
|
let restTime: Int?
|
|
let timestamp: Date
|
|
let createdAt: Date
|
|
|
|
init(
|
|
sessionId: UUID, problemId: UUID, result: AttemptResult, highestHold: String? = nil,
|
|
notes: String? = nil, duration: Int? = nil, restTime: Int? = nil, timestamp: Date = Date()
|
|
) {
|
|
self.id = UUID()
|
|
self.sessionId = sessionId
|
|
self.problemId = problemId
|
|
self.result = result
|
|
self.highestHold = highestHold
|
|
self.notes = notes
|
|
self.duration = duration
|
|
self.restTime = restTime
|
|
self.timestamp = timestamp
|
|
self.createdAt = Date()
|
|
}
|
|
|
|
func updated(
|
|
problemId: UUID? = nil, result: AttemptResult? = nil, highestHold: String? = nil,
|
|
notes: String? = nil,
|
|
duration: Int? = nil, restTime: Int? = nil
|
|
) -> Attempt {
|
|
return Attempt(
|
|
id: self.id,
|
|
sessionId: self.sessionId,
|
|
problemId: problemId ?? self.problemId,
|
|
result: result ?? self.result,
|
|
highestHold: highestHold ?? self.highestHold,
|
|
notes: notes ?? self.notes,
|
|
duration: duration ?? self.duration,
|
|
restTime: restTime ?? self.restTime,
|
|
timestamp: self.timestamp,
|
|
createdAt: self.createdAt
|
|
)
|
|
}
|
|
|
|
private init(
|
|
id: UUID, sessionId: UUID, problemId: UUID, result: AttemptResult, highestHold: String?,
|
|
notes: String?, duration: Int?, restTime: Int?, timestamp: Date, createdAt: Date
|
|
) {
|
|
self.id = id
|
|
self.sessionId = sessionId
|
|
self.problemId = problemId
|
|
self.result = result
|
|
self.highestHold = highestHold
|
|
self.notes = notes
|
|
self.duration = duration
|
|
self.restTime = restTime
|
|
self.timestamp = timestamp
|
|
self.createdAt = createdAt
|
|
}
|
|
|
|
static func fromImport(
|
|
id: UUID, sessionId: UUID, problemId: UUID, result: AttemptResult, highestHold: String?,
|
|
notes: String?, duration: Int?, restTime: Int?, timestamp: Date, createdAt: Date
|
|
) -> Attempt {
|
|
return Attempt(
|
|
id: id,
|
|
sessionId: sessionId,
|
|
problemId: problemId,
|
|
result: result,
|
|
highestHold: highestHold,
|
|
notes: notes,
|
|
duration: duration,
|
|
restTime: restTime,
|
|
timestamp: timestamp,
|
|
createdAt: createdAt
|
|
)
|
|
}
|
|
}
|
|
|
|
extension DifficultyGrade: Comparable {
|
|
static func < (lhs: DifficultyGrade, rhs: DifficultyGrade) -> Bool {
|
|
if lhs.system != rhs.system {
|
|
return false // Can't compare different systems
|
|
}
|
|
return lhs.numericValue < rhs.numericValue
|
|
}
|
|
}
|