Library detail screens now lead with the user's recorded machine settings (per-split when they disagree, empty-state card for machine-based entries) and append the weight progression chart. Starter seeds mark machine exercises with an empty machineSettings list so the settings UI lights up before first use. The figure rig gains a frontal body profile for face-on machines, props that can ride mid joints (knees/elbows), and an alternating four-frame Bird Dog loop. Claude-Session: https://claude.ai/code/session_01LEoff8bXGBS83tK1c55Mf7
143 lines
5.6 KiB
Swift
143 lines
5.6 KiB
Swift
// Generates the bundled starter-split JSON resources for Workouts.
|
|
// Deterministic: fixed ULID timestamp (2020-01-01T00:00:00Z) + SHA256-of-label
|
|
// randomness, fixed document timestamps, encoder matching DocumentCoder
|
|
// (prettyPrinted + sortedKeys + iso8601). Re-running always emits identical bytes.
|
|
import Foundation
|
|
import CryptoKit
|
|
|
|
// MARK: - ULID (Crockford base32, 48-bit ms timestamp + 80 deterministic bits)
|
|
|
|
let crockford = Array("0123456789ABCDEFGHJKMNPQRSTVWXYZ")
|
|
|
|
func deterministicULID(label: String) -> String {
|
|
let timestampMS: UInt64 = 1_577_836_800_000 // 2020-01-01T00:00:00Z
|
|
var bytes = [UInt8]()
|
|
for shift in stride(from: 40, through: 0, by: -8) {
|
|
bytes.append(UInt8((timestampMS >> UInt64(shift)) & 0xFF))
|
|
}
|
|
let digest = SHA256.hash(data: Data(label.utf8))
|
|
bytes.append(contentsOf: Array(digest).prefix(10))
|
|
precondition(bytes.count == 16)
|
|
|
|
// Encode 128 bits as 26 Crockford base32 chars: big-int division by 32, 26 digits.
|
|
var chars = [Character]()
|
|
var num = bytes
|
|
for _ in 0..<26 {
|
|
var remainder = 0
|
|
var quotient = [UInt8]()
|
|
for byte in num {
|
|
let acc = remainder * 256 + Int(byte)
|
|
quotient.append(UInt8(acc / 32))
|
|
remainder = acc % 32
|
|
}
|
|
chars.append(crockford[remainder])
|
|
num = quotient
|
|
}
|
|
return String(chars.reversed())
|
|
}
|
|
|
|
// MARK: - Document shapes (field names must mirror Shared/Model/Documents.swift)
|
|
|
|
struct MachineSetting: Codable {
|
|
var name: String
|
|
var value: String
|
|
}
|
|
|
|
struct ExerciseDocument: Codable {
|
|
var id: String
|
|
var name: String
|
|
var order: Int
|
|
var sets: Int
|
|
var reps: Int
|
|
var weight: Int
|
|
var loadType: Int
|
|
var durationSeconds: Int
|
|
var machineSettings: [MachineSetting]? = nil
|
|
}
|
|
|
|
struct SplitDocument: Codable {
|
|
var schemaVersion: Int
|
|
var id: String
|
|
var name: String
|
|
var color: String
|
|
var systemImage: String
|
|
var order: Int
|
|
var createdAt: Date
|
|
var updatedAt: Date
|
|
var exercises: [ExerciseDocument]
|
|
var activityType: Int?
|
|
}
|
|
|
|
// MARK: - Seed data (canonical source for Workouts/Resources/StarterSplits/*.json)
|
|
|
|
// `machine: true` emits `machineSettings: []` — marks the exercise machine-based
|
|
// (empty = nothing recorded yet), which lights up the machine-settings UI without
|
|
// the user having to flip the editor toggle first.
|
|
struct Ex { let name: String; var weight = 0; var sets = 4; var reps = 10; var load = 1; var dur = 0; var machine = false }
|
|
struct Sp { let name: String; let color: String; let icon: String; let activity: Int; let ex: [Ex] }
|
|
|
|
let seeds: [Sp] = [
|
|
Sp(name: "Upper Body", color: "blue", icon: "figure.strengthtraining.traditional", activity: 0, ex: [
|
|
Ex(name: "Lat Pull Down", weight: 110, machine: true),
|
|
Ex(name: "Seated Row", weight: 90, machine: true),
|
|
Ex(name: "Shoulder Press", weight: 40, machine: true),
|
|
Ex(name: "Chest Press", weight: 40, machine: true),
|
|
Ex(name: "Tricep Press", weight: 100, machine: true),
|
|
Ex(name: "Arm Curl", weight: 40, machine: true),
|
|
]),
|
|
Sp(name: "Core", color: "orange", icon: "figure.core.training", activity: 3, ex: [
|
|
Ex(name: "Abdominal", weight: 40, machine: true),
|
|
Ex(name: "Rotary", weight: 40, machine: true),
|
|
]),
|
|
Sp(name: "Lower Body", color: "green", icon: "figure.run", activity: 0, ex: [
|
|
Ex(name: "Leg Press", weight: 140, machine: true),
|
|
Ex(name: "Leg Curl", weight: 40, machine: true),
|
|
Ex(name: "Leg Extension", weight: 80, machine: true),
|
|
Ex(name: "Abductor", weight: 130, machine: true),
|
|
Ex(name: "Adductor", weight: 130, machine: true),
|
|
Ex(name: "Calfs", weight: 100, machine: true),
|
|
]),
|
|
Sp(name: "Bodyweight Core", color: "teal", icon: "figure.core.training", activity: 3, ex: [
|
|
Ex(name: "Cat-Cow", sets: 1, reps: 10, load: 0),
|
|
Ex(name: "Dead Bug", sets: 1, reps: 8, load: 0),
|
|
Ex(name: "Bird Dog", sets: 1, reps: 8, load: 0),
|
|
Ex(name: "Plank", sets: 3, reps: 0, load: 2, dur: 45),
|
|
Ex(name: "Hollow Body Hold", sets: 3, reps: 0, load: 2, dur: 30),
|
|
Ex(name: "Side Plank", sets: 3, reps: 0, load: 2, dur: 30),
|
|
Ex(name: "Leg Raises", sets: 3, reps: 12, load: 0),
|
|
Ex(name: "Bird Dog", sets: 3, reps: 8, load: 0),
|
|
Ex(name: "Reverse Crunch", sets: 3, reps: 12, load: 0),
|
|
]),
|
|
]
|
|
|
|
// MARK: - Emit
|
|
|
|
let encoder = JSONEncoder()
|
|
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
|
encoder.dateEncodingStrategy = .iso8601
|
|
|
|
let fixedDate = ISO8601DateFormatter().date(from: "2020-01-01T00:00:00Z")!
|
|
let outDir = URL(fileURLWithPath: CommandLine.arguments[1], isDirectory: true)
|
|
try FileManager.default.createDirectory(at: outDir, withIntermediateDirectories: true)
|
|
|
|
for (order, sp) in seeds.enumerated() {
|
|
let slug = sp.name.lowercased().replacingOccurrences(of: " ", with: "-")
|
|
let doc = SplitDocument(
|
|
schemaVersion: 2,
|
|
id: deterministicULID(label: "starter.\(slug)"),
|
|
name: sp.name, color: sp.color, systemImage: sp.icon, order: order,
|
|
createdAt: fixedDate, updatedAt: fixedDate,
|
|
exercises: sp.ex.enumerated().map { idx, e in
|
|
ExerciseDocument(
|
|
id: deterministicULID(label: "starter.\(slug).ex\(idx)"),
|
|
name: e.name, order: idx, sets: e.sets, reps: e.reps, weight: e.weight,
|
|
loadType: e.load, durationSeconds: e.dur,
|
|
machineSettings: e.machine ? [] : nil)
|
|
},
|
|
activityType: sp.activity)
|
|
let data = try encoder.encode(doc)
|
|
let file = outDir.appendingPathComponent("\(sp.name).split.json")
|
|
try data.write(to: file)
|
|
print("\(doc.id) \(sp.name) (\(sp.ex.count) exercises)")
|
|
}
|