Working with fallbacks

This commit is contained in:
Hariharan Mudaliar 2025-11-03 01:28:33 +05:30
commit 2612cf9d7f
6 changed files with 63 additions and 44 deletions

View file

@ -8496,9 +8496,6 @@
} }
} }
}, },
"Full" : {
"comment" : "Full battery level label"
},
"Fully charged" : { "Fully charged" : {
"comment" : "Charging fallback label when already charged" "comment" : "Charging fallback label when already charged"
}, },

View file

@ -13,6 +13,8 @@ struct LockScreenWeatherWidget: View {
private var stackSpacing: CGFloat { isInline ? 14 : 22 } private var stackSpacing: CGFloat { isInline ? 14 : 22 }
private var gaugeLineWidth: CGFloat { 6 } private var gaugeLineWidth: CGFloat { 6 }
private var gaugeDiameter: CGFloat { 64 } private var gaugeDiameter: CGFloat { 64 }
private var topPadding: CGFloat { isInline ? 6 : 22 }
private var bottomPadding: CGFloat { isInline ? 6 : 10 }
private var gaugeBackgroundColor: Color { private var gaugeBackgroundColor: Color {
snapshot.usesGaugeTint ? Color.white.opacity(0.22) : Color.white.opacity(0.32) snapshot.usesGaugeTint ? Color.white.opacity(0.22) : Color.white.opacity(0.32)
@ -45,7 +47,8 @@ struct LockScreenWeatherWidget: View {
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.white.opacity(0.65)) .foregroundStyle(Color.white.opacity(0.65))
.padding(.horizontal, 10) .padding(.horizontal, 10)
.padding(.vertical, 6) .padding(.top, topPadding)
.padding(.bottom, bottomPadding)
.background(Color.clear) .background(Color.clear)
.shadow(color: .black.opacity(0.35), radius: 8, x: 0, y: 3) .shadow(color: .black.opacity(0.35), radius: 8, x: 0, y: 3)
.accessibilityElement(children: .ignore) .accessibilityElement(children: .ignore)
@ -143,7 +146,7 @@ struct LockScreenWeatherWidget: View {
Image(systemName: info.iconName) Image(systemName: info.iconName)
.font(.system(size: 20, weight: .semibold)) .font(.system(size: 20, weight: .semibold))
.symbolRenderingMode(.hierarchical) .symbolRenderingMode(.hierarchical)
Text(bluetoothStatusLabel(for: info.batteryLevel)) Text(bluetoothPercentageText(for: info.batteryLevel))
.font(inlinePrimaryFont) .font(inlinePrimaryFont)
.lineLimit(1) .lineLimit(1)
.minimumScaleFactor(0.85) .minimumScaleFactor(0.85)
@ -159,7 +162,7 @@ struct LockScreenWeatherWidget: View {
.font(.system(size: 22, weight: .semibold)) .font(.system(size: 22, weight: .semibold))
.foregroundStyle(Color.white) .foregroundStyle(Color.white)
} }
Text(bluetoothStatusLabel(for: info.batteryLevel)) Text(bluetoothPercentageText(for: info.batteryLevel))
.font(inlineSecondaryFont) .font(inlineSecondaryFont)
.foregroundStyle(secondaryLabelColor) .foregroundStyle(secondaryLabelColor)
} }
@ -285,18 +288,8 @@ struct LockScreenWeatherWidget: View {
return NSLocalizedString("Charging", comment: "Charging fallback label when no estimate is available") return NSLocalizedString("Charging", comment: "Charging fallback label when no estimate is available")
} }
private func bluetoothStatusLabel(for level: Int) -> String { private func bluetoothPercentageText(for level: Int) -> String {
let value = clampedBatteryLevel(level) "\(clampedBatteryLevel(level))%"
switch value {
case ..<20:
return NSLocalizedString("Low", comment: "Low battery level label")
case 20..<50:
return NSLocalizedString("Medium", comment: "Medium battery level label")
case 50..<80:
return NSLocalizedString("High", comment: "High battery level label")
default:
return NSLocalizedString("Full", comment: "Full battery level label")
}
} }
private func minimumTemperatureLabel(for info: LockScreenWeatherSnapshot.TemperatureInfo) -> String { private func minimumTemperatureLabel(for info: LockScreenWeatherSnapshot.TemperatureInfo) -> String {
@ -362,25 +355,27 @@ struct LockScreenWeatherWidget: View {
private func batteryTint(for level: Int) -> Color { private func batteryTint(for level: Int) -> Color {
guard snapshot.usesGaugeTint else { return monochromeGaugeTint } guard snapshot.usesGaugeTint else { return monochromeGaugeTint }
switch level { let clamped = clampedBatteryLevel(level)
switch clamped {
case ..<20: case ..<20:
return Color(red: 0.91, green: 0.29, blue: 0.25) return Color(.systemRed)
case 20..<50: case 20..<50:
return Color(red: 0.99, green: 0.74, blue: 0.30) return Color(.systemOrange)
default: default:
return Color(red: 0.20, green: 0.79, blue: 0.39) return Color(.systemGreen)
} }
} }
private func bluetoothTint(for level: Int) -> Color { private func bluetoothTint(for level: Int) -> Color {
guard snapshot.usesGaugeTint else { return monochromeGaugeTint } guard snapshot.usesGaugeTint else { return monochromeGaugeTint }
switch level { let clamped = clampedBatteryLevel(level)
switch clamped {
case ..<20: case ..<20:
return Color(red: 0.91, green: 0.29, blue: 0.25) return Color(.systemRed)
case 20..<50: case 20..<50:
return Color(red: 0.97, green: 0.58, blue: 0.29) return Color(.systemOrange)
default: default:
return Color(red: 0.29, green: 0.63, blue: 1.00) return Color(.systemGreen)
} }
} }

View file

@ -4,13 +4,12 @@
// //
// Created by Richard Kunkli on 07/08/2024. // Created by Richard Kunkli on 07/08/2024.
// //
import AVFoundation
import Defaults import Defaults
import EventKit import EventKit
import KeyboardShortcuts import KeyboardShortcuts
import LaunchAtLogin import LaunchAtLogin
import LottieUI import LottieUI
import AVFoundation
import Sparkle import Sparkle
import SwiftUI import SwiftUI
import SwiftUIIntrospect import SwiftUIIntrospect
@ -347,6 +346,7 @@ struct GeneralSettings: View {
} }
.pickerStyle(.segmented) .pickerStyle(.segmented)
Defaults.Toggle("Show location label", key: .lockScreenWeatherShowsLocation) Defaults.Toggle("Show location label", key: .lockScreenWeatherShowsLocation)
.disabled(lockScreenWeatherWidgetStyle == .circular)
Defaults.Toggle("Show charging status", key: .lockScreenWeatherShowsCharging) Defaults.Toggle("Show charging status", key: .lockScreenWeatherShowsCharging)
if lockScreenWeatherShowsCharging { if lockScreenWeatherShowsCharging {
Defaults.Toggle("Show charging percentage", key: .lockScreenWeatherShowsChargingPercentage) Defaults.Toggle("Show charging percentage", key: .lockScreenWeatherShowsChargingPercentage)
@ -662,9 +662,6 @@ struct HUD: View {
Section { Section {
Toggle("Enable Built-in System HUD", isOn: $enableSystemHUD) Toggle("Enable Built-in System HUD", isOn: $enableSystemHUD)
.onChange(of: enableSystemHUD) { _ in
// SystemHUDManager will automatically handle start/stop via its @Default observer
}
if enableSystemHUD { if enableSystemHUD {
Toggle("Volume HUD", isOn: $enableVolumeHUD) Toggle("Volume HUD", isOn: $enableVolumeHUD)
@ -1943,34 +1940,34 @@ struct TimerSettings: View {
let nextIndex = timerPresets.count + 1 let nextIndex = timerPresets.count + 1
let defaultColor = Defaults[.accentColor] let defaultColor = Defaults[.accentColor]
let newPreset = TimerPreset(name: "Preset \(nextIndex)", duration: 5 * 60, color: defaultColor) let newPreset = TimerPreset(name: "Preset \(nextIndex)", duration: 5 * 60, color: defaultColor)
withAnimation(.smooth) { _ = withAnimation(.smooth) {
timerPresets.append(newPreset) timerPresets.append(newPreset)
} }
} }
private func movePresetUp(_ index: Int) { private func movePresetUp(_ index: Int) {
guard index > timerPresets.startIndex else { return } guard index > timerPresets.startIndex else { return }
withAnimation(.smooth) { _ = withAnimation(.smooth) {
timerPresets.swapAt(index, index - 1) timerPresets.swapAt(index, index - 1)
} }
} }
private func movePresetDown(_ index: Int) { private func movePresetDown(_ index: Int) {
guard index < timerPresets.index(before: timerPresets.endIndex) else { return } guard index < timerPresets.index(before: timerPresets.endIndex) else { return }
withAnimation(.smooth) { _ = withAnimation(.smooth) {
timerPresets.swapAt(index, index + 1) timerPresets.swapAt(index, index + 1)
} }
} }
private func removePreset(_ index: Int) { private func removePreset(_ index: Int) {
guard timerPresets.indices.contains(index) else { return } guard timerPresets.indices.contains(index) else { return }
withAnimation(.smooth) { _ = withAnimation(.smooth) {
timerPresets.remove(at: index) timerPresets.remove(at: index)
} }
} }
private func resetPresets() { private func resetPresets() {
withAnimation(.smooth) { _ = withAnimation(.smooth) {
timerPresets = TimerPreset.defaultPresets timerPresets = TimerPreset.defaultPresets
} }
} }

View file

@ -17,6 +17,11 @@ final class LockScreenWeatherManager: ObservableObject {
private init() { private init() {
observeAccessoryChanges() observeAccessoryChanges()
Task { @MainActor [weak self] in
guard let self else { return }
self.locationProvider.prepareAuthorization()
_ = await self.refresh(force: true)
}
} }
func prepareLocationAccess() { func prepareLocationAccess() {
@ -73,7 +78,7 @@ final class LockScreenWeatherManager: ObservableObject {
do { do {
let location = await locationProvider.currentLocation() let location = await locationProvider.currentLocation()
let payload = try await provider.fetchSnapshot(location: location) let payload = try await fetchWeatherPayload(location: location)
latestWeatherPayload = payload latestWeatherPayload = payload
if Defaults[.lockScreenWeatherShowsBluetooth] { if Defaults[.lockScreenWeatherShowsBluetooth] {
BluetoothAudioManager.shared.refreshConnectedDeviceBatteries() BluetoothAudioManager.shared.refreshConnectedDeviceBatteries()
@ -108,6 +113,25 @@ final class LockScreenWeatherManager: ObservableObject {
} }
} }
private func fetchWeatherPayload(location: CLLocation?) async throws -> LockScreenWeatherSnapshot {
let primarySource = Defaults[.lockScreenWeatherProviderSource]
do {
return try await provider.fetchSnapshot(location: location, source: primarySource)
} catch {
if primarySource == .openMeteo {
NSLog("LockScreenWeatherManager: Open Meteo fetch failed - %@. Falling back to wttr.in", error.localizedDescription)
do {
return try await provider.fetchSnapshot(location: location, source: .wttr)
} catch {
NSLog("LockScreenWeatherManager: wttr.in fallback also failed - %@", error.localizedDescription)
throw error
}
}
throw error
}
}
private func observeAccessoryChanges() { private func observeAccessoryChanges() {
let bluetoothManager = BluetoothAudioManager.shared let bluetoothManager = BluetoothAudioManager.shared
@ -199,10 +223,10 @@ final class LockScreenWeatherManager: ObservableObject {
private func makeSnapshot(from payload: LockScreenWeatherSnapshot) -> LockScreenWeatherSnapshot { private func makeSnapshot(from payload: LockScreenWeatherSnapshot) -> LockScreenWeatherSnapshot {
let locationName = payload.locationName let locationName = payload.locationName
let shouldShowLocation = Defaults[.lockScreenWeatherShowsLocation] && !(locationName?.isEmpty ?? true)
let chargingInfo = Defaults[.lockScreenWeatherShowsCharging] ? makeChargingInfo() : nil let chargingInfo = Defaults[.lockScreenWeatherShowsCharging] ? makeChargingInfo() : nil
let bluetoothInfo = Defaults[.lockScreenWeatherShowsBluetooth] ? makeBluetoothInfo() : nil let bluetoothInfo = Defaults[.lockScreenWeatherShowsBluetooth] ? makeBluetoothInfo() : nil
let widgetStyle = Defaults[.lockScreenWeatherWidgetStyle] let widgetStyle = Defaults[.lockScreenWeatherWidgetStyle]
let shouldShowLocation = widgetStyle == .inline && Defaults[.lockScreenWeatherShowsLocation] && !(locationName?.isEmpty ?? true)
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
@ -406,6 +430,10 @@ private actor LockScreenWeatherProvider {
func fetchSnapshot(location: CLLocation?) async throws -> LockScreenWeatherSnapshot { func fetchSnapshot(location: CLLocation?) async throws -> LockScreenWeatherSnapshot {
let source = Defaults[.lockScreenWeatherProviderSource] let source = Defaults[.lockScreenWeatherProviderSource]
return try await fetchSnapshot(location: location, source: source)
}
func fetchSnapshot(location: CLLocation?, source: LockScreenWeatherProviderSource) async throws -> LockScreenWeatherSnapshot {
switch source { switch source {
case .wttr: case .wttr:
return try await fetchWttrSnapshot(location: location) return try await fetchWttrSnapshot(location: location)

View file

@ -80,7 +80,9 @@ final class LockScreenWeatherPanelManager {
let originX = screenFrame.midX - (size.width / 2) let originX = screenFrame.midX - (size.width / 2)
let verticalOffset = screenFrame.height * 0.15 let verticalOffset = screenFrame.height * 0.15
let maxY = screenFrame.maxY - size.height - 48 let maxY = screenFrame.maxY - size.height - 48
let originY = min(maxY, screenFrame.midY + verticalOffset) let baseY = min(maxY, screenFrame.midY + verticalOffset)
return NSRect(x: originX, y: originY, width: size.width, height: size.height) let loweredY = baseY - 36
let clampedY = max(screenFrame.minY + 80, loweredY)
return NSRect(x: originX, y: clampedY, width: size.width, height: size.height)
} }
} }

View file

@ -364,10 +364,10 @@ extension Defaults.Keys {
static let lockScreenWeatherShowsCharging = Key<Bool>("lockScreenWeatherShowsCharging", default: true) static let lockScreenWeatherShowsCharging = Key<Bool>("lockScreenWeatherShowsCharging", default: true)
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 lockScreenWeatherWidgetStyle = Key<LockScreenWeatherWidgetStyle>("lockScreenWeatherWidgetStyle", default: .inline) static let lockScreenWeatherWidgetStyle = Key<LockScreenWeatherWidgetStyle>("lockScreenWeatherWidgetStyle", default: .circular)
static let lockScreenWeatherShowsAQI = Key<Bool>("lockScreenWeatherShowsAQI", default: false) static let lockScreenWeatherShowsAQI = Key<Bool>("lockScreenWeatherShowsAQI", default: false)
static let lockScreenWeatherUsesGaugeTint = Key<Bool>("lockScreenWeatherUsesGaugeTint", default: true) static let lockScreenWeatherUsesGaugeTint = Key<Bool>("lockScreenWeatherUsesGaugeTint", default: false)
static let lockScreenWeatherProviderSource = Key<LockScreenWeatherProviderSource>("lockScreenWeatherProviderSource", default: .wttr) static let lockScreenWeatherProviderSource = Key<LockScreenWeatherProviderSource>("lockScreenWeatherProviderSource", default: .openMeteo)
static let lockScreenGlassStyle = Key<LockScreenGlassStyle>("lockScreenGlassStyle", default: .liquid) static let lockScreenGlassStyle = Key<LockScreenGlassStyle>("lockScreenGlassStyle", default: .liquid)
static let lockScreenShowAppIcon = Key<Bool>("lockScreenShowAppIcon", default: false) static let lockScreenShowAppIcon = Key<Bool>("lockScreenShowAppIcon", default: false)
static let lockScreenPanelShowsBorder = Key<Bool>("lockScreenPanelShowsBorder", default: true) static let lockScreenPanelShowsBorder = Key<Bool>("lockScreenPanelShowsBorder", default: true)