mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
181 lines
6.2 KiB
Swift
181 lines
6.2 KiB
Swift
import SwiftUI
|
|
|
|
struct LockScreenWeatherWidget: View {
|
|
let snapshot: LockScreenWeatherSnapshot
|
|
|
|
private let primaryFont = Font.system(size: 22, weight: .semibold, design: .rounded)
|
|
|
|
var body: some View {
|
|
HStack(alignment: .firstTextBaseline, spacing: 16) {
|
|
if let charging = snapshot.charging {
|
|
chargingSegment(for: charging)
|
|
}
|
|
|
|
if let bluetooth = snapshot.bluetooth {
|
|
bluetoothSegment(for: bluetooth)
|
|
}
|
|
|
|
weatherSegment
|
|
|
|
if shouldShowLocation {
|
|
locationSegment
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.foregroundStyle(Color.white.opacity(0.65))
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 6)
|
|
.background(Color.clear)
|
|
.shadow(color: .black.opacity(0.35), radius: 8, x: 0, y: 3)
|
|
.accessibilityElement(children: .ignore)
|
|
.accessibilityLabel(accessibilityLabel)
|
|
}
|
|
|
|
private var weatherSegment: some View {
|
|
HStack(alignment: .firstTextBaseline, spacing: 6) {
|
|
Image(systemName: snapshot.symbolName)
|
|
.font(.system(size: 26, weight: .medium))
|
|
.symbolRenderingMode(.hierarchical)
|
|
Text(snapshot.temperatureText)
|
|
.font(primaryFont)
|
|
.kerning(-0.3)
|
|
.lineLimit(1)
|
|
.minimumScaleFactor(0.9)
|
|
.layoutPriority(2)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func chargingSegment(for info: LockScreenWeatherSnapshot.ChargingInfo) -> some View {
|
|
if let iconName = chargingIconName(for: info) {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: iconName)
|
|
.font(.system(size: 20, weight: .semibold))
|
|
.symbolRenderingMode(.hierarchical)
|
|
|
|
if let text = formattedChargingTime(for: info) {
|
|
Text(text)
|
|
.font(primaryFont)
|
|
.lineLimit(1)
|
|
.minimumScaleFactor(0.75)
|
|
.layoutPriority(1.2)
|
|
}
|
|
}
|
|
.layoutPriority(1)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func bluetoothSegment(for info: LockScreenWeatherSnapshot.BluetoothInfo) -> some View {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: info.iconName)
|
|
.font(.system(size: 20, weight: .semibold))
|
|
.symbolRenderingMode(.hierarchical)
|
|
Text("\(clampedBatteryLevel(info.batteryLevel))%")
|
|
.font(primaryFont)
|
|
.lineLimit(1)
|
|
.truncationMode(.tail)
|
|
.minimumScaleFactor(0.85)
|
|
.layoutPriority(1.4)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var locationSegment: some View {
|
|
if let locationName = snapshot.locationName {
|
|
Text(locationName)
|
|
.font(primaryFont)
|
|
.lineLimit(1)
|
|
.truncationMode(.tail)
|
|
.minimumScaleFactor(0.75)
|
|
.layoutPriority(0.7)
|
|
}
|
|
}
|
|
|
|
private var shouldShowLocation: Bool {
|
|
snapshot.showsLocation && (snapshot.locationName?.isEmpty == false)
|
|
}
|
|
|
|
private func chargingIconName(for info: LockScreenWeatherSnapshot.ChargingInfo) -> String? {
|
|
let icon = info.iconName
|
|
return icon.isEmpty ? nil : icon
|
|
}
|
|
|
|
private func formattedChargingTime(for info: LockScreenWeatherSnapshot.ChargingInfo) -> String? {
|
|
guard let minutes = info.minutesRemaining, minutes > 0 else {
|
|
return nil
|
|
}
|
|
|
|
let hours = minutes / 60
|
|
let remainingMinutes = minutes % 60
|
|
|
|
if hours > 0 {
|
|
return "\(hours)h \(remainingMinutes)m"
|
|
}
|
|
return "\(remainingMinutes)m"
|
|
}
|
|
|
|
private func clampedBatteryLevel(_ level: Int) -> Int {
|
|
min(max(level, 0), 100)
|
|
}
|
|
|
|
private var accessibilityLabel: String {
|
|
var components: [String] = []
|
|
|
|
if snapshot.showsLocation, let locationName = snapshot.locationName, !locationName.isEmpty {
|
|
components.append(
|
|
String(
|
|
format: NSLocalizedString("Weather: %@ %@ in %@", comment: "Weather description, temperature, and location"),
|
|
snapshot.description,
|
|
snapshot.temperatureText,
|
|
locationName
|
|
)
|
|
)
|
|
} else {
|
|
components.append(
|
|
String(
|
|
format: NSLocalizedString("Weather: %@ %@", comment: "Weather description and temperature"),
|
|
snapshot.description,
|
|
snapshot.temperatureText
|
|
)
|
|
)
|
|
}
|
|
|
|
if let charging = snapshot.charging {
|
|
components.append(accessibilityChargingText(for: charging))
|
|
}
|
|
|
|
if let bluetooth = snapshot.bluetooth {
|
|
components.append(accessibilityBluetoothText(for: bluetooth))
|
|
}
|
|
|
|
return components.joined(separator: ". ")
|
|
}
|
|
|
|
private func accessibilityChargingText(for charging: LockScreenWeatherSnapshot.ChargingInfo) -> String {
|
|
if let minutes = charging.minutesRemaining, minutes > 0 {
|
|
let formatter = DateComponentsFormatter()
|
|
formatter.allowedUnits = [.hour, .minute]
|
|
formatter.unitsStyle = .full
|
|
let duration = formatter.string(from: TimeInterval(minutes * 60)) ?? "\(minutes) minutes"
|
|
return String(
|
|
format: NSLocalizedString("Battery charging, %@ remaining", comment: "Charging time remaining"),
|
|
duration
|
|
)
|
|
}
|
|
|
|
if charging.isPluggedIn && !charging.isCharging {
|
|
return NSLocalizedString("Battery fully charged", comment: "Battery is full")
|
|
}
|
|
|
|
return NSLocalizedString("Battery charging", comment: "Battery charging without estimate")
|
|
}
|
|
|
|
private func accessibilityBluetoothText(for bluetooth: LockScreenWeatherSnapshot.BluetoothInfo) -> String {
|
|
return String(
|
|
format: NSLocalizedString("Bluetooth device %@ at %d percent", comment: "Bluetooth device battery"),
|
|
bluetooth.deviceName,
|
|
bluetooth.batteryLevel
|
|
)
|
|
}
|
|
}
|