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:
@@ -1,13 +1,15 @@
|
||||
import Foundation
|
||||
import IndieSync
|
||||
|
||||
/// The bundled immutable starter-split library. Each seed ships as byte-canonical
|
||||
/// `SplitDocument` JSON in `Resources/StarterSplits/*.split.json`, keyed by a fixed
|
||||
/// ULID (the shared `01DXF6DT00` prefix, minted from a frozen 2020 timestamp) so
|
||||
/// every install and device agrees on the same identity.
|
||||
/// The bundled immutable starter-routine library. Each seed ships as byte-canonical
|
||||
/// `RoutineDocument` JSON in `Resources/StarterSplits/*.split.json` (the resource
|
||||
/// directory and file extension keep their legacy "split" spelling — bundled asset
|
||||
/// names are pinned, see below), keyed by a fixed ULID (the shared `01DXF6DT00`
|
||||
/// prefix, minted from a frozen 2020 timestamp) so every install and device agrees
|
||||
/// on the same identity.
|
||||
///
|
||||
/// Seeds are never mutated in place: editing one clones it to a fresh random ULID
|
||||
/// and soft-deletes the seed (see `SyncEngine.save(split:)`). The seed's tombstone
|
||||
/// and soft-deletes the seed (see `SyncEngine.save(routine:)`). The seed's tombstone
|
||||
/// is exempt from pruning, so it vetoes resurrection forever; the app bundle — not
|
||||
/// the stub — is the canonical restore source, since the seed content is immutable.
|
||||
enum SeedLibrary {
|
||||
@@ -17,7 +19,7 @@ enum SeedLibrary {
|
||||
/// seeding at once is then semantically empty (both wrote the same bytes).
|
||||
struct Seed: Sendable {
|
||||
let id: String
|
||||
let doc: SplitDocument
|
||||
let doc: RoutineDocument
|
||||
let data: Data
|
||||
}
|
||||
|
||||
@@ -31,11 +33,13 @@ enum SeedLibrary {
|
||||
let bundle = Bundle(for: BundleToken.self)
|
||||
// XcodeGen flattens resource groups, so the files land in the bundle root as
|
||||
// "<Name>.split.json" — enumerate all json and filter on the compound suffix.
|
||||
// PINNED: the bundled resource extension stays ".split.json" (asset names in
|
||||
// StarterSplits/ are shipped files, not symbols — the rename must not touch them).
|
||||
let urls = (bundle.urls(forResourcesWithExtension: "json", subdirectory: nil) ?? [])
|
||||
.filter { $0.lastPathComponent.hasSuffix(".split.json") }
|
||||
let loaded: [Seed] = urls.compactMap { url in
|
||||
guard let data = try? Data(contentsOf: url),
|
||||
let doc = try? DocumentCoder.decode(SplitDocument.self, from: data)
|
||||
let doc = try? DocumentCoder.decode(RoutineDocument.self, from: data)
|
||||
else { return nil }
|
||||
return Seed(id: doc.id, doc: doc, data: data)
|
||||
}
|
||||
@@ -53,7 +57,7 @@ enum SeedLibrary {
|
||||
/// would otherwise read as an edit and fork the seed; normalizing both timestamps
|
||||
/// to a common value before the `==` isolates real changes. A non-seed id has no
|
||||
/// seed to compare against — treated as pristine (callers gate on `isSeed` first).
|
||||
static func isPristine(_ doc: SplitDocument) -> Bool {
|
||||
static func isPristine(_ doc: RoutineDocument) -> Bool {
|
||||
guard let seed = seed(id: doc.id) else { return true }
|
||||
var edited = doc
|
||||
var pristine = seed.doc
|
||||
|
||||
Reference in New Issue
Block a user