1.5.0 Initial run as iOS in a monorepo

This commit is contained in:
2025-09-12 22:35:14 -06:00
parent ba6edcd854
commit ce220c7220
127 changed files with 7062 additions and 1039 deletions

View 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
}
}