diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 0acd062..b4b6c0a 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -8496,9 +8496,6 @@ } } }, - "Full" : { - "comment" : "Full battery level label" - }, "Fully charged" : { "comment" : "Charging fallback label when already charged" }, diff --git a/DynamicIsland/components/LockScreen/LockScreenWeatherWidget.swift b/DynamicIsland/components/LockScreen/LockScreenWeatherWidget.swift index 8a2f5f7..8afc0b5 100644 --- a/DynamicIsland/components/LockScreen/LockScreenWeatherWidget.swift +++ b/DynamicIsland/components/LockScreen/LockScreenWeatherWidget.swift @@ -13,6 +13,8 @@ struct LockScreenWeatherWidget: View { private var stackSpacing: CGFloat { isInline ? 14 : 22 } private var gaugeLineWidth: CGFloat { 6 } private var gaugeDiameter: CGFloat { 64 } + private var topPadding: CGFloat { isInline ? 6 : 22 } + private var bottomPadding: CGFloat { isInline ? 6 : 10 } private var gaugeBackgroundColor: Color { snapshot.usesGaugeTint ? Color.white.opacity(0.22) : Color.white.opacity(0.32) @@ -45,7 +47,8 @@ struct LockScreenWeatherWidget: View { .frame(maxWidth: .infinity, alignment: .leading) .foregroundStyle(Color.white.opacity(0.65)) .padding(.horizontal, 10) - .padding(.vertical, 6) + .padding(.top, topPadding) + .padding(.bottom, bottomPadding) .background(Color.clear) .shadow(color: .black.opacity(0.35), radius: 8, x: 0, y: 3) .accessibilityElement(children: .ignore) @@ -143,7 +146,7 @@ struct LockScreenWeatherWidget: View { Image(systemName: info.iconName) .font(.system(size: 20, weight: .semibold)) .symbolRenderingMode(.hierarchical) - Text(bluetoothStatusLabel(for: info.batteryLevel)) + Text(bluetoothPercentageText(for: info.batteryLevel)) .font(inlinePrimaryFont) .lineLimit(1) .minimumScaleFactor(0.85) @@ -159,7 +162,7 @@ struct LockScreenWeatherWidget: View { .font(.system(size: 22, weight: .semibold)) .foregroundStyle(Color.white) } - Text(bluetoothStatusLabel(for: info.batteryLevel)) + Text(bluetoothPercentageText(for: info.batteryLevel)) .font(inlineSecondaryFont) .foregroundStyle(secondaryLabelColor) } @@ -285,18 +288,8 @@ struct LockScreenWeatherWidget: View { return NSLocalizedString("Charging", comment: "Charging fallback label when no estimate is available") } - private func bluetoothStatusLabel(for level: Int) -> String { - let value = 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 bluetoothPercentageText(for level: Int) -> String { + "\(clampedBatteryLevel(level))%" } private func minimumTemperatureLabel(for info: LockScreenWeatherSnapshot.TemperatureInfo) -> String { @@ -362,25 +355,27 @@ struct LockScreenWeatherWidget: View { private func batteryTint(for level: Int) -> Color { guard snapshot.usesGaugeTint else { return monochromeGaugeTint } - switch level { + let clamped = clampedBatteryLevel(level) + switch clamped { case ..<20: - return Color(red: 0.91, green: 0.29, blue: 0.25) + return Color(.systemRed) case 20..<50: - return Color(red: 0.99, green: 0.74, blue: 0.30) + return Color(.systemOrange) default: - return Color(red: 0.20, green: 0.79, blue: 0.39) + return Color(.systemGreen) } } private func bluetoothTint(for level: Int) -> Color { guard snapshot.usesGaugeTint else { return monochromeGaugeTint } - switch level { + let clamped = clampedBatteryLevel(level) + switch clamped { case ..<20: - return Color(red: 0.91, green: 0.29, blue: 0.25) + return Color(.systemRed) case 20..<50: - return Color(red: 0.97, green: 0.58, blue: 0.29) + return Color(.systemOrange) default: - return Color(red: 0.29, green: 0.63, blue: 1.00) + return Color(.systemGreen) } } diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 8a4f7fe..20d76bb 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -4,13 +4,12 @@ // // Created by Richard Kunkli on 07/08/2024. // - -import AVFoundation import Defaults import EventKit import KeyboardShortcuts import LaunchAtLogin import LottieUI +import AVFoundation import Sparkle import SwiftUI import SwiftUIIntrospect @@ -347,6 +346,7 @@ struct GeneralSettings: View { } .pickerStyle(.segmented) Defaults.Toggle("Show location label", key: .lockScreenWeatherShowsLocation) + .disabled(lockScreenWeatherWidgetStyle == .circular) Defaults.Toggle("Show charging status", key: .lockScreenWeatherShowsCharging) if lockScreenWeatherShowsCharging { Defaults.Toggle("Show charging percentage", key: .lockScreenWeatherShowsChargingPercentage) @@ -662,9 +662,6 @@ struct HUD: View { Section { Toggle("Enable Built-in System HUD", isOn: $enableSystemHUD) - .onChange(of: enableSystemHUD) { _ in - // SystemHUDManager will automatically handle start/stop via its @Default observer - } if enableSystemHUD { Toggle("Volume HUD", isOn: $enableVolumeHUD) @@ -1943,34 +1940,34 @@ struct TimerSettings: View { let nextIndex = timerPresets.count + 1 let defaultColor = Defaults[.accentColor] let newPreset = TimerPreset(name: "Preset \(nextIndex)", duration: 5 * 60, color: defaultColor) - withAnimation(.smooth) { + _ = withAnimation(.smooth) { timerPresets.append(newPreset) } } private func movePresetUp(_ index: Int) { guard index > timerPresets.startIndex else { return } - withAnimation(.smooth) { + _ = withAnimation(.smooth) { timerPresets.swapAt(index, index - 1) } } private func movePresetDown(_ index: Int) { guard index < timerPresets.index(before: timerPresets.endIndex) else { return } - withAnimation(.smooth) { + _ = withAnimation(.smooth) { timerPresets.swapAt(index, index + 1) } } private func removePreset(_ index: Int) { guard timerPresets.indices.contains(index) else { return } - withAnimation(.smooth) { + _ = withAnimation(.smooth) { timerPresets.remove(at: index) } } private func resetPresets() { - withAnimation(.smooth) { + _ = withAnimation(.smooth) { timerPresets = TimerPreset.defaultPresets } } diff --git a/DynamicIsland/managers/LockScreenWeatherManager.swift b/DynamicIsland/managers/LockScreenWeatherManager.swift index 87d877e..c938671 100644 --- a/DynamicIsland/managers/LockScreenWeatherManager.swift +++ b/DynamicIsland/managers/LockScreenWeatherManager.swift @@ -17,6 +17,11 @@ final class LockScreenWeatherManager: ObservableObject { private init() { observeAccessoryChanges() + Task { @MainActor [weak self] in + guard let self else { return } + self.locationProvider.prepareAuthorization() + _ = await self.refresh(force: true) + } } func prepareLocationAccess() { @@ -73,7 +78,7 @@ final class LockScreenWeatherManager: ObservableObject { do { let location = await locationProvider.currentLocation() - let payload = try await provider.fetchSnapshot(location: location) + let payload = try await fetchWeatherPayload(location: location) latestWeatherPayload = payload if Defaults[.lockScreenWeatherShowsBluetooth] { 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() { let bluetoothManager = BluetoothAudioManager.shared @@ -198,11 +222,11 @@ final class LockScreenWeatherManager: ObservableObject { } private func makeSnapshot(from payload: LockScreenWeatherSnapshot) -> LockScreenWeatherSnapshot { - let locationName = payload.locationName - let shouldShowLocation = Defaults[.lockScreenWeatherShowsLocation] && !(locationName?.isEmpty ?? true) + let locationName = payload.locationName let chargingInfo = Defaults[.lockScreenWeatherShowsCharging] ? makeChargingInfo() : nil let bluetoothInfo = Defaults[.lockScreenWeatherShowsBluetooth] ? makeBluetoothInfo() : nil let widgetStyle = Defaults[.lockScreenWeatherWidgetStyle] + let shouldShowLocation = widgetStyle == .inline && Defaults[.lockScreenWeatherShowsLocation] && !(locationName?.isEmpty ?? true) let showsChargingPercentage = Defaults[.lockScreenWeatherShowsChargingPercentage] let providerSource = Defaults[.lockScreenWeatherProviderSource] let airQualityInfo = (Defaults[.lockScreenWeatherShowsAQI] && providerSource.supportsAirQuality) ? payload.airQuality : nil @@ -406,6 +430,10 @@ private actor LockScreenWeatherProvider { func fetchSnapshot(location: CLLocation?) async throws -> LockScreenWeatherSnapshot { let source = Defaults[.lockScreenWeatherProviderSource] + return try await fetchSnapshot(location: location, source: source) + } + + func fetchSnapshot(location: CLLocation?, source: LockScreenWeatherProviderSource) async throws -> LockScreenWeatherSnapshot { switch source { case .wttr: return try await fetchWttrSnapshot(location: location) diff --git a/DynamicIsland/managers/LockScreenWeatherPanelManager.swift b/DynamicIsland/managers/LockScreenWeatherPanelManager.swift index 76fef4f..329e808 100644 --- a/DynamicIsland/managers/LockScreenWeatherPanelManager.swift +++ b/DynamicIsland/managers/LockScreenWeatherPanelManager.swift @@ -80,7 +80,9 @@ final class LockScreenWeatherPanelManager { let originX = screenFrame.midX - (size.width / 2) let verticalOffset = screenFrame.height * 0.15 let maxY = screenFrame.maxY - size.height - 48 - let originY = min(maxY, screenFrame.midY + verticalOffset) - return NSRect(x: originX, y: originY, width: size.width, height: size.height) + let baseY = min(maxY, screenFrame.midY + verticalOffset) + let loweredY = baseY - 36 + let clampedY = max(screenFrame.minY + 80, loweredY) + return NSRect(x: originX, y: clampedY, width: size.width, height: size.height) } } diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index d35b332..4c30f34 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -364,10 +364,10 @@ extension Defaults.Keys { static let lockScreenWeatherShowsCharging = Key("lockScreenWeatherShowsCharging", default: true) static let lockScreenWeatherShowsChargingPercentage = Key("lockScreenWeatherShowsChargingPercentage", default: true) static let lockScreenWeatherShowsBluetooth = Key("lockScreenWeatherShowsBluetooth", default: true) - static let lockScreenWeatherWidgetStyle = Key("lockScreenWeatherWidgetStyle", default: .inline) + static let lockScreenWeatherWidgetStyle = Key("lockScreenWeatherWidgetStyle", default: .circular) static let lockScreenWeatherShowsAQI = Key("lockScreenWeatherShowsAQI", default: false) - static let lockScreenWeatherUsesGaugeTint = Key("lockScreenWeatherUsesGaugeTint", default: true) - static let lockScreenWeatherProviderSource = Key("lockScreenWeatherProviderSource", default: .wttr) + static let lockScreenWeatherUsesGaugeTint = Key("lockScreenWeatherUsesGaugeTint", default: false) + static let lockScreenWeatherProviderSource = Key("lockScreenWeatherProviderSource", default: .openMeteo) static let lockScreenGlassStyle = Key("lockScreenGlassStyle", default: .liquid) static let lockScreenShowAppIcon = Key("lockScreenShowAppIcon", default: false) static let lockScreenPanelShowsBorder = Key("lockScreenPanelShowsBorder", default: true)