1.5.0 Initial run as iOS in a monorepo
This commit is contained in:
561
ios/OpenClimb/Models/DataModels.swift
Normal file
561
ios/OpenClimb/Models/DataModels.swift
Normal file
@@ -0,0 +1,561 @@
|
||||
//
|
||||
// DataModels.swift
|
||||
// OpenClimb
|
||||
//
|
||||
// Created by OpenClimb on 2025-01-17.
|
||||
//
|
||||
|
||||
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(
|
||||
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: 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user