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:
114
KisaniCalWidgets/KisaniCalWidgets.swift
Normal file
114
KisaniCalWidgets/KisaniCalWidgets.swift
Normal file
@@ -0,0 +1,114 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user