Merge develop -> main: full session (KC-51 through KC-71) #28
@@ -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`.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user