workout: drop seal icon, confirm every destructive quick action (KC-52 follow-up 2)
checkmark.seal.fill (unused elsewhere, reads as a generic 'AI verified badge') replaced with checkmark.circle.fill, the app's actual convention. All four state-changing card actions (Finish Anyway, Check All Sets, Clear All Sets, Didn't Train) now confirm via one PendingAction enum + confirmationDialog before executing, each with a tailored message; destructive ones use the .destructive role. The header menu's Clear All Sets (any completion level) also now confirms instead of executing instantly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1722,3 +1722,20 @@ progress, and there was no quick way to undo an accidental "Check all sets."
|
||||
the undo is the button itself, always one tap away, no separate menu needed.
|
||||
|
||||
Files: `WorkoutView.swift`, `Components/SharedComponents.swift`.
|
||||
|
||||
### KC-52 — follow-up 2: drop the "seal" icon, confirm every destructive action
|
||||
User: "whats with the ai tick also add clear all sets with a prompt to confirm
|
||||
on all these to avoid accidentals."
|
||||
|
||||
- `checkmark.seal.fill` (a generic "verified badge" glyph, unused anywhere else
|
||||
in the app) replaced with `checkmark.circle.fill` — the codebase's actual
|
||||
convention (TodayView, TaskContextMenu, etc.).
|
||||
- All four state-changing quick actions on the card now confirm first via a
|
||||
single `PendingAction` enum + one `confirmationDialog` (Finish Anyway,
|
||||
Check All Sets, Clear All Sets, Didn't Train) — each with its own message;
|
||||
Clear All / Didn't Train use `.destructive` role. "Finish Workout" at 100%
|
||||
still skips the dialog (nothing to override).
|
||||
- The header's "…" menu **Clear All Sets** (works at any completion level, not
|
||||
just 100%) now also confirms before executing — previously instant/silent.
|
||||
|
||||
Files: `WorkoutView.swift`.
|
||||
|
||||
@@ -67,6 +67,7 @@ struct WorkoutView: View {
|
||||
@State private var showAnalytics = false
|
||||
@State private var isReordering = false
|
||||
@State private var trainAnyway = false // override the rest-day state for today
|
||||
@State private var showClearAllConfirm = false
|
||||
@ObservedObject private var tm = TutorialManager.shared
|
||||
private let restTick = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||
|
||||
@@ -108,7 +109,7 @@ struct WorkoutView: View {
|
||||
IButton(icon: "chart.bar.xaxis") { showStats = true }
|
||||
IButton(icon: "clock.arrow.circlepath") { showHistory = true }
|
||||
Menu {
|
||||
Button { withAnimation(KisaniSpring.snappy) { vm.setAllSetsDone(false) } } label: {
|
||||
Button(role: .destructive) { showClearAllConfirm = true } label: {
|
||||
Label("Clear All Sets", systemImage: "circle")
|
||||
}
|
||||
Divider()
|
||||
@@ -347,6 +348,18 @@ struct WorkoutView: View {
|
||||
.presentationDetents([.large])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
.confirmationDialog(
|
||||
"Clear all logged sets?",
|
||||
isPresented: $showClearAllConfirm,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button("Clear All", role: .destructive) {
|
||||
withAnimation(KisaniSpring.snappy) { vm.setAllSetsDone(false) }
|
||||
}
|
||||
Button("Cancel", role: .cancel) {}
|
||||
} message: {
|
||||
Text("This un-checks every set in today's workout. It won't undo an already-finished workout.")
|
||||
}
|
||||
.sheet(isPresented: $showAddSection) {
|
||||
AddSectionSheet().environmentObject(vm)
|
||||
.presentationDetents([.height(240)])
|
||||
@@ -593,7 +606,23 @@ struct DotProgressCard: View {
|
||||
let onNotCompleted: () -> Void
|
||||
let onUndo: () -> Void
|
||||
|
||||
@State private var showFinishConfirm = false
|
||||
/// Every quick action that changes today's state is confirmed first — a
|
||||
/// stray tap here shouldn't silently rewrite the day.
|
||||
private enum PendingAction: Identifiable {
|
||||
case finish, checkAll, clearAll, notCompleted
|
||||
var id: Self { self }
|
||||
|
||||
var confirmLabel: String {
|
||||
switch self {
|
||||
case .finish: return "Finish Anyway"
|
||||
case .checkAll: return "Check All"
|
||||
case .clearAll: return "Clear All"
|
||||
case .notCompleted: return "Mark Not Completed"
|
||||
}
|
||||
}
|
||||
var isDestructive: Bool { self == .clearAll || self == .notCompleted }
|
||||
}
|
||||
@State private var pendingAction: PendingAction?
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 12) {
|
||||
@@ -625,16 +654,51 @@ struct DotProgressCard: View {
|
||||
.padding(14)
|
||||
.cardStyle(borderColor: isDoneToday ? AppColors.green.opacity(0.3) : nil)
|
||||
.confirmationDialog(
|
||||
"Finish with \(done) of \(total) sets logged?",
|
||||
isPresented: $showFinishConfirm,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button("Finish Anyway") { onFinish() }
|
||||
confirmTitle,
|
||||
isPresented: Binding(get: { pendingAction != nil }, set: { if !$0 { pendingAction = nil } }),
|
||||
titleVisibility: .visible,
|
||||
presenting: pendingAction
|
||||
) { action in
|
||||
Button(action.confirmLabel, role: action.isDestructive ? .destructive : nil) {
|
||||
perform(action)
|
||||
}
|
||||
Button("Cancel", role: .cancel) {}
|
||||
} message: {
|
||||
Text(total > 0
|
||||
? "You've logged \(Int(progress * 100))% of sets. This still counts as today's workout."
|
||||
: "No sets logged yet. This still counts as today's workout.")
|
||||
} message: { action in
|
||||
Text(confirmMessage(action))
|
||||
}
|
||||
}
|
||||
|
||||
private var confirmTitle: String {
|
||||
switch pendingAction {
|
||||
case .finish: return "Finish with \(done) of \(total) sets logged?"
|
||||
case .checkAll: return "Check every set as done?"
|
||||
case .clearAll: return "Clear all logged sets?"
|
||||
case .notCompleted: return "Mark today as not completed?"
|
||||
case nil: return ""
|
||||
}
|
||||
}
|
||||
|
||||
private func confirmMessage(_ action: PendingAction) -> String {
|
||||
switch action {
|
||||
case .finish:
|
||||
return total > 0
|
||||
? "You've logged \(Int(progress * 100))% of sets. This still counts as today's workout."
|
||||
: "No sets logged yet. This still counts as today's workout."
|
||||
case .checkAll:
|
||||
return "This ticks all \(total) sets as done — useful if you trained but didn't log along the way."
|
||||
case .clearAll:
|
||||
return "This un-checks all \(total) sets. It won't undo today's logged workout."
|
||||
case .notCompleted:
|
||||
return isDoneToday ? "This replaces today's logged workout with \"not completed.\"" : "You can undo this afterward."
|
||||
}
|
||||
}
|
||||
|
||||
private func perform(_ action: PendingAction) {
|
||||
switch action {
|
||||
case .finish: onFinish()
|
||||
case .checkAll: onMarkAllSets()
|
||||
case .clearAll: onClearAllSets()
|
||||
case .notCompleted: onNotCompleted()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -654,7 +718,7 @@ struct DotProgressCard: View {
|
||||
|
||||
@ViewBuilder private var statusBadge: some View {
|
||||
if isDoneToday {
|
||||
Image(systemName: "checkmark.seal.fill")
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 18)).foregroundColor(AppColors.green)
|
||||
} else if isMissedToday {
|
||||
Image(systemName: "moon.zzz.fill")
|
||||
@@ -668,14 +732,15 @@ struct DotProgressCard: View {
|
||||
|
||||
/// Primary: finish the workout as-is (honest capture, not gated on 100% sets).
|
||||
/// Secondary: the mechanical "tick every box" action (toggles to a clear once
|
||||
/// all are checked, so a mis-tap is always one tap to undo), and the miss.
|
||||
/// all are checked), and the explicit miss. Every action here that changes
|
||||
/// today's state is confirmed first — see `pendingAction`.
|
||||
private var actionRow: some View {
|
||||
VStack(spacing: 8) {
|
||||
Button {
|
||||
if done == total && total > 0 { onFinish() } else { showFinishConfirm = true }
|
||||
if done == total && total > 0 { onFinish() } else { pendingAction = .finish }
|
||||
} label: {
|
||||
Label(done == total && total > 0 ? "Finish Workout" : "Finish Anyway",
|
||||
systemImage: "checkmark.seal.fill")
|
||||
systemImage: "checkmark.circle.fill")
|
||||
.font(AppFonts.sans(13, weight: .semibold))
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 11)
|
||||
@@ -684,7 +749,7 @@ struct DotProgressCard: View {
|
||||
|
||||
HStack(spacing: 8) {
|
||||
if done == total && total > 0 {
|
||||
Button(action: onClearAllSets) {
|
||||
Button { pendingAction = .clearAll } label: {
|
||||
Label("Clear all sets", systemImage: "circle")
|
||||
.font(AppFonts.sans(11, weight: .medium))
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -695,7 +760,7 @@ struct DotProgressCard: View {
|
||||
.clipShape(RoundedRectangle(cornerRadius: 9))
|
||||
.buttonStyle(PressButtonStyle(scale: 0.97))
|
||||
} else {
|
||||
Button(action: onMarkAllSets) {
|
||||
Button { pendingAction = .checkAll } label: {
|
||||
Label("Check all sets", systemImage: "checkmark.circle")
|
||||
.font(AppFonts.sans(11, weight: .medium))
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -707,7 +772,7 @@ struct DotProgressCard: View {
|
||||
.buttonStyle(PressButtonStyle(scale: 0.97))
|
||||
}
|
||||
|
||||
Button(action: onNotCompleted) {
|
||||
Button { pendingAction = .notCompleted } label: {
|
||||
Label("Didn't train", systemImage: "moon.zzz")
|
||||
.font(AppFonts.sans(11, weight: .medium))
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
Reference in New Issue
Block a user