Restructure into a three-tab app with Progress, goals, and Meditation
The UX redesign's first landing (spec in UX-REDESIGN.md): ContentView becomes a Today / Progress / Settings TabView, "Routine" replaces "Split" in every user-facing string and view name (code-level types keep their names), and workout starting moves to shared WorkoutStarter / StartedWorkoutNavigator plumbing. - New Progress tab: weekly goal streaks, workout trends, per-exercise weight progression, achievements, and the full history list (WorkoutLogsView -> WorkoutHistoryView). - Goals: stable categories workouts roll up to, managed from Settings. - New Meditation exercise + starter routine; timed sits record to Apple Health as Mind & Body sessions. Claude-Session: https://claude.ai/code/session_012qw2itfzKyEJ1HpsFt8Ex4
This commit is contained in:
+79
-35
@@ -21,14 +21,14 @@ extension ExerciseDocument {
|
||||
}
|
||||
}
|
||||
|
||||
extension SplitDocument {
|
||||
init(from split: Split) {
|
||||
self.init(schemaVersion: Self.currentSchemaVersion, id: split.id, name: split.name,
|
||||
color: split.color, systemImage: split.systemImage, order: split.order,
|
||||
createdAt: split.createdAt, updatedAt: split.updatedAt,
|
||||
exercises: split.exercisesArray.map(ExerciseDocument.init(from:)),
|
||||
activityType: split.activityTypeRaw,
|
||||
restSeconds: split.restSeconds, autoAdvance: split.autoAdvance)
|
||||
extension RoutineDocument {
|
||||
init(from routine: Routine) {
|
||||
self.init(schemaVersion: Self.currentSchemaVersion, id: routine.id, name: routine.name,
|
||||
color: routine.color, systemImage: routine.systemImage, order: routine.order,
|
||||
createdAt: routine.createdAt, updatedAt: routine.updatedAt,
|
||||
exercises: routine.exercisesArray.map(ExerciseDocument.init(from:)),
|
||||
activityType: routine.activityTypeRaw,
|
||||
restSeconds: routine.restSeconds, autoAdvance: routine.autoAdvance)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,8 +46,8 @@ extension WorkoutLogDocument {
|
||||
|
||||
extension WorkoutDocument {
|
||||
init(from workout: Workout) {
|
||||
self.init(schemaVersion: Self.currentSchemaVersion, id: workout.id, splitID: workout.splitID,
|
||||
splitName: workout.splitName, start: workout.start, end: workout.end,
|
||||
self.init(schemaVersion: Self.currentSchemaVersion, id: workout.id, routineID: workout.routineID,
|
||||
routineName: workout.routineName, start: workout.start, end: workout.end,
|
||||
status: workout.statusRaw, createdAt: workout.createdAt, updatedAt: workout.updatedAt,
|
||||
logs: workout.logsArray.map(WorkoutLogDocument.init(from:)),
|
||||
metrics: workout.metrics,
|
||||
@@ -86,11 +86,22 @@ extension WorkoutDocument {
|
||||
logs: [])
|
||||
}
|
||||
|
||||
extension ScheduleDocument {
|
||||
init(from schedule: Schedule) {
|
||||
self.init(schemaVersion: Self.currentSchemaVersion, id: schedule.id,
|
||||
routineID: schedule.routineID, routineName: schedule.routineName,
|
||||
goal: schedule.goalRaw, recurrence: schedule.recurrenceRaw,
|
||||
weekdays: schedule.weekdays, timesPerWeek: schedule.timesPerWeek,
|
||||
date: schedule.date,
|
||||
order: schedule.order, createdAt: schedule.createdAt, updatedAt: schedule.updatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Document → Cache (upsert)
|
||||
|
||||
enum CacheMapper {
|
||||
static func fetchSplit(id: String, in context: ModelContext) -> Split? {
|
||||
var d = FetchDescriptor<Split>(predicate: #Predicate { $0.id == id })
|
||||
static func fetchRoutine(id: String, in context: ModelContext) -> Routine? {
|
||||
var d = FetchDescriptor<Routine>(predicate: #Predicate { $0.id == id })
|
||||
d.fetchLimit = 1
|
||||
return try? context.fetch(d).first
|
||||
}
|
||||
@@ -101,30 +112,36 @@ enum CacheMapper {
|
||||
return try? context.fetch(d).first
|
||||
}
|
||||
|
||||
// MARK: Split
|
||||
static func fetchSchedule(id: String, in context: ModelContext) -> Schedule? {
|
||||
var d = FetchDescriptor<Schedule>(predicate: #Predicate { $0.id == id })
|
||||
d.fetchLimit = 1
|
||||
return try? context.fetch(d).first
|
||||
}
|
||||
|
||||
static func upsertSplit(_ doc: SplitDocument, relativePath: String, into context: ModelContext) {
|
||||
let split: Split
|
||||
if let existing = fetchSplit(id: doc.id, in: context) {
|
||||
split = existing
|
||||
// MARK: Routine
|
||||
|
||||
static func upsertRoutine(_ doc: RoutineDocument, relativePath: String, into context: ModelContext) {
|
||||
let routine: Routine
|
||||
if let existing = fetchRoutine(id: doc.id, in: context) {
|
||||
routine = existing
|
||||
} else {
|
||||
split = Split(id: doc.id, name: doc.name, color: doc.color, systemImage: doc.systemImage,
|
||||
routine = Routine(id: doc.id, name: doc.name, color: doc.color, systemImage: doc.systemImage,
|
||||
order: doc.order, createdAt: doc.createdAt, updatedAt: doc.updatedAt,
|
||||
jsonRelativePath: relativePath)
|
||||
context.insert(split)
|
||||
context.insert(routine)
|
||||
}
|
||||
split.name = doc.name
|
||||
split.color = doc.color
|
||||
split.systemImage = doc.systemImage
|
||||
split.order = doc.order
|
||||
split.createdAt = doc.createdAt
|
||||
split.updatedAt = doc.updatedAt
|
||||
split.jsonRelativePath = relativePath
|
||||
split.activityTypeRaw = doc.activityType ?? 0
|
||||
split.restSeconds = doc.restSeconds
|
||||
split.autoAdvance = doc.autoAdvance
|
||||
routine.name = doc.name
|
||||
routine.color = doc.color
|
||||
routine.systemImage = doc.systemImage
|
||||
routine.order = doc.order
|
||||
routine.createdAt = doc.createdAt
|
||||
routine.updatedAt = doc.updatedAt
|
||||
routine.jsonRelativePath = relativePath
|
||||
routine.activityTypeRaw = doc.activityType ?? 0
|
||||
routine.restSeconds = doc.restSeconds
|
||||
routine.autoAdvance = doc.autoAdvance
|
||||
|
||||
let existing = Dictionary(split.exercises.map { ($0.id, $0) }, uniquingKeysWith: { a, _ in a })
|
||||
let existing = Dictionary(routine.exercises.map { ($0.id, $0) }, uniquingKeysWith: { a, _ in a })
|
||||
var keep = Set<String>()
|
||||
for ed in doc.exercises {
|
||||
keep.insert(ed.id)
|
||||
@@ -132,11 +149,11 @@ enum CacheMapper {
|
||||
apply(ed, to: e)
|
||||
} else {
|
||||
let e = makeExercise(ed)
|
||||
e.split = split
|
||||
e.routine = routine
|
||||
context.insert(e)
|
||||
}
|
||||
}
|
||||
for e in Array(split.exercises) where !keep.contains(e.id) {
|
||||
for e in Array(routine.exercises) where !keep.contains(e.id) {
|
||||
context.delete(e)
|
||||
}
|
||||
}
|
||||
@@ -165,14 +182,14 @@ enum CacheMapper {
|
||||
if let existing = fetchWorkout(id: doc.id, in: context) {
|
||||
workout = existing
|
||||
} else {
|
||||
workout = Workout(id: doc.id, splitID: doc.splitID, splitName: doc.splitName,
|
||||
workout = Workout(id: doc.id, routineID: doc.routineID, routineName: doc.routineName,
|
||||
start: doc.start, end: doc.end, statusRaw: doc.status,
|
||||
createdAt: doc.createdAt, updatedAt: doc.updatedAt,
|
||||
jsonRelativePath: relativePath)
|
||||
context.insert(workout)
|
||||
}
|
||||
workout.splitID = doc.splitID
|
||||
workout.splitName = doc.splitName
|
||||
workout.routineID = doc.routineID
|
||||
workout.routineName = doc.routineName
|
||||
workout.start = doc.start
|
||||
workout.end = doc.end
|
||||
workout.statusRaw = doc.status
|
||||
@@ -228,4 +245,31 @@ enum CacheMapper {
|
||||
l.setEntries = d.setEntries
|
||||
l.logUpdatedAt = d.updatedAt
|
||||
}
|
||||
|
||||
// MARK: Schedule
|
||||
|
||||
static func upsertSchedule(_ doc: ScheduleDocument, relativePath: String, into context: ModelContext) {
|
||||
let schedule: Schedule
|
||||
if let existing = fetchSchedule(id: doc.id, in: context) {
|
||||
schedule = existing
|
||||
} else {
|
||||
schedule = Schedule(id: doc.id, routineID: doc.routineID, routineName: doc.routineName,
|
||||
goalRaw: doc.goal, recurrenceRaw: doc.recurrence, weekdays: doc.weekdays,
|
||||
timesPerWeek: doc.timesPerWeek, date: doc.date, order: doc.order,
|
||||
createdAt: doc.createdAt, updatedAt: doc.updatedAt,
|
||||
jsonRelativePath: relativePath)
|
||||
context.insert(schedule)
|
||||
}
|
||||
schedule.routineID = doc.routineID
|
||||
schedule.routineName = doc.routineName
|
||||
schedule.goalRaw = doc.goal
|
||||
schedule.recurrenceRaw = doc.recurrence
|
||||
schedule.weekdays = doc.weekdays
|
||||
schedule.timesPerWeek = doc.timesPerWeek
|
||||
schedule.date = doc.date
|
||||
schedule.order = doc.order
|
||||
schedule.createdAt = doc.createdAt
|
||||
schedule.updatedAt = doc.updatedAt
|
||||
schedule.jsonRelativePath = relativePath
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user