Merge pull request #101 from Ebullioscopic/feature/lock-screen

Update EAQI and Macbook Icon
This commit is contained in:
Hariharan Mudaliar 2025-11-14 00:54:01 +05:30 • committed by GitHub
commit 583e244e8a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 205 additions and 42 deletions

View file

@ -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" : {

View file

@ -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)"
) )
} }

View file

@ -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)

View file

@ -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"

View file

@ -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?

View file

@ -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)