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:
Robin Kutesa
2026-06-03 12:27:45 +03:00
parent e5bd739499
commit 8b094691f5
23 changed files with 1272 additions and 115 deletions

View File

@@ -1039,11 +1039,19 @@ struct ProgramDetailSheet: View {
ForEach(prog.sections) { section in
Section {
if section.exercises.isEmpty {
Text("No exercises — tap + to add")
Text("No exercises — tap + or drag one here")
.font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs))
.frame(maxWidth: .infinity).padding(.vertical, 10)
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.dropDestination(for: String.self) { items, _ in
guard let s = items.first, let id = UUID(uuidString: s) else { return false }
withAnimation(KisaniSpring.snappy) {
vm.moveExercise(programId: programId, exerciseId: id,
toSection: section.id, before: nil)
}
return true
}
} else {
ForEach(section.exercises) { exercise in
HStack(spacing: 10) {
@@ -1065,6 +1073,15 @@ struct ProgramDetailSheet: View {
.padding(.vertical, 4)
.listRowBackground(AppColors.surface(cs))
.listRowSeparator(.hidden)
.draggable(exercise.id.uuidString)
.dropDestination(for: String.self) { items, _ in
guard let s = items.first, let id = UUID(uuidString: s) else { return false }
withAnimation(KisaniSpring.snappy) {
vm.moveExercise(programId: programId, exerciseId: id,
toSection: section.id, before: exercise.id)
}
return true
}
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
vm.deleteExercise(programId: programId, sectionId: section.id, exerciseId: exercise.id)
@@ -1093,6 +1110,14 @@ struct ProgramDetailSheet: View {
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.padding(.vertical, 2)
.dropDestination(for: String.self) { items, _ in
guard let s = items.first, let id = UUID(uuidString: s) else { return false }
withAnimation(KisaniSpring.snappy) {
vm.moveExercise(programId: programId, exerciseId: id,
toSection: section.id, before: nil)
}
return true
}
} header: {
HStack {