mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Merge pull request #101 from Ebullioscopic/feature/lock-screen
Update EAQI and Macbook Icon
This commit is contained in:
commit
583e244e8a
6 changed files with 205 additions and 42 deletions
|
|
@ -729,6 +729,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"%@ · %@" : {
|
||||||
|
"comment" : "A secondary line of text that provides additional information about the air quality index, including its scale label and category name.",
|
||||||
|
"isCommentAutoGenerated" : true,
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "new",
|
||||||
|
"value" : "%1$@ · %2$@"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"%@ Models" : {
|
"%@ Models" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
|
|
@ -2342,6 +2354,9 @@
|
||||||
},
|
},
|
||||||
"Air quality requires the Open Meteo provider." : {
|
"Air quality requires the Open Meteo provider." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Air quality scale" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"AirDrop" : {
|
"AirDrop" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -3185,10 +3200,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AQI · %@" : {
|
|
||||||
"comment" : "A label displaying the air quality index and its corresponding description. The argument is the air quality index.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Are you sure you want to delete \"%@\"?" : {
|
"Are you sure you want to delete \"%@\"?" : {
|
||||||
"comment" : "A confirmation prompt asking the user to confirm the deletion of a custom idle animation. The placeholder text inside the quotation marks should be replaced with the name of the animation.",
|
"comment" : "A confirmation prompt asking the user to confirm the deletion of a custom idle animation. The placeholder text inside the quotation marks should be replaced with the name of the animation.",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -10877,6 +10888,9 @@
|
||||||
},
|
},
|
||||||
"Mac battery at %d percent" : {
|
"Mac battery at %d percent" : {
|
||||||
"comment" : "Mac battery gauge accessibility label"
|
"comment" : "Mac battery gauge accessibility label"
|
||||||
|
},
|
||||||
|
"MacBook icon is available in the circular layout." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Made ❤️ by Ebullioscopic" : {
|
"Made ❤️ by Ebullioscopic" : {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -158,12 +158,13 @@ struct LockScreenWeatherWidget: View {
|
||||||
|
|
||||||
private func circularBatterySegment(for info: LockScreenWeatherSnapshot.BatteryInfo) -> some View {
|
private func circularBatterySegment(for info: LockScreenWeatherSnapshot.BatteryInfo) -> some View {
|
||||||
let level = clampedBatteryLevel(info.batteryLevel)
|
let level = clampedBatteryLevel(info.batteryLevel)
|
||||||
|
let symbolName = info.usesLaptopSymbol ? "laptopcomputer" : batteryIconName(for: level)
|
||||||
|
|
||||||
return VStack(spacing: 6) {
|
return VStack(spacing: 6) {
|
||||||
Gauge(value: Double(level), in: 0...100) {
|
Gauge(value: Double(level), in: 0...100) {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
} currentValueLabel: {
|
} currentValueLabel: {
|
||||||
Image(systemName: batteryIconName(for: level))
|
Image(systemName: symbolName)
|
||||||
.font(.system(size: 20, weight: .semibold))
|
.font(.system(size: 20, weight: .semibold))
|
||||||
.foregroundStyle(Color.white)
|
.foregroundStyle(Color.white)
|
||||||
} minimumValueLabel: {
|
} minimumValueLabel: {
|
||||||
|
|
@ -246,7 +247,7 @@ struct LockScreenWeatherWidget: View {
|
||||||
Image(systemName: "wind")
|
Image(systemName: "wind")
|
||||||
.font(.system(size: 18, weight: .semibold))
|
.font(.system(size: 18, weight: .semibold))
|
||||||
.symbolRenderingMode(.hierarchical)
|
.symbolRenderingMode(.hierarchical)
|
||||||
inlineComposite(primary: "AQI \(info.index)", secondary: info.category.displayName)
|
inlineComposite(primary: "\(info.scale.compactLabel) \(info.index)", secondary: info.category.displayName)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.minimumScaleFactor(0.85)
|
.minimumScaleFactor(0.85)
|
||||||
}
|
}
|
||||||
|
|
@ -254,8 +255,11 @@ struct LockScreenWeatherWidget: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func circularAirQualitySegment(for info: LockScreenWeatherSnapshot.AirQualityInfo) -> some View {
|
private func circularAirQualitySegment(for info: LockScreenWeatherSnapshot.AirQualityInfo) -> some View {
|
||||||
VStack(spacing: 6) {
|
let range = info.scale.gaugeRange
|
||||||
Gauge(value: Double(info.index), in: 0...500) {
|
let clampedValue = min(max(Double(info.index), range.lowerBound), range.upperBound)
|
||||||
|
|
||||||
|
return VStack(spacing: 6) {
|
||||||
|
Gauge(value: clampedValue, in: range) {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
} currentValueLabel: {
|
} currentValueLabel: {
|
||||||
Text("\(info.index)")
|
Text("\(info.index)")
|
||||||
|
|
@ -263,10 +267,10 @@ struct LockScreenWeatherWidget: View {
|
||||||
.foregroundStyle(Color.white)
|
.foregroundStyle(Color.white)
|
||||||
}
|
}
|
||||||
.gaugeStyle(.accessoryCircular)
|
.gaugeStyle(.accessoryCircular)
|
||||||
.tint(aqiTint(for: info.category))
|
.tint(aqiTint(for: info))
|
||||||
.frame(width: gaugeDiameter, height: gaugeDiameter)
|
.frame(width: gaugeDiameter, height: gaugeDiameter)
|
||||||
|
|
||||||
Text("AQI · \(info.category.displayName)")
|
Text("\(info.scale.compactLabel) · \(info.category.displayName)")
|
||||||
.font(inlineSecondaryFont)
|
.font(inlineSecondaryFont)
|
||||||
.foregroundStyle(secondaryLabelColor)
|
.foregroundStyle(secondaryLabelColor)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
|
|
@ -444,19 +448,27 @@ struct LockScreenWeatherWidget: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func aqiTint(for category: LockScreenWeatherSnapshot.AirQualityInfo.Category) -> Color {
|
private func aqiTint(for info: LockScreenWeatherSnapshot.AirQualityInfo) -> Color {
|
||||||
guard snapshot.usesGaugeTint else { return monochromeGaugeTint }
|
guard snapshot.usesGaugeTint else { return monochromeGaugeTint }
|
||||||
switch category {
|
switch info.category {
|
||||||
case .good:
|
case .good:
|
||||||
return Color(red: 0.20, green: 0.79, blue: 0.39)
|
return Color(red: 0.20, green: 0.79, blue: 0.39)
|
||||||
|
case .fair:
|
||||||
|
return Color(red: 0.55, green: 0.85, blue: 0.32)
|
||||||
case .moderate:
|
case .moderate:
|
||||||
return Color(red: 0.97, green: 0.82, blue: 0.30)
|
return Color(red: 0.97, green: 0.82, blue: 0.30)
|
||||||
case .unhealthyForSensitive:
|
case .unhealthyForSensitive:
|
||||||
return Color(red: 0.98, green: 0.57, blue: 0.24)
|
return Color(red: 0.98, green: 0.57, blue: 0.24)
|
||||||
case .unhealthy:
|
case .unhealthy:
|
||||||
return Color(red: 0.91, green: 0.29, blue: 0.25)
|
return Color(red: 0.91, green: 0.29, blue: 0.25)
|
||||||
|
case .poor:
|
||||||
|
return Color(red: 0.98, green: 0.57, blue: 0.24)
|
||||||
|
case .veryPoor:
|
||||||
|
return Color(red: 0.91, green: 0.29, blue: 0.25)
|
||||||
case .veryUnhealthy:
|
case .veryUnhealthy:
|
||||||
return Color(red: 0.65, green: 0.32, blue: 0.86)
|
return Color(red: 0.65, green: 0.32, blue: 0.86)
|
||||||
|
case .extremelyPoor:
|
||||||
|
return Color(red: 0.50, green: 0.13, blue: 0.28)
|
||||||
case .hazardous:
|
case .hazardous:
|
||||||
return Color(red: 0.50, green: 0.13, blue: 0.28)
|
return Color(red: 0.50, green: 0.13, blue: 0.28)
|
||||||
case .unknown:
|
case .unknown:
|
||||||
|
|
@ -560,7 +572,7 @@ struct LockScreenWeatherWidget: View {
|
||||||
String(
|
String(
|
||||||
format: NSLocalizedString("Air quality index %d, %@", comment: "Air quality accessibility label"),
|
format: NSLocalizedString("Air quality index %d, %@", comment: "Air quality accessibility label"),
|
||||||
airQuality.index,
|
airQuality.index,
|
||||||
airQuality.category.displayName
|
"\(airQuality.scale.accessibilityLabel) \(airQuality.category.displayName)"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -188,8 +188,10 @@ struct GeneralSettings: View {
|
||||||
@Default(.lockScreenWeatherWidgetStyle) var lockScreenWeatherWidgetStyle
|
@Default(.lockScreenWeatherWidgetStyle) var lockScreenWeatherWidgetStyle
|
||||||
@Default(.lockScreenWeatherTemperatureUnit) var lockScreenWeatherTemperatureUnit
|
@Default(.lockScreenWeatherTemperatureUnit) var lockScreenWeatherTemperatureUnit
|
||||||
@Default(.lockScreenWeatherShowsAQI) var lockScreenWeatherShowsAQI
|
@Default(.lockScreenWeatherShowsAQI) var lockScreenWeatherShowsAQI
|
||||||
|
@Default(.lockScreenWeatherAQIScale) var lockScreenWeatherAQIScale
|
||||||
@Default(.lockScreenWeatherUsesGaugeTint) var lockScreenWeatherUsesGaugeTint
|
@Default(.lockScreenWeatherUsesGaugeTint) var lockScreenWeatherUsesGaugeTint
|
||||||
@Default(.lockScreenWeatherProviderSource) var lockScreenWeatherProviderSource
|
@Default(.lockScreenWeatherProviderSource) var lockScreenWeatherProviderSource
|
||||||
|
@Default(.lockScreenWeatherBatteryUsesLaptopSymbol) var lockScreenWeatherBatteryUsesLaptopSymbol
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
|
|
@ -402,9 +404,26 @@ struct GeneralSettings: View {
|
||||||
Defaults.Toggle("Show charging percentage", key: .lockScreenWeatherShowsChargingPercentage)
|
Defaults.Toggle("Show charging percentage", key: .lockScreenWeatherShowsChargingPercentage)
|
||||||
}
|
}
|
||||||
Defaults.Toggle("Show battery indicator", key: .lockScreenWeatherShowsBatteryGauge)
|
Defaults.Toggle("Show battery indicator", key: .lockScreenWeatherShowsBatteryGauge)
|
||||||
|
if lockScreenWeatherShowsBatteryGauge {
|
||||||
|
Defaults.Toggle("Use MacBook icon when on battery", key: .lockScreenWeatherBatteryUsesLaptopSymbol)
|
||||||
|
.disabled(lockScreenWeatherWidgetStyle != .circular)
|
||||||
|
if lockScreenWeatherWidgetStyle != .circular {
|
||||||
|
Text("MacBook icon is available in the circular layout.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
Defaults.Toggle("Show Bluetooth battery", key: .lockScreenWeatherShowsBluetooth)
|
Defaults.Toggle("Show Bluetooth battery", key: .lockScreenWeatherShowsBluetooth)
|
||||||
Defaults.Toggle("Show AQI widget", key: .lockScreenWeatherShowsAQI)
|
Defaults.Toggle("Show AQI widget", key: .lockScreenWeatherShowsAQI)
|
||||||
.disabled(!lockScreenWeatherProviderSource.supportsAirQuality)
|
.disabled(!lockScreenWeatherProviderSource.supportsAirQuality)
|
||||||
|
if lockScreenWeatherShowsAQI && lockScreenWeatherProviderSource.supportsAirQuality {
|
||||||
|
Picker("Air quality scale", selection: $lockScreenWeatherAQIScale) {
|
||||||
|
ForEach(LockScreenWeatherAirQualityScale.allCases) { scale in
|
||||||
|
Text(scale.displayName).tag(scale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
}
|
||||||
if !lockScreenWeatherProviderSource.supportsAirQuality {
|
if !lockScreenWeatherProviderSource.supportsAirQuality {
|
||||||
Text("Air quality requires the Open Meteo provider.")
|
Text("Air quality requires the Open Meteo provider.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,51 @@ enum LockScreenWeatherTemperatureUnit: String, CaseIterable, Defaults.Serializab
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum LockScreenWeatherAirQualityScale: String, CaseIterable, Defaults.Serializable, Identifiable {
|
||||||
|
case us = "U.S. AQI"
|
||||||
|
case european = "EAQI"
|
||||||
|
|
||||||
|
var id: String { rawValue }
|
||||||
|
|
||||||
|
var displayName: String { rawValue }
|
||||||
|
|
||||||
|
var compactLabel: String {
|
||||||
|
switch self {
|
||||||
|
case .us:
|
||||||
|
return "AQI"
|
||||||
|
case .european:
|
||||||
|
return "EAQI"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var accessibilityLabel: String {
|
||||||
|
switch self {
|
||||||
|
case .us:
|
||||||
|
return "AQI"
|
||||||
|
case .european:
|
||||||
|
return "EAQI"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var queryParameter: String {
|
||||||
|
switch self {
|
||||||
|
case .us:
|
||||||
|
return "us_aqi"
|
||||||
|
case .european:
|
||||||
|
return "european_aqi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var gaugeRange: ClosedRange<Double> {
|
||||||
|
switch self {
|
||||||
|
case .us:
|
||||||
|
return 0...500
|
||||||
|
case .european:
|
||||||
|
return 0...120
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum LockScreenReminderChipStyle: String, CaseIterable, Defaults.Serializable, Identifiable {
|
enum LockScreenReminderChipStyle: String, CaseIterable, Defaults.Serializable, Identifiable {
|
||||||
case eventColor = "Event color"
|
case eventColor = "Event color"
|
||||||
case monochrome = "White"
|
case monochrome = "White"
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,8 @@ final class LockScreenWeatherManager: ObservableObject {
|
||||||
let showsCharging = Defaults[.lockScreenWeatherShowsCharging]
|
let showsCharging = Defaults[.lockScreenWeatherShowsCharging]
|
||||||
let chargingInfo = showsCharging ? makeChargingInfo() : nil
|
let chargingInfo = showsCharging ? makeChargingInfo() : nil
|
||||||
let showsBatteryGauge = Defaults[.lockScreenWeatherShowsBatteryGauge]
|
let showsBatteryGauge = Defaults[.lockScreenWeatherShowsBatteryGauge]
|
||||||
let batteryInfo = showsBatteryGauge ? makeBatteryGaugeInfo(isCharging: chargingInfo != nil) : nil
|
let widgetStyle = Defaults[.lockScreenWeatherWidgetStyle]
|
||||||
|
let batteryInfo = showsBatteryGauge ? makeBatteryGaugeInfo(isCharging: chargingInfo != nil, widgetStyle: widgetStyle) : nil
|
||||||
let fallback = LockScreenWeatherSnapshot(
|
let fallback = LockScreenWeatherSnapshot(
|
||||||
temperatureText: snapshot?.temperatureText ?? "--",
|
temperatureText: snapshot?.temperatureText ?? "--",
|
||||||
symbolName: snapshot?.symbolName ?? "cloud.fill",
|
symbolName: snapshot?.symbolName ?? "cloud.fill",
|
||||||
|
|
@ -116,7 +117,7 @@ final class LockScreenWeatherManager: ObservableObject {
|
||||||
battery: batteryInfo,
|
battery: batteryInfo,
|
||||||
showsLocation: snapshot?.showsLocation ?? false,
|
showsLocation: snapshot?.showsLocation ?? false,
|
||||||
airQuality: (providerSource.supportsAirQuality && Defaults[.lockScreenWeatherShowsAQI]) ? snapshot?.airQuality : nil,
|
airQuality: (providerSource.supportsAirQuality && Defaults[.lockScreenWeatherShowsAQI]) ? snapshot?.airQuality : nil,
|
||||||
widgetStyle: Defaults[.lockScreenWeatherWidgetStyle],
|
widgetStyle: widgetStyle,
|
||||||
showsChargingPercentage: Defaults[.lockScreenWeatherShowsChargingPercentage],
|
showsChargingPercentage: Defaults[.lockScreenWeatherShowsChargingPercentage],
|
||||||
temperatureInfo: snapshot?.temperatureInfo,
|
temperatureInfo: snapshot?.temperatureInfo,
|
||||||
usesGaugeTint: Defaults[.lockScreenWeatherUsesGaugeTint]
|
usesGaugeTint: Defaults[.lockScreenWeatherUsesGaugeTint]
|
||||||
|
|
@ -189,6 +190,8 @@ final class LockScreenWeatherManager: ObservableObject {
|
||||||
.map { _ in () }.eraseToAnyPublisher(),
|
.map { _ in () }.eraseToAnyPublisher(),
|
||||||
Defaults.publisher(.lockScreenWeatherShowsBatteryGauge, options: [])
|
Defaults.publisher(.lockScreenWeatherShowsBatteryGauge, options: [])
|
||||||
.map { _ in () }.eraseToAnyPublisher(),
|
.map { _ in () }.eraseToAnyPublisher(),
|
||||||
|
Defaults.publisher(.lockScreenWeatherBatteryUsesLaptopSymbol, options: [])
|
||||||
|
.map { _ in () }.eraseToAnyPublisher(),
|
||||||
Defaults.publisher(.lockScreenWeatherWidgetStyle, options: [])
|
Defaults.publisher(.lockScreenWeatherWidgetStyle, options: [])
|
||||||
.map { _ in () }.eraseToAnyPublisher(),
|
.map { _ in () }.eraseToAnyPublisher(),
|
||||||
Defaults.publisher(.lockScreenWeatherTemperatureUnit, options: [])
|
Defaults.publisher(.lockScreenWeatherTemperatureUnit, options: [])
|
||||||
|
|
@ -198,6 +201,8 @@ final class LockScreenWeatherManager: ObservableObject {
|
||||||
Defaults.publisher(.lockScreenWeatherUsesGaugeTint, options: [])
|
Defaults.publisher(.lockScreenWeatherUsesGaugeTint, options: [])
|
||||||
.map { _ in () }.eraseToAnyPublisher(),
|
.map { _ in () }.eraseToAnyPublisher(),
|
||||||
Defaults.publisher(.lockScreenWeatherProviderSource, options: [])
|
Defaults.publisher(.lockScreenWeatherProviderSource, options: [])
|
||||||
|
.map { _ in () }.eraseToAnyPublisher(),
|
||||||
|
Defaults.publisher(.lockScreenWeatherAQIScale, options: [])
|
||||||
.map { _ in () }.eraseToAnyPublisher()
|
.map { _ in () }.eraseToAnyPublisher()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -229,6 +234,17 @@ final class LockScreenWeatherManager: ObservableObject {
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
Defaults.publisher(.lockScreenWeatherAQIScale, options: [])
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
guard let self else { return }
|
||||||
|
self.latestWeatherPayload = nil
|
||||||
|
Task { @MainActor in
|
||||||
|
NSLog("LockScreenWeatherManager: AQI scale changed, forcing refresh")
|
||||||
|
_ = await self.refresh(force: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
Defaults.publisher(.enableLockScreenWeatherWidget, options: [])
|
Defaults.publisher(.enableLockScreenWeatherWidget, options: [])
|
||||||
.sink { [weak self] change in
|
.sink { [weak self] change in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
@ -276,7 +292,7 @@ final class LockScreenWeatherManager: ObservableObject {
|
||||||
let showsChargingPercentage = Defaults[.lockScreenWeatherShowsChargingPercentage]
|
let showsChargingPercentage = Defaults[.lockScreenWeatherShowsChargingPercentage]
|
||||||
let providerSource = Defaults[.lockScreenWeatherProviderSource]
|
let providerSource = Defaults[.lockScreenWeatherProviderSource]
|
||||||
let airQualityInfo = (Defaults[.lockScreenWeatherShowsAQI] && providerSource.supportsAirQuality) ? payload.airQuality : nil
|
let airQualityInfo = (Defaults[.lockScreenWeatherShowsAQI] && providerSource.supportsAirQuality) ? payload.airQuality : nil
|
||||||
let batteryInfo = Defaults[.lockScreenWeatherShowsBatteryGauge] ? makeBatteryGaugeInfo(isCharging: chargingInfo != nil) : nil
|
let batteryInfo = Defaults[.lockScreenWeatherShowsBatteryGauge] ? makeBatteryGaugeInfo(isCharging: chargingInfo != nil, widgetStyle: widgetStyle) : nil
|
||||||
let usesGaugeTint = Defaults[.lockScreenWeatherUsesGaugeTint]
|
let usesGaugeTint = Defaults[.lockScreenWeatherUsesGaugeTint]
|
||||||
|
|
||||||
return LockScreenWeatherSnapshot(
|
return LockScreenWeatherSnapshot(
|
||||||
|
|
@ -321,7 +337,7 @@ final class LockScreenWeatherManager: ObservableObject {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeBatteryGaugeInfo(isCharging: Bool) -> LockScreenWeatherSnapshot.BatteryInfo? {
|
private func makeBatteryGaugeInfo(isCharging: Bool, widgetStyle: LockScreenWeatherWidgetStyle) -> LockScreenWeatherSnapshot.BatteryInfo? {
|
||||||
guard !isCharging else { return nil }
|
guard !isCharging else { return nil }
|
||||||
|
|
||||||
let battery = BatteryStatusViewModel.shared
|
let battery = BatteryStatusViewModel.shared
|
||||||
|
|
@ -330,7 +346,12 @@ final class LockScreenWeatherManager: ObservableObject {
|
||||||
|
|
||||||
guard clampedLevel >= 0 else { return nil }
|
guard clampedLevel >= 0 else { return nil }
|
||||||
|
|
||||||
return LockScreenWeatherSnapshot.BatteryInfo(batteryLevel: clampedLevel)
|
let usesLaptopSymbol = widgetStyle == .circular && Defaults[.lockScreenWeatherBatteryUsesLaptopSymbol]
|
||||||
|
|
||||||
|
return LockScreenWeatherSnapshot.BatteryInfo(
|
||||||
|
batteryLevel: clampedLevel,
|
||||||
|
usesLaptopSymbol: usesLaptopSymbol
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeBluetoothInfo() -> LockScreenWeatherSnapshot.BluetoothInfo? {
|
private func makeBluetoothInfo() -> LockScreenWeatherSnapshot.BluetoothInfo? {
|
||||||
|
|
@ -414,25 +435,34 @@ struct LockScreenWeatherSnapshot: Equatable {
|
||||||
|
|
||||||
struct BatteryInfo: Equatable {
|
struct BatteryInfo: Equatable {
|
||||||
let batteryLevel: Int
|
let batteryLevel: Int
|
||||||
|
let usesLaptopSymbol: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AirQualityInfo: Equatable {
|
struct AirQualityInfo: Equatable {
|
||||||
enum Category: String, Equatable {
|
enum Category: String, Equatable {
|
||||||
case good
|
case good
|
||||||
|
case fair
|
||||||
case moderate
|
case moderate
|
||||||
case unhealthyForSensitive
|
case unhealthyForSensitive
|
||||||
case unhealthy
|
case unhealthy
|
||||||
|
case poor
|
||||||
|
case veryPoor
|
||||||
case veryUnhealthy
|
case veryUnhealthy
|
||||||
|
case extremelyPoor
|
||||||
case hazardous
|
case hazardous
|
||||||
case unknown
|
case unknown
|
||||||
|
|
||||||
var displayName: String {
|
var displayName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .good: return "Good"
|
case .good: return "Good"
|
||||||
|
case .fair: return "Fair"
|
||||||
case .moderate: return "Moderate"
|
case .moderate: return "Moderate"
|
||||||
case .unhealthyForSensitive: return "Sensitive"
|
case .unhealthyForSensitive: return "Sensitive"
|
||||||
case .unhealthy: return "Unhealthy"
|
case .unhealthy: return "Unhealthy"
|
||||||
|
case .poor: return "Poor"
|
||||||
|
case .veryPoor: return "Very Poor"
|
||||||
case .veryUnhealthy: return "Very Unhealthy"
|
case .veryUnhealthy: return "Very Unhealthy"
|
||||||
|
case .extremelyPoor: return "Extremely Poor"
|
||||||
case .hazardous: return "Hazardous"
|
case .hazardous: return "Hazardous"
|
||||||
case .unknown: return "Unknown"
|
case .unknown: return "Unknown"
|
||||||
}
|
}
|
||||||
|
|
@ -441,6 +471,7 @@ struct LockScreenWeatherSnapshot: Equatable {
|
||||||
|
|
||||||
let index: Int
|
let index: Int
|
||||||
let category: Category
|
let category: Category
|
||||||
|
let scale: LockScreenWeatherAirQualityScale
|
||||||
}
|
}
|
||||||
|
|
||||||
let temperatureText: String
|
let temperatureText: String
|
||||||
|
|
@ -459,24 +490,46 @@ struct LockScreenWeatherSnapshot: Equatable {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LockScreenWeatherSnapshot.AirQualityInfo.Category {
|
extension LockScreenWeatherSnapshot.AirQualityInfo.Category {
|
||||||
init(aqiValue: Int) {
|
init(index: Int, scale: LockScreenWeatherAirQualityScale) {
|
||||||
switch aqiValue {
|
switch scale {
|
||||||
case ..<0:
|
case .us:
|
||||||
self = .unknown
|
switch index {
|
||||||
case 0...50:
|
case ..<0:
|
||||||
self = .good
|
self = .unknown
|
||||||
case 51...100:
|
case 0...50:
|
||||||
self = .moderate
|
self = .good
|
||||||
case 101...150:
|
case 51...100:
|
||||||
self = .unhealthyForSensitive
|
self = .moderate
|
||||||
case 151...200:
|
case 101...150:
|
||||||
self = .unhealthy
|
self = .unhealthyForSensitive
|
||||||
case 201...300:
|
case 151...200:
|
||||||
self = .veryUnhealthy
|
self = .unhealthy
|
||||||
case 301...:
|
case 201...300:
|
||||||
self = .hazardous
|
self = .veryUnhealthy
|
||||||
default:
|
case 301...:
|
||||||
self = .unknown
|
self = .hazardous
|
||||||
|
default:
|
||||||
|
self = .unknown
|
||||||
|
}
|
||||||
|
case .european:
|
||||||
|
switch index {
|
||||||
|
case ..<0:
|
||||||
|
self = .unknown
|
||||||
|
case 0...20:
|
||||||
|
self = .good
|
||||||
|
case 21...40:
|
||||||
|
self = .fair
|
||||||
|
case 41...60:
|
||||||
|
self = .moderate
|
||||||
|
case 61...80:
|
||||||
|
self = .poor
|
||||||
|
case 81...100:
|
||||||
|
self = .veryPoor
|
||||||
|
case 101...:
|
||||||
|
self = .extremelyPoor
|
||||||
|
default:
|
||||||
|
self = .unknown
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -570,9 +623,11 @@ private actor LockScreenWeatherProvider {
|
||||||
|
|
||||||
let airQualityInfo: LockScreenWeatherSnapshot.AirQualityInfo?
|
let airQualityInfo: LockScreenWeatherSnapshot.AirQualityInfo?
|
||||||
if let index = condition.airQuality?.usIndexValue {
|
if let index = condition.airQuality?.usIndexValue {
|
||||||
|
let scale: LockScreenWeatherAirQualityScale = .us
|
||||||
airQualityInfo = LockScreenWeatherSnapshot.AirQualityInfo(
|
airQualityInfo = LockScreenWeatherSnapshot.AirQualityInfo(
|
||||||
index: index,
|
index: index,
|
||||||
category: LockScreenWeatherSnapshot.AirQualityInfo.Category(aqiValue: index)
|
category: LockScreenWeatherSnapshot.AirQualityInfo.Category(index: index, scale: scale),
|
||||||
|
scale: scale
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
airQualityInfo = nil
|
airQualityInfo = nil
|
||||||
|
|
@ -648,9 +703,11 @@ private actor LockScreenWeatherProvider {
|
||||||
unitSymbol: unitSymbol
|
unitSymbol: unitSymbol
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let selectedScale = Defaults[.lockScreenWeatherAQIScale]
|
||||||
|
|
||||||
var airQualityInfo: LockScreenWeatherSnapshot.AirQualityInfo?
|
var airQualityInfo: LockScreenWeatherSnapshot.AirQualityInfo?
|
||||||
if Defaults[.lockScreenWeatherShowsAQI] {
|
if Defaults[.lockScreenWeatherShowsAQI] {
|
||||||
airQualityInfo = try? await fetchOpenMeteoAirQuality(latitude: latitude, longitude: longitude)
|
airQualityInfo = try? await fetchOpenMeteoAirQuality(latitude: latitude, longitude: longitude, scale: selectedScale)
|
||||||
}
|
}
|
||||||
|
|
||||||
return LockScreenWeatherSnapshot(
|
return LockScreenWeatherSnapshot(
|
||||||
|
|
@ -670,12 +727,12 @@ private actor LockScreenWeatherProvider {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fetchOpenMeteoAirQuality(latitude: String, longitude: String) async throws -> LockScreenWeatherSnapshot.AirQualityInfo? {
|
private func fetchOpenMeteoAirQuality(latitude: String, longitude: String, scale: LockScreenWeatherAirQualityScale) async throws -> LockScreenWeatherSnapshot.AirQualityInfo? {
|
||||||
var airComponents = URLComponents(string: "https://air-quality-api.open-meteo.com/v1/air-quality")
|
var airComponents = URLComponents(string: "https://air-quality-api.open-meteo.com/v1/air-quality")
|
||||||
airComponents?.queryItems = [
|
airComponents?.queryItems = [
|
||||||
URLQueryItem(name: "latitude", value: latitude),
|
URLQueryItem(name: "latitude", value: latitude),
|
||||||
URLQueryItem(name: "longitude", value: longitude),
|
URLQueryItem(name: "longitude", value: longitude),
|
||||||
URLQueryItem(name: "current", value: "us_aqi"),
|
URLQueryItem(name: "current", value: scale.queryParameter),
|
||||||
URLQueryItem(name: "timezone", value: "auto")
|
URLQueryItem(name: "timezone", value: "auto")
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -691,14 +748,27 @@ private actor LockScreenWeatherProvider {
|
||||||
let airDecoder = JSONDecoder()
|
let airDecoder = JSONDecoder()
|
||||||
airDecoder.keyDecodingStrategy = .convertFromSnakeCase
|
airDecoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
let airPayload = try airDecoder.decode(OpenMeteoAirQualityResponse.self, from: airData)
|
let airPayload = try airDecoder.decode(OpenMeteoAirQualityResponse.self, from: airData)
|
||||||
guard let airCurrent = airPayload.current, let indexValue = airCurrent.usAqi else {
|
guard let airCurrent = airPayload.current else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let rawValue: Double?
|
||||||
|
switch scale {
|
||||||
|
case .us:
|
||||||
|
rawValue = airCurrent.usAqi
|
||||||
|
case .european:
|
||||||
|
rawValue = airCurrent.europeanAqi
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let indexValue = rawValue else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let index = Int(round(indexValue))
|
let index = Int(round(indexValue))
|
||||||
return LockScreenWeatherSnapshot.AirQualityInfo(
|
return LockScreenWeatherSnapshot.AirQualityInfo(
|
||||||
index: index,
|
index: index,
|
||||||
category: LockScreenWeatherSnapshot.AirQualityInfo.Category(aqiValue: index)
|
category: LockScreenWeatherSnapshot.AirQualityInfo.Category(index: index, scale: scale),
|
||||||
|
scale: scale
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -830,6 +900,7 @@ private struct OpenMeteoAirQualityResponse: Decodable {
|
||||||
struct Current: Decodable {
|
struct Current: Decodable {
|
||||||
let time: String?
|
let time: String?
|
||||||
let usAqi: Double?
|
let usAqi: Double?
|
||||||
|
let europeanAqi: Double?
|
||||||
}
|
}
|
||||||
|
|
||||||
let current: Current?
|
let current: Current?
|
||||||
|
|
|
||||||
|
|
@ -382,9 +382,11 @@ extension Defaults.Keys {
|
||||||
static let lockScreenWeatherShowsChargingPercentage = Key<Bool>("lockScreenWeatherShowsChargingPercentage", default: true)
|
static let lockScreenWeatherShowsChargingPercentage = Key<Bool>("lockScreenWeatherShowsChargingPercentage", default: true)
|
||||||
static let lockScreenWeatherShowsBluetooth = Key<Bool>("lockScreenWeatherShowsBluetooth", default: true)
|
static let lockScreenWeatherShowsBluetooth = Key<Bool>("lockScreenWeatherShowsBluetooth", default: true)
|
||||||
static let lockScreenWeatherShowsBatteryGauge = Key<Bool>("lockScreenWeatherShowsBatteryGauge", default: true)
|
static let lockScreenWeatherShowsBatteryGauge = Key<Bool>("lockScreenWeatherShowsBatteryGauge", default: true)
|
||||||
|
static let lockScreenWeatherBatteryUsesLaptopSymbol = Key<Bool>("lockScreenWeatherBatteryUsesLaptopSymbol", default: false)
|
||||||
static let lockScreenWeatherWidgetStyle = Key<LockScreenWeatherWidgetStyle>("lockScreenWeatherWidgetStyle", default: .circular)
|
static let lockScreenWeatherWidgetStyle = Key<LockScreenWeatherWidgetStyle>("lockScreenWeatherWidgetStyle", default: .circular)
|
||||||
static let lockScreenWeatherTemperatureUnit = Key<LockScreenWeatherTemperatureUnit>("lockScreenWeatherTemperatureUnit", default: .celsius)
|
static let lockScreenWeatherTemperatureUnit = Key<LockScreenWeatherTemperatureUnit>("lockScreenWeatherTemperatureUnit", default: .celsius)
|
||||||
static let lockScreenWeatherShowsAQI = Key<Bool>("lockScreenWeatherShowsAQI", default: false)
|
static let lockScreenWeatherShowsAQI = Key<Bool>("lockScreenWeatherShowsAQI", default: false)
|
||||||
|
static let lockScreenWeatherAQIScale = Key<LockScreenWeatherAirQualityScale>("lockScreenWeatherAQIScale", default: .us)
|
||||||
static let lockScreenWeatherUsesGaugeTint = Key<Bool>("lockScreenWeatherUsesGaugeTint", default: false)
|
static let lockScreenWeatherUsesGaugeTint = Key<Bool>("lockScreenWeatherUsesGaugeTint", default: false)
|
||||||
static let lockScreenWeatherProviderSource = Key<LockScreenWeatherProviderSource>("lockScreenWeatherProviderSource", default: .openMeteo)
|
static let lockScreenWeatherProviderSource = Key<LockScreenWeatherProviderSource>("lockScreenWeatherProviderSource", default: .openMeteo)
|
||||||
static let lockScreenGlassStyle = Key<LockScreenGlassStyle>("lockScreenGlassStyle", default: .liquid)
|
static let lockScreenGlassStyle = Key<LockScreenGlassStyle>("lockScreenGlassStyle", default: .liquid)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue