2026-06-06 19:29:25 +03:00
|
|
|
import ActivityKit
|
|
|
|
|
import WidgetKit
|
|
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
|
|
@available(iOS 16.1, *)
|
|
|
|
|
struct TaskLiveActivity: Widget {
|
2026-06-11 19:33:01 +03:00
|
|
|
private let accent = WidgetTheme.accent // brand orange
|
2026-06-06 19:29:25 +03:00
|
|
|
|
|
|
|
|
var body: some WidgetConfiguration {
|
|
|
|
|
ActivityConfiguration(for: TaskActivityAttributes.self) { context in
|
|
|
|
|
// ── Lock screen / banner ──
|
|
|
|
|
HStack(spacing: 12) {
|
2026-07-09 13:33:32 +03:00
|
|
|
Image(systemName: context.state.isComplete ? "checkmark.circle.fill" : "hourglass")
|
|
|
|
|
.font(.system(size: 20))
|
2026-06-06 19:29:25 +03:00
|
|
|
.foregroundColor(context.state.isComplete ? .green : accent)
|
2026-06-15 12:40:39 +03:00
|
|
|
.frame(width: 24)
|
2026-06-06 19:29:25 +03:00
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
|
|
|
Text(context.attributes.title)
|
2026-06-15 12:40:39 +03:00
|
|
|
.font(.system(.body, design: .monospaced))
|
2026-06-06 19:29:25 +03:00
|
|
|
.foregroundColor(.white)
|
|
|
|
|
.lineLimit(1)
|
2026-06-15 12:40:39 +03:00
|
|
|
.minimumScaleFactor(0.8)
|
2026-06-06 19:29:25 +03:00
|
|
|
.strikethrough(context.state.isComplete)
|
|
|
|
|
if let due = context.state.dueDate {
|
|
|
|
|
Text(due, style: context.state.isComplete ? .date : .relative)
|
2026-06-15 12:40:39 +03:00
|
|
|
.font(.system(.caption, design: .monospaced))
|
2026-06-06 19:29:25 +03:00
|
|
|
.foregroundColor(.white.opacity(0.6))
|
2026-06-15 12:40:39 +03:00
|
|
|
.lineLimit(1)
|
2026-06-06 19:29:25 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-06-15 12:40:39 +03:00
|
|
|
.layoutPriority(1)
|
|
|
|
|
|
2026-06-06 19:29:25 +03:00
|
|
|
if let due = context.state.dueDate, !context.state.isComplete {
|
|
|
|
|
Text(due, style: .timer)
|
2026-06-15 12:40:39 +03:00
|
|
|
.font(.system(.callout, design: .monospaced))
|
|
|
|
|
.monospacedDigit()
|
2026-06-06 19:29:25 +03:00
|
|
|
.foregroundColor(accent)
|
2026-06-15 12:40:39 +03:00
|
|
|
.lineLimit(1)
|
|
|
|
|
.minimumScaleFactor(0.72)
|
|
|
|
|
.frame(width: 92, alignment: .trailing)
|
|
|
|
|
.fixedSize(horizontal: false, vertical: true)
|
2026-06-06 19:29:25 +03:00
|
|
|
}
|
2026-07-09 13:33:32 +03:00
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
2026-06-06 19:29:25 +03:00
|
|
|
}
|
2026-06-15 12:40:39 +03:00
|
|
|
.padding(.leading, 14)
|
|
|
|
|
.padding(.trailing, 10)
|
2026-06-06 19:29:25 +03:00
|
|
|
.padding(.vertical, 12)
|
2026-06-11 19:33:01 +03:00
|
|
|
.activityBackgroundTint(WidgetTheme.background.opacity(0.9))
|
2026-06-06 19:29:25 +03:00
|
|
|
.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)
|
2026-06-15 12:40:39 +03:00
|
|
|
.font(.system(.callout, design: .monospaced))
|
|
|
|
|
.monospacedDigit()
|
2026-06-06 19:29:25 +03:00
|
|
|
.foregroundColor(accent)
|
2026-06-15 12:40:39 +03:00
|
|
|
.lineLimit(1)
|
|
|
|
|
.minimumScaleFactor(0.75)
|
|
|
|
|
.frame(width: 86, alignment: .trailing)
|
2026-06-06 19:29:25 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
DynamicIslandExpandedRegion(.center) {
|
|
|
|
|
Text(context.attributes.title)
|
2026-06-15 12:40:39 +03:00
|
|
|
.font(.system(.callout, design: .monospaced))
|
2026-06-06 19:29:25 +03:00
|
|
|
.lineLimit(1)
|
|
|
|
|
}
|
2026-07-09 13:33:32 +03:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-06 19:29:25 +03:00
|
|
|
} 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))
|
2026-06-15 12:40:39 +03:00
|
|
|
.monospacedDigit()
|
2026-06-06 19:29:25 +03:00
|
|
|
.foregroundColor(accent)
|
2026-06-15 12:40:39 +03:00
|
|
|
.lineLimit(1)
|
|
|
|
|
.minimumScaleFactor(0.7)
|
|
|
|
|
.frame(width: 48, alignment: .trailing)
|
2026-06-06 19:29:25 +03:00
|
|
|
}
|
|
|
|
|
} minimal: {
|
|
|
|
|
Image(systemName: "checklist").foregroundColor(accent)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|