Every completed set now writes a SetEntry (reps/weight or seconds), pre-filled from the plan by transition(to:) so the list checkbox, both run flows, and One More all capture for free; reset clears, skip keeps partials. The rest and finish pages show the just-done set as a pill that opens a stepper sheet for correcting reps and weight (2.5 lb / 1.25 kg steps). The Weight Progression chart plots the top-set actual weight and workout volume sums recorded sets, both falling back to the plan for legacy logs via effectiveSetEntries. Storage side of UX #3 rides along: plan weights are Double now. Schema bumps: SplitDocument 2→3, WorkoutDocument 3→4 (a fractional weight fails an older Int decode, and a rewrite would strip the irreplaceable actuals), SwiftData cache 4→5. A per-log updatedAt is reserved for the future cross-device log merge. Claude-Session: https://claude.ai/code/session_01HJDQQDA9QdP8zByg43H5v3
119 lines
6.1 KiB
Swift
119 lines
6.1 KiB
Swift
#if DEBUG
|
|
import IndieSync
|
|
import Foundation
|
|
import SwiftData
|
|
|
|
/// DEBUG-only sample data + launch-arg plumbing for App Store screenshot capture.
|
|
/// Activated by launching with `--screenshot`; `--screen <name>` picks which screen
|
|
/// the app renders (see each target's screenshot root). Never compiled into release.
|
|
enum ScreenshotSeed {
|
|
static var isActive: Bool { CommandLine.arguments.contains("--screenshot") }
|
|
|
|
static func screen(default def: String) -> String {
|
|
let args = CommandLine.arguments
|
|
if let i = args.firstIndex(of: "--screen"), i + 1 < args.count { return args[i + 1] }
|
|
return def
|
|
}
|
|
|
|
/// Insert a believable in-progress workout, a few finished sessions (so charts have
|
|
/// a trend), and the starter splits. Returns the in-progress workout to display.
|
|
@MainActor
|
|
@discardableResult
|
|
static func populate(_ context: ModelContext) -> Workout? {
|
|
// Idempotent: clear any prior seed so repeated launches don't stack duplicates
|
|
// (cascade deletes take the child exercises/logs with them).
|
|
try? context.delete(model: Workout.self)
|
|
try? context.delete(model: Split.self)
|
|
|
|
let cal = Calendar(identifier: .gregorian)
|
|
let today = Date()
|
|
func daysAgo(_ n: Int) -> Date { cal.date(byAdding: .day, value: -n, to: today) ?? today }
|
|
|
|
// ---- Splits (with exercises) ------------------------------------------
|
|
// Names match the bundled Exercise Library rigs so every screen shows its
|
|
// animated figure (an unknown name would leave the figure slot empty).
|
|
let splits: [(String, String, String, [(String, Int, Int, Int)])] = [
|
|
("Upper Body", "blue", "figure.strengthtraining.traditional", [
|
|
("Lat Pull Down", 4, 10, 110), ("Seated Row", 4, 10, 90),
|
|
("Shoulder Press", 4, 10, 40), ("Arm Curl", 3, 12, 40),
|
|
]),
|
|
("Lower Body", "green", "figure.run", [
|
|
("Leg Press", 4, 10, 140), ("Leg Curl", 4, 10, 40),
|
|
("Leg Extension", 4, 10, 80), ("Calfs", 4, 15, 100),
|
|
]),
|
|
("Core", "orange", "figure.core.training", [
|
|
("Plank", 3, 0, 0), ("Abdominal", 4, 10, 40),
|
|
("Rotary", 4, 10, 40),
|
|
]),
|
|
]
|
|
|
|
for (sIndex, s) in splits.enumerated() {
|
|
let split = Split(id: ULID.make(), name: s.0, color: s.1, systemImage: s.2,
|
|
order: sIndex, createdAt: today, updatedAt: today, jsonRelativePath: "")
|
|
context.insert(split)
|
|
for (eIndex, e) in s.3.enumerated() {
|
|
let isDuration = e.0 == "Plank"
|
|
let ex = Exercise(id: ULID.make(), name: e.0, order: eIndex, sets: e.1, reps: e.2,
|
|
weight: Double(e.3), loadType: (isDuration ? LoadType.duration : .weight).rawValue,
|
|
durationTotalSeconds: isDuration ? 45 : 0)
|
|
ex.split = split
|
|
context.insert(ex)
|
|
}
|
|
}
|
|
|
|
let upper = splits[0]
|
|
|
|
// ---- Past finished sessions (Lat Pull Down trend for the chart) --------
|
|
let liftTrend: [Double] = [95, 100, 105, 110]
|
|
for (i, w) in liftTrend.enumerated() {
|
|
let date = daysAgo((liftTrend.count - i) * 4)
|
|
let workout = Workout(id: ULID.make(), splitID: nil, splitName: upper.0, start: date,
|
|
end: date.addingTimeInterval(2400), statusRaw: WorkoutStatus.completed.rawValue,
|
|
createdAt: date, updatedAt: date, jsonRelativePath: "")
|
|
context.insert(workout)
|
|
for (eIndex, e) in upper.3.enumerated() {
|
|
let weight = e.0 == "Lat Pull Down" ? w : Double(e.3)
|
|
let log = WorkoutLog(id: ULID.make(), exerciseName: e.0, order: eIndex, sets: e.1,
|
|
reps: e.2, weight: weight, loadType: LoadType.weight.rawValue,
|
|
durationTotalSeconds: 0, currentStateIndex: e.1,
|
|
statusRaw: WorkoutStatus.completed.rawValue, notes: nil, date: date,
|
|
setEntries: actualEntries(sets: e.1, reps: e.2, weight: weight, at: date))
|
|
log.workout = workout
|
|
context.insert(log)
|
|
}
|
|
}
|
|
|
|
// ---- The current in-progress session -----------------------------------
|
|
let workout = Workout(id: ULID.make(), splitID: nil, splitName: upper.0, start: today,
|
|
end: nil, statusRaw: WorkoutStatus.inProgress.rawValue,
|
|
createdAt: today, updatedAt: today, jsonRelativePath: "")
|
|
context.insert(workout)
|
|
// Lat Pull Down done, Seated Row partway, the rest to go.
|
|
let progress = [(4, WorkoutStatus.completed), (2, .inProgress), (0, .notStarted), (0, .notStarted)]
|
|
for (eIndex, e) in upper.3.enumerated() {
|
|
let (idx, status) = progress[eIndex]
|
|
let weight = e.0 == "Lat Pull Down" ? 110 : Double(e.3)
|
|
let log = WorkoutLog(id: ULID.make(), exerciseName: e.0, order: eIndex, sets: e.1, reps: e.2,
|
|
weight: weight, loadType: LoadType.weight.rawValue,
|
|
durationTotalSeconds: 0, currentStateIndex: idx,
|
|
statusRaw: status.rawValue, notes: nil, date: today,
|
|
setEntries: idx > 0 ? actualEntries(sets: idx, reps: e.2, weight: weight, at: today) : nil)
|
|
log.workout = workout
|
|
context.insert(log)
|
|
}
|
|
|
|
try? context.save()
|
|
return workout
|
|
}
|
|
|
|
/// Plausible per-set actuals for a weighted log: plan values, with the last
|
|
/// set a couple of reps short so screenshots show real-looking variation.
|
|
private static func actualEntries(sets: Int, reps: Int, weight: Double, at date: Date) -> [SetEntry] {
|
|
(0..<sets).map { i in
|
|
SetEntry(reps: i == sets - 1 ? max(1, reps - 2) : reps, weight: weight,
|
|
completedAt: date.addingTimeInterval(Double(i) * 150))
|
|
}
|
|
}
|
|
}
|
|
#endif
|