diff options
Diffstat (limited to 'Solsnu.Widget/Solsnu_Widget.swift')
| -rw-r--r-- | Solsnu.Widget/Solsnu_Widget.swift | 234 |
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 } |
