Files
workouts/Workouts/Views/Settings/SettingsView.swift
T
rzen 7586edd878 Reconcile starter seeds on connect and add restore + duplicate cleanup
Seeding now covers existing installs, not just empty containers: after the
same settle delay as auto-seed, connect() branches to reconcileSeeds(),
driven by a pure tested planner — upgrade an older seed revision in place
(safe: clone-on-edit guarantees no user content at seed ULIDs), skip
up-to-date/quarantined files, respect delete-veto stubs, and write missing
seeds unless a same-name legacy split exists. Settings gains Restore
Starter Splits (the one deliberate veto lift, writing current bundle
bytes) and a dev duplicate-cleanup tool backed by a fail-closed scanner.
The HealthKit estimate now follows the clone redirect when resolving a
workout's split.

Claude-Session: https://claude.ai/code/session_01LEoff8bXGBS83tK1c55Mf7
2026-07-06 16:29:59 -04:00

150 lines
5.8 KiB
Swift

//
// SettingsView.swift
// Workouts
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
import IndieAbout
struct SettingsView: View {
@Environment(SyncEngine.self) private var sync
@Environment(AppServices.self) private var services
@AppStorage("restSeconds") private var restSeconds: Int = 45
@AppStorage("doneCountdownSeconds") private var doneCountdownSeconds: Int = 5
@AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb
@State private var isRestoringSeeds = false
@State private var restoreSeedsMessage: String?
var body: some View {
NavigationStack {
Form {
// MARK: - Workout Section
Section {
Stepper(value: $restSeconds, in: 10...180, step: 5) {
HStack {
Text("Rest Between Sets")
Spacer()
Text("\(restSeconds)s").foregroundColor(.secondary)
}
}
Stepper(value: $doneCountdownSeconds, in: 3...20, step: 1) {
HStack {
Text("Auto-Finish Countdown")
Spacer()
Text("\(doneCountdownSeconds)s").foregroundColor(.secondary)
}
}
Picker("Weight Unit", selection: $weightUnit) {
ForEach(WeightUnit.allCases, id: \.self) { unit in
Text(unit.displayName).tag(unit)
}
}
} header: {
Text("Workout")
} footer: {
Text("How long the watch waits on the finish screen before completing an exercise automatically.")
}
// MARK: - Library Section
Section(header: Text("Library")) {
NavigationLink {
SplitListView()
} label: {
Label("Splits", systemImage: "dumbbell.fill")
}
NavigationLink {
ExerciseLibraryView()
} label: {
Label("Exercises", systemImage: "figure.strengthtraining.traditional")
}
}
// MARK: - Starter Splits Section
Section {
Button {
Task {
isRestoringSeeds = true
restoreSeedsMessage = nil
let n = await sync.restoreSeeds()
isRestoringSeeds = false
restoreSeedsMessage = n == 0
? "All starter splits are already present."
: "Restored \(n) starter split\(n == 1 ? "" : "s")."
}
} label: {
HStack {
Label("Restore Starter Splits", systemImage: "arrow.counterclockwise")
if isRestoringSeeds {
Spacer()
ProgressView()
}
}
}
.disabled(isRestoringSeeds || sync.iCloudStatus != .available)
} header: {
Text("Starter Splits")
} footer: {
if let restoreSeedsMessage {
Text(restoreSeedsMessage)
} else {
Text("Brings back the bundled starter splits you've deleted. Splits you've edited or created are never touched.")
}
}
// MARK: - iCloud Sync Section
Section {
switch sync.iCloudStatus {
case .checking:
Label("Connecting…", systemImage: "arrow.triangle.2.circlepath.icloud")
.foregroundStyle(.secondary)
case .available:
Label("Connected", systemImage: "checkmark.icloud")
.foregroundStyle(.secondary)
case .unavailable:
Label("iCloud unavailable", systemImage: "xmark.icloud")
.foregroundStyle(.orange)
}
if let error = sync.lastSyncError {
Label(error, systemImage: "exclamationmark.icloud.fill")
.foregroundStyle(.orange)
}
} header: {
Text("iCloud Sync")
}
// MARK: - Developer Section
Section {
NavigationLink {
DuplicateCleanupView()
} label: {
Label("Clean Up Duplicates", systemImage: "wrench.and.screwdriver")
}
} header: {
Text("Developer")
}
// MARK: - About Section
Section {
IndieAbout(configuration: AppInfoConfiguration(
documents: [
.license(extension: "md")
],
changelogDocument: .changelog()
))
}
}
.navigationTitle("Settings")
.onChange(of: restSeconds) { _, _ in services.watchBridge.pushAll() }
.onChange(of: doneCountdownSeconds) { _, _ in services.watchBridge.pushAll() }
.onChange(of: weightUnit) { _, _ in services.watchBridge.pushAll() }
}
}
}