- watchOS: add WatchSessionManager (WCSession bridge), WatchNotificationHandler (snooze/dismiss actions), recommendations() for AppIntentTimelineProvider - watchOS: WatchTimerStore tick loop with cascade haptics, App Group sync - macOS: CreateTimerSheet (countdown/alarm/pomodoro), launch-at-login toggle - macOS: MenuBarState showCreateSheet, MacSettingsView data tab - Xcode project updated with new file references
247 lines
7.3 KiB
Swift
247 lines
7.3 KiB
Swift
// ── Watch Complications ───────────────────────────────────────
|
|
// WidgetKit complications for Apple Watch faces
|
|
|
|
import SwiftUI
|
|
import WidgetKit
|
|
|
|
// MARK: - Complication Timeline Provider
|
|
|
|
struct WatchComplicationProvider: AppIntentTimelineProvider {
|
|
typealias Entry = WatchComplicationEntry
|
|
typealias Intent = WatchComplicationIntent
|
|
|
|
func placeholder(in context: Context) -> WatchComplicationEntry {
|
|
WatchComplicationEntry(
|
|
date: Date(),
|
|
label: "Standup",
|
|
targetTime: Date().addingTimeInterval(3600),
|
|
urgency: .important,
|
|
hasTimer: true
|
|
)
|
|
}
|
|
|
|
func snapshot(for configuration: WatchComplicationIntent, in context: Context) async -> WatchComplicationEntry {
|
|
entryFromSharedData()
|
|
}
|
|
|
|
func timeline(for configuration: WatchComplicationIntent, in context: Context) async -> Timeline<WatchComplicationEntry> {
|
|
let entry = entryFromSharedData()
|
|
let now = Date()
|
|
|
|
var entries: [WatchComplicationEntry] = []
|
|
for minuteOffset in stride(from: 0, through: 30, by: 5) {
|
|
let entryDate = Calendar.current.date(byAdding: .minute, value: minuteOffset, to: now)!
|
|
entries.append(WatchComplicationEntry(
|
|
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(15 * 60))
|
|
} else {
|
|
reloadDate = now.addingTimeInterval(15 * 60)
|
|
}
|
|
|
|
return Timeline(entries: entries, policy: .after(reloadDate))
|
|
}
|
|
|
|
func recommendations() -> [AppIntentRecommendation<WatchComplicationIntent>] {
|
|
[AppIntentRecommendation(intent: WatchComplicationIntent(), description: "Next Timer")]
|
|
}
|
|
|
|
private func entryFromSharedData() -> WatchComplicationEntry {
|
|
if let timer = SharedTimerDataManager.shared.readNextFiringTimer() {
|
|
return WatchComplicationEntry(
|
|
date: Date(),
|
|
label: timer.label,
|
|
targetTime: timer.targetTime,
|
|
urgency: timer.urgency,
|
|
hasTimer: true
|
|
)
|
|
}
|
|
return WatchComplicationEntry(
|
|
date: Date(),
|
|
label: "",
|
|
targetTime: Date(),
|
|
urgency: .standard,
|
|
hasTimer: false
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Intent
|
|
|
|
import AppIntents
|
|
|
|
struct WatchComplicationIntent: WidgetConfigurationIntent {
|
|
static var title: LocalizedStringResource = "Timer Complication"
|
|
static var description: IntentDescription = "Shows next timer on watch face"
|
|
}
|
|
|
|
// MARK: - Entry
|
|
|
|
struct WatchComplicationEntry: TimelineEntry {
|
|
let date: Date
|
|
let label: String
|
|
let targetTime: Date
|
|
let urgency: UrgencyLevel
|
|
let hasTimer: Bool
|
|
}
|
|
|
|
// MARK: - Circular Complication
|
|
|
|
struct CircularComplicationView: View {
|
|
let entry: WatchComplicationEntry
|
|
|
|
var body: some View {
|
|
if entry.hasTimer {
|
|
ZStack {
|
|
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: 18))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Rectangular Complication
|
|
|
|
struct RectangularComplicationView: View {
|
|
let entry: WatchComplicationEntry
|
|
|
|
var body: some View {
|
|
if entry.hasTimer {
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(entry.label)
|
|
.font(.system(size: 13, 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: 13, weight: .semibold))
|
|
Text("No timers")
|
|
.font(.system(size: 11))
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
Spacer()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Inline Complication
|
|
|
|
struct InlineComplicationView: View {
|
|
let entry: WatchComplicationEntry
|
|
|
|
var body: some View {
|
|
if entry.hasTimer {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "clock.fill")
|
|
Text("\(entry.label) in")
|
|
Text(entry.targetTime, style: .timer)
|
|
}
|
|
} else {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "clock")
|
|
Text("No timers")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Corner Complication
|
|
|
|
struct CornerComplicationView: View {
|
|
let entry: WatchComplicationEntry
|
|
|
|
var body: some View {
|
|
if entry.hasTimer {
|
|
Text(entry.targetTime, style: .timer)
|
|
.font(.system(size: 12, weight: .bold, design: .monospaced))
|
|
.widgetAccentable()
|
|
.widgetLabel {
|
|
Text(entry.label)
|
|
}
|
|
} else {
|
|
Image(systemName: "clock")
|
|
.widgetLabel {
|
|
Text("ChronoMind")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Family Dispatcher
|
|
|
|
struct WatchComplicationEntryView: View {
|
|
@Environment(\.widgetFamily) var family
|
|
let entry: WatchComplicationEntry
|
|
|
|
var body: some View {
|
|
switch family {
|
|
case .accessoryCircular:
|
|
CircularComplicationView(entry: entry)
|
|
case .accessoryRectangular:
|
|
RectangularComplicationView(entry: entry)
|
|
case .accessoryInline:
|
|
InlineComplicationView(entry: entry)
|
|
case .accessoryCorner:
|
|
CornerComplicationView(entry: entry)
|
|
default:
|
|
CircularComplicationView(entry: entry)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Widget Definition
|
|
|
|
struct WatchTimerComplication: Widget {
|
|
let kind: String = "WatchTimerComplication"
|
|
|
|
var body: some WidgetConfiguration {
|
|
AppIntentConfiguration(
|
|
kind: kind,
|
|
intent: WatchComplicationIntent.self,
|
|
provider: WatchComplicationProvider()
|
|
) { entry in
|
|
WatchComplicationEntryView(entry: entry)
|
|
.containerBackground(for: .widget) { }
|
|
}
|
|
.configurationDisplayName("Timer")
|
|
.description("Shows next timer countdown on your watch face")
|
|
.supportedFamilies([
|
|
.accessoryCircular,
|
|
.accessoryRectangular,
|
|
.accessoryInline,
|
|
.accessoryCorner,
|
|
])
|
|
}
|
|
}
|