// // Solsnu_Widget.swift // Solsnu.Widget // // Created by Ivar Løvlie on 15/12/2025. // import WidgetKit import SwiftUI 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() let entry = SolvervEntry(def: SolvervDef(date: currentDate)) 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?; 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 "" } let formatter = DateFormatter() formatter.dateFormat = "HH:mm" formatter.timeZone = TimeZone.current return formatter.string(from: time) } var sunsetFormatted: String { guard let time = sunsetTime else { return "" } let formatter = DateFormatter() formatter.dateFormat = "HH:mm" formatter.timeZone = TimeZone.current return formatter.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()) } }