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>
115 lines
4.0 KiB
Swift
115 lines
4.0 KiB
Swift
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()
|
||
}
|
||
}
|