Files
KisaniCal/KisaniCalWidgets/KisaniCalWidgets.swift
Robin Kutesa 8b094691f5 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>
2026-06-03 12:27:45 +03:00

115 lines
4.0 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import WidgetKit
import SwiftUI
import AppIntents
// MARK: - Shared Entry
struct KisaniEntry: TimelineEntry {
let date: Date
let task: WidgetTask?
let allTasks: [WidgetTask]
let streak: Int
}
// MARK: - Shared Provider
struct KisaniProvider: TimelineProvider {
func placeholder(in context: Context) -> KisaniEntry {
let sample = WidgetTask(id: UUID(), title: "Mums Birthday", dueDate: Date().addingTimeInterval(86400 * 60),
isComplete: false, quadrant: "Urgent + Important", category: "birthday")
return KisaniEntry(date: .now, task: sample, allTasks: [sample], streak: 7)
}
func getSnapshot(in context: Context, completion: @escaping (KisaniEntry) -> Void) {
let tasks = loadWidgetTasks()
completion(KisaniEntry(date: .now, task: nextDueTask(from: tasks), allTasks: tasks, streak: loadWorkoutStreak()))
}
func getTimeline(in context: Context, completion: @escaping (Timeline<KisaniEntry>) -> Void) {
let tasks = loadWidgetTasks()
let entry = KisaniEntry(date: .now, task: nextDueTask(from: tasks), allTasks: tasks, streak: loadWorkoutStreak())
// Refresh every 30 minutes or when app triggers WidgetCenter.reloadAllTimelines()
let next = Calendar.current.date(byAdding: .minute, value: 30, to: .now)!
completion(Timeline(entries: [entry], policy: .after(next)))
}
}
// MARK: - Day Progress Widget ("Day done %")
struct DayProgressWidget: Widget {
let kind = "KisaniDayProgress"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: KisaniProvider()) { _ in
ProgressDotView(period: .day, accent: Color(red: 0.48, green: 0.87, blue: 0.80))
}
.configurationDisplayName("Day Progress")
.description("How much of today has elapsed, as a dot grid.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
// MARK: - Tasks Done Today Widget ("Tasks complete %")
struct TasksCompleteWidget: Widget {
let kind = "KisaniTasksComplete"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: KisaniProvider()) { _ in
TasksCompleteView()
}
.configurationDisplayName("Tasks Done Today")
.description("Percentage of today's tasks you've completed.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
// MARK: - Lock Screen Widgets
struct LockScreenWidget: Widget {
let kind = "KisaniLockScreen"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: KisaniProvider()) { entry in
if let task = entry.task {
LockScreenRectView(task: task)
} else {
LockScreenRectView(task: WidgetTask(id: UUID(), title: "No tasks",
dueDate: nil, isComplete: false, quadrant: "Urgent + Important", category: "reminder"))
}
}
.configurationDisplayName("Task (Lock Screen)")
.description("Glass countdown on your lock screen.")
.supportedFamilies([.accessoryRectangular])
}
}
struct LockScreenCircularWidget: Widget {
let kind = "KisaniLockCircle"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: KisaniProvider()) { entry in
if let task = entry.task {
LockScreenCircularView(task: task)
} else {
LockScreenCircularView(task: WidgetTask(id: UUID(), title: "",
dueDate: nil, isComplete: false, quadrant: "Urgent + Important", category: "reminder"))
}
}
.configurationDisplayName("Days Left (Lock Screen)")
.description("Circular countdown on your lock screen.")
.supportedFamilies([.accessoryCircular])
}
}
// MARK: - Widget Bundle
@main
struct KisaniWidgetBundle: WidgetBundle {
var body: some Widget {
EventCountdownWidget()
DayProgressWidget()
TasksCompleteWidget()
LockScreenWidget()
LockScreenCircularWidget()
}
}