mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
update to gauges
This commit is contained in:
parent
2612cf9d7f
commit
cb2369e621
6 changed files with 240 additions and 109 deletions
|
|
@ -1323,6 +1323,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"0" : {
|
||||||
|
"comment" : "A label displayed in a circular gauge to indicate the minimum battery level.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"1x" : {
|
"1x" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
|
|
@ -1433,6 +1437,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"100" : {
|
||||||
|
"comment" : "The text displayed below a 100% battery level in the lock screen.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"100%" : {
|
"100%" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
|
|
@ -10823,6 +10831,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Mac battery at %d percent" : {
|
||||||
|
"comment" : "Mac battery gauge accessibility label"
|
||||||
|
},
|
||||||
"Made ❤️ by Ebullioscopic" : {
|
"Made ❤️ by Ebullioscopic" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
@ -18186,6 +18197,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Temperature unit" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"TheDynamicIsland" : {
|
"TheDynamicIsland" : {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,15 +11,10 @@ struct LockScreenWeatherWidget: View {
|
||||||
private var isInline: Bool { snapshot.widgetStyle == .inline }
|
private var isInline: Bool { snapshot.widgetStyle == .inline }
|
||||||
private var stackAlignment: VerticalAlignment { isInline ? .firstTextBaseline : .top }
|
private var stackAlignment: VerticalAlignment { isInline ? .firstTextBaseline : .top }
|
||||||
private var stackSpacing: CGFloat { isInline ? 14 : 22 }
|
private var stackSpacing: CGFloat { isInline ? 14 : 22 }
|
||||||
private var gaugeLineWidth: CGFloat { 6 }
|
|
||||||
private var gaugeDiameter: CGFloat { 64 }
|
private var gaugeDiameter: CGFloat { 64 }
|
||||||
private var topPadding: CGFloat { isInline ? 6 : 22 }
|
private var topPadding: CGFloat { isInline ? 6 : 22 }
|
||||||
private var bottomPadding: CGFloat { isInline ? 6 : 10 }
|
private var bottomPadding: CGFloat { isInline ? 6 : 10 }
|
||||||
|
|
||||||
private var gaugeBackgroundColor: Color {
|
|
||||||
snapshot.usesGaugeTint ? Color.white.opacity(0.22) : Color.white.opacity(0.32)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var monochromeGaugeTint: Color {
|
private var monochromeGaugeTint: Color {
|
||||||
Color.white.opacity(0.9)
|
Color.white.opacity(0.9)
|
||||||
}
|
}
|
||||||
|
|
@ -30,6 +25,10 @@ struct LockScreenWeatherWidget: View {
|
||||||
chargingSegment(for: charging)
|
chargingSegment(for: charging)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !isInline, let battery = snapshot.battery {
|
||||||
|
batterySegment(for: battery)
|
||||||
|
}
|
||||||
|
|
||||||
if let bluetooth = snapshot.bluetooth {
|
if let bluetooth = snapshot.bluetooth {
|
||||||
bluetoothSegment(for: bluetooth)
|
bluetoothSegment(for: bluetooth)
|
||||||
}
|
}
|
||||||
|
|
@ -98,6 +97,16 @@ struct LockScreenWeatherWidget: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func batterySegment(for info: LockScreenWeatherSnapshot.BatteryInfo) -> some View {
|
||||||
|
switch snapshot.widgetStyle {
|
||||||
|
case .inline:
|
||||||
|
EmptyView()
|
||||||
|
case .circular:
|
||||||
|
circularBatterySegment(for: info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func inlineChargingSegment(for info: LockScreenWeatherSnapshot.ChargingInfo) -> some View {
|
private func inlineChargingSegment(for info: LockScreenWeatherSnapshot.ChargingInfo) -> some View {
|
||||||
HStack(alignment: .firstTextBaseline, spacing: 6) {
|
HStack(alignment: .firstTextBaseline, spacing: 6) {
|
||||||
if let iconName = chargingIconName(for: info) {
|
if let iconName = chargingIconName(for: info) {
|
||||||
|
|
@ -115,11 +124,27 @@ struct LockScreenWeatherWidget: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func circularChargingSegment(for info: LockScreenWeatherSnapshot.ChargingInfo) -> some View {
|
private func circularChargingSegment(for info: LockScreenWeatherSnapshot.ChargingInfo) -> some View {
|
||||||
if let level = info.batteryLevel {
|
if let rawLevel = info.batteryLevel {
|
||||||
|
let level = clampedBatteryLevel(rawLevel)
|
||||||
|
|
||||||
VStack(spacing: 6) {
|
VStack(spacing: 6) {
|
||||||
circularBadge(progress: Double(level) / 100, tint: batteryTint(for: level), lineWidth: gaugeLineWidth, diameter: gaugeDiameter) {
|
Gauge(value: Double(level), in: 0...100) {
|
||||||
|
EmptyView()
|
||||||
|
} currentValueLabel: {
|
||||||
chargingGlyph(for: info)
|
chargingGlyph(for: info)
|
||||||
|
} minimumValueLabel: {
|
||||||
|
Text("0")
|
||||||
|
.font(.system(size: 11, weight: .medium, design: .rounded))
|
||||||
|
.foregroundStyle(secondaryLabelColor)
|
||||||
|
} maximumValueLabel: {
|
||||||
|
Text("100")
|
||||||
|
.font(.system(size: 11, weight: .medium, design: .rounded))
|
||||||
|
.foregroundStyle(secondaryLabelColor)
|
||||||
}
|
}
|
||||||
|
.gaugeStyle(.accessoryCircularCapacity)
|
||||||
|
.tint(batteryTint(for: level))
|
||||||
|
.frame(width: gaugeDiameter, height: gaugeDiameter)
|
||||||
|
|
||||||
Text(chargingDetailLabel(for: info))
|
Text(chargingDetailLabel(for: info))
|
||||||
.font(inlineSecondaryFont)
|
.font(inlineSecondaryFont)
|
||||||
.foregroundStyle(secondaryLabelColor)
|
.foregroundStyle(secondaryLabelColor)
|
||||||
|
|
@ -131,6 +156,32 @@ struct LockScreenWeatherWidget: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func circularBatterySegment(for info: LockScreenWeatherSnapshot.BatteryInfo) -> some View {
|
||||||
|
let level = clampedBatteryLevel(info.batteryLevel)
|
||||||
|
|
||||||
|
return VStack(spacing: 6) {
|
||||||
|
Gauge(value: Double(level), in: 0...100) {
|
||||||
|
EmptyView()
|
||||||
|
} currentValueLabel: {
|
||||||
|
Image(systemName: "battery.100")
|
||||||
|
.font(.system(size: 20, weight: .semibold))
|
||||||
|
.foregroundStyle(Color.white)
|
||||||
|
} minimumValueLabel: {
|
||||||
|
EmptyView()
|
||||||
|
} maximumValueLabel: {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
.gaugeStyle(.accessoryCircularCapacity)
|
||||||
|
.tint(batteryTint(for: level))
|
||||||
|
.frame(width: gaugeDiameter, height: gaugeDiameter)
|
||||||
|
|
||||||
|
Text("\(level)%")
|
||||||
|
.font(inlineSecondaryFont)
|
||||||
|
.foregroundStyle(secondaryLabelColor)
|
||||||
|
}
|
||||||
|
.layoutPriority(1)
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func bluetoothSegment(for info: LockScreenWeatherSnapshot.BluetoothInfo) -> some View {
|
private func bluetoothSegment(for info: LockScreenWeatherSnapshot.BluetoothInfo) -> some View {
|
||||||
switch snapshot.widgetStyle {
|
switch snapshot.widgetStyle {
|
||||||
|
|
@ -155,13 +206,24 @@ struct LockScreenWeatherWidget: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func circularBluetoothSegment(for info: LockScreenWeatherSnapshot.BluetoothInfo) -> some View {
|
private func circularBluetoothSegment(for info: LockScreenWeatherSnapshot.BluetoothInfo) -> some View {
|
||||||
VStack(spacing: 6) {
|
|
||||||
let clamped = clampedBatteryLevel(info.batteryLevel)
|
let clamped = clampedBatteryLevel(info.batteryLevel)
|
||||||
circularBadge(progress: Double(clamped) / 100, tint: bluetoothTint(for: clamped), lineWidth: gaugeLineWidth, diameter: gaugeDiameter) {
|
|
||||||
|
return VStack(spacing: 6) {
|
||||||
|
Gauge(value: Double(clamped), in: 0...100) {
|
||||||
|
EmptyView()
|
||||||
|
} currentValueLabel: {
|
||||||
Image(systemName: info.iconName)
|
Image(systemName: info.iconName)
|
||||||
.font(.system(size: 22, weight: .semibold))
|
.font(.system(size: 22, weight: .semibold))
|
||||||
.foregroundStyle(Color.white)
|
.foregroundStyle(Color.white)
|
||||||
|
} minimumValueLabel: {
|
||||||
|
EmptyView()
|
||||||
|
} maximumValueLabel: {
|
||||||
|
EmptyView()
|
||||||
}
|
}
|
||||||
|
.gaugeStyle(.accessoryCircularCapacity)
|
||||||
|
.tint(bluetoothTint(for: clamped))
|
||||||
|
.frame(width: gaugeDiameter, height: gaugeDiameter)
|
||||||
|
|
||||||
Text(bluetoothPercentageText(for: info.batteryLevel))
|
Text(bluetoothPercentageText(for: info.batteryLevel))
|
||||||
.font(inlineSecondaryFont)
|
.font(inlineSecondaryFont)
|
||||||
.foregroundStyle(secondaryLabelColor)
|
.foregroundStyle(secondaryLabelColor)
|
||||||
|
|
@ -193,11 +255,17 @@ struct LockScreenWeatherWidget: View {
|
||||||
|
|
||||||
private func circularAirQualitySegment(for info: LockScreenWeatherSnapshot.AirQualityInfo) -> some View {
|
private func circularAirQualitySegment(for info: LockScreenWeatherSnapshot.AirQualityInfo) -> some View {
|
||||||
VStack(spacing: 6) {
|
VStack(spacing: 6) {
|
||||||
circularBadge(progress: aqiProgress(for: info.index), tint: aqiTint(for: info.category), lineWidth: gaugeLineWidth, diameter: gaugeDiameter) {
|
Gauge(value: Double(info.index), in: 0...500) {
|
||||||
|
EmptyView()
|
||||||
|
} currentValueLabel: {
|
||||||
Text("\(info.index)")
|
Text("\(info.index)")
|
||||||
.font(.system(size: 20, weight: .semibold, design: .rounded))
|
.font(.system(size: 20, weight: .semibold, design: .rounded))
|
||||||
.foregroundStyle(Color.white)
|
.foregroundStyle(Color.white)
|
||||||
}
|
}
|
||||||
|
.gaugeStyle(.accessoryCircular)
|
||||||
|
.tint(aqiTint(for: info.category))
|
||||||
|
.frame(width: gaugeDiameter, height: gaugeDiameter)
|
||||||
|
|
||||||
Text("AQI · \(info.category.displayName)")
|
Text("AQI · \(info.category.displayName)")
|
||||||
.font(inlineSecondaryFont)
|
.font(inlineSecondaryFont)
|
||||||
.foregroundStyle(secondaryLabelColor)
|
.foregroundStyle(secondaryLabelColor)
|
||||||
|
|
@ -207,17 +275,25 @@ struct LockScreenWeatherWidget: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func temperatureGauge(for info: LockScreenWeatherSnapshot.TemperatureInfo) -> some View {
|
private func temperatureGauge(for info: LockScreenWeatherSnapshot.TemperatureInfo) -> some View {
|
||||||
VStack(spacing: 6) {
|
let range = temperatureRange(for: info)
|
||||||
circularBadge(progress: temperatureProgress(for: info), tint: temperatureTint(for: info), lineWidth: gaugeLineWidth, diameter: gaugeDiameter) {
|
|
||||||
|
return VStack(spacing: 6) {
|
||||||
|
Gauge(value: info.current, in: range) {
|
||||||
|
EmptyView()
|
||||||
|
} currentValueLabel: {
|
||||||
temperatureCenterLabel(for: info)
|
temperatureCenterLabel(for: info)
|
||||||
}
|
}
|
||||||
|
.gaugeStyle(.accessoryCircular)
|
||||||
|
.tint(temperatureTint(for: info))
|
||||||
|
.frame(width: gaugeDiameter, height: gaugeDiameter)
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Text(minimumTemperatureLabel(for: info))
|
Text(minimumTemperatureLabel(for: info))
|
||||||
.font(inlineSecondaryFont)
|
.font(.system(size: 11, weight: .medium, design: .rounded))
|
||||||
.foregroundStyle(secondaryLabelColor)
|
.foregroundStyle(secondaryLabelColor)
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(maximumTemperatureLabel(for: info))
|
Text(maximumTemperatureLabel(for: info))
|
||||||
.font(inlineSecondaryFont)
|
.font(.system(size: 11, weight: .medium, design: .rounded))
|
||||||
.foregroundStyle(secondaryLabelColor)
|
.foregroundStyle(secondaryLabelColor)
|
||||||
}
|
}
|
||||||
.frame(width: gaugeDiameter)
|
.frame(width: gaugeDiameter)
|
||||||
|
|
@ -307,21 +383,27 @@ struct LockScreenWeatherWidget: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func temperatureCenterLabel(for info: LockScreenWeatherSnapshot.TemperatureInfo) -> some View {
|
private func temperatureCenterLabel(for info: LockScreenWeatherSnapshot.TemperatureInfo) -> some View {
|
||||||
let symbol = info.unitSymbol
|
|
||||||
let unitSuffix = symbol.replacingOccurrences(of: "°", with: "")
|
|
||||||
return HStack(alignment: .top, spacing: 2) {
|
return HStack(alignment: .top, spacing: 2) {
|
||||||
Text("\(info.displayCurrent)°")
|
Text("\(info.displayCurrent)°")
|
||||||
.font(.system(size: 20, weight: .semibold, design: .rounded))
|
.font(.system(size: 20, weight: .semibold, design: .rounded))
|
||||||
if !unitSuffix.isEmpty {
|
|
||||||
Text(unitSuffix)
|
|
||||||
.font(.system(size: 11, weight: .medium, design: .rounded))
|
|
||||||
.foregroundStyle(secondaryLabelColor)
|
|
||||||
.offset(y: 2)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.foregroundStyle(Color.white)
|
.foregroundStyle(Color.white)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func temperatureRange(for info: LockScreenWeatherSnapshot.TemperatureInfo) -> ClosedRange<Double> {
|
||||||
|
let minimumCandidate = info.minimum ?? info.current
|
||||||
|
let maximumCandidate = info.maximum ?? info.current
|
||||||
|
var lowerBound = min(minimumCandidate, info.current)
|
||||||
|
var upperBound = max(maximumCandidate, info.current)
|
||||||
|
|
||||||
|
if lowerBound == upperBound {
|
||||||
|
lowerBound -= 1
|
||||||
|
upperBound += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return lowerBound...upperBound
|
||||||
|
}
|
||||||
|
|
||||||
private func clampedBatteryLevel(_ level: Int) -> Int {
|
private func clampedBatteryLevel(_ level: Int) -> Int {
|
||||||
min(max(level, 0), 100)
|
min(max(level, 0), 100)
|
||||||
}
|
}
|
||||||
|
|
@ -336,23 +418,6 @@ struct LockScreenWeatherWidget: View {
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
private func aqiProgress(for index: Int) -> Double {
|
|
||||||
let clamped = min(max(Double(index), 0), 500)
|
|
||||||
return clamped / 500
|
|
||||||
}
|
|
||||||
|
|
||||||
private func temperatureProgress(for info: LockScreenWeatherSnapshot.TemperatureInfo) -> Double {
|
|
||||||
let current = info.current
|
|
||||||
let minValue = info.minimum ?? current
|
|
||||||
let maxValue = info.maximum ?? current
|
|
||||||
|
|
||||||
let lowerBound = min(minValue, current)
|
|
||||||
let upperBound = max(maxValue, current)
|
|
||||||
let span = max(upperBound - lowerBound, 1)
|
|
||||||
|
|
||||||
return min(max((current - lowerBound) / span, 0), 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 }
|
||||||
let clamped = clampedBatteryLevel(level)
|
let clamped = clampedBatteryLevel(level)
|
||||||
|
|
@ -416,27 +481,6 @@ struct LockScreenWeatherWidget: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private func circularBadge<Content: View>(
|
|
||||||
progress: Double?,
|
|
||||||
tint: Color,
|
|
||||||
background: Color? = nil,
|
|
||||||
lineWidth: CGFloat,
|
|
||||||
diameter: CGFloat,
|
|
||||||
@ViewBuilder content: @escaping () -> Content
|
|
||||||
) -> some View {
|
|
||||||
if let progress {
|
|
||||||
CircularMetricView(
|
|
||||||
progress: progress,
|
|
||||||
foreground: tint,
|
|
||||||
background: background ?? gaugeBackgroundColor,
|
|
||||||
lineWidth: lineWidth,
|
|
||||||
diameter: diameter,
|
|
||||||
content: content
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var accessibilityLabel: String {
|
private var accessibilityLabel: String {
|
||||||
var components: [String] = []
|
var components: [String] = []
|
||||||
|
|
||||||
|
|
@ -471,6 +515,10 @@ struct LockScreenWeatherWidget: View {
|
||||||
components.append(accessibilityAirQualityText(for: airQuality))
|
components.append(accessibilityAirQualityText(for: airQuality))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let battery = snapshot.battery, !isInline {
|
||||||
|
components.append(accessibilityBatteryText(for: battery))
|
||||||
|
}
|
||||||
|
|
||||||
return components.joined(separator: ". ")
|
return components.joined(separator: ". ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -515,46 +563,12 @@ struct LockScreenWeatherWidget: View {
|
||||||
airQuality.category.displayName
|
airQuality.category.displayName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private struct CircularMetricView<Content: View>: View {
|
private func accessibilityBatteryText(for battery: LockScreenWeatherSnapshot.BatteryInfo) -> String {
|
||||||
private let progress: Double
|
String(
|
||||||
private let foreground: Color
|
format: NSLocalizedString("Mac battery at %d percent", comment: "Mac battery gauge accessibility label"),
|
||||||
private let background: Color
|
clampedBatteryLevel(battery.batteryLevel)
|
||||||
private let lineWidth: CGFloat
|
|
||||||
private let diameter: CGFloat
|
|
||||||
private let contentBuilder: () -> Content
|
|
||||||
|
|
||||||
init(
|
|
||||||
progress: Double,
|
|
||||||
foreground: Color,
|
|
||||||
background: Color,
|
|
||||||
lineWidth: CGFloat = 6,
|
|
||||||
diameter: CGFloat = 64,
|
|
||||||
@ViewBuilder content: @escaping () -> Content
|
|
||||||
) {
|
|
||||||
self.progress = min(max(progress, 0), 1)
|
|
||||||
self.foreground = foreground
|
|
||||||
self.background = background
|
|
||||||
self.lineWidth = lineWidth
|
|
||||||
self.diameter = diameter
|
|
||||||
self.contentBuilder = content
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ZStack {
|
|
||||||
Circle()
|
|
||||||
.stroke(background, lineWidth: lineWidth)
|
|
||||||
Circle()
|
|
||||||
.trim(from: 0, to: CGFloat(progress))
|
|
||||||
.stroke(
|
|
||||||
foreground,
|
|
||||||
style: StrokeStyle(lineWidth: lineWidth, lineCap: .round, lineJoin: .round)
|
|
||||||
)
|
)
|
||||||
.rotationEffect(.degrees(-90))
|
|
||||||
contentBuilder()
|
|
||||||
}
|
|
||||||
.frame(width: diameter, height: diameter)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,9 @@ struct GeneralSettings: View {
|
||||||
@Default(.lockScreenWeatherShowsCharging) var lockScreenWeatherShowsCharging
|
@Default(.lockScreenWeatherShowsCharging) var lockScreenWeatherShowsCharging
|
||||||
@Default(.lockScreenWeatherShowsChargingPercentage) var lockScreenWeatherShowsChargingPercentage
|
@Default(.lockScreenWeatherShowsChargingPercentage) var lockScreenWeatherShowsChargingPercentage
|
||||||
@Default(.lockScreenWeatherShowsBluetooth) var lockScreenWeatherShowsBluetooth
|
@Default(.lockScreenWeatherShowsBluetooth) var lockScreenWeatherShowsBluetooth
|
||||||
|
@Default(.lockScreenWeatherShowsBatteryGauge) var lockScreenWeatherShowsBatteryGauge
|
||||||
@Default(.lockScreenWeatherWidgetStyle) var lockScreenWeatherWidgetStyle
|
@Default(.lockScreenWeatherWidgetStyle) var lockScreenWeatherWidgetStyle
|
||||||
|
@Default(.lockScreenWeatherTemperatureUnit) var lockScreenWeatherTemperatureUnit
|
||||||
@Default(.lockScreenWeatherShowsAQI) var lockScreenWeatherShowsAQI
|
@Default(.lockScreenWeatherShowsAQI) var lockScreenWeatherShowsAQI
|
||||||
@Default(.lockScreenWeatherUsesGaugeTint) var lockScreenWeatherUsesGaugeTint
|
@Default(.lockScreenWeatherUsesGaugeTint) var lockScreenWeatherUsesGaugeTint
|
||||||
@Default(.lockScreenWeatherProviderSource) var lockScreenWeatherProviderSource
|
@Default(.lockScreenWeatherProviderSource) var lockScreenWeatherProviderSource
|
||||||
|
|
@ -345,12 +347,19 @@ struct GeneralSettings: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.pickerStyle(.segmented)
|
.pickerStyle(.segmented)
|
||||||
|
Picker("Temperature unit", selection: $lockScreenWeatherTemperatureUnit) {
|
||||||
|
ForEach(LockScreenWeatherTemperatureUnit.allCases) { unit in
|
||||||
|
Text(unit.rawValue).tag(unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
Defaults.Toggle("Show location label", key: .lockScreenWeatherShowsLocation)
|
Defaults.Toggle("Show location label", key: .lockScreenWeatherShowsLocation)
|
||||||
.disabled(lockScreenWeatherWidgetStyle == .circular)
|
.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)
|
||||||
}
|
}
|
||||||
|
Defaults.Toggle("Show battery indicator", key: .lockScreenWeatherShowsBatteryGauge)
|
||||||
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)
|
||||||
|
|
|
||||||
|
|
@ -102,3 +102,26 @@ enum LockScreenWeatherProviderSource: String, CaseIterable, Defaults.Serializabl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum LockScreenWeatherTemperatureUnit: String, CaseIterable, Defaults.Serializable, Identifiable {
|
||||||
|
case celsius = "Celsius"
|
||||||
|
case fahrenheit = "Fahrenheit"
|
||||||
|
|
||||||
|
var id: String { rawValue }
|
||||||
|
|
||||||
|
var usesMetricSystem: Bool { self == .celsius }
|
||||||
|
|
||||||
|
var symbol: String {
|
||||||
|
switch self {
|
||||||
|
case .celsius: return "°C"
|
||||||
|
case .fahrenheit: return "°F"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var openMeteoTemperatureParameter: String? {
|
||||||
|
switch self {
|
||||||
|
case .celsius: return nil
|
||||||
|
case .fahrenheit: return "fahrenheit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,11 @@ final class LockScreenWeatherManager: ObservableObject {
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func refresh(force: Bool = false) async -> LockScreenWeatherSnapshot? {
|
func refresh(force: Bool = false) async -> LockScreenWeatherSnapshot? {
|
||||||
|
NSLog("LockScreenWeatherManager: refresh requested (force=%@)", force ? "true" : "false")
|
||||||
let interval = Defaults[.lockScreenWeatherRefreshInterval]
|
let interval = Defaults[.lockScreenWeatherRefreshInterval]
|
||||||
if !force, let lastFetchDate, Date().timeIntervalSince(lastFetchDate) < interval {
|
if !force, let lastFetchDate, Date().timeIntervalSince(lastFetchDate) < interval {
|
||||||
|
let remaining = interval - Date().timeIntervalSince(lastFetchDate)
|
||||||
|
NSLog("LockScreenWeatherManager: skipping refresh (%.0f s remaining until next fetch)", max(remaining, 0))
|
||||||
if let payload = latestWeatherPayload {
|
if let payload = latestWeatherPayload {
|
||||||
if Defaults[.lockScreenWeatherShowsBluetooth] {
|
if Defaults[.lockScreenWeatherShowsBluetooth] {
|
||||||
BluetoothAudioManager.shared.refreshConnectedDeviceBatteries()
|
BluetoothAudioManager.shared.refreshConnectedDeviceBatteries()
|
||||||
|
|
@ -78,6 +81,12 @@ final class LockScreenWeatherManager: ObservableObject {
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let location = await locationProvider.currentLocation()
|
let location = await locationProvider.currentLocation()
|
||||||
|
let provider = Defaults[.lockScreenWeatherProviderSource]
|
||||||
|
NSLog(
|
||||||
|
"LockScreenWeatherManager: fetching weather from %@ (location %@)",
|
||||||
|
provider.displayName,
|
||||||
|
location != nil ? "available" : "missing"
|
||||||
|
)
|
||||||
let payload = try await fetchWeatherPayload(location: location)
|
let payload = try await fetchWeatherPayload(location: location)
|
||||||
latestWeatherPayload = payload
|
latestWeatherPayload = payload
|
||||||
if Defaults[.lockScreenWeatherShowsBluetooth] {
|
if Defaults[.lockScreenWeatherShowsBluetooth] {
|
||||||
|
|
@ -87,18 +96,24 @@ final class LockScreenWeatherManager: ObservableObject {
|
||||||
self.snapshot = snapshot
|
self.snapshot = snapshot
|
||||||
lastFetchDate = Date()
|
lastFetchDate = Date()
|
||||||
deliver(snapshot, forceShow: false)
|
deliver(snapshot, forceShow: false)
|
||||||
|
NSLog("LockScreenWeatherManager: weather refresh succeeded")
|
||||||
return snapshot
|
return snapshot
|
||||||
} catch {
|
} catch {
|
||||||
NSLog("LockScreenWeatherManager: failed to fetch weather - \(error.localizedDescription)")
|
NSLog("LockScreenWeatherManager: failed to fetch weather - \(error.localizedDescription)")
|
||||||
|
|
||||||
let providerSource = Defaults[.lockScreenWeatherProviderSource]
|
let providerSource = Defaults[.lockScreenWeatherProviderSource]
|
||||||
|
let showsCharging = Defaults[.lockScreenWeatherShowsCharging]
|
||||||
|
let chargingInfo = showsCharging ? makeChargingInfo() : nil
|
||||||
|
let showsBatteryGauge = Defaults[.lockScreenWeatherShowsBatteryGauge]
|
||||||
|
let batteryInfo = showsBatteryGauge ? makeBatteryGaugeInfo(isCharging: chargingInfo != nil) : nil
|
||||||
let fallback = LockScreenWeatherSnapshot(
|
let fallback = LockScreenWeatherSnapshot(
|
||||||
temperatureText: snapshot?.temperatureText ?? "--",
|
temperatureText: snapshot?.temperatureText ?? "--",
|
||||||
symbolName: snapshot?.symbolName ?? "cloud.fill",
|
symbolName: snapshot?.symbolName ?? "cloud.fill",
|
||||||
description: snapshot?.description ?? "",
|
description: snapshot?.description ?? "",
|
||||||
locationName: snapshot?.locationName,
|
locationName: snapshot?.locationName,
|
||||||
charging: Defaults[.lockScreenWeatherShowsCharging] ? makeChargingInfo() : nil,
|
charging: chargingInfo,
|
||||||
bluetooth: Defaults[.lockScreenWeatherShowsBluetooth] ? makeBluetoothInfo() : nil,
|
bluetooth: Defaults[.lockScreenWeatherShowsBluetooth] ? makeBluetoothInfo() : nil,
|
||||||
|
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: Defaults[.lockScreenWeatherWidgetStyle],
|
||||||
|
|
@ -172,8 +187,12 @@ final class LockScreenWeatherManager: ObservableObject {
|
||||||
.map { _ in () }.eraseToAnyPublisher(),
|
.map { _ in () }.eraseToAnyPublisher(),
|
||||||
Defaults.publisher(.lockScreenWeatherShowsBluetooth, options: [])
|
Defaults.publisher(.lockScreenWeatherShowsBluetooth, options: [])
|
||||||
.map { _ in () }.eraseToAnyPublisher(),
|
.map { _ in () }.eraseToAnyPublisher(),
|
||||||
|
Defaults.publisher(.lockScreenWeatherShowsBatteryGauge, options: [])
|
||||||
|
.map { _ in () }.eraseToAnyPublisher(),
|
||||||
Defaults.publisher(.lockScreenWeatherWidgetStyle, options: [])
|
Defaults.publisher(.lockScreenWeatherWidgetStyle, options: [])
|
||||||
.map { _ in () }.eraseToAnyPublisher(),
|
.map { _ in () }.eraseToAnyPublisher(),
|
||||||
|
Defaults.publisher(.lockScreenWeatherTemperatureUnit, options: [])
|
||||||
|
.map { _ in () }.eraseToAnyPublisher(),
|
||||||
Defaults.publisher(.lockScreenWeatherShowsAQI, options: [])
|
Defaults.publisher(.lockScreenWeatherShowsAQI, options: [])
|
||||||
.map { _ in () }.eraseToAnyPublisher(),
|
.map { _ in () }.eraseToAnyPublisher(),
|
||||||
Defaults.publisher(.lockScreenWeatherUsesGaugeTint, options: [])
|
Defaults.publisher(.lockScreenWeatherUsesGaugeTint, options: [])
|
||||||
|
|
@ -193,10 +212,37 @@ final class LockScreenWeatherManager: ObservableObject {
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
self.latestWeatherPayload = nil
|
self.latestWeatherPayload = nil
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
|
NSLog("LockScreenWeatherManager: provider changed, forcing refresh")
|
||||||
_ = await self.refresh(force: true)
|
_ = await self.refresh(force: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
Defaults.publisher(.lockScreenWeatherTemperatureUnit, options: [])
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
guard let self else { return }
|
||||||
|
self.latestWeatherPayload = nil
|
||||||
|
Task { @MainActor in
|
||||||
|
NSLog("LockScreenWeatherManager: temperature unit changed, forcing refresh")
|
||||||
|
_ = await self.refresh(force: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
Defaults.publisher(.enableLockScreenWeatherWidget, options: [])
|
||||||
|
.sink { [weak self] change in
|
||||||
|
guard let self else { return }
|
||||||
|
Task { @MainActor in
|
||||||
|
if change.newValue {
|
||||||
|
NSLog("LockScreenWeatherManager: widget enabled, triggering refresh")
|
||||||
|
self.locationProvider.prepareAuthorization()
|
||||||
|
_ = await self.refresh(force: true)
|
||||||
|
} else {
|
||||||
|
LockScreenWeatherPanelManager.shared.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleAccessoryUpdate(triggerBluetoothRefresh: Bool) {
|
private func handleAccessoryUpdate(triggerBluetoothRefresh: Bool) {
|
||||||
|
|
@ -230,6 +276,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 usesGaugeTint = Defaults[.lockScreenWeatherUsesGaugeTint]
|
let usesGaugeTint = Defaults[.lockScreenWeatherUsesGaugeTint]
|
||||||
|
|
||||||
return LockScreenWeatherSnapshot(
|
return LockScreenWeatherSnapshot(
|
||||||
|
|
@ -239,6 +286,7 @@ final class LockScreenWeatherManager: ObservableObject {
|
||||||
locationName: locationName,
|
locationName: locationName,
|
||||||
charging: chargingInfo,
|
charging: chargingInfo,
|
||||||
bluetooth: bluetoothInfo,
|
bluetooth: bluetoothInfo,
|
||||||
|
battery: batteryInfo,
|
||||||
showsLocation: shouldShowLocation,
|
showsLocation: shouldShowLocation,
|
||||||
airQuality: airQualityInfo,
|
airQuality: airQualityInfo,
|
||||||
widgetStyle: widgetStyle,
|
widgetStyle: widgetStyle,
|
||||||
|
|
@ -273,6 +321,18 @@ final class LockScreenWeatherManager: ObservableObject {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func makeBatteryGaugeInfo(isCharging: Bool) -> LockScreenWeatherSnapshot.BatteryInfo? {
|
||||||
|
guard !isCharging else { return nil }
|
||||||
|
|
||||||
|
let battery = BatteryStatusViewModel.shared
|
||||||
|
let rawLevel = Int(round(Double(battery.levelBattery)))
|
||||||
|
let clampedLevel = min(max(rawLevel, 0), 100)
|
||||||
|
|
||||||
|
guard clampedLevel >= 0 else { return nil }
|
||||||
|
|
||||||
|
return LockScreenWeatherSnapshot.BatteryInfo(batteryLevel: clampedLevel)
|
||||||
|
}
|
||||||
|
|
||||||
private func makeBluetoothInfo() -> LockScreenWeatherSnapshot.BluetoothInfo? {
|
private func makeBluetoothInfo() -> LockScreenWeatherSnapshot.BluetoothInfo? {
|
||||||
let manager = BluetoothAudioManager.shared
|
let manager = BluetoothAudioManager.shared
|
||||||
|
|
||||||
|
|
@ -352,6 +412,10 @@ struct LockScreenWeatherSnapshot: Equatable {
|
||||||
let iconName: String
|
let iconName: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct BatteryInfo: Equatable {
|
||||||
|
let batteryLevel: Int
|
||||||
|
}
|
||||||
|
|
||||||
struct AirQualityInfo: Equatable {
|
struct AirQualityInfo: Equatable {
|
||||||
enum Category: String, Equatable {
|
enum Category: String, Equatable {
|
||||||
case good
|
case good
|
||||||
|
|
@ -385,6 +449,7 @@ struct LockScreenWeatherSnapshot: Equatable {
|
||||||
let locationName: String?
|
let locationName: String?
|
||||||
let charging: ChargingInfo?
|
let charging: ChargingInfo?
|
||||||
let bluetooth: BluetoothInfo?
|
let bluetooth: BluetoothInfo?
|
||||||
|
let battery: BatteryInfo?
|
||||||
let showsLocation: Bool
|
let showsLocation: Bool
|
||||||
let airQuality: AirQualityInfo?
|
let airQuality: AirQualityInfo?
|
||||||
let widgetStyle: LockScreenWeatherWidgetStyle
|
let widgetStyle: LockScreenWeatherWidgetStyle
|
||||||
|
|
@ -472,11 +537,12 @@ private actor LockScreenWeatherProvider {
|
||||||
throw WeatherProviderError.noData
|
throw WeatherProviderError.noData
|
||||||
}
|
}
|
||||||
|
|
||||||
let usesMetric = Locale.current.usesMetricSystem
|
let unit = Defaults[.lockScreenWeatherTemperatureUnit]
|
||||||
|
let usesMetric = unit.usesMetricSystem
|
||||||
let rawTemperature = usesMetric ? condition.tempC : condition.tempF
|
let rawTemperature = usesMetric ? condition.tempC : condition.tempF
|
||||||
let temperatureValue = Double(rawTemperature) ?? 0
|
let temperatureValue = Double(rawTemperature) ?? 0
|
||||||
let temperatureText = "\(Int(round(temperatureValue)))°"
|
let temperatureText = "\(Int(round(temperatureValue)))°"
|
||||||
let unitSymbol = usesMetric ? "°C" : "°F"
|
let unitSymbol = unit.symbol
|
||||||
|
|
||||||
let forecast = payload.dailyWeather.first
|
let forecast = payload.dailyWeather.first
|
||||||
let minTempValue = forecast.flatMap { daily in
|
let minTempValue = forecast.flatMap { daily in
|
||||||
|
|
@ -519,6 +585,7 @@ private actor LockScreenWeatherProvider {
|
||||||
locationName: locationName,
|
locationName: locationName,
|
||||||
charging: nil,
|
charging: nil,
|
||||||
bluetooth: nil,
|
bluetooth: nil,
|
||||||
|
battery: nil,
|
||||||
showsLocation: true,
|
showsLocation: true,
|
||||||
airQuality: airQualityInfo,
|
airQuality: airQualityInfo,
|
||||||
widgetStyle: .inline,
|
widgetStyle: .inline,
|
||||||
|
|
@ -532,7 +599,8 @@ private actor LockScreenWeatherProvider {
|
||||||
let latitude = String(format: "%.4f", location.coordinate.latitude)
|
let latitude = String(format: "%.4f", location.coordinate.latitude)
|
||||||
let longitude = String(format: "%.4f", location.coordinate.longitude)
|
let longitude = String(format: "%.4f", location.coordinate.longitude)
|
||||||
|
|
||||||
let usesMetric = Locale.current.usesMetricSystem
|
let unit = Defaults[.lockScreenWeatherTemperatureUnit]
|
||||||
|
let usesMetric = unit.usesMetricSystem
|
||||||
var weatherComponents = URLComponents(string: "https://api.open-meteo.com/v1/forecast")
|
var weatherComponents = URLComponents(string: "https://api.open-meteo.com/v1/forecast")
|
||||||
var weatherQueryItems: [URLQueryItem] = [
|
var weatherQueryItems: [URLQueryItem] = [
|
||||||
URLQueryItem(name: "latitude", value: latitude),
|
URLQueryItem(name: "latitude", value: latitude),
|
||||||
|
|
@ -543,8 +611,8 @@ private actor LockScreenWeatherProvider {
|
||||||
URLQueryItem(name: "timezone", value: "auto")
|
URLQueryItem(name: "timezone", value: "auto")
|
||||||
]
|
]
|
||||||
|
|
||||||
if !usesMetric {
|
if let temperatureUnitParameter = unit.openMeteoTemperatureParameter {
|
||||||
weatherQueryItems.append(URLQueryItem(name: "temperature_unit", value: "fahrenheit"))
|
weatherQueryItems.append(URLQueryItem(name: "temperature_unit", value: temperatureUnitParameter))
|
||||||
}
|
}
|
||||||
|
|
||||||
weatherComponents?.queryItems = weatherQueryItems
|
weatherComponents?.queryItems = weatherQueryItems
|
||||||
|
|
@ -569,7 +637,7 @@ private actor LockScreenWeatherProvider {
|
||||||
let temperatureText = "\(Int(round(temperatureValue)))°"
|
let temperatureText = "\(Int(round(temperatureValue)))°"
|
||||||
let code = current.weatherCode ?? 0
|
let code = current.weatherCode ?? 0
|
||||||
let mapping = OpenMeteoSymbolMapper.mapping(for: code)
|
let mapping = OpenMeteoSymbolMapper.mapping(for: code)
|
||||||
let unitSymbol = usesMetric ? "°C" : "°F"
|
let unitSymbol = unit.symbol
|
||||||
let minTempValue = weatherPayload.daily?.temperature2MMin?.first
|
let minTempValue = weatherPayload.daily?.temperature2MMin?.first
|
||||||
let maxTempValue = weatherPayload.daily?.temperature2MMax?.first
|
let maxTempValue = weatherPayload.daily?.temperature2MMax?.first
|
||||||
|
|
||||||
|
|
@ -592,6 +660,7 @@ private actor LockScreenWeatherProvider {
|
||||||
locationName: nil,
|
locationName: nil,
|
||||||
charging: nil,
|
charging: nil,
|
||||||
bluetooth: nil,
|
bluetooth: nil,
|
||||||
|
battery: nil,
|
||||||
showsLocation: true,
|
showsLocation: true,
|
||||||
airQuality: airQualityInfo,
|
airQuality: airQualityInfo,
|
||||||
widgetStyle: .inline,
|
widgetStyle: .inline,
|
||||||
|
|
|
||||||
|
|
@ -364,7 +364,9 @@ 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 lockScreenWeatherShowsBatteryGauge = Key<Bool>("lockScreenWeatherShowsBatteryGauge", default: true)
|
||||||
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 lockScreenWeatherShowsAQI = Key<Bool>("lockScreenWeatherShowsAQI", default: false)
|
static let lockScreenWeatherShowsAQI = Key<Bool>("lockScreenWeatherShowsAQI", default: false)
|
||||||
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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue