summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorivar <i@oiee.no>2026-05-06 21:21:17 +0200
committerivar <i@oiee.no>2026-05-06 21:21:17 +0200
commit54dd55db8c19667939536e18535ac9c45817e442 (patch)
tree7ccb609b16e731c6281aa8d23969fb51c42eb60f
parentefae4d08083f454975f08a2c0c6871c6a3d41e95 (diff)
downloadsolverv-54dd55db8c19667939536e18535ac9c45817e442.tar.xz
solverv-54dd55db8c19667939536e18535ac9c45817e442.zip
refactor: remove duplicate source files now served from Shared/
-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.swift8
-rw-r--r--Solsnu.Widget/Solsnu_WidgetBundle.swift25
-rw-r--r--Solsnu.Widget/Solsnu_WidgetControl.swift1
-rw-r--r--Solsnu.Widget/Utilities/AppGroupManager.swift67
-rw-r--r--Solsnu.Widget/Utilities/SunTimes.swift148
-rw-r--r--Solsnu.Widget/Views/SmallWidgetView.swift5
-rw-r--r--Solverv.xcodeproj/project.pbxproj78
-rw-r--r--Solverv.xcodeproj/xcuserdata/ivarlovlie.xcuserdatad/xcschemes/xcschememanagement.plist2
-rw-r--r--Solverv/Models/Season.swift49
-rw-r--r--Solverv/Models/SolsticeData.swift114
-rw-r--r--Solverv/Models/SolsticeEvent.swift38
-rw-r--r--Solverv/SolvervApp.swift10
-rw-r--r--Solverv/Utilities/AppGroupManager.swift67
-rw-r--r--Solverv/Utilities/SunTimes.swift148
17 files changed, 39 insertions, 922 deletions
diff --git a/Solsnu.Widget/Models/Season.swift b/Solsnu.Widget/Models/Season.swift
deleted file mode 100644
index 01eaf99..0000000
--- a/Solsnu.Widget/Models/Season.swift
+++ /dev/null
@@ -1,49 +0,0 @@
-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
deleted file mode 100644
index 5a36da7..0000000
--- a/Solsnu.Widget/Models/SolsticeData.swift
+++ /dev/null
@@ -1,114 +0,0 @@
-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
deleted file mode 100644
index d8c4a7b..0000000
--- a/Solsnu.Widget/Models/SolsticeEvent.swift
+++ /dev/null
@@ -1,38 +0,0 @@
-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 20e8a8e..c36cc34 100644
--- a/Solsnu.Widget/Solsnu_Widget.swift
+++ b/Solsnu.Widget/Solsnu_Widget.swift
@@ -171,22 +171,22 @@ extension SolvervDef {
}
var nextEvent: SolsticeEvent? {
- SolsticeData.shared.nextEvent()
+ SolsticeData.shared.nextEvent(from: date)
}
func daysUntilNext() -> Int {
guard let next = nextEvent else { return 0 }
- return next.daysUntil()
+ return next.daysUntil(from: date)
}
func progressRatio() -> Double {
- guard let progress = SolsticeData.shared.progressToNextEvent() else { return 0.0 }
+ 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)
+ SolsticeData.shared.upcomingEvents(count: count, from: date)
}
static var preview: SolvervDef {
diff --git a/Solsnu.Widget/Solsnu_WidgetBundle.swift b/Solsnu.Widget/Solsnu_WidgetBundle.swift
index 8a7b718..7bd3f1a 100644
--- a/Solsnu.Widget/Solsnu_WidgetBundle.swift
+++ b/Solsnu.Widget/Solsnu_WidgetBundle.swift
@@ -13,11 +13,9 @@ struct Solsnu_WidgetBundle: WidgetBundle {
var body: some Widget {
Solsnu_Widget()
Solsnu_WidgetMedium()
- Solsnu_WidgetLarge()
}
}
-// Medium widget
struct Solsnu_WidgetMedium: Widget {
let kind: String = "Solsnu_Widget_Medium"
@@ -34,27 +32,6 @@ struct Solsnu_WidgetMedium: Widget {
}
.configurationDisplayName("Solstice Countdown")
.description("Days until next solstice or equinox")
- .supportedFamilies([.systemMedium])
- }
-}
-
-// Large widget
-struct Solsnu_WidgetLarge: Widget {
- let kind: String = "Solsnu_Widget_Large"
-
- var body: some WidgetConfiguration {
- StaticConfiguration(kind: kind, provider: Provider()) { entry in
- if #available(iOS 17.0, *) {
- LargeWidgetView(entry: entry)
- .containerBackground(.fill.tertiary, for: .widget)
- } else {
- LargeWidgetView(entry: entry)
- .padding()
- .background()
- }
- }
- .configurationDisplayName("Solstice Countdown")
- .description("Days until next solstice or equinox")
- .supportedFamilies([.systemLarge])
+ .supportedFamilies([.systemMedium,.systemSmall])
}
}
diff --git a/Solsnu.Widget/Solsnu_WidgetControl.swift b/Solsnu.Widget/Solsnu_WidgetControl.swift
index 62cd817..686a824 100644
--- a/Solsnu.Widget/Solsnu_WidgetControl.swift
+++ b/Solsnu.Widget/Solsnu_WidgetControl.swift
@@ -52,3 +52,4 @@ struct StartTimerIntent: SetValueIntent {
return .result()
}
}
+ \ No newline at end of file
diff --git a/Solsnu.Widget/Utilities/AppGroupManager.swift b/Solsnu.Widget/Utilities/AppGroupManager.swift
deleted file mode 100644
index 95ff2de..0000000
--- a/Solsnu.Widget/Utilities/AppGroupManager.swift
+++ /dev/null
@@ -1,67 +0,0 @@
-import Foundation
-
-class AppGroupManager {
- static let shared = AppGroupManager()
- static let appGroupID = "group.com.ivarlovlie.solverv"
-
- private lazy var userDefaults: UserDefaults? = {
- UserDefaults(suiteName: Self.appGroupID)
- }()
-
- // MARK: - Location Storage
-
- struct UserLocation: Codable {
- let latitude: Double
- let longitude: Double
- let timestamp: String // ISO 8601
- let isDefaultLocation: Bool
- }
-
- func saveLocation(_ location: UserLocation) {
- guard let ud = userDefaults else { return }
- if let encoded = try? JSONEncoder().encode(location) {
- ud.set(encoded, forKey: "userLocation")
- }
- }
-
- func getLocation() -> UserLocation? {
- guard let ud = userDefaults,
- let data = ud.data(forKey: "userLocation"),
- let location = try? JSONDecoder().decode(UserLocation.self, from: data) else {
- return nil
- }
- return location
- }
-
- // MARK: - Sunrise/Sunset Storage
-
- struct SunTimes: Codable {
- let date: String // ISO 8601 date only (YYYY-MM-DD)
- let sunrise: String // ISO 8601 datetime
- let sunset: String // ISO 8601 datetime
- let timestamp: String // ISO 8601 when calculated
- }
-
- func saveSunTimes(_ sunTimes: SunTimes) {
- guard let ud = userDefaults else { return }
- if let encoded = try? JSONEncoder().encode(sunTimes) {
- ud.set(encoded, forKey: "sunTimes")
- }
- }
-
- func getSunTimes() -> SunTimes? {
- guard let ud = userDefaults,
- let data = ud.data(forKey: "sunTimes"),
- let sunTimes = try? JSONDecoder().decode(SunTimes.self, from: data) else {
- return nil
- }
- return sunTimes
- }
-
- // MARK: - Helpers
-
- func clearAllData() {
- userDefaults?.removeObject(forKey: "userLocation")
- userDefaults?.removeObject(forKey: "sunTimes")
- }
-}
diff --git a/Solsnu.Widget/Utilities/SunTimes.swift b/Solsnu.Widget/Utilities/SunTimes.swift
deleted file mode 100644
index 8c7132e..0000000
--- a/Solsnu.Widget/Utilities/SunTimes.swift
+++ /dev/null
@@ -1,148 +0,0 @@
-import Foundation
-
-class SunTimes {
- let latitude: Double
- let longitude: Double
- let date: Date
-
- init(latitude: Double, longitude: Double, date: Date) {
- self.latitude = latitude
- self.longitude = longitude
- self.date = date
- }
-
- /// Calculate sunrise time for the location and date
- func sunrise() -> Date? {
- guard let result = calculateSunTimes() else { return nil }
- return result.sunrise
- }
-
- /// Calculate sunset time for the location and date
- func sunset() -> Date? {
- guard let result = calculateSunTimes() else { return nil }
- return result.sunset
- }
-
- // NOAA solar position algorithm
- // Reference: https://www.esrl.noaa.gov/gmd/grad/solcalc/
- private func calculateSunTimes() -> (sunrise: Date, sunset: Date)? {
- let calendar = Calendar(identifier: .gregorian)
- let components = calendar.dateComponents([.year, .month, .day], from: date)
- guard let year = components.year else {
- return nil
- }
-
- // Step 1: Calculate day of year
- let jan1 = calendar.date(from: DateComponents(year: year, month: 1, day: 1))!
- let dayOfYear = calendar.dateComponents([.day], from: jan1, to: date).day! + 1
-
- // Step 2: Fractional year in radians
- let daysInYear = Double(isLeapYear(year) ? 366 : 365)
- let gamma = 2.0 * Double.pi * Double(dayOfYear - 1) / daysInYear
-
- // Step 3: Solar declination (radians)
- let decl = 0.006918 - 0.399912 * cos(gamma) + 0.070257 * sin(gamma)
- - 0.006758 * cos(2.0 * gamma) + 0.000907 * sin(2.0 * gamma)
- - 0.002697 * cos(3.0 * gamma) + 0.00148 * sin(3.0 * gamma)
-
- // Step 4: Equation of time (minutes)
- let eot = 229.18 * (0.000075 + 0.001868 * cos(gamma) - 0.032077 * sin(gamma)
- - 0.014615 * cos(2.0 * gamma) - 0.040849 * sin(2.0 * gamma))
-
- // Step 5: Hour angle at sunrise/sunset
- let latRad = latitude * Double.pi / 180.0
- let cosH = -tan(latRad) * tan(decl)
-
- guard cosH >= -1.0 && cosH <= 1.0 else {
- // Sun is always up or always down at this latitude/date
- return nil
- }
-
- let h = acos(cosH) * 180.0 / Double.pi
-
- // Step 6: Solar noon in UTC (decimal hours)
- let solarNoonUTC = 12.0 - (longitude / 15.0) - (eot / 60.0)
-
- // Step 7: Sunrise and sunset in UTC (decimal hours)
- let sunriseUTC = solarNoonUTC - (h / 15.0)
- let sunsetUTC = solarNoonUTC + (h / 15.0)
-
- // Step 8: Convert to local time (add timezone offset)
- let tzOffset = Double(TimeZone.current.secondsFromGMT(for: date)) / 3600.0
- let sunriseLocal = sunriseUTC + tzOffset
- let sunsetLocal = sunsetUTC + tzOffset
-
- // Step 9: Create date components, handling day boundary crossing
- let currentCalendar = Calendar.current
- let baseComponents = currentCalendar.dateComponents([.year, .month, .day], from: date)
-
- // Sunrise
- var sunriseComp = baseComponents
- let srHourDouble = sunriseLocal
- let srHour = Int(srHourDouble)
- let srMinuteDouble = (srHourDouble - Double(srHour)) * 60.0
- let srMinute = Int(srMinuteDouble)
-
- if srHour < 0 {
- if let prevDay = currentCalendar.date(byAdding: .day, value: -1, to: date) {
- let prevComponents = currentCalendar.dateComponents([.year, .month, .day], from: prevDay)
- sunriseComp = prevComponents
- sunriseComp.hour = 24 + srHour
- } else {
- sunriseComp.hour = 0
- }
- } else if srHour >= 24 {
- if let nextDay = currentCalendar.date(byAdding: .day, value: 1, to: date) {
- let nextComponents = currentCalendar.dateComponents([.year, .month, .day], from: nextDay)
- sunriseComp = nextComponents
- sunriseComp.hour = srHour - 24
- } else {
- sunriseComp.hour = 23
- }
- } else {
- sunriseComp.hour = srHour
- }
- sunriseComp.minute = max(0, min(59, srMinute))
- sunriseComp.second = 0
-
- // Sunset
- var sunsetComp = baseComponents
- let ssHourDouble = sunsetLocal
- let ssHour = Int(ssHourDouble)
- let ssMinuteDouble = (ssHourDouble - Double(ssHour)) * 60.0
- let ssMinute = Int(ssMinuteDouble)
-
- if ssHour < 0 {
- if let prevDay = currentCalendar.date(byAdding: .day, value: -1, to: date) {
- let prevComponents = currentCalendar.dateComponents([.year, .month, .day], from: prevDay)
- sunsetComp = prevComponents
- sunsetComp.hour = 24 + ssHour
- } else {
- sunsetComp.hour = 0
- }
- } else if ssHour >= 24 {
- if let nextDay = currentCalendar.date(byAdding: .day, value: 1, to: date) {
- let nextComponents = currentCalendar.dateComponents([.year, .month, .day], from: nextDay)
- sunsetComp = nextComponents
- sunsetComp.hour = ssHour - 24
- } else {
- sunsetComp.hour = 23
- }
- } else {
- sunsetComp.hour = ssHour
- }
- sunsetComp.minute = max(0, min(59, ssMinute))
- sunsetComp.second = 0
-
- guard let sunriseDate = currentCalendar.date(from: sunriseComp),
- let sunsetDate = currentCalendar.date(from: sunsetComp) else {
- return nil
- }
-
- return (sunriseDate, sunsetDate)
- }
-
- private func isLeapYear(_ year: Int) -> Bool {
- return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
- }
-}
diff --git a/Solsnu.Widget/Views/SmallWidgetView.swift b/Solsnu.Widget/Views/SmallWidgetView.swift
index 86aa913..16ca102 100644
--- a/Solsnu.Widget/Views/SmallWidgetView.swift
+++ b/Solsnu.Widget/Views/SmallWidgetView.swift
@@ -10,22 +10,19 @@ struct SmallWidgetView: View {
var body: some View {
ZStack {
Image(entry.def.bg)
- .resizable()
- .scaledToFill()
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()
- if !entry.def.sunriseFormatted.isEmpty && !entry.def.sunsetFormatted.isEmpty {
HStack(spacing: 4) {
Text("↑ \(entry.def.sunriseFormatted)")
Text("↓ \(entry.def.sunsetFormatted)")
}
.font(.system(size: 11, weight: .regular))
.foregroundStyle(Color(red: 0.152, green: 0.136, blue: 0.056))
- }
}
}
.containerBackground(for: .widget, alignment: .center) { Color.clear }
diff --git a/Solverv.xcodeproj/project.pbxproj b/Solverv.xcodeproj/project.pbxproj
index 093609f..f5e3e64 100644
--- a/Solverv.xcodeproj/project.pbxproj
+++ b/Solverv.xcodeproj/project.pbxproj
@@ -7,20 +7,19 @@
objects = {
/* Begin PBXBuildFile section */
- A4D1E5F62BE4C16BF9C35920 /* Season.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAD0F6249013EFDE6CF3BDB /* Season.swift */; };
- BDE41CA8E12EF58194F0FB28 /* Season.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAD0F6249013EFDE6CF3BDB /* Season.swift */; };
0602B5968563FDCC6AAE214B /* SolsticeEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53B1F64353769F3F3D52DCC /* SolsticeEvent.swift */; };
- C1829841EA43782BD52878E9 /* SolsticeEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53B1F64353769F3F3D52DCC /* SolsticeEvent.swift */; };
+ 1B8629D62EF0C656005A1C75 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B8629D52EF0C656005A1C75 /* WidgetKit.framework */; };
+ 1B8629D82EF0C656005A1C75 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B8629D72EF0C656005A1C75 /* SwiftUI.framework */; };
+ 1B8629E52EF0C657005A1C75 /* Solsnu.WidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 1B8629D32EF0C656005A1C75 /* Solsnu.WidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
58EC7B50BC5232D031299280 /* SolsticeData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A3A45E5A25BDBCC47A23EC /* SolsticeData.swift */; };
- DB32B658D259B49C522786C8 /* SolsticeData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A3A45E5A25BDBCC47A23EC /* SolsticeData.swift */; };
+ 594A94559E2251F892BC158B /* SunTimes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4C2D0816DA7D3E6E6A4358 /* SunTimes.swift */; };
63374A5CD6AAFBBA4A5E87AC /* AppGroupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B12784AD1DE5013F9E3B677 /* AppGroupManager.swift */; };
6FFB4853DA8AF00282F20F96 /* AppGroupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B12784AD1DE5013F9E3B677 /* AppGroupManager.swift */; };
- 594A94559E2251F892BC158B /* SunTimes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4C2D0816DA7D3E6E6A4358 /* SunTimes.swift */; };
90AEA7599D196072837AE994 /* SunTimes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4C2D0816DA7D3E6E6A4358 /* SunTimes.swift */; };
-
- 1B8629D62EF0C656005A1C75 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B8629D52EF0C656005A1C75 /* WidgetKit.framework */; };
- 1B8629D82EF0C656005A1C75 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B8629D72EF0C656005A1C75 /* SwiftUI.framework */; };
- 1B8629E52EF0C657005A1C75 /* Solsnu.WidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 1B8629D32EF0C656005A1C75 /* Solsnu.WidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ A4D1E5F62BE4C16BF9C35920 /* Season.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAD0F6249013EFDE6CF3BDB /* Season.swift */; };
+ BDE41CA8E12EF58194F0FB28 /* Season.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAD0F6249013EFDE6CF3BDB /* Season.swift */; };
+ C1829841EA43782BD52878E9 /* SolsticeEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53B1F64353769F3F3D52DCC /* SolsticeEvent.swift */; };
+ DB32B658D259B49C522786C8 /* SolsticeData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A3A45E5A25BDBCC47A23EC /* SolsticeData.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -48,16 +47,15 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
- BAAD0F6249013EFDE6CF3BDB /* Season.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Season.swift; sourceTree = "<group>"; };
- A53B1F64353769F3F3D52DCC /* SolsticeEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolsticeEvent.swift; sourceTree = "<group>"; };
- 56A3A45E5A25BDBCC47A23EC /* SolsticeData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolsticeData.swift; sourceTree = "<group>"; };
- 3B12784AD1DE5013F9E3B677 /* AppGroupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroupManager.swift; sourceTree = "<group>"; };
- 5C4C2D0816DA7D3E6E6A4358 /* SunTimes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SunTimes.swift; sourceTree = "<group>"; };
-
1B8629BF2EF0C636005A1C75 /* Solverv.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Solverv.app; sourceTree = BUILT_PRODUCTS_DIR; };
1B8629D32EF0C656005A1C75 /* Solsnu.WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Solsnu.WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
1B8629D52EF0C656005A1C75 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
1B8629D72EF0C656005A1C75 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
+ 3B12784AD1DE5013F9E3B677 /* AppGroupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroupManager.swift; sourceTree = "<group>"; };
+ 56A3A45E5A25BDBCC47A23EC /* SolsticeData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolsticeData.swift; sourceTree = "<group>"; };
+ 5C4C2D0816DA7D3E6E6A4358 /* SunTimes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SunTimes.swift; sourceTree = "<group>"; };
+ A53B1F64353769F3F3D52DCC /* SolsticeEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolsticeEvent.swift; sourceTree = "<group>"; };
+ BAAD0F6249013EFDE6CF3BDB /* Season.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Season.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -106,35 +104,6 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
- 0BFB6DC6E4F437012CF2990E /* Models */ = {
- isa = PBXGroup;
- children = (
- BAAD0F6249013EFDE6CF3BDB /* Season.swift */,
- A53B1F64353769F3F3D52DCC /* SolsticeEvent.swift */,
- 56A3A45E5A25BDBCC47A23EC /* SolsticeData.swift */,
- );
- path = Models;
- sourceTree = "<group>";
- };
- 5F48ADA595F6B3DE3DFE2A32 /* Utilities */ = {
- isa = PBXGroup;
- children = (
- 3B12784AD1DE5013F9E3B677 /* AppGroupManager.swift */,
- 5C4C2D0816DA7D3E6E6A4358 /* SunTimes.swift */,
- );
- path = Utilities;
- sourceTree = "<group>";
- };
- 0EEEC5869B3AB7A2A2154C10 /* Shared */ = {
- isa = PBXGroup;
- children = (
- 0BFB6DC6E4F437012CF2990E /* Models */,
- 5F48ADA595F6B3DE3DFE2A32 /* Utilities */,
- );
- path = Shared;
- sourceTree = "<group>";
- };
-
1B8629B62EF0C636005A1C75 = {
isa = PBXGroup;
children = (
@@ -142,6 +111,7 @@
1B8629D92EF0C656005A1C75 /* Solsnu.Widget */,
1B8629D42EF0C656005A1C75 /* Frameworks */,
1B8629C02EF0C636005A1C75 /* Products */,
+ 1BC006B72FABC8FE009BB0E6 /* Recovered References */,
);
sourceTree = "<group>";
};
@@ -163,6 +133,18 @@
name = Frameworks;
sourceTree = "<group>";
};
+ 1BC006B72FABC8FE009BB0E6 /* Recovered References */ = {
+ isa = PBXGroup;
+ children = (
+ BAAD0F6249013EFDE6CF3BDB /* Season.swift */,
+ A53B1F64353769F3F3D52DCC /* SolsticeEvent.swift */,
+ 56A3A45E5A25BDBCC47A23EC /* SolsticeData.swift */,
+ 3B12784AD1DE5013F9E3B677 /* AppGroupManager.swift */,
+ 5C4C2D0816DA7D3E6E6A4358 /* SunTimes.swift */,
+ );
+ name = "Recovered References";
+ sourceTree = "<group>";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -272,24 +254,24 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- A4D1E5F62BE4C16BF9C35920 /* Season.swift in Sources */,
+ A4D1E5F62BE4C16BF9C35920 /* Season.swift in Sources */,
0602B5968563FDCC6AAE214B /* SolsticeEvent.swift in Sources */,
58EC7B50BC5232D031299280 /* SolsticeData.swift in Sources */,
63374A5CD6AAFBBA4A5E87AC /* AppGroupManager.swift in Sources */,
594A94559E2251F892BC158B /* SunTimes.swift in Sources */,
-);
+ );
runOnlyForDeploymentPostprocessing = 0;
};
1B8629CF2EF0C656005A1C75 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- BDE41CA8E12EF58194F0FB28 /* Season.swift in Sources */,
+ BDE41CA8E12EF58194F0FB28 /* Season.swift in Sources */,
C1829841EA43782BD52878E9 /* SolsticeEvent.swift in Sources */,
DB32B658D259B49C522786C8 /* SolsticeData.swift in Sources */,
6FFB4853DA8AF00282F20F96 /* AppGroupManager.swift in Sources */,
90AEA7599D196072837AE994 /* SunTimes.swift in Sources */,
-);
+ );
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
diff --git a/Solverv.xcodeproj/xcuserdata/ivarlovlie.xcuserdatad/xcschemes/xcschememanagement.plist b/Solverv.xcodeproj/xcuserdata/ivarlovlie.xcuserdatad/xcschemes/xcschememanagement.plist
index d8ecd4f..f76564c 100644
--- a/Solverv.xcodeproj/xcuserdata/ivarlovlie.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/Solverv.xcodeproj/xcuserdata/ivarlovlie.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -7,7 +7,7 @@
<key>Solsnu.WidgetExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
- <integer>1</integer>
+ <integer>0</integer>
</dict>
<key>Solverv.xcscheme_^#shared#^_</key>
<dict>
diff --git a/Solverv/Models/Season.swift b/Solverv/Models/Season.swift
deleted file mode 100644
index 01eaf99..0000000
--- a/Solverv/Models/Season.swift
+++ /dev/null
@@ -1,49 +0,0 @@
-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/Solverv/Models/SolsticeData.swift b/Solverv/Models/SolsticeData.swift
deleted file mode 100644
index 5a36da7..0000000
--- a/Solverv/Models/SolsticeData.swift
+++ /dev/null
@@ -1,114 +0,0 @@
-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/Solverv/Models/SolsticeEvent.swift b/Solverv/Models/SolsticeEvent.swift
deleted file mode 100644
index d8c4a7b..0000000
--- a/Solverv/Models/SolsticeEvent.swift
+++ /dev/null
@@ -1,38 +0,0 @@
-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/Solverv/SolvervApp.swift b/Solverv/SolvervApp.swift
index a2beb1d..955a946 100644
--- a/Solverv/SolvervApp.swift
+++ b/Solverv/SolvervApp.swift
@@ -52,15 +52,6 @@ class LocationManager: NSObject, CLLocationManagerDelegate, ObservableObject {
manager.requestWhenInUseAuthorization()
} else if manager.authorizationStatus == .authorizedWhenInUse || manager.authorizationStatus == .authorizedAlways {
fetchLocation()
- } else {
- // Use default location (Greenwich)
- let defaultLocation = AppGroupManager.UserLocation(
- latitude: 0.0,
- longitude: 0.0,
- timestamp: ISO8601DateFormatter().string(from: Date()),
- isDefaultLocation: true
- )
- AppGroupManager.shared.saveLocation(defaultLocation)
}
}
@@ -83,6 +74,7 @@ class LocationManager: NSObject, CLLocationManagerDelegate, ObservableObject {
timestamp: ISO8601DateFormatter().string(from: Date()),
isDefaultLocation: false
)
+
AppGroupManager.shared.saveLocation(userLocation)
// Calculate and cache sunrise/sunset
diff --git a/Solverv/Utilities/AppGroupManager.swift b/Solverv/Utilities/AppGroupManager.swift
deleted file mode 100644
index 95ff2de..0000000
--- a/Solverv/Utilities/AppGroupManager.swift
+++ /dev/null
@@ -1,67 +0,0 @@
-import Foundation
-
-class AppGroupManager {
- static let shared = AppGroupManager()
- static let appGroupID = "group.com.ivarlovlie.solverv"
-
- private lazy var userDefaults: UserDefaults? = {
- UserDefaults(suiteName: Self.appGroupID)
- }()
-
- // MARK: - Location Storage
-
- struct UserLocation: Codable {
- let latitude: Double
- let longitude: Double
- let timestamp: String // ISO 8601
- let isDefaultLocation: Bool
- }
-
- func saveLocation(_ location: UserLocation) {
- guard let ud = userDefaults else { return }
- if let encoded = try? JSONEncoder().encode(location) {
- ud.set(encoded, forKey: "userLocation")
- }
- }
-
- func getLocation() -> UserLocation? {
- guard let ud = userDefaults,
- let data = ud.data(forKey: "userLocation"),
- let location = try? JSONDecoder().decode(UserLocation.self, from: data) else {
- return nil
- }
- return location
- }
-
- // MARK: - Sunrise/Sunset Storage
-
- struct SunTimes: Codable {
- let date: String // ISO 8601 date only (YYYY-MM-DD)
- let sunrise: String // ISO 8601 datetime
- let sunset: String // ISO 8601 datetime
- let timestamp: String // ISO 8601 when calculated
- }
-
- func saveSunTimes(_ sunTimes: SunTimes) {
- guard let ud = userDefaults else { return }
- if let encoded = try? JSONEncoder().encode(sunTimes) {
- ud.set(encoded, forKey: "sunTimes")
- }
- }
-
- func getSunTimes() -> SunTimes? {
- guard let ud = userDefaults,
- let data = ud.data(forKey: "sunTimes"),
- let sunTimes = try? JSONDecoder().decode(SunTimes.self, from: data) else {
- return nil
- }
- return sunTimes
- }
-
- // MARK: - Helpers
-
- func clearAllData() {
- userDefaults?.removeObject(forKey: "userLocation")
- userDefaults?.removeObject(forKey: "sunTimes")
- }
-}
diff --git a/Solverv/Utilities/SunTimes.swift b/Solverv/Utilities/SunTimes.swift
deleted file mode 100644
index 3b8f049..0000000
--- a/Solverv/Utilities/SunTimes.swift
+++ /dev/null
@@ -1,148 +0,0 @@
-import Foundation
-
-class SunTimes {
- let latitude: Double
- let longitude: Double
- let date: Date
-
- init(latitude: Double, longitude: Double, date: Date) {
- self.latitude = latitude
- self.longitude = longitude
- self.date = date
- }
-
- /// Calculate sunrise time for the location and date
- func sunrise() -> Date? {
- guard let result = calculateSunTimes() else { return nil }
- return result.sunrise
- }
-
- /// Calculate sunset time for the location and date
- func sunset() -> Date? {
- guard let result = calculateSunTimes() else { return nil }
- return result.sunset
- }
-
- // NOAA solar position algorithm
- // Reference: https://www.esrl.noaa.gov/gmd/grad/solcalc/
- private func calculateSunTimes() -> (sunrise: Date, sunset: Date)? {
- let calendar = Calendar(identifier: .gregorian)
- let components = calendar.dateComponents([.year, .month, .day], from: date)
- guard let year = components.year else {
- return nil
- }
-
- // Step 1: Calculate day of year
- let jan1 = calendar.date(from: DateComponents(year: year, month: 1, day: 1))!
- let dayOfYear = calendar.dateComponents([.day], from: jan1, to: date).day! + 1
-
- // Step 2: Fractional year in radians
- let daysInYear = Double(isLeapYear(year) ? 366 : 365)
- let gamma = 2.0 * Double.pi * Double(dayOfYear - 1) / daysInYear
-
- // Step 3: Solar declination (radians)
- let decl = 0.006918 - 0.399912 * cos(gamma) + 0.070257 * sin(gamma)
- - 0.006758 * cos(2.0 * gamma) + 0.000907 * sin(2.0 * gamma)
- - 0.002697 * cos(3.0 * gamma) + 0.00148 * sin(3.0 * gamma)
-
- // Step 4: Equation of time (minutes)
- let eot = 229.18 * (0.000075 + 0.001868 * cos(gamma) - 0.032077 * sin(gamma)
- - 0.014615 * cos(2.0 * gamma) - 0.040849 * sin(2.0 * gamma))
-
- // Step 5: Hour angle at sunrise/sunset
- let latRad = latitude * Double.pi / 180.0
- let cosH = -tan(latRad) * tan(decl)
-
- guard cosH >= -1.0 && cosH <= 1.0 else {
- // Sun is always up or always down at this latitude/date
- return nil
- }
-
- let h = acos(cosH) * 180.0 / Double.pi
-
- // Step 6: Solar noon in UTC (decimal hours)
- let solarNoonUTC = 12.0 - (longitude / 15.0) - (eot / 60.0)
-
- // Step 7: Sunrise and sunset in UTC (decimal hours)
- let sunriseUTC = solarNoonUTC - (h / 15.0)
- let sunsetUTC = solarNoonUTC + (h / 15.0)
-
- // Step 8: Convert to local time (add timezone offset)
- let tzOffset = Double(TimeZone.current.secondsFromGMT(for: date)) / 3600.0
- let sunriseLocal = sunriseUTC + tzOffset
- let sunsetLocal = sunsetUTC + tzOffset
-
- // Step 9: Create date components, handling day boundary crossing
- let calendar2 = Calendar.current
- let baseComponents = calendar2.dateComponents([.year, .month, .day], from: date)
-
- // Sunrise
- var sunriseComp = baseComponents
- let srHourDouble = sunriseLocal
- let srHour = Int(srHourDouble)
- let srMinuteDouble = (srHourDouble - Double(srHour)) * 60.0
- let srMinute = Int(srMinuteDouble)
-
- if srHour < 0 {
- if let prevDay = calendar2.date(byAdding: .day, value: -1, to: date) {
- let prevComponents = calendar2.dateComponents([.year, .month, .day], from: prevDay)
- sunriseComp = prevComponents
- sunriseComp.hour = 24 + srHour
- } else {
- sunriseComp.hour = 0
- }
- } else if srHour >= 24 {
- if let nextDay = calendar2.date(byAdding: .day, value: 1, to: date) {
- let nextComponents = calendar2.dateComponents([.year, .month, .day], from: nextDay)
- sunriseComp = nextComponents
- sunriseComp.hour = srHour - 24
- } else {
- sunriseComp.hour = 23
- }
- } else {
- sunriseComp.hour = srHour
- }
- sunriseComp.minute = max(0, min(59, srMinute))
- sunriseComp.second = 0
-
- // Sunset
- var sunsetComp = baseComponents
- let ssHourDouble = sunsetLocal
- let ssHour = Int(ssHourDouble)
- let ssMinuteDouble = (ssHourDouble - Double(ssHour)) * 60.0
- let ssMinute = Int(ssMinuteDouble)
-
- if ssHour < 0 {
- if let prevDay = calendar2.date(byAdding: .day, value: -1, to: date) {
- let prevComponents = calendar2.dateComponents([.year, .month, .day], from: prevDay)
- sunsetComp = prevComponents
- sunsetComp.hour = 24 + ssHour
- } else {
- sunsetComp.hour = 0
- }
- } else if ssHour >= 24 {
- if let nextDay = calendar2.date(byAdding: .day, value: 1, to: date) {
- let nextComponents = calendar2.dateComponents([.year, .month, .day], from: nextDay)
- sunsetComp = nextComponents
- sunsetComp.hour = ssHour - 24
- } else {
- sunsetComp.hour = 23
- }
- } else {
- sunsetComp.hour = ssHour
- }
- sunsetComp.minute = max(0, min(59, ssMinute))
- sunsetComp.second = 0
-
- guard let sunriseDate = calendar2.date(from: sunriseComp),
- let sunsetDate = calendar2.date(from: sunsetComp) else {
- return nil
- }
-
- return (sunriseDate, sunsetDate)
- }
-
- private func isLeapYear(_ year: Int) -> Bool {
- return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
- }
-}