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:
kutesir
2026-07-08 13:48:26 +03:00
committed by kutesir
parent 76366a0065
commit 65c93769c5
2 changed files with 100 additions and 18 deletions

View File

@@ -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. the undo is the button itself, always one tap away, no separate menu needed.
Files: `WorkoutView.swift`, `Components/SharedComponents.swift`. 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`.

View File

@@ -67,6 +67,7 @@ struct WorkoutView: View {
@State private var showAnalytics = false @State private var showAnalytics = false
@State private var isReordering = false @State private var isReordering = false
@State private var trainAnyway = false // override the rest-day state for today @State private var trainAnyway = false // override the rest-day state for today
@State private var showClearAllConfirm = false
@ObservedObject private var tm = TutorialManager.shared @ObservedObject private var tm = TutorialManager.shared
private let restTick = Timer.publish(every: 1, on: .main, in: .common).autoconnect() 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: "chart.bar.xaxis") { showStats = true }
IButton(icon: "clock.arrow.circlepath") { showHistory = true } IButton(icon: "clock.arrow.circlepath") { showHistory = true }
Menu { Menu {
Button { withAnimation(KisaniSpring.snappy) { vm.setAllSetsDone(false) } } label: { Button(role: .destructive) { showClearAllConfirm = true } label: {
Label("Clear All Sets", systemImage: "circle") Label("Clear All Sets", systemImage: "circle")
} }
Divider() Divider()
@@ -347,6 +348,18 @@ struct WorkoutView: View {
.presentationDetents([.large]) .presentationDetents([.large])
.presentationDragIndicator(.visible) .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) { .sheet(isPresented: $showAddSection) {
AddSectionSheet().environmentObject(vm) AddSectionSheet().environmentObject(vm)
.presentationDetents([.height(240)]) .presentationDetents([.height(240)])
@@ -593,7 +606,23 @@ struct DotProgressCard: View {
let onNotCompleted: () -> Void let onNotCompleted: () -> Void
let onUndo: () -> 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 { var body: some View {
VStack(spacing: 12) { VStack(spacing: 12) {
@@ -625,16 +654,51 @@ struct DotProgressCard: View {
.padding(14) .padding(14)
.cardStyle(borderColor: isDoneToday ? AppColors.green.opacity(0.3) : nil) .cardStyle(borderColor: isDoneToday ? AppColors.green.opacity(0.3) : nil)
.confirmationDialog( .confirmationDialog(
"Finish with \(done) of \(total) sets logged?", confirmTitle,
isPresented: $showFinishConfirm, isPresented: Binding(get: { pendingAction != nil }, set: { if !$0 { pendingAction = nil } }),
titleVisibility: .visible titleVisibility: .visible,
) { presenting: pendingAction
Button("Finish Anyway") { onFinish() } ) { action in
Button(action.confirmLabel, role: action.isDestructive ? .destructive : nil) {
perform(action)
}
Button("Cancel", role: .cancel) {} Button("Cancel", role: .cancel) {}
} message: { } message: { action in
Text(total > 0 Text(confirmMessage(action))
? "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.") }
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 { @ViewBuilder private var statusBadge: some View {
if isDoneToday { if isDoneToday {
Image(systemName: "checkmark.seal.fill") Image(systemName: "checkmark.circle.fill")
.font(.system(size: 18)).foregroundColor(AppColors.green) .font(.system(size: 18)).foregroundColor(AppColors.green)
} else if isMissedToday { } else if isMissedToday {
Image(systemName: "moon.zzz.fill") 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). /// 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 /// 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 { private var actionRow: some View {
VStack(spacing: 8) { VStack(spacing: 8) {
Button { Button {
if done == total && total > 0 { onFinish() } else { showFinishConfirm = true } if done == total && total > 0 { onFinish() } else { pendingAction = .finish }
} label: { } label: {
Label(done == total && total > 0 ? "Finish Workout" : "Finish Anyway", Label(done == total && total > 0 ? "Finish Workout" : "Finish Anyway",
systemImage: "checkmark.seal.fill") systemImage: "checkmark.circle.fill")
.font(AppFonts.sans(13, weight: .semibold)) .font(AppFonts.sans(13, weight: .semibold))
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, 11) .padding(.vertical, 11)
@@ -684,7 +749,7 @@ struct DotProgressCard: View {
HStack(spacing: 8) { HStack(spacing: 8) {
if done == total && total > 0 { if done == total && total > 0 {
Button(action: onClearAllSets) { Button { pendingAction = .clearAll } label: {
Label("Clear all sets", systemImage: "circle") Label("Clear all sets", systemImage: "circle")
.font(AppFonts.sans(11, weight: .medium)) .font(AppFonts.sans(11, weight: .medium))
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@@ -695,7 +760,7 @@ struct DotProgressCard: View {
.clipShape(RoundedRectangle(cornerRadius: 9)) .clipShape(RoundedRectangle(cornerRadius: 9))
.buttonStyle(PressButtonStyle(scale: 0.97)) .buttonStyle(PressButtonStyle(scale: 0.97))
} else { } else {
Button(action: onMarkAllSets) { Button { pendingAction = .checkAll } label: {
Label("Check all sets", systemImage: "checkmark.circle") Label("Check all sets", systemImage: "checkmark.circle")
.font(AppFonts.sans(11, weight: .medium)) .font(AppFonts.sans(11, weight: .medium))
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@@ -707,7 +772,7 @@ struct DotProgressCard: View {
.buttonStyle(PressButtonStyle(scale: 0.97)) .buttonStyle(PressButtonStyle(scale: 0.97))
} }
Button(action: onNotCompleted) { Button { pendingAction = .notCompleted } label: {
Label("Didn't train", systemImage: "moon.zzz") Label("Didn't train", systemImage: "moon.zzz")
.font(AppFonts.sans(11, weight: .medium)) .font(AppFonts.sans(11, weight: .medium))
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)