feat: home/lock widgets, Health-aware streaks, workout editor, and entitlement fixes
Widgets (new KisaniCalWidgets extension) - Event Countdown widget: configurable via AppIntent, pick from your events or set a custom name/date; dot-grid progress toward the event. - Day Progress and Tasks-Done-Today widgets; lock screen widgets. - Shared dot grid sizes circles to fill the widget evenly at any count. - Tasks/calendar prefs now persist to the App Group so widgets read live data. Health & streaks - Persist HealthKit authorization and re-establish on launch (fixes the Today dashboard and streak sync disappearing after relaunch). - Add a Health permission toggle to onboarding; unify Settings/Profile on the manager's state. - Day streak counts from a logged Health workout OR marking all exercises complete; nudge to mark exercises complete even when Health logged the workout. Workout editor - Drag to reorder exercises and move them across sections (drag-and-drop); swipe to delete. - Add-exercise sheet: global search and keep-adding-multiple with a running count. Fixes - Restore app entitlements link (Sign in with Apple, HealthKit, App Group, iCloud). - Add HealthKit usage strings; widget Info.plist NSExtension + nested bundle id. - Calendar "Connect" routes to Settings when access was denied. - Bake DEVELOPMENT_TEAM and shared versions into project.yml. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,8 @@ struct ExercisePickerSheet: View {
|
||||
@State private var setsText = "3"
|
||||
@State private var repsText = "10"
|
||||
@State private var showCustom = false
|
||||
@State private var addedCount = 0
|
||||
@State private var flashedId: UUID? = nil
|
||||
@FocusState private var customFocused: Bool
|
||||
|
||||
private func addExercise(name: String) {
|
||||
@@ -25,27 +27,48 @@ struct ExercisePickerSheet: View {
|
||||
} else {
|
||||
vm.addExercise(to: sectionId, name: name, setsCount: sets, repsCount: reps)
|
||||
}
|
||||
addedCount += 1
|
||||
}
|
||||
|
||||
// Briefly mark a template row as "added" so the user gets feedback while keeping the sheet open.
|
||||
private func flash(_ id: UUID) {
|
||||
withAnimation(KisaniSpring.micro) { flashedId = id }
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
|
||||
if flashedId == id { withAnimation(KisaniSpring.micro) { flashedId = nil } }
|
||||
}
|
||||
}
|
||||
|
||||
private var filtered: [ExerciseTemplate] {
|
||||
let byMuscle = exerciseLibrary.filter { $0.muscle == selectedMuscle }
|
||||
guard !searchText.isEmpty else { return byMuscle }
|
||||
return byMuscle.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
|
||||
// When searching, look across the whole library (ignore the muscle tab).
|
||||
guard searchText.isEmpty else {
|
||||
return exerciseLibrary.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
|
||||
}
|
||||
return exerciseLibrary.filter { $0.muscle == selectedMuscle }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
// ── Header ──
|
||||
HStack {
|
||||
HStack(spacing: 8) {
|
||||
Text("Add Exercise")
|
||||
.font(AppFonts.sans(17, weight: .bold))
|
||||
.foregroundColor(AppColors.text(cs))
|
||||
if addedCount > 0 {
|
||||
Text("\(addedCount) added")
|
||||
.font(AppFonts.mono(9.5, weight: .bold))
|
||||
.foregroundColor(AppColors.green)
|
||||
.padding(.horizontal, 7).padding(.vertical, 3)
|
||||
.background(AppColors.greenSoft)
|
||||
.clipShape(Capsule())
|
||||
.transition(.scale.combined(with: .opacity))
|
||||
}
|
||||
Spacer()
|
||||
Button("Cancel") { dismiss() }
|
||||
.font(AppFonts.sans(13))
|
||||
.foregroundColor(AppColors.text3(cs))
|
||||
Button(addedCount > 0 ? "Done" : "Cancel") { dismiss() }
|
||||
.font(AppFonts.sans(13, weight: addedCount > 0 ? .bold : .regular))
|
||||
.foregroundColor(addedCount > 0 ? AppColors.accent : AppColors.text3(cs))
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.animation(KisaniSpring.snappy, value: addedCount)
|
||||
.padding(.horizontal, 20).padding(.top, 20).padding(.bottom, 12)
|
||||
|
||||
Divider().background(AppColors.border(cs))
|
||||
@@ -114,7 +137,8 @@ struct ExercisePickerSheet: View {
|
||||
let n = customName.trimmingCharacters(in: .whitespaces)
|
||||
guard !n.isEmpty else { return }
|
||||
addExercise(name: n)
|
||||
dismiss()
|
||||
customName = ""
|
||||
customFocused = true
|
||||
} label: {
|
||||
Text("Add Custom Exercise")
|
||||
.font(AppFonts.sans(13, weight: .bold)).foregroundColor(.white)
|
||||
@@ -134,9 +158,9 @@ struct ExercisePickerSheet: View {
|
||||
|
||||
// Library exercises
|
||||
ForEach(filtered) { template in
|
||||
ExerciseTemplateRow(template: template) {
|
||||
ExerciseTemplateRow(template: template, added: flashedId == template.id) {
|
||||
addExercise(name: template.name)
|
||||
dismiss()
|
||||
flash(template.id)
|
||||
}
|
||||
Divider().background(AppColors.border(cs)).padding(.leading, 60)
|
||||
}
|
||||
@@ -188,6 +212,7 @@ struct ExercisePickerSheet: View {
|
||||
private struct ExerciseTemplateRow: View {
|
||||
@Environment(\.colorScheme) private var cs
|
||||
let template: ExerciseTemplate
|
||||
var added: Bool = false
|
||||
let onAdd: () -> Void
|
||||
|
||||
var body: some View {
|
||||
@@ -206,9 +231,10 @@ private struct ExerciseTemplateRow: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "plus.circle.fill")
|
||||
Image(systemName: added ? "checkmark.circle.fill" : "plus.circle.fill")
|
||||
.font(.system(size: 18))
|
||||
.foregroundColor(AppColors.accent.opacity(0.7))
|
||||
.foregroundColor(added ? AppColors.green : AppColors.accent.opacity(0.7))
|
||||
.scaleEffect(added ? 1.15 : 1)
|
||||
}
|
||||
.padding(.vertical, 10)
|
||||
.contentShape(Rectangle())
|
||||
|
||||
Reference in New Issue
Block a user