Initial commit: KisaniCal iOS app
Task management, Eisenhower matrix, workout logging with persistence, and calendar with 6 view modes (Month, List, Year, Week, 3 Day, Day). Custom floating tab bar, dynamic calendar icon, and workout data seeded.
This commit is contained in:
601
KisaniCal/Models/ExerciseModels.swift
Normal file
601
KisaniCal/Models/ExerciseModels.swift
Normal file
@@ -0,0 +1,601 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
// MARK: - Exercise Models
|
||||
struct ExerciseSet: Identifiable, Codable {
|
||||
var id = UUID()
|
||||
var weight: Double = 0
|
||||
var reps: Int = 10
|
||||
var isDone: Bool = false
|
||||
}
|
||||
|
||||
struct Exercise: Identifiable, Codable {
|
||||
var id = UUID()
|
||||
var name: String
|
||||
var sfSymbol: String
|
||||
var colorIndex: Int = 0
|
||||
var sets: [ExerciseSet]
|
||||
var isExpanded: Bool = true
|
||||
var isDumbbell: Bool = false // per-side input → total = weight × 2
|
||||
|
||||
var totalSets: Int { sets.count }
|
||||
var doneSets: Int { sets.filter(\.isDone).count }
|
||||
var isPartialDone: Bool { doneSets > 0 && doneSets < totalSets }
|
||||
var isAllDone: Bool { doneSets == totalSets && totalSets > 0 }
|
||||
}
|
||||
|
||||
// MARK: - Workout Section
|
||||
struct WorkoutSection: Identifiable, Codable {
|
||||
var id = UUID()
|
||||
var name: String
|
||||
var exercises: [Exercise]
|
||||
var isExpanded: Bool = true
|
||||
|
||||
var totalSets: Int { exercises.reduce(0) { $0 + $1.sets.count } }
|
||||
var doneSets: Int { exercises.reduce(0) { $0 + $1.doneSets } }
|
||||
var exerciseCount: Int { exercises.count }
|
||||
}
|
||||
|
||||
// MARK: - Workout Program
|
||||
struct WorkoutProgram: Identifiable, Codable {
|
||||
let id: UUID
|
||||
var name: String
|
||||
var emoji: String
|
||||
var colorIndex: Int
|
||||
var sections: [WorkoutSection]
|
||||
|
||||
var allExercises: [Exercise] { sections.flatMap(\.exercises) }
|
||||
var totalSets: Int { sections.reduce(0) { $0 + $1.totalSets } }
|
||||
var doneSets: Int { sections.reduce(0) { $0 + $1.doneSets } }
|
||||
var totalExercises: Int { sections.reduce(0) { $0 + $1.exerciseCount } }
|
||||
|
||||
init(id: UUID = UUID(), name: String, emoji: String, colorIndex: Int = 0, sections: [WorkoutSection] = []) {
|
||||
self.id = id; self.name = name; self.emoji = emoji
|
||||
self.colorIndex = colorIndex; self.sections = sections
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Exercise Template Library
|
||||
enum ExerciseMuscle: String, CaseIterable, Identifiable {
|
||||
var id: String { rawValue }
|
||||
case chest = "Chest"
|
||||
case back = "Back"
|
||||
case legs = "Legs"
|
||||
case shoulders = "Shoulders"
|
||||
case arms = "Arms"
|
||||
case core = "Core"
|
||||
case cardio = "Cardio"
|
||||
|
||||
var sfSymbol: String {
|
||||
switch self {
|
||||
case .chest: return "figure.strengthtraining.traditional"
|
||||
case .back: return "figure.pull.up"
|
||||
case .legs: return "figure.squat"
|
||||
case .shoulders: return "figure.arms.open"
|
||||
case .arms: return "dumbbell.fill"
|
||||
case .core: return "figure.core.training"
|
||||
case .cardio: return "figure.run"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ExerciseTemplate: Identifiable {
|
||||
let id = UUID()
|
||||
let name: String
|
||||
let sfSymbol: String
|
||||
let muscle: ExerciseMuscle
|
||||
var isDumbbell: Bool = false
|
||||
}
|
||||
|
||||
let exerciseLibrary: [ExerciseTemplate] = [
|
||||
// Chest
|
||||
ExerciseTemplate(name: "Bench Press", sfSymbol: "figure.strengthtraining.traditional", muscle: .chest),
|
||||
ExerciseTemplate(name: "Incline Bench Press", sfSymbol: "figure.strengthtraining.traditional", muscle: .chest, isDumbbell: true),
|
||||
ExerciseTemplate(name: "Incline Dumbbell Press", sfSymbol: "dumbbell.fill", muscle: .chest, isDumbbell: true),
|
||||
ExerciseTemplate(name: "Decline Bench Press", sfSymbol: "figure.strengthtraining.traditional", muscle: .chest),
|
||||
ExerciseTemplate(name: "Cable Fly", sfSymbol: "figure.strengthtraining.functional", muscle: .chest),
|
||||
ExerciseTemplate(name: "Chest Flys", sfSymbol: "dumbbell.fill", muscle: .chest, isDumbbell: true),
|
||||
ExerciseTemplate(name: "Push-ups", sfSymbol: "figure.gymnastics", muscle: .chest),
|
||||
ExerciseTemplate(name: "Chest Dips", sfSymbol: "figure.gymnastics", muscle: .chest),
|
||||
ExerciseTemplate(name: "Cable Crossover", sfSymbol: "figure.strengthtraining.functional", muscle: .chest),
|
||||
// Back
|
||||
ExerciseTemplate(name: "Pull-ups", sfSymbol: "figure.pull.up", muscle: .back),
|
||||
ExerciseTemplate(name: "Wide Pull-ups", sfSymbol: "figure.pull.up", muscle: .back),
|
||||
ExerciseTemplate(name: "Lat Pulldown", sfSymbol: "figure.pull.up", muscle: .back),
|
||||
ExerciseTemplate(name: "Barbell Row", sfSymbol: "figure.strengthtraining.traditional", muscle: .back),
|
||||
ExerciseTemplate(name: "Seated Cable Row", sfSymbol: "figure.rowing", muscle: .back),
|
||||
ExerciseTemplate(name: "Cable Row", sfSymbol: "figure.rowing", muscle: .back),
|
||||
ExerciseTemplate(name: "Incline High Row", sfSymbol: "figure.strengthtraining.functional", muscle: .back),
|
||||
ExerciseTemplate(name: "Face Pull", sfSymbol: "figure.strengthtraining.functional", muscle: .back),
|
||||
ExerciseTemplate(name: "Deadlift", sfSymbol: "figure.strengthtraining.traditional", muscle: .back),
|
||||
ExerciseTemplate(name: "T-Bar Row", sfSymbol: "figure.strengthtraining.traditional", muscle: .back),
|
||||
ExerciseTemplate(name: "Shrugs", sfSymbol: "figure.arms.open", muscle: .back),
|
||||
ExerciseTemplate(name: "Lat Pulldown Wide Grip", sfSymbol: "figure.pull.up", muscle: .back),
|
||||
// Legs
|
||||
ExerciseTemplate(name: "Squat", sfSymbol: "figure.squat", muscle: .legs),
|
||||
ExerciseTemplate(name: "Hack Squat", sfSymbol: "figure.squat", muscle: .legs),
|
||||
ExerciseTemplate(name: "Romanian Deadlift", sfSymbol: "figure.strengthtraining.traditional", muscle: .legs),
|
||||
ExerciseTemplate(name: "Leg Press", sfSymbol: "figure.strengthtraining.functional", muscle: .legs),
|
||||
ExerciseTemplate(name: "Leg Extension", sfSymbol: "figure.strengthtraining.functional", muscle: .legs),
|
||||
ExerciseTemplate(name: "Adductor Machine", sfSymbol: "figure.strengthtraining.functional", muscle: .legs),
|
||||
ExerciseTemplate(name: "Lying Leg Curl", sfSymbol: "figure.strengthtraining.functional", muscle: .legs),
|
||||
ExerciseTemplate(name: "Seated Leg Curl", sfSymbol: "figure.strengthtraining.functional", muscle: .legs),
|
||||
ExerciseTemplate(name: "Hamstring Curl", sfSymbol: "figure.strengthtraining.functional", muscle: .legs),
|
||||
ExerciseTemplate(name: "Standing Calf Raise", sfSymbol: "figure.walk", muscle: .legs),
|
||||
ExerciseTemplate(name: "Seated Calf Raise", sfSymbol: "figure.strengthtraining.functional", muscle: .legs),
|
||||
ExerciseTemplate(name: "Bulgarian Split Squat", sfSymbol: "figure.squat", muscle: .legs),
|
||||
ExerciseTemplate(name: "Lunges", sfSymbol: "figure.walk", muscle: .legs),
|
||||
ExerciseTemplate(name: "Stiff-Legged Deadlift", sfSymbol: "figure.strengthtraining.traditional", muscle: .legs),
|
||||
ExerciseTemplate(name: "Leg Curl", sfSymbol: "figure.strengthtraining.functional", muscle: .legs),
|
||||
// Shoulders
|
||||
ExerciseTemplate(name: "Overhead Press", sfSymbol: "figure.arms.open", muscle: .shoulders),
|
||||
ExerciseTemplate(name: "Dumbbell Shoulder Press",sfSymbol: "dumbbell.fill", muscle: .shoulders, isDumbbell: true),
|
||||
ExerciseTemplate(name: "Arnold Press", sfSymbol: "dumbbell.fill", muscle: .shoulders, isDumbbell: true),
|
||||
ExerciseTemplate(name: "Lateral Raises", sfSymbol: "figure.arms.open", muscle: .shoulders, isDumbbell: true),
|
||||
ExerciseTemplate(name: "Front Raises", sfSymbol: "figure.arms.open", muscle: .shoulders, isDumbbell: true),
|
||||
ExerciseTemplate(name: "Cable Lateral Raise", sfSymbol: "figure.strengthtraining.functional", muscle: .shoulders),
|
||||
ExerciseTemplate(name: "Reverse Fly", sfSymbol: "dumbbell.fill", muscle: .shoulders, isDumbbell: true),
|
||||
// Arms
|
||||
ExerciseTemplate(name: "Bicep Curls", sfSymbol: "dumbbell.fill", muscle: .arms, isDumbbell: true),
|
||||
ExerciseTemplate(name: "Hammer Curls", sfSymbol: "dumbbell.fill", muscle: .arms, isDumbbell: true),
|
||||
ExerciseTemplate(name: "Incline Dumbbell Curls", sfSymbol: "dumbbell.fill", muscle: .arms, isDumbbell: true),
|
||||
ExerciseTemplate(name: "Incline Hammer Curl", sfSymbol: "dumbbell.fill", muscle: .arms, isDumbbell: true),
|
||||
ExerciseTemplate(name: "Barbell Curls", sfSymbol: "figure.strengthtraining.traditional", muscle: .arms),
|
||||
ExerciseTemplate(name: "Preacher Curl", sfSymbol: "dumbbell.fill", muscle: .arms),
|
||||
ExerciseTemplate(name: "Concentration Curls", sfSymbol: "dumbbell.fill", muscle: .arms, isDumbbell: true),
|
||||
ExerciseTemplate(name: "Dips", sfSymbol: "figure.gymnastics", muscle: .arms),
|
||||
ExerciseTemplate(name: "Tricep Dips", sfSymbol: "figure.gymnastics", muscle: .arms),
|
||||
ExerciseTemplate(name: "Tricep Pushdown", sfSymbol: "figure.strengthtraining.functional", muscle: .arms),
|
||||
ExerciseTemplate(name: "Straight Bar Pushdown", sfSymbol: "figure.strengthtraining.functional", muscle: .arms),
|
||||
ExerciseTemplate(name: "Skull Crushers", sfSymbol: "figure.strengthtraining.traditional", muscle: .arms),
|
||||
ExerciseTemplate(name: "Overhead Tricep Ext", sfSymbol: "dumbbell.fill", muscle: .arms),
|
||||
ExerciseTemplate(name: "Rope Pushdown", sfSymbol: "figure.strengthtraining.functional", muscle: .arms),
|
||||
// Core
|
||||
ExerciseTemplate(name: "Plank", sfSymbol: "figure.core.training", muscle: .core),
|
||||
ExerciseTemplate(name: "Crunches", sfSymbol: "figure.core.training", muscle: .core),
|
||||
ExerciseTemplate(name: "Ab Wheel Rollout", sfSymbol: "figure.roll", muscle: .core),
|
||||
ExerciseTemplate(name: "Russian Twists", sfSymbol: "figure.cross.training", muscle: .core),
|
||||
ExerciseTemplate(name: "Hanging Leg Raise", sfSymbol: "figure.pull.up", muscle: .core),
|
||||
ExerciseTemplate(name: "Cable Crunches", sfSymbol: "figure.cooldown", muscle: .core),
|
||||
// Cardio
|
||||
ExerciseTemplate(name: "Treadmill", sfSymbol: "figure.run", muscle: .cardio),
|
||||
ExerciseTemplate(name: "Cycling", sfSymbol: "figure.outdoor.cycle", muscle: .cardio),
|
||||
ExerciseTemplate(name: "Jump Rope", sfSymbol: "figure.jumprope", muscle: .cardio),
|
||||
ExerciseTemplate(name: "Rowing Machine", sfSymbol: "figure.rowing", muscle: .cardio),
|
||||
ExerciseTemplate(name: "Stair Climber", sfSymbol: "figure.stair.stepper", muscle: .cardio),
|
||||
]
|
||||
|
||||
let exerciseSFSymbol: [String: String] = Dictionary(
|
||||
uniqueKeysWithValues: exerciseLibrary.map { ($0.name, $0.sfSymbol) }
|
||||
)
|
||||
|
||||
// MARK: - Workout View Model
|
||||
class WorkoutViewModel: ObservableObject {
|
||||
@Published var programs: [WorkoutProgram]
|
||||
@Published var schedule: [Int: UUID]
|
||||
@Published var activeProgramId: UUID
|
||||
@Published var activeSections: [WorkoutSection]
|
||||
@Published var streakDays: Int = 5
|
||||
@Published var restTimerSeconds: Int = 60
|
||||
@Published var showAddExercise: Bool = false
|
||||
@Published var addExerciseTargetSectionId: UUID?
|
||||
|
||||
private static let kPrograms = "kisani.workout.programs"
|
||||
private static let kSchedule = "kisani.workout.schedule"
|
||||
private static let kActivePid = "kisani.workout.activePid"
|
||||
|
||||
init() {
|
||||
// ── Attempt to restore persisted state ──
|
||||
if let data = UserDefaults.standard.data(forKey: Self.kPrograms),
|
||||
let saved = try? JSONDecoder().decode([WorkoutProgram].self, from: data),
|
||||
!saved.isEmpty {
|
||||
|
||||
programs = saved
|
||||
|
||||
var sched: [Int: UUID] = [:]
|
||||
if let dict = UserDefaults.standard.dictionary(forKey: Self.kSchedule) as? [String: String] {
|
||||
for (k, v) in dict {
|
||||
if let day = Int(k), let uid = UUID(uuidString: v) { sched[day] = uid }
|
||||
}
|
||||
}
|
||||
schedule = sched
|
||||
|
||||
var pid = saved[0].id
|
||||
if let str = UserDefaults.standard.string(forKey: Self.kActivePid),
|
||||
let uid = UUID(uuidString: str),
|
||||
saved.contains(where: { $0.id == uid }) { pid = uid }
|
||||
activeProgramId = pid
|
||||
activeSections = saved.first { $0.id == pid }?.sections ?? saved[0].sections
|
||||
|
||||
} else {
|
||||
// ── First launch: seed with sample programs ──
|
||||
let progs = WorkoutViewModel.samplePrograms()
|
||||
programs = progs
|
||||
|
||||
var sched: [Int: UUID] = [:]
|
||||
if progs.count >= 6 {
|
||||
sched[2] = progs[0].id
|
||||
sched[3] = progs[1].id
|
||||
sched[4] = progs[2].id
|
||||
sched[5] = progs[3].id
|
||||
sched[6] = progs[4].id
|
||||
sched[7] = progs[5].id
|
||||
}
|
||||
schedule = sched
|
||||
|
||||
let today = Calendar.current.component(.weekday, from: Date())
|
||||
if let pid = sched[today], progs.contains(where: { $0.id == pid }) {
|
||||
activeProgramId = pid
|
||||
activeSections = progs.first { $0.id == pid }!.sections
|
||||
} else {
|
||||
activeProgramId = progs[0].id
|
||||
activeSections = progs[0].sections
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func save() {
|
||||
if let data = try? JSONEncoder().encode(programs) {
|
||||
UserDefaults.standard.set(data, forKey: Self.kPrograms)
|
||||
}
|
||||
let schedDict = schedule.reduce(into: [String: String]()) { $0[String($1.key)] = $1.value.uuidString }
|
||||
UserDefaults.standard.set(schedDict, forKey: Self.kSchedule)
|
||||
UserDefaults.standard.set(activeProgramId.uuidString, forKey: Self.kActivePid)
|
||||
}
|
||||
|
||||
var activeProgram: WorkoutProgram? { programs.first { $0.id == activeProgramId } }
|
||||
var workoutTitle: String { activeProgram?.name ?? "Workout" }
|
||||
var workoutEmoji: String { activeProgram?.emoji ?? "💪" }
|
||||
var totalSets: Int { activeSections.reduce(0) { $0 + $1.totalSets } }
|
||||
var doneSets: Int { activeSections.reduce(0) { $0 + $1.doneSets } }
|
||||
var progress: Double { totalSets > 0 ? Double(doneSets) / Double(totalSets) : 0 }
|
||||
|
||||
// MARK: - Program management
|
||||
func switchProgram(to id: UUID) {
|
||||
syncBack()
|
||||
guard let prog = programs.first(where: { $0.id == id }) else { return }
|
||||
activeProgramId = id
|
||||
activeSections = prog.sections
|
||||
save()
|
||||
}
|
||||
|
||||
func addProgram(name: String, emoji: String, colorIndex: Int = 0) {
|
||||
let defaultSection = WorkoutSection(name: "Main", exercises: [])
|
||||
programs.append(WorkoutProgram(name: name, emoji: emoji, colorIndex: colorIndex, sections: [defaultSection]))
|
||||
save()
|
||||
}
|
||||
|
||||
func deleteProgram(_ id: UUID) {
|
||||
guard id != activeProgramId, programs.count > 1 else { return }
|
||||
programs.removeAll { $0.id == id }
|
||||
schedule = schedule.filter { $0.value != id }
|
||||
save()
|
||||
}
|
||||
|
||||
func renameProgram(_ id: UUID, name: String, emoji: String? = nil) {
|
||||
guard let idx = programs.firstIndex(where: { $0.id == id }) else { return }
|
||||
programs[idx].name = name
|
||||
if let emoji { programs[idx].emoji = emoji }
|
||||
if id == activeProgramId { objectWillChange.send() }
|
||||
save()
|
||||
}
|
||||
|
||||
func setSchedule(weekday: Int, programId: UUID?) {
|
||||
if let pid = programId { schedule[weekday] = pid }
|
||||
else { schedule.removeValue(forKey: weekday) }
|
||||
save()
|
||||
}
|
||||
|
||||
func program(for date: Date) -> WorkoutProgram? {
|
||||
let weekday = Calendar.current.component(.weekday, from: date)
|
||||
guard let pid = schedule[weekday] else { return nil }
|
||||
return programs.first { $0.id == pid }
|
||||
}
|
||||
|
||||
// MARK: - Section management
|
||||
func addSection(name: String) {
|
||||
activeSections.append(WorkoutSection(name: name, exercises: []))
|
||||
syncBack()
|
||||
}
|
||||
|
||||
func deleteSection(_ id: UUID) {
|
||||
activeSections.removeAll { $0.id == id }
|
||||
syncBack()
|
||||
}
|
||||
|
||||
func renameSection(_ id: UUID, name: String) {
|
||||
guard let si = activeSections.firstIndex(where: { $0.id == id }) else { return }
|
||||
activeSections[si].name = name
|
||||
syncBack()
|
||||
}
|
||||
|
||||
func moveSectionUp(_ id: UUID) {
|
||||
guard let idx = activeSections.firstIndex(where: { $0.id == id }), idx > 0 else { return }
|
||||
activeSections.swapAt(idx, idx - 1)
|
||||
syncBack()
|
||||
}
|
||||
|
||||
func moveSectionDown(_ id: UUID) {
|
||||
guard let idx = activeSections.firstIndex(where: { $0.id == id }),
|
||||
idx < activeSections.count - 1 else { return }
|
||||
activeSections.swapAt(idx, idx + 1)
|
||||
syncBack()
|
||||
}
|
||||
|
||||
func toggleSectionExpanded(_ id: UUID) {
|
||||
guard let si = activeSections.firstIndex(where: { $0.id == id }) else { return }
|
||||
activeSections[si].isExpanded.toggle()
|
||||
}
|
||||
|
||||
// MARK: - Exercise management
|
||||
func toggleSet(sectionId: UUID, exerciseId: UUID, setId: UUID) {
|
||||
guard let si = activeSections.firstIndex(where: { $0.id == sectionId }),
|
||||
let ei = activeSections[si].exercises.firstIndex(where: { $0.id == exerciseId }),
|
||||
let setIdx = activeSections[si].exercises[ei].sets.firstIndex(where: { $0.id == setId })
|
||||
else { return }
|
||||
activeSections[si].exercises[ei].sets[setIdx].isDone.toggle()
|
||||
syncBack()
|
||||
}
|
||||
|
||||
func addSet(sectionId: UUID, to exerciseId: UUID) {
|
||||
guard let si = activeSections.firstIndex(where: { $0.id == sectionId }),
|
||||
let ei = activeSections[si].exercises.firstIndex(where: { $0.id == exerciseId })
|
||||
else { return }
|
||||
let last = activeSections[si].exercises[ei].sets.last
|
||||
activeSections[si].exercises[ei].sets.append(ExerciseSet(weight: last?.weight ?? 0, reps: last?.reps ?? 10))
|
||||
syncBack()
|
||||
}
|
||||
|
||||
// Active-program convenience wrappers
|
||||
func addExercise(to sectionId: UUID, name: String, setsCount: Int, repsCount: Int) {
|
||||
addExercise(programId: activeProgramId, sectionId: sectionId, name: name, setsCount: setsCount, repsCount: repsCount)
|
||||
}
|
||||
|
||||
func deleteExercise(sectionId: UUID, exerciseId: UUID) {
|
||||
deleteExercise(programId: activeProgramId, sectionId: sectionId, exerciseId: exerciseId)
|
||||
}
|
||||
|
||||
func moveExercise(sectionId: UUID, from: IndexSet, to: Int) {
|
||||
guard let si = activeSections.firstIndex(where: { $0.id == sectionId }) else { return }
|
||||
activeSections[si].exercises.move(fromOffsets: from, toOffset: to)
|
||||
syncBack()
|
||||
}
|
||||
|
||||
// MARK: - Universal (any program) operations
|
||||
func addExercise(programId: UUID, sectionId: UUID, name: String, setsCount: Int, repsCount: Int) {
|
||||
guard let pi = programs.firstIndex(where: { $0.id == programId }),
|
||||
let si = programs[pi].sections.firstIndex(where: { $0.id == sectionId })
|
||||
else { return }
|
||||
let template = exerciseLibrary.first { $0.name == name }
|
||||
let symbol = template?.sfSymbol ?? "figure.strengthtraining.functional"
|
||||
let isDumbb = template?.isDumbbell ?? false
|
||||
let sets = (0..<setsCount).map { _ in ExerciseSet(weight: 0, reps: repsCount) }
|
||||
let colorIdx = programs[pi].allExercises.count % 4
|
||||
programs[pi].sections[si].exercises.append(
|
||||
Exercise(name: name, sfSymbol: symbol, colorIndex: colorIdx, sets: sets, isDumbbell: isDumbb)
|
||||
)
|
||||
if programId == activeProgramId { activeSections = programs[pi].sections }
|
||||
}
|
||||
|
||||
func deleteExercise(programId: UUID, sectionId: UUID, exerciseId: UUID) {
|
||||
guard let pi = programs.firstIndex(where: { $0.id == programId }),
|
||||
let si = programs[pi].sections.firstIndex(where: { $0.id == sectionId })
|
||||
else { return }
|
||||
programs[pi].sections[si].exercises.removeAll { $0.id == exerciseId }
|
||||
if programId == activeProgramId { activeSections = programs[pi].sections }
|
||||
}
|
||||
|
||||
func addSection(to programId: UUID, name: String) {
|
||||
guard let pi = programs.firstIndex(where: { $0.id == programId }) else { return }
|
||||
programs[pi].sections.append(WorkoutSection(name: name, exercises: []))
|
||||
if programId == activeProgramId { activeSections = programs[pi].sections }
|
||||
}
|
||||
|
||||
func deleteSection(from programId: UUID, sectionId: UUID) {
|
||||
guard let pi = programs.firstIndex(where: { $0.id == programId }) else { return }
|
||||
programs[pi].sections.removeAll { $0.id == sectionId }
|
||||
if programId == activeProgramId { activeSections = programs[pi].sections }
|
||||
}
|
||||
|
||||
func renameSection(in programId: UUID, sectionId: UUID, name: String) {
|
||||
guard let pi = programs.firstIndex(where: { $0.id == programId }),
|
||||
let si = programs[pi].sections.firstIndex(where: { $0.id == sectionId })
|
||||
else { return }
|
||||
programs[pi].sections[si].name = name
|
||||
if programId == activeProgramId { activeSections = programs[pi].sections }
|
||||
}
|
||||
|
||||
func toggleExpanded(sectionId: UUID, exerciseId: UUID) {
|
||||
guard let si = activeSections.firstIndex(where: { $0.id == sectionId }),
|
||||
let ei = activeSections[si].exercises.firstIndex(where: { $0.id == exerciseId })
|
||||
else { return }
|
||||
activeSections[si].exercises[ei].isExpanded.toggle()
|
||||
}
|
||||
|
||||
func toggleDumbbell(sectionId: UUID, exerciseId: UUID) {
|
||||
guard let si = activeSections.firstIndex(where: { $0.id == sectionId }),
|
||||
let ei = activeSections[si].exercises.firstIndex(where: { $0.id == exerciseId })
|
||||
else { return }
|
||||
activeSections[si].exercises[ei].isDumbbell.toggle()
|
||||
syncBack()
|
||||
}
|
||||
|
||||
private func syncBack() {
|
||||
guard let idx = programs.firstIndex(where: { $0.id == activeProgramId }) else { return }
|
||||
programs[idx].sections = activeSections
|
||||
save()
|
||||
}
|
||||
|
||||
// MARK: - Sample data
|
||||
static func samplePrograms() -> [WorkoutProgram] {
|
||||
func s(_ count: Int, reps: Int = 12) -> [ExerciseSet] {
|
||||
(0..<count).map { _ in ExerciseSet(reps: reps) }
|
||||
}
|
||||
|
||||
let warmUp = WorkoutSection(name: "Warm Up", exercises: [
|
||||
Exercise(name: "Wide Pull-ups", sfSymbol: "figure.pull.up", colorIndex: 1, sets: s(3, reps: 10)),
|
||||
Exercise(name: "Dips", sfSymbol: "figure.gymnastics", colorIndex: 0, sets: s(3, reps: 10)),
|
||||
Exercise(name: "Ab Wheel Rollout", sfSymbol: "figure.roll", colorIndex: 2, sets: s(3, reps: 10)),
|
||||
])
|
||||
|
||||
// lbs → kg conversion factor
|
||||
let lb: Double = 0.453592
|
||||
|
||||
// ── Monday: Chest & Triceps ──────────────────────────────────────────
|
||||
let mon = WorkoutProgram(name: "Chest & Triceps", emoji: "🏋️", colorIndex: 0, sections: [
|
||||
warmUp,
|
||||
WorkoutSection(name: "Chest", exercises: [
|
||||
// Incline first — 70 / 65 / 45 / 35 lbs × 12 reps (per-side dumbbell)
|
||||
Exercise(name: "Incline Bench Press", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: [
|
||||
ExerciseSet(weight: 70 * lb, reps: 12),
|
||||
ExerciseSet(weight: 65 * lb, reps: 12),
|
||||
ExerciseSet(weight: 45 * lb, reps: 12),
|
||||
ExerciseSet(weight: 35 * lb, reps: 12),
|
||||
], isDumbbell: true),
|
||||
// Flat — 65 / 45 / 35 lbs × 12 reps
|
||||
Exercise(name: "Bench Press", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: [
|
||||
ExerciseSet(weight: 65 * lb, reps: 12),
|
||||
ExerciseSet(weight: 45 * lb, reps: 12),
|
||||
ExerciseSet(weight: 35 * lb, reps: 12),
|
||||
]),
|
||||
// Chest Flys — 35 / 25 / 25 lbs × 12 reps (per-side dumbbell)
|
||||
Exercise(name: "Chest Flys", sfSymbol: "dumbbell.fill", colorIndex: 2, sets: [
|
||||
ExerciseSet(weight: 35 * lb, reps: 12),
|
||||
ExerciseSet(weight: 25 * lb, reps: 12),
|
||||
ExerciseSet(weight: 25 * lb, reps: 12),
|
||||
], isDumbbell: true),
|
||||
]),
|
||||
WorkoutSection(name: "Triceps", exercises: [
|
||||
// Tricep Pushdown — 50 / 45 / 45 kg × 12 reps
|
||||
Exercise(name: "Tricep Pushdown", sfSymbol: "figure.strengthtraining.functional", colorIndex: 1, sets: [
|
||||
ExerciseSet(weight: 50, reps: 12),
|
||||
ExerciseSet(weight: 45, reps: 12),
|
||||
ExerciseSet(weight: 45, reps: 12),
|
||||
]),
|
||||
// Overhead — 50 kg × 3 sets
|
||||
Exercise(name: "Overhead Tricep Ext", sfSymbol: "dumbbell.fill", colorIndex: 3, sets: [
|
||||
ExerciseSet(weight: 50, reps: 12),
|
||||
ExerciseSet(weight: 50, reps: 12),
|
||||
ExerciseSet(weight: 50, reps: 12),
|
||||
]),
|
||||
// Skull Crushers — 15 kg × 3 sets
|
||||
Exercise(name: "Skull Crushers", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: [
|
||||
ExerciseSet(weight: 15, reps: 12),
|
||||
ExerciseSet(weight: 15, reps: 12),
|
||||
ExerciseSet(weight: 15, reps: 12),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
|
||||
// ── Tuesday: Back & Biceps ───────────────────────────────────────────
|
||||
let tue = WorkoutProgram(name: "Back & Biceps", emoji: "💪", colorIndex: 1, sections: [
|
||||
warmUp,
|
||||
WorkoutSection(name: "Back", exercises: [
|
||||
Exercise(name: "Lat Pulldown", sfSymbol: "figure.pull.up", colorIndex: 1, sets: [
|
||||
ExerciseSet(weight: 70, reps: 12),
|
||||
ExerciseSet(weight: 60, reps: 12),
|
||||
ExerciseSet(weight: 60, reps: 12),
|
||||
]),
|
||||
Exercise(name: "Cable Row", sfSymbol: "figure.rowing", colorIndex: 0, sets: [
|
||||
ExerciseSet(weight: 60 * lb, reps: 12),
|
||||
ExerciseSet(weight: 55 * lb, reps: 12),
|
||||
ExerciseSet(weight: 55 * lb, reps: 12),
|
||||
]),
|
||||
Exercise(name: "Deadlift", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 2, sets: [
|
||||
ExerciseSet(weight: 70 * lb, reps: 10),
|
||||
ExerciseSet(weight: 70, reps: 10),
|
||||
ExerciseSet(weight: 70, reps: 10),
|
||||
], isDumbbell: true),
|
||||
]),
|
||||
WorkoutSection(name: "Biceps", exercises: [
|
||||
Exercise(name: "Barbell Curls", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: [
|
||||
ExerciseSet(weight: 10, reps: 12),
|
||||
ExerciseSet(weight: 10, reps: 12),
|
||||
ExerciseSet(weight: 10, reps: 12),
|
||||
], isDumbbell: true),
|
||||
Exercise(name: "Incline Hammer Curl", sfSymbol: "dumbbell.fill", colorIndex: 3, sets: [
|
||||
ExerciseSet(weight: 30, reps: 12),
|
||||
ExerciseSet(weight: 30, reps: 12),
|
||||
ExerciseSet(weight: 30, reps: 12),
|
||||
], isDumbbell: true),
|
||||
Exercise(name: "Preacher Curl", sfSymbol: "dumbbell.fill", colorIndex: 1, sets: [
|
||||
ExerciseSet(weight: 60, reps: 12),
|
||||
ExerciseSet(weight: 60, reps: 12),
|
||||
ExerciseSet(weight: 60, reps: 12),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
|
||||
// ── Wednesday: Shoulders, Triceps & Biceps ───────────────────────────
|
||||
let wed = WorkoutProgram(name: "Shoulders & Arms", emoji: "🔥", colorIndex: 2, sections: [
|
||||
warmUp,
|
||||
WorkoutSection(name: "Shoulders", exercises: [
|
||||
Exercise(name: "Reverse Fly", sfSymbol: "dumbbell.fill", colorIndex: 2, sets: s(3)),
|
||||
Exercise(name: "Front Raises", sfSymbol: "figure.arms.open", colorIndex: 0, sets: s(3)),
|
||||
Exercise(name: "Lateral Raises", sfSymbol: "figure.arms.open", colorIndex: 1, sets: s(3)),
|
||||
Exercise(name: "Dumbbell Shoulder Press",sfSymbol: "dumbbell.fill", colorIndex: 3, sets: s(3)),
|
||||
]),
|
||||
WorkoutSection(name: "Triceps", exercises: [
|
||||
Exercise(name: "Tricep Pushdown", sfSymbol: "figure.strengthtraining.functional", colorIndex: 1, sets: s(3)),
|
||||
Exercise(name: "Overhead Tricep Ext",sfSymbol: "dumbbell.fill", colorIndex: 0, sets: s(3)),
|
||||
]),
|
||||
WorkoutSection(name: "Biceps", exercises: [
|
||||
Exercise(name: "Barbell Curls", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: s(3)),
|
||||
Exercise(name: "Hammer Curls", sfSymbol: "dumbbell.fill", colorIndex: 3, sets: s(3)),
|
||||
]),
|
||||
])
|
||||
|
||||
// ── Thursday: Legs & Chest ───────────────────────────────────────────
|
||||
let thu = WorkoutProgram(name: "Legs & Chest", emoji: "🦵", colorIndex: 3, sections: [
|
||||
warmUp,
|
||||
WorkoutSection(name: "Legs", exercises: [
|
||||
Exercise(name: "Hack Squat", sfSymbol: "figure.squat", colorIndex: 2, sets: s(3)),
|
||||
Exercise(name: "Leg Extension", sfSymbol: "figure.strengthtraining.functional", colorIndex: 1, sets: s(3)),
|
||||
Exercise(name: "Adductor Machine",sfSymbol: "figure.strengthtraining.functional", colorIndex: 3, sets: s(3)),
|
||||
Exercise(name: "Lying Leg Curl", sfSymbol: "figure.strengthtraining.functional", colorIndex: 0, sets: s(3)),
|
||||
Exercise(name: "Seated Leg Curl", sfSymbol: "figure.strengthtraining.functional", colorIndex: 2, sets: s(3)),
|
||||
Exercise(name: "Leg Press", sfSymbol: "figure.strengthtraining.functional", colorIndex: 1, sets: s(3)),
|
||||
]),
|
||||
WorkoutSection(name: "Chest", exercises: [
|
||||
Exercise(name: "Bench Press", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: s(3)),
|
||||
Exercise(name: "Chest Flys", sfSymbol: "dumbbell.fill", colorIndex: 2, sets: s(3)),
|
||||
]),
|
||||
])
|
||||
|
||||
// ── Friday: Full Body ────────────────────────────────────────────────
|
||||
let fri = WorkoutProgram(name: "Full Body", emoji: "⚡", colorIndex: 0, sections: [
|
||||
warmUp,
|
||||
WorkoutSection(name: "Upper Body", exercises: [
|
||||
Exercise(name: "Bench Press", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: s(3)),
|
||||
Exercise(name: "Lat Pulldown", sfSymbol: "figure.pull.up", colorIndex: 1, sets: s(3)),
|
||||
Exercise(name: "Dumbbell Shoulder Press", sfSymbol: "dumbbell.fill", colorIndex: 3, sets: s(3)),
|
||||
Exercise(name: "Tricep Pushdown", sfSymbol: "figure.strengthtraining.functional", colorIndex: 1, sets: s(3)),
|
||||
Exercise(name: "Barbell Curls", sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: s(3)),
|
||||
]),
|
||||
WorkoutSection(name: "Lower Body", exercises: [
|
||||
Exercise(name: "Leg Press", sfSymbol: "figure.strengthtraining.functional", colorIndex: 2, sets: s(3)),
|
||||
Exercise(name: "Hack Squat", sfSymbol: "figure.squat", colorIndex: 1, sets: s(3)),
|
||||
Exercise(name: "Lying Leg Curl", sfSymbol: "figure.strengthtraining.functional", colorIndex: 3, sets: s(3)),
|
||||
]),
|
||||
])
|
||||
|
||||
// ── Saturday: Full Body ──────────────────────────────────────────────
|
||||
let sat = WorkoutProgram(name: "Full Body", emoji: "💥", colorIndex: 1, sections: [
|
||||
warmUp,
|
||||
WorkoutSection(name: "Upper Body", exercises: [
|
||||
Exercise(name: "Incline Bench Press",sfSymbol: "figure.strengthtraining.traditional", colorIndex: 0, sets: s(3)),
|
||||
Exercise(name: "Cable Row", sfSymbol: "figure.rowing", colorIndex: 1, sets: s(3)),
|
||||
Exercise(name: "Lateral Raises", sfSymbol: "figure.arms.open", colorIndex: 3, sets: s(3)),
|
||||
Exercise(name: "Overhead Tricep Ext",sfSymbol: "dumbbell.fill", colorIndex: 0, sets: s(3)),
|
||||
Exercise(name: "Hammer Curls", sfSymbol: "dumbbell.fill", colorIndex: 2, sets: s(3)),
|
||||
]),
|
||||
WorkoutSection(name: "Lower Body", exercises: [
|
||||
Exercise(name: "Hack Squat", sfSymbol: "figure.squat", colorIndex: 2, sets: s(3)),
|
||||
Exercise(name: "Leg Extension", sfSymbol: "figure.strengthtraining.functional", colorIndex: 1, sets: s(3)),
|
||||
Exercise(name: "Seated Leg Curl", sfSymbol: "figure.strengthtraining.functional", colorIndex: 3, sets: s(3)),
|
||||
]),
|
||||
])
|
||||
|
||||
return [mon, tue, wed, thu, fri, sat]
|
||||
}
|
||||
}
|
||||
151
KisaniCal/Models/TaskItem.swift
Normal file
151
KisaniCal/Models/TaskItem.swift
Normal file
@@ -0,0 +1,151 @@
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Task Model
|
||||
struct TaskItem: Identifiable {
|
||||
var id = UUID()
|
||||
var title: String
|
||||
var dueDate: Date?
|
||||
var isComplete: Bool = false
|
||||
var completedAt: Date? = nil
|
||||
var quadrant: Quadrant = .urgent
|
||||
var category: TaskCategory = .reminder
|
||||
var taskColor: TaskColor = .orange
|
||||
var isRecurring: Bool = false
|
||||
}
|
||||
|
||||
enum Quadrant: String, CaseIterable, Identifiable {
|
||||
var id: String { rawValue }
|
||||
case urgent = "Urgent + Important"
|
||||
case schedule = "Schedule It"
|
||||
case delegate_ = "Delegate"
|
||||
case eliminate = "Eliminate"
|
||||
}
|
||||
|
||||
enum TaskCategory: String {
|
||||
case reminder = "Reminder"
|
||||
case birthday = "Birthday"
|
||||
case domain = "Domain"
|
||||
case annual = "Annual"
|
||||
case custom = "Custom"
|
||||
}
|
||||
|
||||
enum TaskColor {
|
||||
case orange, green, blue, yellow
|
||||
|
||||
func color() -> Color {
|
||||
switch self {
|
||||
case .orange: return AppColors.accent
|
||||
case .green: return AppColors.green
|
||||
case .blue: return AppColors.blue
|
||||
case .yellow: return AppColors.yellow
|
||||
}
|
||||
}
|
||||
func soft() -> Color {
|
||||
switch self {
|
||||
case .orange: return AppColors.accentSoft
|
||||
case .green: return AppColors.greenSoft
|
||||
case .blue: return AppColors.blueSoft
|
||||
case .yellow: return AppColors.yellowSoft
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Task View Model
|
||||
class TaskViewModel: ObservableObject {
|
||||
@Published var tasks: [TaskItem] = sampleTasks
|
||||
|
||||
var overdueTasks: [TaskItem] {
|
||||
let startOfDay = Calendar.current.startOfDay(for: Date())
|
||||
return tasks.filter { !$0.isComplete && ($0.dueDate ?? .distantFuture) < startOfDay }
|
||||
}
|
||||
var todayTasks: [TaskItem] {
|
||||
let start = Calendar.current.startOfDay(for: Date())
|
||||
let end = Calendar.current.date(byAdding: .day, value: 1, to: start)!
|
||||
return tasks
|
||||
.filter { !$0.isComplete && ($0.dueDate ?? .distantFuture) >= start && ($0.dueDate ?? .distantFuture) < end }
|
||||
.sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) }
|
||||
}
|
||||
var upcomingTasks: [TaskItem] {
|
||||
let tomorrow = Calendar.current.date(byAdding: .day, value: 1,
|
||||
to: Calendar.current.startOfDay(for: Date()))!
|
||||
return tasks
|
||||
.filter { !$0.isComplete && ($0.dueDate ?? .distantFuture) >= tomorrow }
|
||||
.sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) }
|
||||
}
|
||||
var completedTodayTasks: [TaskItem] {
|
||||
let startOfDay = Calendar.current.startOfDay(for: Date())
|
||||
return tasks
|
||||
.filter { $0.isComplete && ($0.completedAt ?? .distantPast) >= startOfDay }
|
||||
.sorted { ($0.completedAt ?? .distantPast) > ($1.completedAt ?? .distantPast) }
|
||||
}
|
||||
|
||||
func toggle(_ task: TaskItem) {
|
||||
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
||||
tasks[idx].isComplete.toggle()
|
||||
tasks[idx].completedAt = tasks[idx].isComplete ? Date() : nil
|
||||
}
|
||||
func tasks(for quadrant: Quadrant) -> [TaskItem] {
|
||||
tasks.filter { $0.quadrant == quadrant }
|
||||
}
|
||||
|
||||
func addTask(title: String, dueDate: Date?, quadrant: Quadrant = .urgent,
|
||||
category: TaskCategory = .reminder, taskColor: TaskColor = .orange,
|
||||
isRecurring: Bool = false) {
|
||||
tasks.append(TaskItem(title: title, dueDate: dueDate, quadrant: quadrant,
|
||||
category: category, taskColor: taskColor, isRecurring: isRecurring))
|
||||
}
|
||||
|
||||
func postpone(_ task: TaskItem, days: Int = 1) {
|
||||
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
||||
let base = tasks[idx].dueDate ?? Date()
|
||||
tasks[idx].dueDate = Calendar.current.date(byAdding: .day, value: days, to: base)
|
||||
}
|
||||
|
||||
func postponeAllOverdue(days: Int = 1) {
|
||||
let ids = overdueTasks.map { $0.id }
|
||||
for id in ids {
|
||||
guard let idx = tasks.firstIndex(where: { $0.id == id }) else { continue }
|
||||
let base = tasks[idx].dueDate ?? Date()
|
||||
tasks[idx].dueDate = Calendar.current.date(byAdding: .day, value: days, to: base)
|
||||
}
|
||||
}
|
||||
|
||||
func delete(_ task: TaskItem) {
|
||||
tasks.removeAll { $0.id == task.id }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sample Data
|
||||
private let sampleTasks: [TaskItem] = [
|
||||
.init(title: "Azure exam preparation",
|
||||
dueDate: cal.date(byAdding: .day, value: -7, to: Date()),
|
||||
quadrant: .urgent, category: .custom, taskColor: .orange),
|
||||
.init(title: "Leona's Birthday",
|
||||
dueDate: md(6, 17), quadrant: .urgent, category: .birthday, taskColor: .orange, isRecurring: true),
|
||||
.init(title: "Provoc.ug Domain Expiry",
|
||||
dueDate: md(7, 3), quadrant: .urgent, category: .domain, taskColor: .blue),
|
||||
.init(title: "Farouk's Birthday",
|
||||
dueDate: md(11, 8), quadrant: .delegate_, category: .birthday, taskColor: .green, isRecurring: true),
|
||||
.init(title: "Enoch's Birthday",
|
||||
dueDate: md(2, 23, 2027), quadrant: .schedule, category: .annual, taskColor: .yellow, isRecurring: true),
|
||||
.init(title: "Dad's birthday",
|
||||
dueDate: md(6, 8), quadrant: .delegate_, category: .birthday, taskColor: .blue, isRecurring: true),
|
||||
.init(title: "Moms bd",
|
||||
dueDate: md(2, 15, 2027), quadrant: .delegate_, category: .birthday, taskColor: .blue, isRecurring: true),
|
||||
.init(title: "Amooti's birthday",
|
||||
dueDate: md(6, 26), quadrant: .urgent, category: .birthday, taskColor: .orange, isRecurring: true),
|
||||
.init(title: "Provoc hosting",
|
||||
dueDate: md(11, 25, 2027), quadrant: .delegate_, category: .domain, taskColor: .blue),
|
||||
.init(title: "DV lottery preps",
|
||||
dueDate: md(9, 30, 2025), isComplete: true, quadrant: .eliminate, category: .custom, taskColor: .green),
|
||||
.init(title: "Catch up Umutoni",
|
||||
dueDate: md(11, 15, 2025), isComplete: true, quadrant: .eliminate, category: .custom, taskColor: .green),
|
||||
.init(title: "Siraj wedding preps",
|
||||
dueDate: md(8, 24, 2025), isComplete: true, quadrant: .eliminate, category: .custom, taskColor: .green),
|
||||
]
|
||||
|
||||
private let cal = Calendar.current
|
||||
private func md(_ month: Int, _ day: Int, _ year: Int = 2026) -> Date {
|
||||
var c = DateComponents(); c.year = year; c.month = month; c.day = day
|
||||
return cal.date(from: c) ?? Date()
|
||||
}
|
||||
Reference in New Issue
Block a user