summaryrefslogtreecommitdiffstats
path: root/Solsnu.Widget/Solsnu_Widget.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Solsnu.Widget/Solsnu_Widget.swift')
-rw-r--r--Solsnu.Widget/Solsnu_Widget.swift234
1 files changed, 87 insertions, 147 deletions
diff --git a/Solsnu.Widget/Solsnu_Widget.swift b/Solsnu.Widget/Solsnu_Widget.swift
index c36cc34..bc6fc0e 100644
--- a/Solsnu.Widget/Solsnu_Widget.swift
+++ b/Solsnu.Widget/Solsnu_Widget.swift
@@ -7,189 +7,129 @@
import WidgetKit
import SwiftUI
-import Foundation
+
+// MARK: - Provider
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SolvervEntry {
- SolvervEntry(def: SolvervDef(date: Date()))
+ .placeholder
}
- func getSnapshot(in context: Context, completion: @escaping (SolvervEntry) -> ()) {
- let entry = SolvervEntry(def: SolvervDef(date: Date()))
- completion(entry)
+ func getSnapshot(in context: Context, completion: @escaping (SolvervEntry) -> Void) {
+ completion(makeEntry(for: Date()))
}
- func getTimeline(in context: Context, completion: @escaping (Timeline<SolvervEntry>) -> ()) {
- var entries: [SolvervEntry] = []
- let currentDate = Date()
-
- // Fetch location from AppGroupManager
- var sunriseTime: Date? = nil
- var sunsetTime: Date? = nil
+ func getTimeline(in context: Context, completion: @escaping (Timeline<SolvervEntry>) -> Void) {
+ let entry = makeEntry(for: Date())
+ let nextMidnight = Calendar.current.nextDate(
+ after: Date(),
+ matching: DateComponents(hour: 0, minute: 0, second: 0),
+ matchingPolicy: .nextTime
+ ) ?? Date().addingTimeInterval(86_400)
+ completion(Timeline(entries: [entry], policy: .after(nextMidnight)))
+ }
- if let location = AppGroupManager.shared.getLocation() {
- // Check if location is fresh (< 24 hours old)
- let isoFormatter = ISO8601DateFormatter()
- if let locationTimestamp = isoFormatter.date(from: location.timestamp) {
- let hoursSinceCache = currentDate.timeIntervalSince(locationTimestamp) / 3600.0
+ private func makeEntry(for date: Date) -> SolvervEntry {
+ let progress = SolsticeData.shared.progressToNextEvent(from: date)
+ let ratio = progress.map {
+ max(0, min(1, Double($0.elapsed) / Double($0.total)))
+ } ?? 0
- if hoursSinceCache < 24.0 {
- // Location is fresh, calculate sun times
- let calculator = SunTimes(latitude: location.latitude, longitude: location.longitude, date: currentDate)
- sunriseTime = calculator.sunrise()
- sunsetTime = calculator.sunset()
- }
- }
+ let sunTimes = AppGroupManager.shared.getLocation().map {
+ SunTimes(latitude: $0.latitude, longitude: $0.longitude, date: date)
}
- let entry = SolvervEntry(def: SolvervDef(date: currentDate, sunriseTime: sunriseTime, sunsetTime: sunsetTime))
- entries.append(entry)
-
- let calendar = Calendar.current
- var components = calendar.dateComponents([.year, .month, .day], from: currentDate)
- components.hour = 0
- components.minute = 0
- components.second = 0
- let todayMidnight = calendar.date(from: components)!
- let nextMidnight = calendar.date(byAdding: .day, value: 1, to: todayMidnight)!
-
- let timeline = Timeline(entries: entries, policy: .after(nextMidnight))
- completion(timeline)
+ return SolvervEntry(
+ date: date,
+ nextEvent: SolsticeData.shared.nextEvent(from: date),
+ currentEvent: SolsticeData.shared.currentEvent(from: date),
+ progressRatio: ratio,
+ sunriseTime: sunTimes?.sunrise(),
+ sunsetTime: sunTimes?.sunset()
+ )
}
}
+// MARK: - Entry
+
struct SolvervEntry: TimelineEntry {
let date: Date
- let def: SolvervDef
-
- init(def: SolvervDef, date: Date? = nil) {
- self.date = date ?? def.date
- self.def = def
+ let nextEvent: SolsticeEvent?
+ let currentEvent: SolsticeEvent?
+ let progressRatio: Double
+ let sunriseTime: Date?
+ let sunsetTime: Date?
+
+ static let placeholder = SolvervEntry(
+ date: Date(),
+ nextEvent: nil,
+ currentEvent: nil,
+ progressRatio: 0.4,
+ sunriseTime: nil,
+ sunsetTime: nil
+ )
+}
+
+// MARK: - Family dispatch
+
+struct SolsticeWidgetView: View {
+ let entry: SolvervEntry
+ @Environment(\.widgetFamily) var widgetFamily
+
+ var body: some View {
+ switch widgetFamily {
+ case .systemMedium:
+ MediumWidgetView(entry: entry)
+ default:
+ SmallWidgetView(entry: entry)
+ }
}
}
+// MARK: - Widget
+
struct Solsnu_Widget: Widget {
- let kind: String = "Solsnu_Widget_Small"
+ let kind = "Solsnu_Widget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
- SmallWidgetView(entry: entry)
+ SolsticeWidgetView(entry: entry)
}
.contentMarginsDisabled()
.configurationDisplayName("Solstice Countdown")
.description("Days until next solstice or equinox")
- .supportedFamilies([.systemSmall,.systemMedium])
+ .supportedFamilies([.systemSmall, .systemMedium])
}
}
+// MARK: - Previews
+
#Preview(as: .systemSmall) {
Solsnu_Widget()
} timeline: {
- // Test with valid sunrise/sunset times (spring equinox, Oslo approx)
- let springDate = Calendar.current.date(from: DateComponents(year: 2026, month: 3, day: 20))!
- let sunriseTime = Calendar.current.date(bySettingHour: 7, minute: 30, second: 0, of: springDate)!
- let sunsetTime = Calendar.current.date(bySettingHour: 19, minute: 45, second: 0, of: springDate)!
-
- SolvervEntry(def: SolvervDef(date: springDate, sunriseTime: sunriseTime, sunsetTime: sunsetTime))
-
- // Test with nil times (no location)
- SolvervEntry(def: SolvervDef(date: springDate, sunriseTime: nil, sunsetTime: nil))
-
- // Original equinox dates
- SolvervEntry(def: SolvervDef(utcString: "2026-03-20 14:46:00"))
- SolvervEntry(def: SolvervDef(utcString: "2026-06-21 08:25:00"))
- SolvervEntry(def: SolvervDef(utcString: "2026-09-23 00:06:00"))
- SolvervEntry(def: SolvervDef(utcString: "2026-12-21 20:50:00"))
+ let now = Date()
+ SolvervEntry(
+ date: now,
+ nextEvent: SolsticeData.shared.nextEvent(from: now),
+ currentEvent: SolsticeData.shared.currentEvent(from: now),
+ progressRatio: 0.4,
+ sunriseTime: Calendar.current.date(bySettingHour: 6, minute: 28, second: 0, of: now),
+ sunsetTime: Calendar.current.date(bySettingHour: 21, minute: 15, second: 0, of: now)
+ )
+ SolvervEntry.placeholder
}
#Preview(as: .systemMedium) {
Solsnu_Widget()
} timeline: {
- // Test medium widget with valid sunrise/sunset times
- let springDate = Calendar.current.date(from: DateComponents(year: 2026, month: 3, day: 20))!
- let sunriseTime = Calendar.current.date(bySettingHour: 7, minute: 30, second: 0, of: springDate)!
- let sunsetTime = Calendar.current.date(bySettingHour: 19, minute: 45, second: 0, of: springDate)!
-
- SolvervEntry(def: SolvervDef(date: springDate, sunriseTime: sunriseTime, sunsetTime: sunsetTime))
-
- // Test medium widget with nil times
- SolvervEntry(def: SolvervDef(date: springDate, sunriseTime: nil, sunsetTime: nil))
-
- // Original equinox dates
- SolvervEntry(def: SolvervDef(utcString: "2026-03-20 14:46:00"))
- SolvervEntry(def: SolvervDef(utcString: "2026-06-21 08:25:00"))
-}
-
-struct SolvervDef {
- let date: Date
- let bg: String
- let sunriseTime: Date?
- let sunsetTime: Date?
-
- // Cached formatter for performance
- private static let timeFormatter: DateFormatter = {
- let formatter = DateFormatter()
- formatter.dateFormat = "HH:mm"
- formatter.timeZone = TimeZone.current
- return formatter
- }()
-
- init(date: Date, sunriseTime: Date? = nil, sunsetTime: Date? = nil) {
- self.date = date
- self.sunriseTime = sunriseTime
- self.sunsetTime = sunsetTime
- self.bg = "smallbg"
- }
-
- init(utcString: String, sunriseTime: Date? = nil, sunsetTime: Date? = nil) {
- let formatter = DateFormatter()
- formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
- formatter.timeZone = TimeZone(abbreviation: "UTC")
- // Should probably guard this, but not now
- let date = formatter.date(from: utcString)!
- self.date = date
- self.sunriseTime = sunriseTime
- self.sunsetTime = sunsetTime
- self.bg = "smallbg"
- }
-
- var sunriseFormatted: String {
- guard let time = sunriseTime else { return "" }
- return Self.timeFormatter.string(from: time)
- }
-
- var sunsetFormatted: String {
- guard let time = sunsetTime else { return "" }
- return Self.timeFormatter.string(from: time)
- }
-}
-
-extension SolvervDef {
- var season: Season {
- guard let next = nextEvent else { return .winter }
- return next.season
- }
-
- var nextEvent: SolsticeEvent? {
- SolsticeData.shared.nextEvent(from: date)
- }
-
- func daysUntilNext() -> Int {
- guard let next = nextEvent else { return 0 }
- return next.daysUntil(from: date)
- }
-
- func progressRatio() -> Double {
- guard let progress = SolsticeData.shared.progressToNextEvent(from: date) else { return 0.0 }
- let ratio = Double(progress.elapsed) / Double(progress.total)
- return max(0, min(1.0, ratio))
- }
-
- func upcomingEventsPreview(count: Int) -> [SolsticeEvent] {
- SolsticeData.shared.upcomingEvents(count: count, from: date)
- }
-
- static var preview: SolvervDef {
- SolvervDef(date: Date())
- }
+ let now = Date()
+ SolvervEntry(
+ date: now,
+ nextEvent: SolsticeData.shared.nextEvent(from: now),
+ currentEvent: SolsticeData.shared.currentEvent(from: now),
+ progressRatio: 0.4,
+ sunriseTime: Calendar.current.date(bySettingHour: 6, minute: 28, second: 0, of: now),
+ sunsetTime: Calendar.current.date(bySettingHour: 21, minute: 15, second: 0, of: now)
+ )
+ SolvervEntry.placeholder
}