Files
KisaniCal/KisaniCalWidgets/TaskLiveActivity.swift
kutesir 9723775cb6
Some checks failed
CI / build-and-test (push) Has been cancelled
onboarding+liveactivity+habits: 3 fixes from device feedback (KC-61)
1. Onboarding Habits caption now explicitly mentions the toggles are
   changeable in Settings, not just times/custom reminder.

2. TaskLiveActivity: removed the checkbox-look circle glyph (hourglass
   instead). iOS has no API for an app to block the system's swipe/dismiss
   gesture on a Live Activity -- not attempted. Added a real Stop control via
   a new StopCountdownIntent (LiveActivityIntent, iOS 17+) embedded directly
   in the card (lock screen '×' + Dynamic Island 'Stop Countdown' button) --
   the actual iOS-supported equivalent of 'long press for options', since
   Live Activities don't support notification-style long-press menus.

3. Habit notifications gain 'Not Today' (dismiss only, intentional no-op --
   the lifetime-tally philosophy has no concept of a logged miss) and 'Stop
   Tracking' (destructive-styled; flips the habit's master toggle off and
   cancels every pending notification for that type across all its slots).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 13:33:32 +03:00

112 lines
5.0 KiB
Swift

import ActivityKit
import WidgetKit
import SwiftUI
@available(iOS 16.1, *)
struct TaskLiveActivity: Widget {
private let accent = WidgetTheme.accent // brand orange
var body: some WidgetConfiguration {
ActivityConfiguration(for: TaskActivityAttributes.self) { context in
// Lock screen / banner
HStack(spacing: 12) {
Image(systemName: context.state.isComplete ? "checkmark.circle.fill" : "hourglass")
.font(.system(size: 20))
.foregroundColor(context.state.isComplete ? .green : accent)
.frame(width: 24)
VStack(alignment: .leading, spacing: 2) {
Text(context.attributes.title)
.font(.system(.body, design: .monospaced))
.foregroundColor(.white)
.lineLimit(1)
.minimumScaleFactor(0.8)
.strikethrough(context.state.isComplete)
if let due = context.state.dueDate {
Text(due, style: context.state.isComplete ? .date : .relative)
.font(.system(.caption, design: .monospaced))
.foregroundColor(.white.opacity(0.6))
.lineLimit(1)
}
}
.layoutPriority(1)
if let due = context.state.dueDate, !context.state.isComplete {
Text(due, style: .timer)
.font(.system(.callout, design: .monospaced))
.monospacedDigit()
.foregroundColor(accent)
.lineLimit(1)
.minimumScaleFactor(0.72)
.frame(width: 92, alignment: .trailing)
.fixedSize(horizontal: false, vertical: true)
}
// Explicit stop control a Live Activity can't be prevented
// from being swiped away by the system, so this gives an
// intentional, in-card way to end tracking instead.
if #available(iOS 17.0, *), !context.state.isComplete {
Button(intent: StopCountdownIntent(taskID: context.attributes.taskID)) {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 18))
.foregroundColor(.white.opacity(0.5))
}
.buttonStyle(.plain)
}
}
.padding(.leading, 14)
.padding(.trailing, 10)
.padding(.vertical, 12)
.activityBackgroundTint(WidgetTheme.background.opacity(0.9))
.activitySystemActionForegroundColor(.white)
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
Image(systemName: "checklist")
.foregroundColor(accent)
}
DynamicIslandExpandedRegion(.trailing) {
if let due = context.state.dueDate, !context.state.isComplete {
Text(due, style: .timer)
.font(.system(.callout, design: .monospaced))
.monospacedDigit()
.foregroundColor(accent)
.lineLimit(1)
.minimumScaleFactor(0.75)
.frame(width: 86, alignment: .trailing)
}
}
DynamicIslandExpandedRegion(.center) {
Text(context.attributes.title)
.font(.system(.callout, design: .monospaced))
.lineLimit(1)
}
DynamicIslandExpandedRegion(.bottom) {
if #available(iOS 17.0, *), !context.state.isComplete {
Button(intent: StopCountdownIntent(taskID: context.attributes.taskID)) {
Label("Stop Countdown", systemImage: "xmark.circle")
.font(.system(.caption, design: .monospaced))
}
.buttonStyle(.plain)
.tint(accent)
}
}
} compactLeading: {
Image(systemName: "checklist").foregroundColor(accent)
} compactTrailing: {
if let due = context.state.dueDate, !context.state.isComplete {
Text(due, style: .timer)
.font(.system(.caption2, design: .monospaced))
.monospacedDigit()
.foregroundColor(accent)
.lineLimit(1)
.minimumScaleFactor(0.7)
.frame(width: 48, alignment: .trailing)
}
} minimal: {
Image(systemName: "checklist").foregroundColor(accent)
}
}
}
}