Update Settings and Decoupled Bluetooth

This commit is contained in:
Hariharan Mudaliar 2025-12-20 13:39:13 +05:30
commit 97b30858ca
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
6 changed files with 156 additions and 49 deletions

View file

@ -164,11 +164,9 @@ class DynamicIslandViewCoordinator: ObservableObject {
resolvedDuration = duration resolvedDuration = duration
} }
sneakPeekDuration = resolvedDuration sneakPeekDuration = resolvedDuration
if type != .music && type != .timer && type != .reminder { let bypassedTypes: [SneakContentType] = [.music, .timer, .reminder, .bluetoothAudio]
// close() if !bypassedTypes.contains(type) && !Defaults[.enableSystemHUD] {
if !Defaults[.enableSystemHUD] { return
return
}
} }
DispatchQueue.main.async { DispatchQueue.main.async {
withAnimation(.smooth) { withAnimation(.smooth) {

View file

@ -3667,6 +3667,10 @@
"Battery fully charged" : { "Battery fully charged" : {
"comment" : "Battery is full" "comment" : "Battery is full"
}, },
"Battery Indicator Styling" : {
"comment" : "A section header in the Settings app that lists options for customizing the appearance of the battery indicator.",
"isCommentAutoGenerated" : true
},
"Battery Information" : { "Battery Information" : {
"localizations" : { "localizations" : {
"ar" : { "ar" : {
@ -5599,8 +5603,8 @@
"comment" : "An explanatory text that appears when the user tries to enable color-coded color gradients in Segmented progress bars. It instructs the user to switch to Hierarchical or Gradient progress bars to enable these features.", "comment" : "An explanatory text that appears when the user tries to enable color-coded color gradients in Segmented progress bars. It instructs the user to switch to Hierarchical or Gradient progress bars to enable these features.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Color-Coded Progress Bars" : { "Color-coded fills are unavailable in Segmented mode. Switch to Hierarchical or Gradient inside Controls › Dynamic Island to adjust advanced options." : {
"comment" : "A section header that lists the options for color-coded progress bars.", "comment" : "A message explaining that color-coded battery indicators are unavailable in segmented progress bar style. It also includes a suggestion to switch to a more advanced progress bar style to adjust advanced options.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"ColorPicker Disabled" : { "ColorPicker Disabled" : {
@ -6827,6 +6831,10 @@
} }
} }
}, },
"Devices" : {
"comment" : "The title of the \"Devices\" section in the settings menu.",
"isCommentAutoGenerated" : true
},
"Diagnostics" : { "Diagnostics" : {
"comment" : "A section header in the Lock Screen Settings view, describing diagnostics-related functionality.", "comment" : "A section header in the Lock Screen Settings view, describing diagnostics-related functionality.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -7459,6 +7467,10 @@
} }
} }
}, },
"Dynamic Island Progress Bars" : {
"comment" : "A section header that lists the options for customizing the appearance of the progress bars displayed by Dynamic Island.",
"isCommentAutoGenerated" : true
},
"dynamic.island" : { "dynamic.island" : {
"localizations" : { "localizations" : {
"fr" : { "fr" : {
@ -10214,6 +10226,7 @@
} }
}, },
"HUDs" : { "HUDs" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"ar" : { "ar" : {
"stringUnit" : { "stringUnit" : {
@ -10312,9 +10325,6 @@
} }
} }
} }
},
"HUDs & OSD" : {
}, },
"Icon & Progress Color" : { "Icon & Progress Color" : {
"comment" : "A picker that lets users choose the color scheme for the icon and progress bar in the OSD.", "comment" : "A picker that lets users choose the color scheme for the icon and progress bar in the OSD.",
@ -17658,6 +17668,9 @@
"Smooth transitions blend Green (0–60%), Yellow (60–85%), and Red (85–100%) through the entire fill." : { "Smooth transitions blend Green (0–60%), Yellow (60–85%), and Red (85–100%) through the entire fill." : {
"comment" : "A description of how the color-coding feature works.", "comment" : "A description of how the color-coding feature works.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
},
"Smooth transitions blend Green (0–60%), Yellow (60–85%), and Red (85–100%) through the entire fill. Adjust gradient behavior from Controls › Dynamic Island." : {
}, },
"Sneak peek duration" : { "Sneak peek duration" : {
"comment" : "A label describing the duration of the \"sneak peek\" feature in the calendar settings.", "comment" : "A label describing the duration of the \"sneak peek\" feature in the calendar settings.",

View file

@ -23,6 +23,7 @@ private enum SettingsTab: String, CaseIterable, Identifiable {
case appearance case appearance
case lockScreen case lockScreen
case media case media
case devices
case timer case timer
case calendar case calendar
case hudAndOSD case hudAndOSD
@ -45,9 +46,10 @@ private enum SettingsTab: String, CaseIterable, Identifiable {
case .appearance: return "Appearance" case .appearance: return "Appearance"
case .lockScreen: return "Lock Screen" case .lockScreen: return "Lock Screen"
case .media: return "Media" case .media: return "Media"
case .devices: return "Devices"
case .timer: return "Timer" case .timer: return "Timer"
case .calendar: return "Calendar" case .calendar: return "Calendar"
case .hudAndOSD: return "HUDs & OSD" case .hudAndOSD: return "Controls"
case .battery: return "Battery" case .battery: return "Battery"
case .stats: return "Stats" case .stats: return "Stats"
case .clipboard: return "Clipboard" case .clipboard: return "Clipboard"
@ -67,6 +69,7 @@ private enum SettingsTab: String, CaseIterable, Identifiable {
case .appearance: return "paintpalette" case .appearance: return "paintpalette"
case .lockScreen: return "lock.laptopcomputer" case .lockScreen: return "lock.laptopcomputer"
case .media: return "play.laptopcomputer" case .media: return "play.laptopcomputer"
case .devices: return "headphones"
case .timer: return "timer" case .timer: return "timer"
case .calendar: return "calendar" case .calendar: return "calendar"
case .hudAndOSD: return "dial.medium.fill" case .hudAndOSD: return "dial.medium.fill"
@ -89,6 +92,7 @@ private enum SettingsTab: String, CaseIterable, Identifiable {
case .appearance: return .purple case .appearance: return .purple
case .lockScreen: return .orange case .lockScreen: return .orange
case .media: return .green case .media: return .green
case .devices: return .cyan
case .timer: return .red case .timer: return .red
case .calendar: return .cyan case .calendar: return .cyan
case .hudAndOSD: return .indigo case .hudAndOSD: return .indigo
@ -377,6 +381,7 @@ struct SettingsView: View {
.appearance, .appearance,
.lockScreen, .lockScreen,
.media, .media,
.devices,
.timer, .timer,
.calendar, .calendar,
.hudAndOSD, .hudAndOSD,
@ -591,11 +596,11 @@ struct SettingsView: View {
SettingsSearchEntry(tab: .battery, title: "Play low battery alert sound", keywords: ["low battery", "alert", "sound"], highlightID: SettingsTab.battery.highlightID(for: "Play low battery alert sound")), SettingsSearchEntry(tab: .battery, title: "Play low battery alert sound", keywords: ["low battery", "alert", "sound"], highlightID: SettingsTab.battery.highlightID(for: "Play low battery alert sound")),
// HUDs // HUDs
SettingsSearchEntry(tab: .hudAndOSD, title: "Show Bluetooth device connections", keywords: ["bluetooth", "hud"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Show Bluetooth device connections")), SettingsSearchEntry(tab: .devices, title: "Show Bluetooth device connections", keywords: ["bluetooth", "hud"], highlightID: SettingsTab.devices.highlightID(for: "Show Bluetooth device connections")),
SettingsSearchEntry(tab: .hudAndOSD, title: "Use circular battery indicator", keywords: ["battery", "circular"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Use circular battery indicator")), SettingsSearchEntry(tab: .devices, title: "Use circular battery indicator", keywords: ["battery", "circular"], highlightID: SettingsTab.devices.highlightID(for: "Use circular battery indicator")),
SettingsSearchEntry(tab: .hudAndOSD, title: "Show battery percentage text in HUD", keywords: ["battery text"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Show battery percentage text in HUD")), SettingsSearchEntry(tab: .devices, title: "Show battery percentage text in HUD", keywords: ["battery text"], highlightID: SettingsTab.devices.highlightID(for: "Show battery percentage text in HUD")),
SettingsSearchEntry(tab: .hudAndOSD, title: "Scroll device name in HUD", keywords: ["marquee", "device name"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Scroll device name in HUD")), SettingsSearchEntry(tab: .devices, title: "Scroll device name in HUD", keywords: ["marquee", "device name"], highlightID: SettingsTab.devices.highlightID(for: "Scroll device name in HUD")),
SettingsSearchEntry(tab: .hudAndOSD, title: "Color-coded battery display", keywords: ["color", "battery"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Color-coded battery display")), SettingsSearchEntry(tab: .devices, title: "Color-coded battery display", keywords: ["color", "battery"], highlightID: SettingsTab.devices.highlightID(for: "Color-coded battery display")),
SettingsSearchEntry(tab: .hudAndOSD, title: "Color-coded volume display", keywords: ["volume", "color"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Color-coded volume display")), SettingsSearchEntry(tab: .hudAndOSD, title: "Color-coded volume display", keywords: ["volume", "color"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Color-coded volume display")),
SettingsSearchEntry(tab: .hudAndOSD, title: "Smooth color transitions", keywords: ["gradient", "smooth"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Smooth color transitions")), SettingsSearchEntry(tab: .hudAndOSD, title: "Smooth color transitions", keywords: ["gradient", "smooth"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Smooth color transitions")),
SettingsSearchEntry(tab: .hudAndOSD, title: "Show percentages beside progress bars", keywords: ["percentages", "progress"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Show percentages beside progress bars")), SettingsSearchEntry(tab: .hudAndOSD, title: "Show percentages beside progress bars", keywords: ["percentages", "progress"], highlightID: SettingsTab.hudAndOSD.highlightID(for: "Show percentages beside progress bars")),
@ -747,6 +752,10 @@ struct SettingsView: View {
SettingsForm(tab: .media) { SettingsForm(tab: .media) {
Media() Media()
} }
case .devices:
SettingsForm(tab: .devices) {
DevicesSettingsView()
}
case .timer: case .timer:
SettingsForm(tab: .timer) { SettingsForm(tab: .timer) {
TimerSettings() TimerSettings()
@ -1572,7 +1581,7 @@ private struct HUDAndOSDSettingsView: View {
.padding(.top, 10) .padding(.top, 10)
} }
} }
.navigationTitle("HUDs & OSD") .navigationTitle("Controls")
} }
} }
@ -1629,6 +1638,63 @@ private struct HUDSelectionCard<Preview: View>: View {
} }
} }
private struct DevicesSettingsView: View {
@Default(.progressBarStyle) var progressBarStyle
private func highlightID(_ title: String) -> String {
SettingsTab.devices.highlightID(for: title)
}
private var colorCodingDisabled: Bool {
progressBarStyle == .segmented
}
var body: some View {
Form {
Section {
Defaults.Toggle("Show Bluetooth device connections", key: .showBluetoothDeviceConnections)
.settingsHighlight(id: highlightID("Show Bluetooth device connections"))
Defaults.Toggle("Use circular battery indicator", key: .useCircularBluetoothBatteryIndicator)
.settingsHighlight(id: highlightID("Use circular battery indicator"))
Defaults.Toggle("Show battery percentage text in HUD", key: .showBluetoothBatteryPercentageText)
.settingsHighlight(id: highlightID("Show battery percentage text in HUD"))
Defaults.Toggle("Scroll device name in HUD", key: .showBluetoothDeviceNameMarquee)
.settingsHighlight(id: highlightID("Scroll device name in HUD"))
} header: {
Text("Bluetooth Audio Devices")
} footer: {
Text("Displays a HUD notification when Bluetooth audio devices (headphones, AirPods, speakers) connect, showing device name and battery level.")
.foregroundStyle(.secondary)
.font(.caption)
}
Section {
Defaults.Toggle("Color-coded battery display", key: .useColorCodedBatteryDisplay)
.disabled(colorCodingDisabled)
.settingsHighlight(id: highlightID("Color-coded battery display"))
} header: {
Text("Battery Indicator Styling")
} footer: {
if progressBarStyle == .segmented {
Text("Color-coded fills are unavailable in Segmented mode. Switch to Hierarchical or Gradient inside Controls › Dynamic Island to adjust advanced options.")
.foregroundStyle(.secondary)
.font(.caption)
} else if Defaults[.useSmoothColorGradient] {
Text("Smooth transitions blend Green (0–60%), Yellow (60–85%), and Red (85–100%) through the entire fill. Adjust gradient behavior from Controls › Dynamic Island.")
.foregroundStyle(.secondary)
.font(.caption)
} else {
Text("Discrete transitions snap between Green (0–60%), Yellow (60–85%), and Red (85–100%).")
.foregroundStyle(.secondary)
.font(.caption)
}
}
}
.padding(.top, 10)
.navigationTitle("Devices")
}
}
struct HUD: View { struct HUD: View {
@EnvironmentObject var vm: DynamicIslandViewModel @EnvironmentObject var vm: DynamicIslandViewModel
@Default(.inlineHUD) var inlineHUD @Default(.inlineHUD) var inlineHUD
@ -1648,6 +1714,10 @@ struct HUD: View {
private var hasAccessibilityPermission: Bool { private var hasAccessibilityPermission: Bool {
accessibilityPermission.isAuthorized accessibilityPermission.isAuthorized
} }
private var colorCodingDisabled: Bool {
progressBarStyle == .segmented
}
var body: some View { var body: some View {
Form { Form {
@ -1678,29 +1748,8 @@ struct HUD: View {
.font(.caption) .font(.caption)
} }
} }
Section { Section {
Defaults.Toggle("Show Bluetooth device connections", key: .showBluetoothDeviceConnections)
.settingsHighlight(id: highlightID("Show Bluetooth device connections"))
Defaults.Toggle("Use circular battery indicator", key: .useCircularBluetoothBatteryIndicator)
.settingsHighlight(id: highlightID("Use circular battery indicator"))
Defaults.Toggle("Show battery percentage text in HUD", key: .showBluetoothBatteryPercentageText)
.settingsHighlight(id: highlightID("Show battery percentage text in HUD"))
Defaults.Toggle("Scroll device name in HUD", key: .showBluetoothDeviceNameMarquee)
.settingsHighlight(id: highlightID("Scroll device name in HUD"))
} header: {
Text("Bluetooth Audio Devices")
} footer: {
Text("Displays a HUD notification when Bluetooth audio devices (headphones, AirPods, speakers) connect, showing device name and battery level.")
.foregroundStyle(.secondary)
.font(.caption)
}
Section {
let colorCodingDisabled = progressBarStyle == .segmented
Defaults.Toggle("Color-coded battery display", key: .useColorCodedBatteryDisplay)
.disabled(colorCodingDisabled)
.settingsHighlight(id: highlightID("Color-coded battery display"))
Defaults.Toggle("Color-coded volume display", key: .useColorCodedVolumeDisplay) Defaults.Toggle("Color-coded volume display", key: .useColorCodedVolumeDisplay)
.disabled(colorCodingDisabled) .disabled(colorCodingDisabled)
.settingsHighlight(id: highlightID("Color-coded volume display")) .settingsHighlight(id: highlightID("Color-coded volume display"))
@ -1713,9 +1762,9 @@ struct HUD: View {
Defaults.Toggle("Show percentages beside progress bars", key: .showProgressPercentages) Defaults.Toggle("Show percentages beside progress bars", key: .showProgressPercentages)
.settingsHighlight(id: highlightID("Show percentages beside progress bars")) .settingsHighlight(id: highlightID("Show percentages beside progress bars"))
} header: { } header: {
Text("Color-Coded Progress Bars") Text("Dynamic Island Progress Bars")
} footer: { } footer: {
if progressBarStyle == .segmented { if colorCodingDisabled {
Text("Color-coded fills and smooth gradients are unavailable in Segmented mode. Switch to Hierarchical or Gradient to adjust these options.") Text("Color-coded fills and smooth gradients are unavailable in Segmented mode. Switch to Hierarchical or Gradient to adjust these options.")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.font(.caption) .font(.caption)
@ -1765,7 +1814,7 @@ struct HUD: View {
} }
} }
} }
.navigationTitle("HUDs") .navigationTitle("Controls")
.onAppear { .onAppear {
accessibilityPermission.refreshStatus() accessibilityPermission.refreshStatus()
} }

View file

@ -1456,6 +1456,14 @@ class BluetoothAudioManager: ObservableObject {
func refreshConnectedDeviceBatteries() { func refreshConnectedDeviceBatteries() {
refreshBatteryLevelsForConnectedDevices() refreshBatteryLevelsForConnectedDevices()
} }
@MainActor
func activeDeviceIconSymbol() -> String? {
if let prioritizedDevice = connectedDevices.last ?? lastConnectedDevice {
return prioritizedDevice.deviceType.sfSymbol
}
return nil
}
} }
// MARK: - CoreBluetooth Battery Reader // MARK: - CoreBluetooth Battery Reader

View file

@ -178,7 +178,8 @@ final class SystemChangesObserver: MediaKeyInterceptorDelegate {
// Send to Vertical HUD if enabled // Send to Vertical HUD if enabled
if Defaults[.enableVerticalHUD] { if Defaults[.enableVerticalHUD] {
Task { @MainActor in Task { @MainActor in
VerticalHUDWindowManager.shared.show(type: .volume, value: CGFloat(value)) let icon = BluetoothAudioManager.shared.activeDeviceIconSymbol() ?? ""
VerticalHUDWindowManager.shared.show(type: .volume, value: CGFloat(value), icon: icon)
} }
return return
} }

View file

@ -42,6 +42,12 @@ final class VerticalHUDWindowManager {
Defaults.publisher(.verticalHUDWidth, options: []).sink(receiveValue: updateBlock).store(in: &cancellables) Defaults.publisher(.verticalHUDWidth, options: []).sink(receiveValue: updateBlock).store(in: &cancellables)
Defaults.publisher(.verticalHUDPadding, options: []).sink(receiveValue: updateBlock).store(in: &cancellables) Defaults.publisher(.verticalHUDPadding, options: []).sink(receiveValue: updateBlock).store(in: &cancellables)
Defaults.publisher(.verticalHUDInteractive, options: []).sink(receiveValue: updateBlock).store(in: &cancellables) Defaults.publisher(.verticalHUDInteractive, options: []).sink(receiveValue: updateBlock).store(in: &cancellables)
Defaults.publisher(.enableVerticalHUD, options: []).sink { [weak self] change in
Task { @MainActor [weak self] in
self?.handleEnablementChange(change.newValue)
}
}.store(in: &cancellables)
} }
private func updateWindowLayout() { private func updateWindowLayout() {
@ -53,7 +59,6 @@ final class VerticalHUDWindowManager {
let width = Defaults[.verticalHUDWidth] let width = Defaults[.verticalHUDWidth]
let height = Defaults[.verticalHUDHeight] let height = Defaults[.verticalHUDHeight]
let padding = Defaults[.verticalHUDPadding] let padding = Defaults[.verticalHUDPadding]
let interactive = Defaults[.verticalHUDInteractive]
// Add padding for shadow and elastic stretch // Add padding for shadow and elastic stretch
// We use 120px total padding (60px per side) to absolutely guarantee no clipping. // We use 120px total padding (60px per side) to absolutely guarantee no clipping.
@ -77,9 +82,41 @@ final class VerticalHUDWindowManager {
let newFrame = NSRect(x: x, y: y, width: totalWidth, height: totalHeight) let newFrame = NSRect(x: x, y: y, width: totalWidth, height: totalHeight)
window.nsWindow.setFrame(newFrame, display: true, animate: true) window.nsWindow.setFrame(newFrame, display: true, animate: true)
window.nsWindow.ignoresMouseEvents = !interactive applyInteractivity(window)
} }
} }
private func handleEnablementChange(_ isEnabled: Bool) {
if isEnabled {
updateWindowLayout()
} else {
teardownWindows()
}
}
private func applyInteractivity(_ window: OSDWindow, visibleOverride: Bool? = nil) {
let isVisible = visibleOverride ?? (window.nsWindow.alphaValue > 0.01)
let shouldAllowInteraction = Defaults[.enableVerticalHUD] && Defaults[.verticalHUDInteractive] && isVisible
window.nsWindow.ignoresMouseEvents = !shouldAllowInteraction
}
private func updateWindowInteractivity() {
for window in windows.values {
applyInteractivity(window)
}
}
private func teardownWindows() {
hideWorkItem?.cancel()
hideWorkItem = nil
for window in windows.values {
window.nsWindow.orderOut(nil)
window.nsWindow.ignoresMouseEvents = true
}
windows.removeAll()
}
func show(type: SneakContentType, value: CGFloat, icon: String = "") { func show(type: SneakContentType, value: CGFloat, icon: String = "") {
guard Defaults[.enableVerticalHUD] else { return } guard Defaults[.enableVerticalHUD] else { return }
@ -94,6 +131,7 @@ final class VerticalHUDWindowManager {
// Ensure visible // Ensure visible
windowContext.nsWindow.orderFrontRegardless() windowContext.nsWindow.orderFrontRegardless()
applyInteractivity(windowContext, visibleOverride: true)
NSAnimationContext.runAnimationGroup { context in NSAnimationContext.runAnimationGroup { context in
context.duration = animationDuration context.duration = animationDuration
@ -161,8 +199,8 @@ final class VerticalHUDWindowManager {
win.contentView = hostingView win.contentView = hostingView
win.alphaValue = 0 win.alphaValue = 0
// Allow mouse interaction if enabled // Default to ignoring events until explicitly shown
win.ignoresMouseEvents = !Defaults[.verticalHUDInteractive] win.ignoresMouseEvents = true
SkyLightOperator.shared.delegateWindow(win) SkyLightOperator.shared.delegateWindow(win)
@ -197,8 +235,8 @@ final class VerticalHUDWindowManager {
NSAnimationContext.runAnimationGroup { context in NSAnimationContext.runAnimationGroup { context in
context.duration = animationDuration context.duration = animationDuration
window.nsWindow.animator().alphaValue = 0 window.nsWindow.animator().alphaValue = 0
} completionHandler: { } completionHandler: { [weak self] in
// Keep window around self?.applyInteractivity(window, visibleOverride: false)
} }
} }
} }