summaryrefslogtreecommitdiffstats
path: root/Solsnu.Widget
diff options
context:
space:
mode:
authorivar <i@oiee.no>2026-03-23 16:47:59 +0100
committerivar <i@oiee.no>2026-03-23 16:47:59 +0100
commit94bf65b7e6b0cb683ea9c952aed86b034d1c8363 (patch)
tree6158072e01f9ffe2dd8754e9b136f8978bb11d6e /Solsnu.Widget
parente45dd001f178a640e1f799616868accb4254c313 (diff)
downloadsolverv-94bf65b7e6b0cb683ea9c952aed86b034d1c8363.tar.xz
solverv-94bf65b7e6b0cb683ea9c952aed86b034d1c8363.zip
feat: implement SolvervDef methods and update widget timeline to refresh at midnight
Diffstat (limited to 'Solsnu.Widget')
-rw-r--r--Solsnu.Widget/Models/Season.swift49
-rw-r--r--Solsnu.Widget/Models/SolsticeData.swift114
-rw-r--r--Solsnu.Widget/Models/SolsticeEvent.swift38
-rw-r--r--Solsnu.Widget/Solsnu_Widget.swift45
4 files changed, 233 insertions, 13 deletions
diff --git a/Solsnu.Widget/Models/Season.swift b/Solsnu.Widget/Models/Season.swift
new file mode 100644
index 0000000..01eaf99
--- /dev/null
+++ b/Solsnu.Widget/Models/Season.swift
@@ -0,0 +1,49 @@
+import SwiftUI
+
+enum Season: String, Codable {
+ case spring
+ case summer
+ case autumn
+ case winter
+
+ var displayName: String {
+ switch self {
+ case .spring: return "Spring"
+ case .summer: return "Summer"
+ case .autumn: return "Autumn"
+ case .winter: return "Winter"
+ }
+ }
+
+ var description: String {
+ switch self {
+ case .spring: return "Day and night are approximately equal length"
+ case .summer: return "Longest day of the year"
+ case .autumn: return "Day and night are approximately equal length"
+ case .winter: return "Shortest day of the year"
+ }
+ }
+
+ var colorLight: Color {
+ switch self {
+ case .spring: return Color(red: 0.298, green: 0.686, blue: 0.314) // #4CAF50
+ case .summer: return Color(red: 1.0, green: 0.761, blue: 0.039) // #FFC107
+ case .autumn: return Color(red: 1.0, green: 0.596, blue: 0.0) // #FF9800
+ case .winter: return Color(red: 0.129, green: 0.588, blue: 0.953) // #2196F3
+ }
+ }
+
+ var assetName: String {
+ return "Season\(displayName)"
+ }
+
+ static func fromDate(_ date: Date) -> Season {
+ let month = Calendar.current.component(.month, from: date)
+ switch month {
+ case 3, 4, 5: return .spring
+ case 6, 7, 8: return .summer
+ case 9, 10, 11: return .autumn
+ default: return .winter
+ }
+ }
+}
diff --git a/Solsnu.Widget/Models/SolsticeData.swift b/Solsnu.Widget/Models/SolsticeData.swift
new file mode 100644
index 0000000..5a36da7
--- /dev/null
+++ b/Solsnu.Widget/Models/SolsticeData.swift
@@ -0,0 +1,114 @@
+import Foundation
+
+class SolsticeData {
+ static let shared = SolsticeData()
+
+ private let events: [SolsticeEvent]
+
+ private init() {
+ // Hardcoded solstice/equinox events for 2025-2030 (all in UTC)
+ var allEvents: [SolsticeEvent] = []
+
+ // 2025
+ allEvents.append(SolsticeEvent(name: "Spring Equinox 2025", date: SolsticeData.dateFromUTC(year: 2025, month: 3, day: 20, hour: 9, minute: 1), season: .spring))
+ allEvents.append(SolsticeEvent(name: "Summer Solstice 2025", date: SolsticeData.dateFromUTC(year: 2025, month: 6, day: 20, hour: 14, minute: 42), season: .summer))
+ allEvents.append(SolsticeEvent(name: "Autumn Equinox 2025", date: SolsticeData.dateFromUTC(year: 2025, month: 9, day: 22, hour: 18, minute: 20), season: .autumn))
+ allEvents.append(SolsticeEvent(name: "Winter Solstice 2025", date: SolsticeData.dateFromUTC(year: 2025, month: 12, day: 21, hour: 15, minute: 3), season: .winter))
+
+ // 2026
+ allEvents.append(SolsticeEvent(name: "Spring Equinox 2026", date: SolsticeData.dateFromUTC(year: 2026, month: 3, day: 20, hour: 14, minute: 46), season: .spring))
+ allEvents.append(SolsticeEvent(name: "Summer Solstice 2026", date: SolsticeData.dateFromUTC(year: 2026, month: 6, day: 21, hour: 8, minute: 25), season: .summer))
+ allEvents.append(SolsticeEvent(name: "Autumn Equinox 2026", date: SolsticeData.dateFromUTC(year: 2026, month: 9, day: 23, hour: 0, minute: 6), season: .autumn))
+ allEvents.append(SolsticeEvent(name: "Winter Solstice 2026", date: SolsticeData.dateFromUTC(year: 2026, month: 12, day: 21, hour: 20, minute: 50), season: .winter))
+
+ // 2027
+ allEvents.append(SolsticeEvent(name: "Spring Equinox 2027", date: SolsticeData.dateFromUTC(year: 2027, month: 3, day: 20, hour: 20, minute: 25), season: .spring))
+ allEvents.append(SolsticeEvent(name: "Summer Solstice 2027", date: SolsticeData.dateFromUTC(year: 2027, month: 6, day: 21, hour: 14, minute: 11), season: .summer))
+ allEvents.append(SolsticeEvent(name: "Autumn Equinox 2027", date: SolsticeData.dateFromUTC(year: 2027, month: 9, day: 23, hour: 6, minute: 2), season: .autumn))
+ allEvents.append(SolsticeEvent(name: "Winter Solstice 2027", date: SolsticeData.dateFromUTC(year: 2027, month: 12, day: 22, hour: 2, minute: 43), season: .winter))
+
+ // 2028
+ allEvents.append(SolsticeEvent(name: "Spring Equinox 2028", date: SolsticeData.dateFromUTC(year: 2028, month: 3, day: 20, hour: 2, minute: 17), season: .spring))
+ allEvents.append(SolsticeEvent(name: "Summer Solstice 2028", date: SolsticeData.dateFromUTC(year: 2028, month: 6, day: 20, hour: 20, minute: 2), season: .summer))
+ allEvents.append(SolsticeEvent(name: "Autumn Equinox 2028", date: SolsticeData.dateFromUTC(year: 2028, month: 9, day: 22, hour: 11, minute: 45), season: .autumn))
+ allEvents.append(SolsticeEvent(name: "Winter Solstice 2028", date: SolsticeData.dateFromUTC(year: 2028, month: 12, day: 21, hour: 8, minute: 20), season: .winter))
+
+ // 2029
+ allEvents.append(SolsticeEvent(name: "Spring Equinox 2029", date: SolsticeData.dateFromUTC(year: 2029, month: 3, day: 20, hour: 8, minute: 1), season: .spring))
+ allEvents.append(SolsticeEvent(name: "Summer Solstice 2029", date: SolsticeData.dateFromUTC(year: 2029, month: 6, day: 21, hour: 1, minute: 48), season: .summer))
+ allEvents.append(SolsticeEvent(name: "Autumn Equinox 2029", date: SolsticeData.dateFromUTC(year: 2029, month: 9, day: 22, hour: 17, minute: 37), season: .autumn))
+ allEvents.append(SolsticeEvent(name: "Winter Solstice 2029", date: SolsticeData.dateFromUTC(year: 2029, month: 12, day: 21, hour: 14, minute: 14), season: .winter))
+
+ // 2030
+ allEvents.append(SolsticeEvent(name: "Spring Equinox 2030", date: SolsticeData.dateFromUTC(year: 2030, month: 3, day: 20, hour: 13, minute: 51), season: .spring))
+ allEvents.append(SolsticeEvent(name: "Summer Solstice 2030", date: SolsticeData.dateFromUTC(year: 2030, month: 6, day: 21, hour: 7, minute: 31), season: .summer))
+ allEvents.append(SolsticeEvent(name: "Autumn Equinox 2030", date: SolsticeData.dateFromUTC(year: 2030, month: 9, day: 22, hour: 23, minute: 27), season: .autumn))
+ allEvents.append(SolsticeEvent(name: "Winter Solstice 2030", date: SolsticeData.dateFromUTC(year: 2030, month: 12, day: 21, hour: 20, minute: 9), season: .winter))
+
+ // Sort events by date
+ self.events = allEvents.sorted { $0.date < $1.date }
+ }
+
+ /// Helper function to create UTC dates (static to be usable during init)
+ private static func dateFromUTC(year: Int, month: Int, day: Int, hour: Int, minute: Int) -> Date {
+ var components = DateComponents()
+ components.year = year
+ components.month = month
+ components.day = day
+ components.hour = hour
+ components.minute = minute
+ components.second = 0
+ components.timeZone = TimeZone(abbreviation: "UTC")
+
+ return Calendar(identifier: .gregorian).date(from: components) ?? Date()
+ }
+
+ /// Returns the next upcoming solstice/equinox event
+ func nextEvent() -> SolsticeEvent? {
+ let now = Date()
+ return events.first { $0.date > now }
+ }
+
+ /// Returns the next N upcoming events
+ func upcomingEvents(count: Int) -> [SolsticeEvent] {
+ let now = Date()
+ let futureEvents = events.filter { $0.date > now }
+ return Array(futureEvents.prefix(count))
+ }
+
+ /// Returns the progress to the next event as (elapsed days, total days)
+ func progressToNextEvent() -> (elapsed: Int, total: Int)? {
+ guard let nextEvent = nextEvent() else {
+ return nil
+ }
+
+ let now = Date()
+ let today = Calendar.current.startOfDay(for: now)
+ let eventDay = Calendar.current.startOfDay(for: nextEvent.date)
+
+ // Find the previous event to calculate total days
+ guard let previousEventIndex = events.firstIndex(where: { $0.date > now }) else {
+ return nil
+ }
+
+ let previousEvent: Date
+ if previousEventIndex > 0 {
+ previousEvent = events[previousEventIndex - 1].date
+ } else {
+ // If this is the first event, we need to handle this case
+ // Use the event itself minus some arbitrary period (not applicable for first event)
+ return nil
+ }
+
+ let previousEventDay = Calendar.current.startOfDay(for: previousEvent)
+
+ // Calculate elapsed and total days
+ let elapsedComponents = Calendar.current.dateComponents([.day], from: previousEventDay, to: today)
+ let totalComponents = Calendar.current.dateComponents([.day], from: previousEventDay, to: eventDay)
+
+ let elapsed = max(0, elapsedComponents.day ?? 0)
+ let total = max(1, totalComponents.day ?? 1)
+
+ return (elapsed: elapsed, total: total)
+ }
+}
diff --git a/Solsnu.Widget/Models/SolsticeEvent.swift b/Solsnu.Widget/Models/SolsticeEvent.swift
new file mode 100644
index 0000000..d8c4a7b
--- /dev/null
+++ b/Solsnu.Widget/Models/SolsticeEvent.swift
@@ -0,0 +1,38 @@
+import Foundation
+
+struct SolsticeEvent: Identifiable, Codable {
+ let id: UUID
+ let name: String
+ let date: Date // UTC
+ let season: Season
+
+ init(name: String, date: Date, season: Season) {
+ self.id = UUID()
+ self.name = name
+ self.date = date
+ self.season = season
+ }
+
+ /// Convert UTC date to user's local timezone
+ func localDateTime() -> Date {
+ let utcCalendar = Calendar(identifier: .gregorian)
+ let utcComponents = utcCalendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date)
+ let timeZone = TimeZone.current
+ let offset = timeZone.secondsFromGMT(for: date)
+
+ var localCalendar = Calendar.current
+ localCalendar.timeZone = timeZone
+ var localComponents = utcComponents
+ localComponents.second = (localComponents.second ?? 0) + offset
+
+ return localCalendar.date(from: localComponents) ?? date
+ }
+
+ /// Days until this event from today
+ func daysUntil() -> Int {
+ let today = Calendar.current.startOfDay(for: Date())
+ let eventDay = Calendar.current.startOfDay(for: date)
+ let components = Calendar.current.dateComponents([.day], from: today, to: eventDay)
+ return max(0, components.day ?? 0)
+ }
+}
diff --git a/Solsnu.Widget/Solsnu_Widget.swift b/Solsnu.Widget/Solsnu_Widget.swift
index 1021db1..980725e 100644
--- a/Solsnu.Widget/Solsnu_Widget.swift
+++ b/Solsnu.Widget/Solsnu_Widget.swift
@@ -21,13 +21,21 @@ struct Provider: TimelineProvider {
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SolvervEntry] = []
let currentDate = Date()
- for hourOffset in 0 ..< 5 {
- let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
- let entry = SolvervEntry(def: SolvervDef(date: currentDate))
- entries.append(entry)
- }
- let timeline = Timeline(entries: entries, policy: .atEnd)
+ // Single entry for today
+ let entry = SolvervEntry(def: SolvervDef(date: currentDate))
+ entries.append(entry)
+
+ // Calculate next midnight for refresh
+ 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)
}
}
@@ -133,20 +141,31 @@ struct SolvervDef {
}
extension SolvervDef {
- var season: String {
- // Will be fully implemented in Task 7
- return "winter"
+ var season: Season {
+ guard let next = nextEvent else { return .winter }
+ return next.season
+ }
+
+ var nextEvent: SolsticeEvent? {
+ SolsticeData.shared.nextEvent()
}
func daysUntilNext() -> Int {
- return 0
+ guard let next = nextEvent else { return 0 }
+ return next.daysUntil()
}
func progressRatio() -> Double {
- return 0.0
+ 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)
}
- func upcomingEventsPreview(count: Int) -> [String] {
- return []
+ static var preview: SolvervDef {
+ SolvervDef(date: Date())
}
}