Queue document writes durably and surface sync trouble in the UI
iCloud Drive writes now flow through a persistent WriteBacklog sidecar (drained with backoff, flushed on backgrounding, wiped with the cache on account change), so a save can never be lost to a transient coordinator error. A status banner on the workout list surfaces stuck syncing. Also: the split picker gains a Recent section with day labels, split rows fold SplitItem into SplitListView, and list rows dim the multiply sign in sets-by-reps. Claude-Session: https://claude.ai/code/session_01HJDQQDA9QdP8zByg43H5v3
This commit is contained in:
@@ -461,7 +461,6 @@ private struct WorkoutLogRow: View {
|
||||
/// "3 × 12" (or "3 × 5 min" for timed exercises) with the numbers in full
|
||||
/// strength and the × dimmed, so the counts read at a glance.
|
||||
private var setsAndReps: Text {
|
||||
let times = Text(" × ").foregroundStyle(.secondary)
|
||||
let count: String
|
||||
if loadType == .duration {
|
||||
let mins = log.durationSeconds / 60
|
||||
@@ -476,7 +475,7 @@ private struct WorkoutLogRow: View {
|
||||
} else {
|
||||
count = "\(log.reps)"
|
||||
}
|
||||
return Text("\(log.sets)") + times + Text(count)
|
||||
return Text("\(log.sets)\(Text(" × ").foregroundStyle(.secondary))\(count)")
|
||||
}
|
||||
|
||||
private var showsWeight: Bool { loadType == .weight }
|
||||
|
||||
@@ -61,6 +61,11 @@ struct WorkoutLogsView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Workout Logs")
|
||||
// Write-queue banner tiers (calm "pending" capsule / prominent fault).
|
||||
// On the root screen so a stalled save is visible where editing happens.
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
SyncStatusBanner()
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
@@ -152,24 +157,70 @@ struct SplitPickerSheet: View {
|
||||
workouts.filter { $0.status == .inProgress || $0.status == .notStarted }
|
||||
}
|
||||
|
||||
/// The splits most recently trained, newest first, each with the start date
|
||||
/// of its latest workout — derived from the workout log (`workouts` is
|
||||
/// already sorted by start descending). Each workout's `splitID` follows the
|
||||
/// seed clone-on-edit redirect so a workout started from a since-forked seed
|
||||
/// still credits the live clone. Deduped, capped, and limited to splits that
|
||||
/// still exist.
|
||||
private var recentSplits: [(split: Split, lastStart: Date)] {
|
||||
var seen = Set<String>()
|
||||
var recents: [(split: Split, lastStart: Date)] = []
|
||||
for workout in workouts {
|
||||
guard let splitID = workout.splitID else { continue }
|
||||
let liveID = sync.currentSplitID(for: splitID)
|
||||
guard !seen.contains(liveID) else { continue }
|
||||
seen.insert(liveID)
|
||||
if let split = splits.first(where: { $0.id == liveID }) {
|
||||
recents.append((split, workout.start))
|
||||
if recents.count == 3 { break }
|
||||
}
|
||||
}
|
||||
return recents
|
||||
}
|
||||
|
||||
/// One selectable split row, shared by the Recent and All Splits sections.
|
||||
/// Recent rows pass `lastTrained` to show how long ago the split was run.
|
||||
private func splitRow(_ split: Split, lastTrained: Date? = nil) -> some View {
|
||||
Button {
|
||||
confirmAndStart(with: split)
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: split.systemImage)
|
||||
.foregroundColor(Color.color(from: split.color))
|
||||
.frame(width: 28)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(split.name)
|
||||
if let lastTrained {
|
||||
Text(lastTrained.daysAgoLabel())
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Text("\(split.exercisesArray.count) exercises")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
ForEach(splits) { split in
|
||||
Button {
|
||||
confirmAndStart(with: split)
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: split.systemImage)
|
||||
.foregroundColor(Color.color(from: split.color))
|
||||
Text(split.name)
|
||||
Spacer()
|
||||
Text("\(split.exercisesArray.count) exercises")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
if !recentSplits.isEmpty {
|
||||
Section("Recent") {
|
||||
ForEach(recentSplits, id: \.split.id) { recent in
|
||||
splitRow(recent.split, lastTrained: recent.lastStart)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(recentSplits.isEmpty ? "" : "All Splits") {
|
||||
ForEach(splits) { split in
|
||||
splitRow(split)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Select a Split")
|
||||
.toolbar {
|
||||
|
||||
Reference in New Issue
Block a user