216 lines
6.5 KiB
Swift
216 lines
6.5 KiB
Swift
// ── Lock Screen Widget ────────────────────────────────────────
|
|
// Lock screen inline/circular/rectangular widgets for next timer
|
|
|
|
import SwiftUI
|
|
import WidgetKit
|
|
import AppIntents
|
|
|
|
// MARK: - Timeline Provider
|
|
|
|
struct LockScreenTimerProvider: AppIntentTimelineProvider {
|
|
typealias Entry = LockScreenTimerEntry
|
|
typealias Intent = LockScreenTimerIntent
|
|
|
|
func placeholder(in context: Context) -> LockScreenTimerEntry {
|
|
LockScreenTimerEntry(
|
|
date: Date(),
|
|
label: "Standup",
|
|
targetTime: Date().addingTimeInterval(3600),
|
|
urgency: .important,
|
|
hasTimer: true
|
|
)
|
|
}
|
|
|
|
func snapshot(for configuration: LockScreenTimerIntent, in context: Context) async -> LockScreenTimerEntry {
|
|
entryFromSharedData()
|
|
}
|
|
|
|
func timeline(for configuration: LockScreenTimerIntent, in context: Context) async -> Timeline<LockScreenTimerEntry> {
|
|
let entry = entryFromSharedData()
|
|
let now = Date()
|
|
|
|
var entries: [LockScreenTimerEntry] = []
|
|
for minuteOffset in stride(from: 0, through: 30, by: 5) {
|
|
let entryDate = Calendar.current.date(byAdding: .minute, value: minuteOffset, to: now)!
|
|
entries.append(LockScreenTimerEntry(
|
|
date: entryDate,
|
|
label: entry.label,
|
|
targetTime: entry.targetTime,
|
|
urgency: entry.urgency,
|
|
hasTimer: entry.hasTimer
|
|
))
|
|
}
|
|
|
|
let reloadDate: Date
|
|
if entry.hasTimer {
|
|
reloadDate = min(entry.targetTime, now.addingTimeInterval(30 * 60))
|
|
} else {
|
|
reloadDate = now.addingTimeInterval(30 * 60)
|
|
}
|
|
|
|
return Timeline(entries: entries, policy: .after(reloadDate))
|
|
}
|
|
|
|
private func entryFromSharedData() -> LockScreenTimerEntry {
|
|
if let timer = SharedTimerDataManager.shared.readNextFiringTimer() {
|
|
return LockScreenTimerEntry(
|
|
date: Date(),
|
|
label: timer.label,
|
|
targetTime: timer.targetTime,
|
|
urgency: timer.urgency,
|
|
hasTimer: true
|
|
)
|
|
}
|
|
return LockScreenTimerEntry(
|
|
date: Date(),
|
|
label: "",
|
|
targetTime: Date(),
|
|
urgency: .standard,
|
|
hasTimer: false
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Intent
|
|
|
|
struct LockScreenTimerIntent: WidgetConfigurationIntent {
|
|
static var title: LocalizedStringResource = "Lock Screen Timer"
|
|
static var description: IntentDescription = "Shows next timer countdown on lock screen"
|
|
}
|
|
|
|
// MARK: - Entry
|
|
|
|
struct LockScreenTimerEntry: TimelineEntry {
|
|
let date: Date
|
|
let label: String
|
|
let targetTime: Date
|
|
let urgency: UrgencyLevel
|
|
let hasTimer: Bool
|
|
}
|
|
|
|
// MARK: - Widget Views
|
|
|
|
struct LockScreenInlineView: View {
|
|
let entry: LockScreenTimerEntry
|
|
|
|
var body: some View {
|
|
if entry.hasTimer {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "clock.fill")
|
|
Text(entry.label)
|
|
Text(entry.targetTime, style: .timer)
|
|
}
|
|
} else {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "clock")
|
|
Text("No timers")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct LockScreenCircularView: View {
|
|
let entry: LockScreenTimerEntry
|
|
|
|
var body: some View {
|
|
if entry.hasTimer {
|
|
ZStack {
|
|
// Progress ring based on remaining time
|
|
AccessoryWidgetBackground()
|
|
VStack(spacing: 0) {
|
|
Text(entry.targetTime, style: .timer)
|
|
.font(.system(size: 14, weight: .bold, design: .monospaced))
|
|
.minimumScaleFactor(0.5)
|
|
.widgetAccentable()
|
|
}
|
|
}
|
|
} else {
|
|
ZStack {
|
|
AccessoryWidgetBackground()
|
|
Image(systemName: "clock")
|
|
.font(.system(size: 20))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct LockScreenRectangularView: View {
|
|
let entry: LockScreenTimerEntry
|
|
|
|
var body: some View {
|
|
if entry.hasTimer {
|
|
HStack(spacing: 8) {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(entry.label)
|
|
.font(.system(size: 14, weight: .semibold))
|
|
.lineLimit(1)
|
|
.widgetAccentable()
|
|
Text(entry.targetTime, style: .timer)
|
|
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
|
Text(formatTime(entry.targetTime))
|
|
.font(.system(size: 10))
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
Spacer()
|
|
}
|
|
} else {
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text("ChronoMind")
|
|
.font(.system(size: 14, weight: .semibold))
|
|
Text("No active timers")
|
|
.font(.system(size: 12))
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
Spacer()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Widget Definition
|
|
|
|
// MARK: - Family Dispatcher
|
|
|
|
struct LockScreenWidgetEntryView: View {
|
|
@Environment(\.widgetFamily) var family
|
|
let entry: LockScreenTimerEntry
|
|
|
|
var body: some View {
|
|
switch family {
|
|
case .accessoryInline:
|
|
LockScreenInlineView(entry: entry)
|
|
case .accessoryCircular:
|
|
LockScreenCircularView(entry: entry)
|
|
case .accessoryRectangular:
|
|
LockScreenRectangularView(entry: entry)
|
|
default:
|
|
LockScreenRectangularView(entry: entry)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Widget Definition
|
|
|
|
struct NextTimerLockScreenWidget: Widget {
|
|
let kind: String = "NextTimerLockScreenWidget"
|
|
|
|
var body: some WidgetConfiguration {
|
|
AppIntentConfiguration(
|
|
kind: kind,
|
|
intent: LockScreenTimerIntent.self,
|
|
provider: LockScreenTimerProvider()
|
|
) { entry in
|
|
LockScreenWidgetEntryView(entry: entry)
|
|
.containerBackground(for: .widget) { }
|
|
}
|
|
.configurationDisplayName("Timer Countdown")
|
|
.description("Shows next timer countdown on your lock screen")
|
|
.supportedFamilies([
|
|
.accessoryInline,
|
|
.accessoryCircular,
|
|
.accessoryRectangular,
|
|
])
|
|
}
|
|
}
|