diff options
Diffstat (limited to 'Solsnu.Widget')
| -rw-r--r-- | Solsnu.Widget/Solsnu_Widget.swift | 234 | ||||
| -rw-r--r-- | Solsnu.Widget/Solsnu_WidgetBundle.swift | 21 | ||||
| -rw-r--r-- | Solsnu.Widget/Solsnu_WidgetControl.swift | 55 | ||||
| -rw-r--r-- | Solsnu.Widget/Views/MediumWidgetView.swift | 30 | ||||
| -rw-r--r-- | Solsnu.Widget/Views/SmallWidgetView.swift | 38 |
5 files changed, 128 insertions, 250 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 } diff --git a/Solsnu.Widget/Solsnu_WidgetBundle.swift b/Solsnu.Widget/Solsnu_WidgetBundle.swift index 7bd3f1a..5a41904 100644 --- a/Solsnu.Widget/Solsnu_WidgetBundle.swift +++ b/Solsnu.Widget/Solsnu_WidgetBundle.swift @@ -12,26 +12,5 @@ import SwiftUI struct Solsnu_WidgetBundle: WidgetBundle { var body: some Widget { Solsnu_Widget() - Solsnu_WidgetMedium() - } -} - -struct Solsnu_WidgetMedium: Widget { - let kind: String = "Solsnu_Widget_Medium" - - var body: some WidgetConfiguration { - StaticConfiguration(kind: kind, provider: Provider()) { entry in - if #available(iOS 17.0, *) { - MediumWidgetView(entry: entry) - .containerBackground(.fill.tertiary, for: .widget) - } else { - MediumWidgetView(entry: entry) - .padding() - .background() - } - } - .configurationDisplayName("Solstice Countdown") - .description("Days until next solstice or equinox") - .supportedFamilies([.systemMedium,.systemSmall]) } } diff --git a/Solsnu.Widget/Solsnu_WidgetControl.swift b/Solsnu.Widget/Solsnu_WidgetControl.swift deleted file mode 100644 index 686a824..0000000 --- a/Solsnu.Widget/Solsnu_WidgetControl.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// Solsnu_WidgetControl.swift -// Solsnu.Widget -// -// Created by Ivar Løvlie on 15/12/2025. -// - -import AppIntents -import SwiftUI -import WidgetKit - -struct Solsnu_WidgetControl: ControlWidget { - var body: some ControlWidgetConfiguration { - StaticControlConfiguration( - kind: "ivarivarivar.Solverv.Solsnu.Widget", - provider: Provider() - ) { value in - ControlWidgetToggle( - "Start Timer", - isOn: value, - action: StartTimerIntent() - ) { isRunning in - Label(isRunning ? "On" : "Off", systemImage: "timer") - } - } - .displayName("Timer") - .description("A an example control that runs a timer.") - } -} - -extension Solsnu_WidgetControl { - struct Provider: ControlValueProvider { - var previewValue: Bool { - false - } - - func currentValue() async throws -> Bool { - let isRunning = true // Check if the timer is running - return isRunning - } - } -} - -struct StartTimerIntent: SetValueIntent { - static let title: LocalizedStringResource = "Start a timer" - - @Parameter(title: "Timer is running") - var value: Bool - - func perform() async throws -> some IntentResult { - // Start / stop the timer based on `value`. - return .result() - } -} -
\ No newline at end of file diff --git a/Solsnu.Widget/Views/MediumWidgetView.swift b/Solsnu.Widget/Views/MediumWidgetView.swift index 69bbbe2..f6fdbd9 100644 --- a/Solsnu.Widget/Views/MediumWidgetView.swift +++ b/Solsnu.Widget/Views/MediumWidgetView.swift @@ -7,20 +7,30 @@ struct MediumWidgetView: View { var body: some View { HStack(spacing: 12) { VStack(alignment: .leading, spacing: 8) { - Text("Season Event") - .font(.headline) - .lineLimit(1) + if let next = entry.nextEvent { + Text(next.shortName) + .font(.headline) + .lineLimit(1) - Text("\(entry.def.daysUntilNext()) days") - .font(.system(.title3, design: .default).weight(.semibold)) + Text("\(next.daysUntil(from: entry.date)) dager") + .font(.system(.title3, design: .default).weight(.semibold)) + } - ProgressView(value: Double(entry.def.progressRatio())) + ProgressView(value: entry.progressRatio) - if !entry.def.sunriseFormatted.isEmpty && !entry.def.sunsetFormatted.isEmpty { + if let sunrise = entry.sunriseTime, let sunset = entry.sunsetTime { HStack { - Text("Sunrise: \(entry.def.sunriseFormatted)") + Label { + Text(sunrise, style: .time) + } icon: { + Image(systemName: "sunrise.fill") + } Spacer() - Text("Sunset: \(entry.def.sunsetFormatted)") + Label { + Text(sunset, style: .time) + } icon: { + Image(systemName: "sunset.fill") + } } .font(.system(size: 12, weight: .regular)) .foregroundStyle(Color(red: 0.152, green: 0.136, blue: 0.056)) @@ -32,6 +42,6 @@ struct MediumWidgetView: View { Spacer() } .padding() + .containerBackground(.fill.tertiary, for: .widget) } } - diff --git a/Solsnu.Widget/Views/SmallWidgetView.swift b/Solsnu.Widget/Views/SmallWidgetView.swift index 16ca102..b7dbf32 100644 --- a/Solsnu.Widget/Views/SmallWidgetView.swift +++ b/Solsnu.Widget/Views/SmallWidgetView.swift @@ -1,30 +1,34 @@ import SwiftUI import WidgetKit -import Combine struct SmallWidgetView: View { let entry: SolvervEntry @Environment(\.widgetRenderingMode) var renderingMode - @Environment(\.locale) var locale - + var body: some View { - ZStack { - Image(entry.def.bg) - VStack(spacing: 8) { - Text("\(entry.def.daysUntilNext())") - .font(.system(size: 26, weight: .bold, design: .serif)) - .position(x: 50,y: 50) - .foregroundStyle(Color(red: 0.152, green: 0.136, blue: 0.056)) - .italic() + VStack(spacing: 8) { + Text("\(entry.nextEvent?.daysUntil(from: entry.date) ?? 0)") + .font(.system(size: 26, weight: .bold, design: .serif)) + .italic() + .foregroundStyle(Color(red: 0.152, green: 0.136, blue: 0.056)) - HStack(spacing: 4) { - Text("↑ \(entry.def.sunriseFormatted)") - Text("↓ \(entry.def.sunsetFormatted)") + if let sunrise = entry.sunriseTime, let sunset = entry.sunsetTime { + HStack(spacing: 4) { + Label { + Text(sunrise, style: .time) + } icon: { + Image(systemName: "sunrise.fill") + } + Label { + Text(sunset, style: .time) + } icon: { + Image(systemName: "sunset.fill") } - .font(.system(size: 11, weight: .regular)) - .foregroundStyle(Color(red: 0.152, green: 0.136, blue: 0.056)) + } + .font(.system(size: 11, weight: .regular)) + .foregroundStyle(Color(red: 0.152, green: 0.136, blue: 0.056)) } } - .containerBackground(for: .widget, alignment: .center) { Color.clear } + .containerBackground(for: .widget, alignment: .center) { Color.blue } } } |
