// // Solsnu_Widget.swift // Solsnu.Widget // // Created by Ivar Løvlie on 15/12/2025. // import WidgetKit import SwiftUI import Foundation struct Provider: TimelineProvider { func placeholder(in context: Context) -> SolvervEntry { SolvervEntry(def: SolvervDef(date: Date())) } func getSnapshot(in context: Context, completion: @escaping (SolvervEntry) -> ()) { let entry = SolvervEntry(def: SolvervDef(date: Date())) completion(entry) } func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { var entries: [SolvervEntry] = [] let currentDate = Date() // Fetch location from AppGroupManager var sunriseTime: Date? = nil var sunsetTime: Date? = nil 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 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 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) } } struct SolvervEntry: TimelineEntry { let date: Date let def: SolvervDef init(def: SolvervDef, date: Date? = nil) { self.date = date ?? def.date self.def = def } } struct Solsnu_Widget: Widget { let kind: String = "Solsnu_Widget_Small" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: Provider()) { entry in SmallWidgetView(entry: entry) } .contentMarginsDisabled() .configurationDisplayName("Solstice Countdown") .description("Days until next solstice or equinox") .supportedFamilies([.systemSmall,.systemMedium]) } } #Preview(as: .systemSmall) { Solsnu_Widget() } timeline: { 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")) } 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() } func daysUntilNext() -> Int { guard let next = nextEvent else { return 0 } return next.daysUntil() } func progressRatio() -> Double { guard let progress = SolsticeData.shared.progressToNextEvent() 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) } static var preview: SolvervDef { SolvervDef(date: Date()) } }