Merge develop -> main: full session (KC-51 through KC-71) #28

Merged
kutesir merged 64 commits from develop into main 2026-07-12 22:57:14 +00:00
2 changed files with 100 additions and 18 deletions
Showing only changes of commit 5b3d4e5f9e - Show all commits

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.
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 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)