mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Add Sunrise widget and update the icons
This commit is contained in:
parent
f7242e85e8
commit
eddca8b4dd
5 changed files with 138 additions and 9 deletions
|
|
@ -18478,6 +18478,10 @@
|
|||
},
|
||||
"Style" : {
|
||||
|
||||
},
|
||||
"Sunrise at %@" : {
|
||||
"comment" : "A view representing the sunrise time, displayed in a horizontal stack with an icon and the time itself.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Support Us" : {
|
||||
"extractionState" : "stale",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,13 @@ struct LockScreenWeatherWidget: View {
|
|||
private let inlinePrimaryFont = Font.system(size: 22, weight: .semibold, design: .rounded)
|
||||
private let inlineSecondaryFont = Font.system(size: 13, weight: .medium, design: .rounded)
|
||||
private let secondaryLabelColor = Color.white.opacity(0.7)
|
||||
private static let sunriseFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .none
|
||||
formatter.timeStyle = .short
|
||||
formatter.locale = .current
|
||||
return formatter
|
||||
}()
|
||||
|
||||
private var isInline: Bool { snapshot.widgetStyle == .inline }
|
||||
private var stackAlignment: VerticalAlignment { isInline ? .firstTextBaseline : .top }
|
||||
|
|
@ -62,12 +69,21 @@ struct LockScreenWeatherWidget: View {
|
|||
|
||||
weatherSegment
|
||||
|
||||
if snapshot.showsSunrise, let sunriseText = sunriseTimeText {
|
||||
sunriseSegment(text: sunriseText)
|
||||
}
|
||||
|
||||
if shouldShowLocation {
|
||||
locationSegment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var sunriseTimeText: String? {
|
||||
guard let sunrise = snapshot.sunCycle?.sunrise else { return nil }
|
||||
return Self.sunriseFormatter.string(from: sunrise)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var weatherSegment: some View {
|
||||
switch snapshot.widgetStyle {
|
||||
|
|
@ -346,6 +362,20 @@ struct LockScreenWeatherWidget: View {
|
|||
.layoutPriority(0.7)
|
||||
}
|
||||
|
||||
private func sunriseSegment(text: String) -> some View {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
||||
Image(systemName: "sunrise.fill")
|
||||
.font(.system(size: 20, weight: .semibold))
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text(text)
|
||||
.font(inlinePrimaryFont)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.85)
|
||||
}
|
||||
.layoutPriority(0.8)
|
||||
.accessibilityLabel("Sunrise at \(text)")
|
||||
}
|
||||
|
||||
private var shouldShowLocation: Bool {
|
||||
snapshot.showsLocation && (snapshot.locationName?.isEmpty == false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2974,6 +2974,7 @@ struct LockScreenSettings: View {
|
|||
@Default(.lockScreenWeatherShowsCharging) private var lockScreenWeatherShowsCharging
|
||||
@Default(.lockScreenWeatherShowsBatteryGauge) private var lockScreenWeatherShowsBatteryGauge
|
||||
@Default(.lockScreenWeatherShowsAQI) private var lockScreenWeatherShowsAQI
|
||||
@Default(.lockScreenWeatherShowsSunrise) private var lockScreenWeatherShowsSunrise
|
||||
@Default(.lockScreenWeatherAQIScale) private var lockScreenWeatherAQIScale
|
||||
|
||||
private func highlightID(_ title: String) -> String {
|
||||
|
|
@ -3089,6 +3090,10 @@ struct LockScreenSettings: View {
|
|||
.settingsHighlight(id: highlightID("Show charging percentage"))
|
||||
}
|
||||
|
||||
Defaults.Toggle("Show sunrise time", key: .lockScreenWeatherShowsSunrise)
|
||||
.disabled(lockScreenWeatherWidgetStyle != .inline)
|
||||
.settingsHighlight(id: highlightID("Show sunrise time"))
|
||||
|
||||
Defaults.Toggle("Show battery indicator", key: .lockScreenWeatherShowsBatteryGauge)
|
||||
.settingsHighlight(id: highlightID("Show battery indicator"))
|
||||
|
||||
|
|
|
|||
|
|
@ -107,6 +107,9 @@ final class LockScreenWeatherManager: ObservableObject {
|
|||
let showsBatteryGauge = Defaults[.lockScreenWeatherShowsBatteryGauge]
|
||||
let widgetStyle = Defaults[.lockScreenWeatherWidgetStyle]
|
||||
let batteryInfo = showsBatteryGauge ? makeBatteryGaugeInfo(isCharging: chargingInfo != nil, widgetStyle: widgetStyle) : nil
|
||||
let showsSunriseSetting = Defaults[.lockScreenWeatherShowsSunrise]
|
||||
let sunCycle = snapshot?.sunCycle
|
||||
let shouldShowSunrise = showsSunriseSetting && widgetStyle == .inline && sunCycle?.sunrise != nil
|
||||
let fallback = LockScreenWeatherSnapshot(
|
||||
temperatureText: snapshot?.temperatureText ?? "--",
|
||||
symbolName: snapshot?.symbolName ?? "cloud.fill",
|
||||
|
|
@ -120,7 +123,9 @@ final class LockScreenWeatherManager: ObservableObject {
|
|||
widgetStyle: widgetStyle,
|
||||
showsChargingPercentage: Defaults[.lockScreenWeatherShowsChargingPercentage],
|
||||
temperatureInfo: snapshot?.temperatureInfo,
|
||||
usesGaugeTint: Defaults[.lockScreenWeatherUsesGaugeTint]
|
||||
usesGaugeTint: Defaults[.lockScreenWeatherUsesGaugeTint],
|
||||
sunCycle: sunCycle,
|
||||
showsSunrise: shouldShowSunrise
|
||||
)
|
||||
|
||||
self.snapshot = fallback
|
||||
|
|
@ -192,6 +197,8 @@ final class LockScreenWeatherManager: ObservableObject {
|
|||
.map { _ in () }.eraseToAnyPublisher(),
|
||||
Defaults.publisher(.lockScreenWeatherBatteryUsesLaptopSymbol, options: [])
|
||||
.map { _ in () }.eraseToAnyPublisher(),
|
||||
Defaults.publisher(.lockScreenWeatherShowsSunrise, options: [])
|
||||
.map { _ in () }.eraseToAnyPublisher(),
|
||||
Defaults.publisher(.lockScreenWeatherWidgetStyle, options: [])
|
||||
.map { _ in () }.eraseToAnyPublisher(),
|
||||
Defaults.publisher(.lockScreenWeatherTemperatureUnit, options: [])
|
||||
|
|
@ -294,6 +301,8 @@ final class LockScreenWeatherManager: ObservableObject {
|
|||
let airQualityInfo = (Defaults[.lockScreenWeatherShowsAQI] && providerSource.supportsAirQuality) ? payload.airQuality : nil
|
||||
let batteryInfo = Defaults[.lockScreenWeatherShowsBatteryGauge] ? makeBatteryGaugeInfo(isCharging: chargingInfo != nil, widgetStyle: widgetStyle) : nil
|
||||
let usesGaugeTint = Defaults[.lockScreenWeatherUsesGaugeTint]
|
||||
let showsSunriseSetting = Defaults[.lockScreenWeatherShowsSunrise]
|
||||
let shouldShowSunrise = showsSunriseSetting && widgetStyle == .inline && payload.sunCycle?.sunrise != nil
|
||||
|
||||
return LockScreenWeatherSnapshot(
|
||||
temperatureText: payload.temperatureText,
|
||||
|
|
@ -308,7 +317,9 @@ final class LockScreenWeatherManager: ObservableObject {
|
|||
widgetStyle: widgetStyle,
|
||||
showsChargingPercentage: showsChargingPercentage,
|
||||
temperatureInfo: payload.temperatureInfo,
|
||||
usesGaugeTint: usesGaugeTint
|
||||
usesGaugeTint: usesGaugeTint,
|
||||
sunCycle: payload.sunCycle,
|
||||
showsSunrise: shouldShowSunrise
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -384,6 +395,11 @@ final class LockScreenWeatherManager: ObservableObject {
|
|||
}
|
||||
|
||||
struct LockScreenWeatherSnapshot: Equatable {
|
||||
struct SunCycleInfo: Equatable {
|
||||
let sunrise: Date?
|
||||
let sunset: Date?
|
||||
}
|
||||
|
||||
struct TemperatureInfo: Equatable {
|
||||
let current: Double
|
||||
let minimum: Double?
|
||||
|
|
@ -487,6 +503,8 @@ struct LockScreenWeatherSnapshot: Equatable {
|
|||
let showsChargingPercentage: Bool
|
||||
let temperatureInfo: TemperatureInfo?
|
||||
let usesGaugeTint: Bool
|
||||
let sunCycle: SunCycleInfo?
|
||||
let showsSunrise: Bool
|
||||
}
|
||||
|
||||
extension LockScreenWeatherSnapshot.AirQualityInfo.Category {
|
||||
|
|
@ -615,7 +633,9 @@ private actor LockScreenWeatherProvider {
|
|||
)
|
||||
|
||||
let code = Int(condition.weatherCode) ?? 113
|
||||
let symbol = WeatherSymbolMapper.symbol(for: code)
|
||||
let isDaytime = condition.isDaytime ?? true
|
||||
let baseSymbol = WeatherSymbolMapper.symbol(for: code)
|
||||
let symbol = symbolAdjustedForDaylight(baseSymbol, isDaytime: isDaytime)
|
||||
let description = condition.localizedDescription
|
||||
|
||||
let nearest = payload.nearestArea.first
|
||||
|
|
@ -646,7 +666,9 @@ private actor LockScreenWeatherProvider {
|
|||
widgetStyle: .inline,
|
||||
showsChargingPercentage: true,
|
||||
temperatureInfo: temperatureInfo,
|
||||
usesGaugeTint: true
|
||||
usesGaugeTint: true,
|
||||
sunCycle: nil,
|
||||
showsSunrise: false
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -660,9 +682,9 @@ private actor LockScreenWeatherProvider {
|
|||
var weatherQueryItems: [URLQueryItem] = [
|
||||
URLQueryItem(name: "latitude", value: latitude),
|
||||
URLQueryItem(name: "longitude", value: longitude),
|
||||
URLQueryItem(name: "current", value: "temperature_2m,weather_code,pressure_msl"),
|
||||
URLQueryItem(name: "daily", value: "temperature_2m_max,temperature_2m_min"),
|
||||
URLQueryItem(name: "forecast_days", value: "1"),
|
||||
URLQueryItem(name: "current", value: "temperature_2m,weather_code,is_day,pressure_msl"),
|
||||
URLQueryItem(name: "daily", value: "temperature_2m_max,temperature_2m_min,sunrise,sunset"),
|
||||
URLQueryItem(name: "forecast_days", value: "2"),
|
||||
URLQueryItem(name: "timezone", value: "auto")
|
||||
]
|
||||
|
||||
|
|
@ -692,9 +714,21 @@ private actor LockScreenWeatherProvider {
|
|||
let temperatureText = "\(Int(round(temperatureValue)))°"
|
||||
let code = current.weatherCode ?? 0
|
||||
let mapping = OpenMeteoSymbolMapper.mapping(for: code)
|
||||
let isDaytime = (current.isDay ?? 1) == 1
|
||||
let symbolName = symbolAdjustedForDaylight(mapping.symbol, isDaytime: isDaytime)
|
||||
let unitSymbol = unit.symbol
|
||||
let minTempValue = weatherPayload.daily?.temperature2MMin?.first
|
||||
let maxTempValue = weatherPayload.daily?.temperature2MMax?.first
|
||||
let timezoneIdentifier = weatherPayload.timezone
|
||||
let utcOffsetSeconds = weatherPayload.utcOffsetSeconds
|
||||
let sunriseDate = nextSunEvent(from: weatherPayload.daily?.sunrise, timezoneIdentifier: timezoneIdentifier, offsetSeconds: utcOffsetSeconds)
|
||||
let sunsetDate = nextSunEvent(from: weatherPayload.daily?.sunset, timezoneIdentifier: timezoneIdentifier, offsetSeconds: utcOffsetSeconds)
|
||||
let sunCycle: LockScreenWeatherSnapshot.SunCycleInfo?
|
||||
if sunriseDate != nil || sunsetDate != nil {
|
||||
sunCycle = LockScreenWeatherSnapshot.SunCycleInfo(sunrise: sunriseDate, sunset: sunsetDate)
|
||||
} else {
|
||||
sunCycle = nil
|
||||
}
|
||||
|
||||
let temperatureInfo = LockScreenWeatherSnapshot.TemperatureInfo(
|
||||
current: temperatureValue,
|
||||
|
|
@ -712,7 +746,7 @@ private actor LockScreenWeatherProvider {
|
|||
|
||||
return LockScreenWeatherSnapshot(
|
||||
temperatureText: temperatureText,
|
||||
symbolName: mapping.symbol,
|
||||
symbolName: symbolName,
|
||||
description: mapping.description,
|
||||
locationName: nil,
|
||||
charging: nil,
|
||||
|
|
@ -723,7 +757,9 @@ private actor LockScreenWeatherProvider {
|
|||
widgetStyle: .inline,
|
||||
showsChargingPercentage: true,
|
||||
temperatureInfo: temperatureInfo,
|
||||
usesGaugeTint: true
|
||||
usesGaugeTint: true,
|
||||
sunCycle: sunCycle,
|
||||
showsSunrise: false
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -771,6 +807,30 @@ private actor LockScreenWeatherProvider {
|
|||
scale: scale
|
||||
)
|
||||
}
|
||||
|
||||
private func nextSunEvent(from values: [String]?, timezoneIdentifier: String?, offsetSeconds: Int?) -> Date? {
|
||||
guard let values else { return nil }
|
||||
let now = Date()
|
||||
for value in values {
|
||||
if let date = parseLocalSunTime(value, timezoneIdentifier: timezoneIdentifier, offsetSeconds: offsetSeconds), date >= now {
|
||||
return date
|
||||
}
|
||||
}
|
||||
guard let fallbackValue = values.last else { return nil }
|
||||
return parseLocalSunTime(fallbackValue, timezoneIdentifier: timezoneIdentifier, offsetSeconds: offsetSeconds)
|
||||
}
|
||||
|
||||
private func parseLocalSunTime(_ value: String, timezoneIdentifier: String?, offsetSeconds: Int?) -> Date? {
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm"
|
||||
if let identifier = timezoneIdentifier, let timeZone = TimeZone(identifier: identifier) {
|
||||
formatter.timeZone = timeZone
|
||||
} else if let offset = offsetSeconds, let timeZone = TimeZone(secondsFromGMT: offset) {
|
||||
formatter.timeZone = timeZone
|
||||
}
|
||||
return formatter.date(from: value)
|
||||
}
|
||||
}
|
||||
|
||||
enum WeatherProviderError: Error {
|
||||
|
|
@ -799,6 +859,7 @@ private struct WTTRCurrentCondition: Decodable {
|
|||
case pressure = "pressure"
|
||||
case pressureInches = "pressureInches"
|
||||
case airQuality = "air_quality"
|
||||
case isday = "isday"
|
||||
}
|
||||
|
||||
let tempC: String
|
||||
|
|
@ -809,6 +870,7 @@ private struct WTTRCurrentCondition: Decodable {
|
|||
let pressure: String?
|
||||
let pressureInches: String?
|
||||
let airQuality: WTTRAirQuality?
|
||||
let isday: String?
|
||||
|
||||
var localizedDescription: String {
|
||||
if let english = langEn?.first?.value, !english.isEmpty {
|
||||
|
|
@ -819,6 +881,12 @@ private struct WTTRCurrentCondition: Decodable {
|
|||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var isDaytime: Bool? {
|
||||
guard let isday else { return nil }
|
||||
let normalized = isday.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
return normalized == "1" || normalized == "yes"
|
||||
}
|
||||
}
|
||||
|
||||
private struct WTTRTextValue: Decodable {
|
||||
|
|
@ -885,14 +953,19 @@ private struct OpenMeteoForecastResponse: Decodable {
|
|||
let temperature2M: Double?
|
||||
let weatherCode: Int?
|
||||
let pressureMsl: Double?
|
||||
let isDay: Int?
|
||||
}
|
||||
|
||||
let current: Current?
|
||||
let daily: Daily?
|
||||
let timezone: String?
|
||||
let utcOffsetSeconds: Int?
|
||||
|
||||
struct Daily: Decodable {
|
||||
let temperature2MMax: [Double]?
|
||||
let temperature2MMin: [Double]?
|
||||
let sunrise: [String]?
|
||||
let sunset: [String]?
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -968,6 +1041,22 @@ private enum WeatherSymbolMapper {
|
|||
}
|
||||
}
|
||||
|
||||
private func symbolAdjustedForDaylight(_ symbol: String, isDaytime: Bool) -> String {
|
||||
guard !isDaytime else { return symbol }
|
||||
switch symbol {
|
||||
case "sun.max.fill":
|
||||
return "moon.stars.fill"
|
||||
case "cloud.sun.fill":
|
||||
return "cloud.moon.fill"
|
||||
case "cloud.sun.rain.fill":
|
||||
return "cloud.moon.rain.fill"
|
||||
case "cloud.sun.bolt.fill":
|
||||
return "cloud.moon.bolt.fill"
|
||||
default:
|
||||
return symbol
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class LockScreenWeatherLocationProvider: NSObject, CLLocationManagerDelegate {
|
||||
private let manager: CLLocationManager
|
||||
|
|
|
|||
|
|
@ -466,6 +466,7 @@ extension Defaults.Keys {
|
|||
static let lockScreenWeatherShowsBluetooth = Key<Bool>("lockScreenWeatherShowsBluetooth", default: true)
|
||||
static let lockScreenWeatherShowsBatteryGauge = Key<Bool>("lockScreenWeatherShowsBatteryGauge", default: true)
|
||||
static let lockScreenWeatherBatteryUsesLaptopSymbol = Key<Bool>("lockScreenWeatherBatteryUsesLaptopSymbol", default: true)
|
||||
static let lockScreenWeatherShowsSunrise = Key<Bool>("lockScreenWeatherShowsSunrise", default: true)
|
||||
static let lockScreenWeatherWidgetStyle = Key<LockScreenWeatherWidgetStyle>("lockScreenWeatherWidgetStyle", default: .circular)
|
||||
static let lockScreenWeatherTemperatureUnit = Key<LockScreenWeatherTemperatureUnit>("lockScreenWeatherTemperatureUnit", default: .celsius)
|
||||
static let lockScreenWeatherShowsAQI = Key<Bool>("lockScreenWeatherShowsAQI", default: true)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue