Working lock screen stuff

This commit is contained in:
Hariharan Mudaliar 2025-10-27 00:40:03 +05:30
commit 5a722373ce
5 changed files with 171 additions and 72 deletions

View file

@ -4742,9 +4742,6 @@
},
"Configuration" : {
},
"Connected" : {
},
"Continue" : {
"localizations" : {

View file

@ -150,9 +150,8 @@ struct InlineHUD: View {
.frame(maxWidth: .infinity, alignment: .trailing)
.contentTransition(.interpolate)
} else if (type == .bluetoothAudio) {
// Bluetooth device battery display
HStack(spacing: useCircularIndicator ? 8 : 6) {
if hasBatteryLevel {
if hasBatteryLevel {
HStack(spacing: useCircularIndicator ? 8 : 6) {
if useCircularIndicator {
CircularBatteryIndicator(
value: value,
@ -176,16 +175,9 @@ struct InlineHUD: View {
.foregroundStyle(.white)
.lineLimit(1)
}
} else {
// No battery info available
Text("Connected")
.font(.caption)
.fontWeight(.medium)
.foregroundStyle(.gray)
.lineLimit(1)
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
.frame(maxWidth: .infinity, alignment: .trailing)
} else {
// Volume and brightness displays
Group {

View file

@ -3,27 +3,25 @@ import SwiftUI
struct LockScreenWeatherWidget: View {
let snapshot: LockScreenWeatherSnapshot
var body: some View {
let segments = infoSegments
HStack(spacing: 8) {
Image(systemName: snapshot.symbolName)
.font(.system(size: 26, weight: .medium))
.symbolRenderingMode(.hierarchical)
Text(snapshot.temperatureText)
.font(.system(size: 24, weight: .semibold, design: .rounded))
.kerning(-0.4)
private let primaryFont = Font.system(size: 22, weight: .semibold, design: .rounded)
if !segments.isEmpty {
Color.clear.frame(width: 6)
var body: some View {
HStack(alignment: .firstTextBaseline, spacing: 16) {
if let charging = snapshot.charging {
chargingSegment(for: charging)
}
ForEach(Array(segments.enumerated()), id: \.offset) { index, segment in
if index > 0 {
Color.clear.frame(width: 12)
}
segmentView(for: segment)
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)
@ -33,58 +31,90 @@ struct LockScreenWeatherWidget: View {
.accessibilityLabel(accessibilityLabel)
}
private enum InfoSegment: Equatable {
case location(String)
case charging(String)
case bluetooth(String, Int)
}
private var infoSegments: [InfoSegment] {
var segments: [InfoSegment] = []
if snapshot.showsLocation, let locationName = snapshot.locationName, !locationName.isEmpty {
segments.append(.location(locationName))
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)
}
if let charging = snapshot.charging {
let iconName = charging.iconName
if !iconName.isEmpty {
segments.append(.charging(iconName))
}
}
if let bluetooth = snapshot.bluetooth {
segments.append(.bluetooth(bluetooth.iconName, bluetooth.batteryLevel))
}
return segments
}
@ViewBuilder
private func segmentView(for segment: InfoSegment) -> some View {
switch segment {
case .location(let value):
Text(value)
.font(.system(size: 20, weight: .medium, design: .rounded))
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)
case .charging(let icon):
Image(systemName: icon)
.font(.system(size: 18, weight: .semibold))
.symbolRenderingMode(.hierarchical)
case .bluetooth(let icon, let value):
HStack(spacing: 4) {
Image(systemName: icon)
.font(.system(size: 18, weight: .semibold))
.symbolRenderingMode(.hierarchical)
Text("\(clampedBatteryLevel(value))%")
.font(.system(size: 20, weight: .medium, design: .rounded))
.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)
}

View file

@ -85,7 +85,20 @@ final class LockScreenWeatherManager: ObservableObject {
return snapshot
} catch {
NSLog("LockScreenWeatherManager: failed to fetch weather - \(error.localizedDescription)")
return snapshot
let fallback = LockScreenWeatherSnapshot(
temperatureText: snapshot?.temperatureText ?? "--",
symbolName: snapshot?.symbolName ?? "cloud.fill",
description: snapshot?.description ?? "",
locationName: snapshot?.locationName,
charging: Defaults[.lockScreenWeatherShowsCharging] ? makeChargingInfo() : nil,
bluetooth: Defaults[.lockScreenWeatherShowsBluetooth] ? makeBluetoothInfo() : nil,
showsLocation: snapshot?.showsLocation ?? false
)
self.snapshot = fallback
deliver(fallback, forceShow: false)
return fallback
}
}
@ -177,15 +190,17 @@ final class LockScreenWeatherManager: ObservableObject {
private func makeChargingInfo() -> LockScreenWeatherSnapshot.ChargingInfo? {
let battery = BatteryStatusViewModel.shared
let isPluggedIn = battery.isPluggedIn
let isCharging = battery.isCharging
let macStatus = MacBatteryManager.shared.currentStatus()
let isPluggedIn = battery.isPluggedIn || battery.isCharging
let isCharging = macStatus.isCharging || battery.isCharging
guard isPluggedIn || isCharging else {
return nil
}
let minutes = battery.timeToFullCharge
let remaining = minutes > 0 ? minutes : nil
let rawMinutes = macStatus.timeRemainingMinutes ?? (battery.timeToFullCharge > 0 ? battery.timeToFullCharge : nil)
let remaining = (rawMinutes ?? 0) > 0 ? rawMinutes : nil
return LockScreenWeatherSnapshot.ChargingInfo(
minutesRemaining: remaining,
@ -234,7 +249,7 @@ struct LockScreenWeatherSnapshot: Equatable {
return "bolt.fill"
}
if isPluggedIn {
return "powerplug.fill"
return "powerplug.portrait.fill"
}
return ""
}

View file

@ -0,0 +1,65 @@
import Foundation
import IOKit.ps
/// Lightweight helper for querying macOS battery charging status and ETA.
final class MacBatteryManager {
static let shared = MacBatteryManager()
private init() {}
struct BatteryStatus {
let timeRemainingMinutes: Int?
let isCharging: Bool
let percentage: Int?
}
func currentStatus() -> BatteryStatus {
guard let sourcesInfo = IOPSCopyPowerSourcesInfo()?.takeRetainedValue(),
let sourcesList = IOPSCopyPowerSourcesList(sourcesInfo)?.takeRetainedValue() as? [CFTypeRef] else {
return BatteryStatus(timeRemainingMinutes: nil, isCharging: false, percentage: nil)
}
for source in sourcesList {
guard let description = IOPSGetPowerSourceDescription(sourcesInfo, source)?.takeUnretainedValue() as? [String: Any],
let type = description[kIOPSTypeKey] as? String,
type == kIOPSInternalBatteryType else {
continue
}
let isCharging = description[kIOPSIsChargingKey] as? Bool ?? false
let timeRemaining = description[kIOPSTimeToFullChargeKey] as? Int
let currentCapacity = description[kIOPSCurrentCapacityKey] as? Int
let maxCapacity = description[kIOPSMaxCapacityKey] as? Int
let percentage: Int?
if let current = currentCapacity, let max = maxCapacity, max > 0 {
percentage = (current * 100) / max
} else {
percentage = nil
}
return BatteryStatus(
timeRemainingMinutes: timeRemaining,
isCharging: isCharging,
percentage: percentage
)
}
return BatteryStatus(timeRemainingMinutes: nil, isCharging: false, percentage: nil)
}
func formattedTimeToFullCharge() -> String? {
let status = currentStatus()
guard status.isCharging, let minutes = status.timeRemainingMinutes, minutes > 0 else {
return nil
}
let hours = minutes / 60
let remainingMinutes = minutes % 60
if hours > 0 {
return "\(hours)h \(remainingMinutes)m"
}
return "\(remainingMinutes)m"
}
}