mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Update Onboarding and temperature stuff
This commit is contained in:
parent
5accee5d02
commit
6d01f6a1b2
7 changed files with 335 additions and 72 deletions
|
|
@ -1607,6 +1607,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"±10 Seconds" : {
|
||||||
|
"comment" : "Display name for the \"±10 Seconds\" music skip behavior option.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"0" : {
|
"0" : {
|
||||||
"comment" : "A label displayed in a circular gauge to indicate the minimum battery level.",
|
"comment" : "A label displayed in a circular gauge to indicate the minimum battery level.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
|
|
@ -1923,6 +1927,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Accent" : {
|
||||||
|
"comment" : "Display name for the \"Accent\" caps lock indicator tint mode.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Accent color" : {
|
"Accent color" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
@ -2268,6 +2276,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Adaptive gradient" : {
|
||||||
|
"comment" : "Name of the timer icon color mode option that uses an adaptive gradient.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Add" : {
|
"Add" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
@ -3863,6 +3875,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Apple Music" : {
|
||||||
|
"comment" : "Names for the different media player types.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Applies the notch-style parallax effect to the lock screen media widget album art." : {
|
"Applies the notch-style parallax effect to the lock screen media widget album art." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
|
|
@ -4277,6 +4293,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Bar" : {
|
||||||
|
"comment" : "Localized name for the \"Bar\" timer progress style.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Battery" : {
|
"Battery" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
@ -8547,6 +8567,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Digital" : {
|
||||||
|
"comment" : "The name of the \"Digital\" timer style.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Dimensions" : {
|
"Dimensions" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
|
|
@ -11945,6 +11969,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Green" : {
|
||||||
|
"comment" : "Display name for the \"Green\" Caps Lock Indicator Tint Mode.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Height:" : {
|
"Height:" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -15625,7 +15653,6 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Minutes" : {
|
"Minutes" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -17842,6 +17869,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Now Playing" : {
|
||||||
|
"comment" : "Localized name for each media controller type.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Number of lines kept in the scrollback buffer. Higher values use more memory." : {
|
"Number of lines kept in the scrollback buffer. Higher values use more memory." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
@ -18217,6 +18248,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Panel" : {
|
||||||
|
"comment" : "Name for the \"Panel\" clipboard display mode.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Panel mode shows clipboard in a floating window near the notch." : {
|
"Panel mode shows clipboard in a floating window near the notch." : {
|
||||||
"comment" : "A description of the \"Panel\" clipboard display mode.",
|
"comment" : "A description of the \"Panel\" clipboard display mode.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
|
|
@ -19831,6 +19866,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Ring" : {
|
||||||
|
"comment" : "Localized name for the 'Ring' timer progress style.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Rotation:" : {
|
"Rotation:" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -20657,6 +20696,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Separate Tab" : {
|
||||||
|
"comment" : "Name of the clipboard display mode option that opens a separate tab for pasting.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Separate Tab mode integrates Copied Items and Notes into a single view. If both are enabled, Notes appear on the right and Clipboard on the left." : {
|
"Separate Tab mode integrates Copied Items and Notes into a single view. If both are enabled, Notes appear on the right and Clipboard on the left." : {
|
||||||
"comment" : "A description of the \"Separate Tab\" clipboard display mode.",
|
"comment" : "A description of the \"Separate Tab\" clipboard display mode.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
|
|
@ -21718,6 +21761,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Shows screen assistant as a dropdown attached to the AI button" : {
|
||||||
|
"comment" : "Description of the \"popover\" display mode for the screen assistant.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Shows screen assistant in a floating panel near the notch" : {
|
||||||
|
"comment" : "Description of the \"panel\" display mode for the screen assistant.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Shows the model's reasoning process before the final answer" : {
|
"Shows the model's reasoning process before the final answer" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
|
|
@ -21780,6 +21831,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Skip forward or backward by ten seconds" : {
|
||||||
|
"comment" : "Description of a music skip behavior option.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Slider color" : {
|
"Slider color" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
@ -22348,6 +22403,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Spotify" : {
|
||||||
|
"comment" : "Add more media player types here.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Square" : {
|
"Square" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
@ -22460,6 +22519,10 @@
|
||||||
},
|
},
|
||||||
"Standard notch media controls are hidden. Re-enable the toggle above to restore them." : {
|
"Standard notch media controls are hidden. Re-enable the toggle above to restore them." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Standard previous/next track controls" : {
|
||||||
|
"comment" : "Description of a music skip behavior option that uses standard previous/next track controls.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Start" : {
|
"Start" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -23821,6 +23884,10 @@
|
||||||
},
|
},
|
||||||
"shouldTranslate" : false
|
"shouldTranslate" : false
|
||||||
},
|
},
|
||||||
|
"Track Skip" : {
|
||||||
|
"comment" : "Name of the music skip behavior option that skips to the next track.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Transform" : {
|
"Transform" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -25742,6 +25809,10 @@
|
||||||
"comment" : "A footer note explaining the app's funding.",
|
"comment" : "A footer note explaining the app's funding.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
|
"Youtube Music" : {
|
||||||
|
"comment" : "Option in settings to choose a media player that streams music from YouTube Music.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"YouTube Music requires this third-party app to be installed: " : {
|
"YouTube Music requires this third-party app to be installed: " : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ struct DoNotDisturbLiveActivity: View {
|
||||||
.accessibilityLabel(accessibilityDescription)
|
.accessibilityLabel(accessibilityDescription)
|
||||||
.onAppear(perform: handleInitialState)
|
.onAppear(perform: handleInitialState)
|
||||||
.onChange(of: manager.isDoNotDisturbActive, handleFocusStateChange)
|
.onChange(of: manager.isDoNotDisturbActive, handleFocusStateChange)
|
||||||
|
.onChange(of: manager.focusToastTrigger, handleFocusModeSwitchToast)
|
||||||
.onDisappear(perform: cancelPendingTasks)
|
.onDisappear(perform: cancelPendingTasks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -333,19 +334,57 @@ struct DoNotDisturbLiveActivity: View {
|
||||||
|
|
||||||
private func handleFocusStateChange(_ oldValue: Bool, _ isActive: Bool) {
|
private func handleFocusStateChange(_ oldValue: Bool, _ isActive: Bool) {
|
||||||
cancelPendingTasks()
|
cancelPendingTasks()
|
||||||
|
|
||||||
if isActive {
|
if isActive {
|
||||||
withAnimation(.smooth(duration: 0.2)) {
|
// Force a clean collapsed baseline before animating ON.
|
||||||
|
// This avoids cases where prior OFF state leaves the view mid-transition.
|
||||||
|
withAnimation(.none) {
|
||||||
showInactiveIcon = false
|
showInactiveIcon = false
|
||||||
|
isExpanded = false
|
||||||
|
iconScale = 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run the expansion on the next runloop tick so the reset takes effect first.
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) {
|
||||||
|
iconScale = 1.0
|
||||||
|
isExpanded = true
|
||||||
|
}
|
||||||
|
if focusToastMode {
|
||||||
|
scheduleTransientCollapse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
triggerInactiveAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleFocusModeSwitchToast(_ oldValue: UUID, _ newValue: UUID) {
|
||||||
|
// Only show a toast when Focus is currently active.
|
||||||
|
guard manager.isDoNotDisturbActive else { return }
|
||||||
|
|
||||||
|
// Only relevant for the non-persistent (brief toast) mode.
|
||||||
|
guard focusToastMode else { return }
|
||||||
|
|
||||||
|
// Cancel any pending collapse so the new mode can animate in cleanly.
|
||||||
|
cancelPendingTasks()
|
||||||
|
|
||||||
|
// Force a clean collapsed baseline before animating ON for the *new* mode.
|
||||||
|
// Without this, a mode switch can appear to start then cancel.
|
||||||
|
withAnimation(.none) {
|
||||||
|
showInactiveIcon = false
|
||||||
|
isExpanded = false
|
||||||
|
iconScale = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the expansion on the next runloop tick so the reset takes effect first.
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard manager.isDoNotDisturbActive else { return }
|
||||||
withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) {
|
withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) {
|
||||||
iconScale = 1.0
|
iconScale = 1.0
|
||||||
isExpanded = true
|
isExpanded = true
|
||||||
}
|
}
|
||||||
if focusToastMode {
|
scheduleTransientCollapse()
|
||||||
scheduleTransientCollapse()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
triggerInactiveAnimation()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -851,6 +851,7 @@ struct SettingsView: View {
|
||||||
SettingsSearchEntry(tab: .stats, title: "Enable system stats monitoring", keywords: ["stats", "monitoring"], highlightID: SettingsTab.stats.highlightID(for: "Enable system stats monitoring")),
|
SettingsSearchEntry(tab: .stats, title: "Enable system stats monitoring", keywords: ["stats", "monitoring"], highlightID: SettingsTab.stats.highlightID(for: "Enable system stats monitoring")),
|
||||||
SettingsSearchEntry(tab: .stats, title: "Stop monitoring after closing the notch", keywords: ["stats", "auto stop"], highlightID: SettingsTab.stats.highlightID(for: "Stop monitoring after closing the notch")),
|
SettingsSearchEntry(tab: .stats, title: "Stop monitoring after closing the notch", keywords: ["stats", "auto stop"], highlightID: SettingsTab.stats.highlightID(for: "Stop monitoring after closing the notch")),
|
||||||
SettingsSearchEntry(tab: .stats, title: "CPU Usage", keywords: ["cpu", "graph"], highlightID: SettingsTab.stats.highlightID(for: "CPU Usage")),
|
SettingsSearchEntry(tab: .stats, title: "CPU Usage", keywords: ["cpu", "graph"], highlightID: SettingsTab.stats.highlightID(for: "CPU Usage")),
|
||||||
|
SettingsSearchEntry(tab: .stats, title: "Temperature unit", keywords: ["cpu", "temperature", "celsius", "fahrenheit"], highlightID: SettingsTab.stats.highlightID(for: "Temperature unit")),
|
||||||
SettingsSearchEntry(tab: .stats, title: "Memory Usage", keywords: ["memory", "ram"], highlightID: SettingsTab.stats.highlightID(for: "Memory Usage")),
|
SettingsSearchEntry(tab: .stats, title: "Memory Usage", keywords: ["memory", "ram"], highlightID: SettingsTab.stats.highlightID(for: "Memory Usage")),
|
||||||
SettingsSearchEntry(tab: .stats, title: "GPU Usage", keywords: ["gpu", "graphics"], highlightID: SettingsTab.stats.highlightID(for: "GPU Usage")),
|
SettingsSearchEntry(tab: .stats, title: "GPU Usage", keywords: ["gpu", "graphics"], highlightID: SettingsTab.stats.highlightID(for: "GPU Usage")),
|
||||||
SettingsSearchEntry(tab: .stats, title: "Network Activity", keywords: ["network", "graph"], highlightID: SettingsTab.stats.highlightID(for: "Network Activity")),
|
SettingsSearchEntry(tab: .stats, title: "Network Activity", keywords: ["network", "graph"], highlightID: SettingsTab.stats.highlightID(for: "Network Activity")),
|
||||||
|
|
@ -6157,6 +6158,7 @@ struct StatsSettings: View {
|
||||||
@Default(.showGpuGraph) var showGpuGraph
|
@Default(.showGpuGraph) var showGpuGraph
|
||||||
@Default(.showNetworkGraph) var showNetworkGraph
|
@Default(.showNetworkGraph) var showNetworkGraph
|
||||||
@Default(.showDiskGraph) var showDiskGraph
|
@Default(.showDiskGraph) var showDiskGraph
|
||||||
|
@Default(.cpuTemperatureUnit) var cpuTemperatureUnit
|
||||||
|
|
||||||
private func highlightID(_ title: String) -> String {
|
private func highlightID(_ title: String) -> String {
|
||||||
SettingsTab.stats.highlightID(for: title)
|
SettingsTab.stats.highlightID(for: title)
|
||||||
|
|
@ -6246,6 +6248,16 @@ struct StatsSettings: View {
|
||||||
Section {
|
Section {
|
||||||
Defaults.Toggle("CPU Usage", key: .showCpuGraph)
|
Defaults.Toggle("CPU Usage", key: .showCpuGraph)
|
||||||
.settingsHighlight(id: highlightID("CPU Usage"))
|
.settingsHighlight(id: highlightID("CPU Usage"))
|
||||||
|
|
||||||
|
if showCpuGraph {
|
||||||
|
Picker("Temperature unit", selection: $cpuTemperatureUnit) {
|
||||||
|
ForEach(LockScreenWeatherTemperatureUnit.allCases) { unit in
|
||||||
|
Text(unit.rawValue).tag(unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
.settingsHighlight(id: highlightID("Temperature unit"))
|
||||||
|
}
|
||||||
Defaults.Toggle("Memory Usage", key: .showMemoryGraph)
|
Defaults.Toggle("Memory Usage", key: .showMemoryGraph)
|
||||||
.settingsHighlight(id: highlightID("Memory Usage"))
|
.settingsHighlight(id: highlightID("Memory Usage"))
|
||||||
Defaults.Toggle("GPU Usage", key: .showGpuGraph)
|
Defaults.Toggle("GPU Usage", key: .showGpuGraph)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import Defaults
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct StatsCard<Content: View>: View {
|
struct StatsCard<Content: View>: View {
|
||||||
|
|
@ -163,7 +164,10 @@ private struct CPUTemperatureGauge: View {
|
||||||
let tint: Color
|
let tint: Color
|
||||||
var size: CGFloat = 84
|
var size: CGFloat = 84
|
||||||
var lineWidth: CGFloat = 9
|
var lineWidth: CGFloat = 9
|
||||||
private let maxTemperature: Double = 110
|
@Default(.cpuTemperatureUnit) private var temperatureUnit
|
||||||
|
private var maxTemperature: Double {
|
||||||
|
temperatureUnit == .celsius ? 110 : 230
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
CircularGaugeView(
|
CircularGaugeView(
|
||||||
|
|
@ -177,14 +181,19 @@ private struct CPUTemperatureGauge: View {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var displayTemperature: Double? {
|
||||||
|
guard let temperature else { return nil }
|
||||||
|
return temperatureUnit == .celsius ? temperature : temperature * 9.0 / 5.0 + 32.0
|
||||||
|
}
|
||||||
|
|
||||||
private var normalizedValue: Double {
|
private var normalizedValue: Double {
|
||||||
guard let temperature, maxTemperature > 0 else { return 0 }
|
guard let displayTemperature, maxTemperature > 0 else { return 0 }
|
||||||
return min(max(temperature / maxTemperature, 0), 1)
|
return min(max(displayTemperature / maxTemperature, 0), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var centerPrimary: String {
|
private var centerPrimary: String {
|
||||||
guard let temperature else { return "—" }
|
guard let displayTemperature else { return "—" }
|
||||||
return String(format: "%.0f°", temperature)
|
return String(format: "%.0f%@", displayTemperature, temperatureUnit.symbol)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ final class DoNotDisturbManager: ObservableObject {
|
||||||
@Published var currentFocusModeName: String = ""
|
@Published var currentFocusModeName: String = ""
|
||||||
@Published var currentFocusModeIdentifier: String = ""
|
@Published var currentFocusModeIdentifier: String = ""
|
||||||
|
|
||||||
|
// Used by the brief-toast UI to show an ON toast when the active mode switches while Focus stays enabled.
|
||||||
|
@Published var focusToastTrigger: UUID = UUID()
|
||||||
|
|
||||||
private let notificationCenter = DistributedNotificationCenter.default()
|
private let notificationCenter = DistributedNotificationCenter.default()
|
||||||
private let metadataExtractionQueue = DispatchQueue(label: "com.dynamicisland.focus.metadata", qos: .userInitiated)
|
private let metadataExtractionQueue = DispatchQueue(label: "com.dynamicisland.focus.metadata", qos: .userInitiated)
|
||||||
private let pollingQueue = DispatchQueue(label: "com.dynamicisland.focus.polling", qos: .utility)
|
private let pollingQueue = DispatchQueue(label: "com.dynamicisland.focus.polling", qos: .utility)
|
||||||
|
|
@ -82,9 +85,8 @@ final class DoNotDisturbManager: ObservableObject {
|
||||||
suspensionBehavior: .deliverImmediately
|
suspensionBehavior: .deliverImmediately
|
||||||
)
|
)
|
||||||
|
|
||||||
startAssertionsPolling()
|
|
||||||
applyMonitoringModeChange(Defaults[.focusMonitoringMode])
|
|
||||||
isMonitoring = true
|
isMonitoring = true
|
||||||
|
applyMonitoringModeChange(Defaults[.focusMonitoringMode])
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopMonitoring() {
|
func stopMonitoring() {
|
||||||
|
|
@ -128,30 +130,69 @@ final class DoNotDisturbManager: ObservableObject {
|
||||||
let trimmedIdentifier = identifier?.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmedIdentifier = identifier?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let trimmedName = name?.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmedName = name?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
let resolvedMode = FocusModeType.resolve(identifier: trimmedIdentifier, name: trimmedName)
|
let isGenericFocusIdentifier = (trimmedIdentifier?.lowercased() == "com.apple.focus")
|
||||||
|
let usableIdentifier = (trimmedIdentifier?.isEmpty == false && !isGenericFocusIdentifier) ? trimmedIdentifier : nil
|
||||||
let finalIdentifier: String
|
let usableName = (trimmedName?.isEmpty == false) ? trimmedName : nil
|
||||||
if let identifier = trimmedIdentifier, !identifier.isEmpty {
|
|
||||||
finalIdentifier = identifier
|
|
||||||
} else {
|
|
||||||
finalIdentifier = resolvedMode.rawValue
|
|
||||||
}
|
|
||||||
|
|
||||||
let finalName: String
|
|
||||||
if let name = trimmedName, !name.isEmpty {
|
|
||||||
finalName = name
|
|
||||||
} else if !resolvedMode.displayName.isEmpty {
|
|
||||||
finalName = resolvedMode.displayName
|
|
||||||
} else if let identifier = trimmedIdentifier, !identifier.isEmpty {
|
|
||||||
finalName = identifier
|
|
||||||
} else {
|
|
||||||
finalName = "Focus"
|
|
||||||
}
|
|
||||||
|
|
||||||
let previousIdentifier = self.currentFocusModeIdentifier
|
let previousIdentifier = self.currentFocusModeIdentifier
|
||||||
let previousName = self.currentFocusModeName
|
let previousName = self.currentFocusModeName
|
||||||
let previousActive = self.isDoNotDisturbActive
|
let previousActive = self.isDoNotDisturbActive
|
||||||
|
|
||||||
|
// When neither identifier nor name is available, only update the active
|
||||||
|
// state without overwriting existing mode metadata. This prevents the
|
||||||
|
// assertions-poll (which often lacks metadata) from reverting a correct
|
||||||
|
// mode set by the log-stream or notification handler.
|
||||||
|
if usableIdentifier == nil && usableName == nil {
|
||||||
|
if let isActive = isActive, isActive != previousActive {
|
||||||
|
withAnimation(.smooth(duration: 0.25)) {
|
||||||
|
self.isDoNotDisturbActive = isActive
|
||||||
|
}
|
||||||
|
// First activation with no prior mode: default to DND.
|
||||||
|
if isActive && previousIdentifier.isEmpty {
|
||||||
|
self.currentFocusModeIdentifier = FocusModeType.doNotDisturb.rawValue
|
||||||
|
self.currentFocusModeName = FocusModeType.doNotDisturb.displayName
|
||||||
|
}
|
||||||
|
debugPrint("[DoNotDisturbManager] Focus active-only update -> source: \(source) | isActive: \(isActive)")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolvedMode = FocusModeType.resolve(identifier: usableIdentifier, name: usableName)
|
||||||
|
|
||||||
|
let finalIdentifier: String = usableIdentifier ?? resolvedMode.rawValue
|
||||||
|
|
||||||
|
// Compute display name
|
||||||
|
let finalName: String
|
||||||
|
if resolvedMode == .custom, !FullDiskAccessAuthorization.hasPermission() {
|
||||||
|
finalName = "Focus"
|
||||||
|
} else if resolvedMode == .custom, FullDiskAccessAuthorization.hasPermission() {
|
||||||
|
// With FDA, prefer the real name from ModeConfigurations.json (fixes slug names like graduationcap.fill).
|
||||||
|
let lookedUp = FocusMetadataReader.shared.getDisplayName(for: trimmedName ?? "", identifier: finalIdentifier)
|
||||||
|
finalName = lookedUp.isEmpty ? "Focus" : lookedUp
|
||||||
|
} else if let name = trimmedName, !name.isEmpty {
|
||||||
|
let lower = name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||||
|
switch lower {
|
||||||
|
case "work":
|
||||||
|
finalName = "Work"
|
||||||
|
case "personal", "personal-time":
|
||||||
|
finalName = "Personal"
|
||||||
|
case "reduce-interruptions":
|
||||||
|
finalName = "Reduce Interruptions"
|
||||||
|
case "sleep", "sleep-mode":
|
||||||
|
finalName = "Sleep"
|
||||||
|
case "driving":
|
||||||
|
finalName = "Driving"
|
||||||
|
case "default", "dnd", "do-not-disturb", "do not disturb", "donotdisturb":
|
||||||
|
finalName = "Do Not Disturb"
|
||||||
|
default:
|
||||||
|
finalName = name
|
||||||
|
}
|
||||||
|
} else if !resolvedMode.displayName.isEmpty {
|
||||||
|
finalName = resolvedMode.displayName
|
||||||
|
} else {
|
||||||
|
finalName = "Focus"
|
||||||
|
}
|
||||||
|
|
||||||
let identifierChanged = finalIdentifier != previousIdentifier
|
let identifierChanged = finalIdentifier != previousIdentifier
|
||||||
let nameChanged = finalName != previousName
|
let nameChanged = finalName != previousName
|
||||||
let shouldToggleActive = isActive.map { $0 != previousActive } ?? false
|
let shouldToggleActive = isActive.map { $0 != previousActive } ?? false
|
||||||
|
|
@ -161,10 +202,12 @@ final class DoNotDisturbManager: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
if nameChanged {
|
if nameChanged {
|
||||||
self.currentFocusModeName = finalName
|
self.currentFocusModeName = finalName.localizedCaseInsensitiveContains("Reduce Interruptions") ? "Reduce Interr." : finalName
|
||||||
.localizedCaseInsensitiveContains(
|
}
|
||||||
"Reduce Interruptions"
|
|
||||||
) ? "Reduce Interr." : finalName
|
// If Focus remains active and the mode switches (e.g., DND -> Sleep), trigger an ON toast for the new mode.
|
||||||
|
if isActive == nil, previousActive == true, identifierChanged {
|
||||||
|
self.focusToastTrigger = UUID()
|
||||||
}
|
}
|
||||||
|
|
||||||
if identifierChanged || nameChanged || shouldToggleActive {
|
if identifierChanged || nameChanged || shouldToggleActive {
|
||||||
|
|
@ -176,12 +219,16 @@ final class DoNotDisturbManager: ObservableObject {
|
||||||
withAnimation(.smooth(duration: 0.25)) {
|
withAnimation(.smooth(duration: 0.25)) {
|
||||||
self.isDoNotDisturbActive = isActive
|
self.isDoNotDisturbActive = isActive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If Focus turned OFF, clear cached mode metadata after the OFF toast can use it.
|
||||||
|
if isActive == false {
|
||||||
|
self.currentFocusModeIdentifier = previousIdentifier
|
||||||
|
self.currentFocusModeName = previousName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleLogMetadataUpdate(identifier: String?, name: String?) {
|
private func handleLogMetadataUpdate(identifier: String?, name: String?) {
|
||||||
guard Defaults[.focusMonitoringMode] == .useDevTools else { return }
|
|
||||||
|
|
||||||
metadataExtractionQueue.async { [weak self] in
|
metadataExtractionQueue.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
let trimmedIdentifier = identifier?.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmedIdentifier = identifier?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
@ -336,9 +383,11 @@ private extension DoNotDisturbManager {
|
||||||
guard isMonitoring else { return }
|
guard isMonitoring else { return }
|
||||||
|
|
||||||
if mode == .useDevTools {
|
if mode == .useDevTools {
|
||||||
|
stopAssertionsPolling()
|
||||||
focusLogStream.start()
|
focusLogStream.start()
|
||||||
} else {
|
} else {
|
||||||
focusLogStream.stop()
|
focusLogStream.stop()
|
||||||
|
startAssertionsPolling()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -541,11 +590,24 @@ extension FocusModeType {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1. Exact rawValue match (e.g., "com.apple.focus.work", "com.apple.donotdisturb.mode").
|
||||||
if let direct = FocusModeType(rawValue: normalized) ?? FocusModeType(rawValue: normalizedLowercased) {
|
if let direct = FocusModeType(rawValue: normalized) ?? FocusModeType(rawValue: normalizedLowercased) {
|
||||||
self = direct
|
self = direct
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. macOS 26.3 custom Focus modes use `com.apple.donotdisturb.mode.<symbol>`.
|
||||||
|
// Must be checked BEFORE the generic prefix match, otherwise the prefix
|
||||||
|
// `com.apple.donotdisturb.mode` would incorrectly resolve to .doNotDisturb.
|
||||||
|
if normalizedLowercased.hasPrefix("com.apple.donotdisturb.mode.") {
|
||||||
|
let suffix = String(normalizedLowercased.dropFirst("com.apple.donotdisturb.mode.".count))
|
||||||
|
if !suffix.isEmpty && suffix != "default" {
|
||||||
|
self = .custom
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Prefix match for known built-in modes (e.g., com.apple.focus.personal-time -> .personal).
|
||||||
if let resolved = FocusModeType.allCases.first(where: {
|
if let resolved = FocusModeType.allCases.first(where: {
|
||||||
guard !$0.rawValue.isEmpty else { return false }
|
guard !$0.rawValue.isEmpty else { return false }
|
||||||
return normalized.hasPrefix($0.rawValue) || normalizedLowercased.hasPrefix($0.rawValue)
|
return normalized.hasPrefix($0.rawValue) || normalizedLowercased.hasPrefix($0.rawValue)
|
||||||
|
|
@ -554,6 +616,7 @@ extension FocusModeType {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. Anything else under com.apple.focus is custom.
|
||||||
if normalizedLowercased.hasPrefix("com.apple.focus") {
|
if normalizedLowercased.hasPrefix("com.apple.focus") {
|
||||||
self = .custom
|
self = .custom
|
||||||
return
|
return
|
||||||
|
|
@ -570,6 +633,22 @@ extension FocusModeType {
|
||||||
}) {
|
}) {
|
||||||
return match
|
return match
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also try matching the name against known raw identifiers (e.g., a log line may emit "work" or "sleep" as the name).
|
||||||
|
let lower = name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||||
|
switch lower {
|
||||||
|
case "work": return .work
|
||||||
|
case "personal", "personal-time": return .personal
|
||||||
|
case "sleep", "sleep-mode": return .sleep
|
||||||
|
case "driving": return .driving
|
||||||
|
case "fitness": return .fitness
|
||||||
|
case "gaming": return .gaming
|
||||||
|
case "mindfulness": return .mindfulness
|
||||||
|
case "reading": return .reading
|
||||||
|
case "reduce-interruptions", "reduce interruptions": return .reduceInterruptions
|
||||||
|
case "do not disturb", "dnd", "donotdisturb", "do-not-disturb", "default": return .doNotDisturb
|
||||||
|
default: break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let identifier, !identifier.isEmpty {
|
if let identifier, !identifier.isEmpty {
|
||||||
|
|
@ -580,13 +659,17 @@ extension FocusModeType {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCustomIconFromFile() -> String {
|
func getCustomIconFromFile() -> String {
|
||||||
return FocusMetadataReader.shared
|
return FocusMetadataReader.shared.getIcon(
|
||||||
.getIcon(for: DoNotDisturbManager.shared.currentFocusModeName)
|
for: DoNotDisturbManager.shared.currentFocusModeName,
|
||||||
|
identifier: DoNotDisturbManager.shared.currentFocusModeIdentifier
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCustomAccentColorFromFile() -> Color {
|
func getCustomAccentColorFromFile() -> Color {
|
||||||
return FocusMetadataReader.shared
|
return FocusMetadataReader.shared.getAccentColor(
|
||||||
.getAccentColor(for: DoNotDisturbManager.shared.currentFocusModeName)
|
for: DoNotDisturbManager.shared.currentFocusModeName,
|
||||||
|
identifier: DoNotDisturbManager.shared.currentFocusModeIdentifier
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -815,6 +898,7 @@ private final class FocusLogStream {
|
||||||
private var pipe: Pipe?
|
private var pipe: Pipe?
|
||||||
private var buffer = Data()
|
private var buffer = Data()
|
||||||
private var isRunning = false
|
private var isRunning = false
|
||||||
|
private var didTerminate = false
|
||||||
|
|
||||||
private let metadataLock = NSLock()
|
private let metadataLock = NSLock()
|
||||||
private var lastIdentifier: String?
|
private var lastIdentifier: String?
|
||||||
|
|
@ -831,12 +915,13 @@ private final class FocusLogStream {
|
||||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/log")
|
process.executableURL = URL(fileURLWithPath: "/usr/bin/log")
|
||||||
process.arguments = [
|
process.arguments = [
|
||||||
"stream",
|
"stream",
|
||||||
|
"--no-backtrace",
|
||||||
"--style",
|
"--style",
|
||||||
"compact",
|
"compact",
|
||||||
"--level",
|
"--level",
|
||||||
"info",
|
"info",
|
||||||
"--predicate",
|
"--predicate",
|
||||||
"subsystem == \"com.apple.focus\""
|
"process == \"duetexpertd\" AND eventMessage CONTAINS \"semanticModeIdentifier\""
|
||||||
]
|
]
|
||||||
|
|
||||||
let pipe = Pipe()
|
let pipe = Pipe()
|
||||||
|
|
@ -870,7 +955,8 @@ private final class FocusLogStream {
|
||||||
self.process = process
|
self.process = process
|
||||||
self.pipe = pipe
|
self.pipe = pipe
|
||||||
self.isRunning = true
|
self.isRunning = true
|
||||||
debugPrint("[FocusLogStream] Started unified log tail for com.apple.focus")
|
self.didTerminate = false
|
||||||
|
debugPrint("[FocusLogStream] Started unified log tail for duetexpertd/donotdisturbd Focus metadata")
|
||||||
} catch {
|
} catch {
|
||||||
debugPrint("[FocusLogStream] Failed to start log stream: \(error)")
|
debugPrint("[FocusLogStream] Failed to start log stream: \(error)")
|
||||||
pipe.fileHandleForReading.readabilityHandler = nil
|
pipe.fileHandleForReading.readabilityHandler = nil
|
||||||
|
|
@ -937,7 +1023,12 @@ private final class FocusLogStream {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if trimmed.contains("active mode assertion: (null)") || trimmed.contains("active activity: (null)") {
|
if trimmed.lowercased().contains("error") && trimmed.lowercased().contains("predicate") {
|
||||||
|
debugPrint("[FocusLogStream] log stream error: \(trimmed)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear only when logs explicitly indicate no active mode (helps avoid wiping state during transitions).
|
||||||
|
if trimmed.contains("active mode assertion: (null)") || trimmed.contains("activeModeIdentifier: (null)") {
|
||||||
clearMetadata()
|
clearMetadata()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -945,11 +1036,26 @@ private final class FocusLogStream {
|
||||||
var updatedIdentifier: String?
|
var updatedIdentifier: String?
|
||||||
var updatedName: String?
|
var updatedName: String?
|
||||||
|
|
||||||
if let identifier = FocusMetadataDecoder.extractIdentifier(from: trimmed), !identifier.isEmpty {
|
// Special-case parsing for donotdisturbd logs which include a full DNDMode description.
|
||||||
|
// Example: <DNDMode: ... name: Lock In; modeIdentifier: com.apple.donotdisturb.mode.graduationcap.fill; ...>
|
||||||
|
if trimmed.contains("<DNDMode:") {
|
||||||
|
func extractField(_ key: String) -> String? {
|
||||||
|
guard let r = trimmed.range(of: key) else { return nil }
|
||||||
|
let suffix = trimmed[r.upperBound...]
|
||||||
|
guard let end = suffix.range(of: ";") else { return nil }
|
||||||
|
let value = suffix[..<end.lowerBound].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
return value.isEmpty ? nil : value
|
||||||
|
}
|
||||||
|
|
||||||
|
if let v = extractField("modeIdentifier:") { updatedIdentifier = v }
|
||||||
|
if let v = extractField("name:") { updatedName = v }
|
||||||
|
}
|
||||||
|
|
||||||
|
if updatedIdentifier == nil, let identifier = FocusMetadataDecoder.extractIdentifier(from: trimmed), !identifier.isEmpty {
|
||||||
updatedIdentifier = identifier
|
updatedIdentifier = identifier
|
||||||
}
|
}
|
||||||
|
|
||||||
if let name = FocusMetadataDecoder.extractName(from: trimmed), !name.isEmpty {
|
if updatedName == nil, let name = FocusMetadataDecoder.extractName(from: trimmed), !name.isEmpty {
|
||||||
updatedName = name
|
updatedName = name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -983,6 +1089,8 @@ private final class FocusLogStream {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleTermination(terminateProcess: Bool = false) {
|
private func handleTermination(terminateProcess: Bool = false) {
|
||||||
|
if didTerminate { return }
|
||||||
|
didTerminate = true
|
||||||
if terminateProcess, let process, process.isRunning {
|
if terminateProcess, let process, process.isRunning {
|
||||||
process.terminate()
|
process.terminate()
|
||||||
}
|
}
|
||||||
|
|
@ -995,7 +1103,7 @@ private final class FocusLogStream {
|
||||||
buffer.removeAll(keepingCapacity: false)
|
buffer.removeAll(keepingCapacity: false)
|
||||||
isRunning = false
|
isRunning = false
|
||||||
clearMetadata()
|
clearMetadata()
|
||||||
debugPrint("[FocusLogStream] Stopped unified log tail for com.apple.focus")
|
debugPrint("[FocusLogStream] Stopped unified log tail for duetexpertd/donotdisturbd Focus metadata")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func notifyMetadataUpdate(identifier: String?, name: String?) {
|
private func notifyMetadataUpdate(identifier: String?, name: String?) {
|
||||||
|
|
@ -1006,14 +1114,15 @@ private final class FocusLogStream {
|
||||||
|
|
||||||
private enum FocusNotificationParsing {
|
private enum FocusNotificationParsing {
|
||||||
static let identifierPattern: NSRegularExpression? = {
|
static let identifierPattern: NSRegularExpression? = {
|
||||||
let pattern = "com\\.apple\\.(?:focus|donotdisturb)[A-Za-z0-9_.-]*"
|
let pattern = "com\\.apple\\.(?:focus|donotdisturb|sleep)[A-Za-z0-9_.-]*"
|
||||||
return try? NSRegularExpression(pattern: pattern, options: [])
|
return try? NSRegularExpression(pattern: pattern, options: [])
|
||||||
}()
|
}()
|
||||||
|
|
||||||
static let identifierDetailPatterns: [NSRegularExpression] = {
|
static let identifierDetailPatterns: [NSRegularExpression] = {
|
||||||
let patterns = [
|
let patterns = [
|
||||||
"modeIdentifier:\\s*'([^'\\s]+)'",
|
"modeIdentifier:\\s*'([^'\\s]+)'",
|
||||||
"activityIdentifier:\\s*([A-Za-z0-9._-]+)"
|
"activityIdentifier:\\s*([A-Za-z0-9._-]+)",
|
||||||
|
"semanticModeIdentifier:\\s*([A-Za-z0-9._-]+)"
|
||||||
]
|
]
|
||||||
return patterns.compactMap { try? NSRegularExpression(pattern: $0, options: []) }
|
return patterns.compactMap { try? NSRegularExpression(pattern: $0, options: []) }
|
||||||
}()
|
}()
|
||||||
|
|
@ -1093,11 +1202,12 @@ private final class FocusMetadataReader {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DNDModeWrapper: Codable {
|
struct DNDModeWrapper: Codable {
|
||||||
let mode: DNDMode
|
let mode: DNDMode
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DNDMode: Codable {
|
struct DNDMode: Codable {
|
||||||
let name: String
|
let name: String
|
||||||
|
let modeIdentifier: String
|
||||||
let symbolImageName: String?
|
let symbolImageName: String?
|
||||||
let tintColorName: String?
|
let tintColorName: String?
|
||||||
}
|
}
|
||||||
|
|
@ -1106,42 +1216,58 @@ private final class FocusMetadataReader {
|
||||||
|
|
||||||
static let shared = FocusMetadataReader()
|
static let shared = FocusMetadataReader()
|
||||||
|
|
||||||
private func getModeConfig(for focusName: String) -> DNDMode? {
|
private func getModeConfig(for focusName: String, identifier: String? = nil) -> DNDMode? {
|
||||||
guard FullDiskAccessAuthorization.hasPermission() else { return nil }
|
guard FullDiskAccessAuthorization.hasPermission() else { return nil }
|
||||||
|
|
||||||
|
let trimmedName = focusName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let trimmedIdentifier = identifier?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let data = try Data(contentsOf: pathToDatabase)
|
let data = try Data(contentsOf: pathToDatabase)
|
||||||
let root = try JSONDecoder().decode(DNDConfigRoot.self, from: data)
|
let root = try JSONDecoder().decode(DNDConfigRoot.self, from: data)
|
||||||
|
|
||||||
for entry in root.data {
|
for entry in root.data {
|
||||||
for wrapper in entry.modeConfigurations.values {
|
for wrapper in entry.modeConfigurations.values {
|
||||||
if wrapper.mode.name
|
let mode = wrapper.mode
|
||||||
.localizedCaseInsensitiveCompare(focusName) == .orderedSame {
|
|
||||||
return wrapper.mode
|
if let id = trimmedIdentifier, !id.isEmpty,
|
||||||
|
mode.modeIdentifier.compare(id, options: [.caseInsensitive, .diacriticInsensitive]) == .orderedSame {
|
||||||
|
return mode
|
||||||
|
}
|
||||||
|
|
||||||
|
if !trimmedName.isEmpty,
|
||||||
|
mode.name.compare(trimmedName, options: [.caseInsensitive, .diacriticInsensitive]) == .orderedSame {
|
||||||
|
return mode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
print("JSON Error: \(error)")
|
print("ModeConfigurations.json decode error: \(error)")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getDisplayName(for focus: String, identifier: String? = nil) -> String {
|
||||||
|
guard let mode = getModeConfig(for: focus, identifier: identifier) else { return "" }
|
||||||
|
return mode.name
|
||||||
|
}
|
||||||
|
|
||||||
/// Fetch the icon for the current focus from disk. If the focus is not found return the placeholder `app.badge`
|
/// Fetch the icon for the current focus from disk. If the focus is not found return the placeholder `app.badge`
|
||||||
/// - Returns A string representing the sfSymbol of the current focus
|
/// - Returns A string representing the sfSymbol of the current focus
|
||||||
func getIcon(for focus: String) -> String {
|
func getIcon(for focus: String, identifier: String? = nil) -> String {
|
||||||
guard let mode = getModeConfig(for: focus) else { return "app.badge" }
|
guard let mode = getModeConfig(for: focus, identifier: identifier) else { return "app.badge" }
|
||||||
return mode.symbolImageName ?? "app.badge"
|
return mode.symbolImageName ?? "app.badge"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch the accent color for the current focus from disk. If the focus is not found return the placeholder `Color.indigo`
|
/// Fetch the accent color for the current focus from disk. If the focus is not found return the placeholder `Color.indigo`
|
||||||
/// - Returns A Color representing the accent color for the current focus
|
/// - Returns A Color representing the accent color for the current focus
|
||||||
func getAccentColor(for focus: String) -> Color {
|
func getAccentColor(for focus: String, identifier: String? = nil) -> Color {
|
||||||
guard let mode = getModeConfig(for: focus),
|
guard let mode = getModeConfig(for: focus, identifier: identifier),
|
||||||
let colorName = mode.tintColorName else { return .indigo }
|
let colorName = mode.tintColorName else { return .indigo }
|
||||||
|
|
||||||
return Color.stringToColor(for: colorName)
|
return Color.stringToColor(for: colorName)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Color {
|
extension Color {
|
||||||
|
|
|
||||||
|
|
@ -794,10 +794,10 @@ extension Defaults.Keys {
|
||||||
static let lockScreenWeatherProviderSource = Key<LockScreenWeatherProviderSource>("lockScreenWeatherProviderSource", default: .openMeteo)
|
static let lockScreenWeatherProviderSource = Key<LockScreenWeatherProviderSource>("lockScreenWeatherProviderSource", default: .openMeteo)
|
||||||
static let lockScreenWeatherVerticalOffset = Key<Double>("lockScreenWeatherVerticalOffset", default: 0)
|
static let lockScreenWeatherVerticalOffset = Key<Double>("lockScreenWeatherVerticalOffset", default: 0)
|
||||||
static let lockScreenMusicVerticalOffset = Key<Double>("lockScreenMusicVerticalOffset", default: 0)
|
static let lockScreenMusicVerticalOffset = Key<Double>("lockScreenMusicVerticalOffset", default: 0)
|
||||||
static let lockScreenMusicPanelWidth = Key<Double>("lockScreenMusicPanelWidth", default: 420)
|
static let lockScreenMusicPanelWidth = Key<Double>("lockScreenMusicPanelWidth", default: 350)
|
||||||
static let lockScreenMusicAlbumParallaxEnabled = Key<Bool>("lockScreenMusicAlbumParallaxEnabled", default: false)
|
static let lockScreenMusicAlbumParallaxEnabled = Key<Bool>("lockScreenMusicAlbumParallaxEnabled", default: false)
|
||||||
static let lockScreenTimerVerticalOffset = Key<Double>("lockScreenTimerVerticalOffset", default: 0)
|
static let lockScreenTimerVerticalOffset = Key<Double>("lockScreenTimerVerticalOffset", default: 0)
|
||||||
static let lockScreenTimerWidgetWidth = Key<Double>("lockScreenTimerWidgetWidth", default: 420)
|
static let lockScreenTimerWidgetWidth = Key<Double>("lockScreenTimerWidgetWidth", default: 350)
|
||||||
static let lockScreenGlassStyle = Key<LockScreenGlassStyle>("lockScreenGlassStyle", default: .liquid)
|
static let lockScreenGlassStyle = Key<LockScreenGlassStyle>("lockScreenGlassStyle", default: .liquid)
|
||||||
static let lockScreenGlassCustomizationMode = Key<LockScreenGlassCustomizationMode>(
|
static let lockScreenGlassCustomizationMode = Key<LockScreenGlassCustomizationMode>(
|
||||||
"lockScreenGlassCustomizationMode",
|
"lockScreenGlassCustomizationMode",
|
||||||
|
|
@ -903,13 +903,14 @@ extension Defaults.Keys {
|
||||||
// MARK: Stats Feature
|
// MARK: Stats Feature
|
||||||
static let enableStatsFeature = Key<Bool>("enableStatsFeature", default: false)
|
static let enableStatsFeature = Key<Bool>("enableStatsFeature", default: false)
|
||||||
static let autoStartStatsMonitoring = Key<Bool>("autoStartStatsMonitoring", default: true)
|
static let autoStartStatsMonitoring = Key<Bool>("autoStartStatsMonitoring", default: true)
|
||||||
static let statsStopWhenNotchCloses = Key<Bool>("statsStopWhenNotchCloses", default: false)
|
static let statsStopWhenNotchCloses = Key<Bool>("statsStopWhenNotchCloses", default: true)
|
||||||
static let statsUpdateInterval = Key<Double>("statsUpdateInterval", default: 1.0)
|
static let statsUpdateInterval = Key<Double>("statsUpdateInterval", default: 1.0)
|
||||||
static let showCpuGraph = Key<Bool>("showCpuGraph", default: true)
|
static let showCpuGraph = Key<Bool>("showCpuGraph", default: true)
|
||||||
static let showMemoryGraph = Key<Bool>("showMemoryGraph", default: true)
|
static let showMemoryGraph = Key<Bool>("showMemoryGraph", default: true)
|
||||||
static let showGpuGraph = Key<Bool>("showGpuGraph", default: true)
|
static let showGpuGraph = Key<Bool>("showGpuGraph", default: true)
|
||||||
static let showNetworkGraph = Key<Bool>("showNetworkGraph", default: false)
|
static let showNetworkGraph = Key<Bool>("showNetworkGraph", default: false)
|
||||||
static let showDiskGraph = Key<Bool>("showDiskGraph", default: false)
|
static let showDiskGraph = Key<Bool>("showDiskGraph", default: false)
|
||||||
|
static let cpuTemperatureUnit = Key<LockScreenWeatherTemperatureUnit>("cpuTemperatureUnit", default: .celsius)
|
||||||
|
|
||||||
// MARK: Terminal Feature
|
// MARK: Terminal Feature
|
||||||
static let enableTerminalFeature = Key<Bool>("enableTerminalFeature", default: false)
|
static let enableTerminalFeature = Key<Bool>("enableTerminalFeature", default: false)
|
||||||
|
|
@ -1069,7 +1070,7 @@ extension Defaults.Keys {
|
||||||
static let capsLockIndicatorUseGreenColor = Key<Bool>("capsLockIndicatorUseGreenColor", default: false) // Legacy toggle
|
static let capsLockIndicatorUseGreenColor = Key<Bool>("capsLockIndicatorUseGreenColor", default: false) // Legacy toggle
|
||||||
static let capsLockIndicatorTintMode = Key<CapsLockIndicatorTintMode>("capsLockIndicatorTintMode", default: .white)
|
static let capsLockIndicatorTintMode = Key<CapsLockIndicatorTintMode>("capsLockIndicatorTintMode", default: .white)
|
||||||
static let didMigrateCapsLockTintMode = Key<Bool>("didMigrateCapsLockTintMode", default: false)
|
static let didMigrateCapsLockTintMode = Key<Bool>("didMigrateCapsLockTintMode", default: false)
|
||||||
static let showCapsLockLabel = Key<Bool>("showCapsLockLabel", default: true)
|
static let showCapsLockLabel = Key<Bool>("showCapsLockLabel", default: false)
|
||||||
|
|
||||||
// MARK: ImageService
|
// MARK: ImageService
|
||||||
static let didClearLegacyURLCacheV1 = Key<Bool>("didClearLegacyURLCacheV1", default: false)
|
static let didClearLegacyURLCacheV1 = Key<Bool>("didClearLegacyURLCacheV1", default: false)
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,11 @@ struct CPUFrequencyMetrics: Equatable {
|
||||||
|
|
||||||
struct CPUTemperatureMetrics: Equatable {
|
struct CPUTemperatureMetrics: Equatable {
|
||||||
let celsius: Double?
|
let celsius: Double?
|
||||||
|
|
||||||
|
var fahrenheit: Double? {
|
||||||
|
guard let celsius else { return nil }
|
||||||
|
return celsius * 9.0 / 5.0 + 32.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class CPUSensorCollector {
|
final class CPUSensorCollector {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue