Files
workouts/Shared/Screenshots/ScreenshotSeed.swift
T
rzen 7cb2d6da26 Show only the day's docket on the Today board and add ad hoc Now workouts
- Due-filter schedules: daily always, fixed days by weekday, one-offs on
  their date; rest days get an empty state
- Workouts with no due schedule row (ad hoc or off-day starts) now render
  as their own board rows
- The + sheet is now "New Workout" with a "When" picker; "Now" (offered
  when adding on today) skips the schedule and starts the workout directly
- Remove the times-per-week scheduling mode everywhere (enum, document,
  entity, mappers, planner, seeds, tests)
2026-07-11 11:09:11 -04:00

182 lines
9.7 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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 routines. 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: Routine.self)
try? context.delete(model: Schedule.self)
let cal = Calendar(identifier: .gregorian)
let today = Date()
func daysAgo(_ n: Int) -> Date { cal.date(byAdding: .day, value: -n, to: today) ?? today }
// ---- Routines (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 routines: [(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),
]),
]
var created: [Routine] = []
for (sIndex, s) in routines.enumerated() {
let routine = Routine(id: ULID.make(), name: s.0, color: s.1, systemImage: s.2,
order: sIndex, createdAt: today, updatedAt: today, jsonRelativePath: "")
context.insert(routine)
created.append(routine)
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.routine = routine
context.insert(ex)
}
}
// ---- Schedules (the Today board's rows, spanning two goal groups) -------
let schedulePlans: [(routine: Routine, goal: GoalKind, recurrence: ScheduleRecurrence,
weekdays: [Int]?)] = [
(created[0], .strength, .fixedDays, [2, 5]), // Upper Body · Mon & Thu
(created[1], .strength, .fixedDays, [3, 7]), // Lower Body · Tue & Sat
(created[2], .mobility, .daily, nil), // Core · daily
]
// Schedules predate the history below, so the Progress tab's adherence
// tracks cover the seeded weeks instead of starting "today".
let scheduledSince = daysAgo(70)
for (i, plan) in schedulePlans.enumerated() {
context.insert(Schedule(
id: ULID.make(), routineID: plan.routine.id, routineName: plan.routine.name,
goalRaw: plan.goal.rawValue, recurrenceRaw: plan.recurrence.rawValue,
weekdays: plan.weekdays, order: i,
createdAt: scheduledSince, updatedAt: scheduledSince, jsonRelativePath: ""))
}
let upper = routines[0]
let upperID = created[0].id
// ---- Past finished sessions ---------------------------------------------
// Ten weeks of believable history across all three routines so Progress has
// living goal tracks, streaks, trends, and history: Upper Body lands on its
// Mon & Thu slots (one missed), Lower Body twice a week (one missed), Core
// most mornings — unbroken in the last four weeks, so the Mobility streak is
// alive. Upper's Lat Pull Down ramps toward 110 for the progression chart.
// All deterministic — patterns, not randomness.
func addCompleted(routine: Routine, spec: [(String, Int, Int, Int)],
on day: Date, hour: Int, lengthMinutes: Int, latPullDown: Double?) {
let start = cal.date(bySettingHour: hour, minute: 15, second: 0, of: day) ?? day
let end = start.addingTimeInterval(Double(lengthMinutes) * 60)
let workout = Workout(id: ULID.make(), routineID: routine.id, routineName: routine.name,
start: start, end: end, statusRaw: WorkoutStatus.completed.rawValue,
createdAt: start, updatedAt: end, jsonRelativePath: "")
context.insert(workout)
for (eIndex, e) in spec.enumerated() {
let isDuration = e.0 == "Plank"
let weight = e.0 == "Lat Pull Down" ? (latPullDown ?? Double(e.3)) : Double(e.3)
let log = WorkoutLog(id: ULID.make(), exerciseName: e.0, order: eIndex, sets: e.1,
reps: e.2, weight: isDuration ? 0 : weight,
loadType: (isDuration ? LoadType.duration : .weight).rawValue,
durationTotalSeconds: isDuration ? 45 : 0, currentStateIndex: e.1,
statusRaw: WorkoutStatus.completed.rawValue, notes: nil, date: start,
setEntries: isDuration ? nil
: actualEntries(sets: e.1, reps: e.2, weight: weight, at: start))
log.workout = workout
context.insert(log)
}
}
var upperSlot = 0, lowerSlot = 0
for d in stride(from: 69, through: 1, by: -1) {
let day = daysAgo(d)
let weekday = cal.component(.weekday, from: day)
// Upper Body — Mon & Thu; the 5th slot is the believable miss.
if weekday == 2 || weekday == 5 {
upperSlot += 1
if upperSlot != 5 {
let lift = min(110, 87.5 + 2.5 * Double((upperSlot + 1) / 2))
addCompleted(routine: created[0], spec: routines[0].3,
on: day, hour: 17, lengthMinutes: 40, latPullDown: lift)
}
}
// Lower Body — Tue & Sat, its 2×-per-week rhythm; the 4th slot missed.
if weekday == 3 || weekday == 7 {
lowerSlot += 1
if lowerSlot != 4 {
addCompleted(routine: created[1], spec: routines[1].3,
on: day, hour: 18, lengthMinutes: 35, latPullDown: nil)
}
}
// Core — daily mornings; Sundays skipped beyond the last four weeks.
if d <= 28 || weekday != 1 {
addCompleted(routine: created[2], spec: routines[2].3,
on: day, hour: 6, lengthMinutes: 15, latPullDown: nil)
}
}
// ---- The current in-progress session -----------------------------------
// Carries the routine's id so the Today board's schedule row shows it as
// today's in-progress run.
let workout = Workout(id: ULID.make(), routineID: upperID, routineName: 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