From 6d01f6a1b2a87363d93b00cbd4f185ce97e57d9f Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Thu, 5 Mar 2026 20:56:15 +0530 Subject: [PATCH] Update Onboarding and temperature stuff --- DynamicIsland/Localizable.xcstrings | 73 +++++- .../Focus/DoNotDisturbLiveActivity.swift | 51 +++- .../components/Settings/SettingsView.swift | 12 + .../Stats/CPUStatsSharedViews.swift | 19 +- .../managers/DoNotDisturbManager.swift | 248 +++++++++++++----- DynamicIsland/models/Constants.swift | 9 +- DynamicIsland/utils/CPUSensorCollector.swift | 5 + 7 files changed, 340 insertions(+), 77 deletions(-) diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 37382a8..0638a2b 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -1607,6 +1607,10 @@ } } }, + "±10 Seconds" : { + "comment" : "Display name for the \"±10 Seconds\" music skip behavior option.", + "isCommentAutoGenerated" : true + }, "0" : { "comment" : "A label displayed in a circular gauge to indicate the minimum battery level.", "isCommentAutoGenerated" : true, @@ -1923,6 +1927,10 @@ } } }, + "Accent" : { + "comment" : "Display name for the \"Accent\" caps lock indicator tint mode.", + "isCommentAutoGenerated" : true + }, "Accent color" : { "localizations" : { "ar" : { @@ -2268,6 +2276,10 @@ } } }, + "Adaptive gradient" : { + "comment" : "Name of the timer icon color mode option that uses an adaptive gradient.", + "isCommentAutoGenerated" : true + }, "Add" : { "localizations" : { "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." : { "localizations" : { "pt-BR" : { @@ -4277,6 +4293,10 @@ } } }, + "Bar" : { + "comment" : "Localized name for the \"Bar\" timer progress style.", + "isCommentAutoGenerated" : true + }, "Battery" : { "localizations" : { "ar" : { @@ -8547,6 +8567,10 @@ } } }, + "Digital" : { + "comment" : "The name of the \"Digital\" timer style.", + "isCommentAutoGenerated" : true + }, "Dimensions" : { "localizations" : { "pt-BR" : { @@ -11945,6 +11969,10 @@ } } }, + "Green" : { + "comment" : "Display name for the \"Green\" Caps Lock Indicator Tint Mode.", + "isCommentAutoGenerated" : true + }, "Height:" : { "extractionState" : "stale", "localizations" : { @@ -15625,7 +15653,6 @@ } }, "Minutes" : { - "extractionState" : "stale", "localizations" : { "fr" : { "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." : { }, @@ -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." : { "comment" : "A description of the \"Panel\" clipboard display mode.", "isCommentAutoGenerated" : true, @@ -19831,6 +19866,10 @@ } } }, + "Ring" : { + "comment" : "Localized name for the 'Ring' timer progress style.", + "isCommentAutoGenerated" : true + }, "Rotation:" : { "extractionState" : "stale", "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." : { "comment" : "A description of the \"Separate Tab\" clipboard display mode.", "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" : { "localizations" : { "fr" : { @@ -21780,6 +21831,10 @@ } } }, + "Skip forward or backward by ten seconds" : { + "comment" : "Description of a music skip behavior option.", + "isCommentAutoGenerated" : true + }, "Slider color" : { "localizations" : { "ar" : { @@ -22348,6 +22403,10 @@ } } }, + "Spotify" : { + "comment" : "Add more media player types here.", + "isCommentAutoGenerated" : true + }, "Square" : { "localizations" : { "ar" : { @@ -22460,6 +22519,10 @@ }, "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" : { "localizations" : { @@ -23821,6 +23884,10 @@ }, "shouldTranslate" : false }, + "Track Skip" : { + "comment" : "Name of the music skip behavior option that skips to the next track.", + "isCommentAutoGenerated" : true + }, "Transform" : { "extractionState" : "stale", "localizations" : { @@ -25742,6 +25809,10 @@ "comment" : "A footer note explaining the app's funding.", "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: " : { "localizations" : { "ar" : { diff --git a/DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift b/DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift index 98bf70b..5dc17e3 100644 --- a/DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift +++ b/DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift @@ -55,6 +55,7 @@ struct DoNotDisturbLiveActivity: View { .accessibilityLabel(accessibilityDescription) .onAppear(perform: handleInitialState) .onChange(of: manager.isDoNotDisturbActive, handleFocusStateChange) + .onChange(of: manager.focusToastTrigger, handleFocusModeSwitchToast) .onDisappear(perform: cancelPendingTasks) } @@ -333,19 +334,57 @@ struct DoNotDisturbLiveActivity: View { private func handleFocusStateChange(_ oldValue: Bool, _ isActive: Bool) { cancelPendingTasks() + 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 + 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)) { iconScale = 1.0 isExpanded = true } - if focusToastMode { - scheduleTransientCollapse() - } - } else { - triggerInactiveAnimation() + scheduleTransientCollapse() } } diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 6c92f66..bb0194d 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -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: "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: "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: "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")), @@ -6157,6 +6158,7 @@ struct StatsSettings: View { @Default(.showGpuGraph) var showGpuGraph @Default(.showNetworkGraph) var showNetworkGraph @Default(.showDiskGraph) var showDiskGraph + @Default(.cpuTemperatureUnit) var cpuTemperatureUnit private func highlightID(_ title: String) -> String { SettingsTab.stats.highlightID(for: title) @@ -6246,6 +6248,16 @@ struct StatsSettings: View { Section { Defaults.Toggle("CPU Usage", key: .showCpuGraph) .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) .settingsHighlight(id: highlightID("Memory Usage")) Defaults.Toggle("GPU Usage", key: .showGpuGraph) diff --git a/DynamicIsland/components/Stats/CPUStatsSharedViews.swift b/DynamicIsland/components/Stats/CPUStatsSharedViews.swift index 0b5a651..68150c2 100644 --- a/DynamicIsland/components/Stats/CPUStatsSharedViews.swift +++ b/DynamicIsland/components/Stats/CPUStatsSharedViews.swift @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import Defaults import SwiftUI struct StatsCard: View { @@ -163,7 +164,10 @@ private struct CPUTemperatureGauge: View { let tint: Color var size: CGFloat = 84 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 { 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 { - guard let temperature, maxTemperature > 0 else { return 0 } - return min(max(temperature / maxTemperature, 0), 1) + guard let displayTemperature, maxTemperature > 0 else { return 0 } + return min(max(displayTemperature / maxTemperature, 0), 1) } private var centerPrimary: String { - guard let temperature else { return "—" } - return String(format: "%.0f°", temperature) + guard let displayTemperature else { return "—" } + return String(format: "%.0f%@", displayTemperature, temperatureUnit.symbol) } } diff --git a/DynamicIsland/managers/DoNotDisturbManager.swift b/DynamicIsland/managers/DoNotDisturbManager.swift index 073693a..d2da5c0 100644 --- a/DynamicIsland/managers/DoNotDisturbManager.swift +++ b/DynamicIsland/managers/DoNotDisturbManager.swift @@ -30,6 +30,9 @@ final class DoNotDisturbManager: ObservableObject { @Published var currentFocusModeName: 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 metadataExtractionQueue = DispatchQueue(label: "com.dynamicisland.focus.metadata", qos: .userInitiated) private let pollingQueue = DispatchQueue(label: "com.dynamicisland.focus.polling", qos: .utility) @@ -82,9 +85,8 @@ final class DoNotDisturbManager: ObservableObject { suspensionBehavior: .deliverImmediately ) - startAssertionsPolling() - applyMonitoringModeChange(Defaults[.focusMonitoringMode]) isMonitoring = true + applyMonitoringModeChange(Defaults[.focusMonitoringMode]) } func stopMonitoring() { @@ -128,30 +130,69 @@ final class DoNotDisturbManager: ObservableObject { let trimmedIdentifier = identifier?.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedName = name?.trimmingCharacters(in: .whitespacesAndNewlines) - let resolvedMode = FocusModeType.resolve(identifier: trimmedIdentifier, name: trimmedName) - - let finalIdentifier: String - 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 isGenericFocusIdentifier = (trimmedIdentifier?.lowercased() == "com.apple.focus") + let usableIdentifier = (trimmedIdentifier?.isEmpty == false && !isGenericFocusIdentifier) ? trimmedIdentifier : nil + let usableName = (trimmedName?.isEmpty == false) ? trimmedName : nil let previousIdentifier = self.currentFocusModeIdentifier let previousName = self.currentFocusModeName 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 nameChanged = finalName != previousName let shouldToggleActive = isActive.map { $0 != previousActive } ?? false @@ -161,10 +202,12 @@ final class DoNotDisturbManager: ObservableObject { } if nameChanged { - self.currentFocusModeName = finalName - .localizedCaseInsensitiveContains( - "Reduce Interruptions" - ) ? "Reduce Interr." : finalName + self.currentFocusModeName = 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 { @@ -176,12 +219,16 @@ final class DoNotDisturbManager: ObservableObject { withAnimation(.smooth(duration: 0.25)) { 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?) { - guard Defaults[.focusMonitoringMode] == .useDevTools else { return } - metadataExtractionQueue.async { [weak self] in guard let self = self else { return } let trimmedIdentifier = identifier?.trimmingCharacters(in: .whitespacesAndNewlines) @@ -336,9 +383,11 @@ private extension DoNotDisturbManager { guard isMonitoring else { return } if mode == .useDevTools { + stopAssertionsPolling() focusLogStream.start() } else { focusLogStream.stop() + startAssertionsPolling() } } @@ -541,11 +590,24 @@ extension FocusModeType { return } + // 1. Exact rawValue match (e.g., "com.apple.focus.work", "com.apple.donotdisturb.mode"). if let direct = FocusModeType(rawValue: normalized) ?? FocusModeType(rawValue: normalizedLowercased) { self = direct return } + // 2. macOS 26.3 custom Focus modes use `com.apple.donotdisturb.mode.`. + // 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: { guard !$0.rawValue.isEmpty else { return false } return normalized.hasPrefix($0.rawValue) || normalizedLowercased.hasPrefix($0.rawValue) @@ -554,6 +616,7 @@ extension FocusModeType { return } + // 4. Anything else under com.apple.focus is custom. if normalizedLowercased.hasPrefix("com.apple.focus") { self = .custom return @@ -570,6 +633,22 @@ extension FocusModeType { }) { 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 { @@ -580,13 +659,17 @@ extension FocusModeType { } func getCustomIconFromFile() -> String { - return FocusMetadataReader.shared - .getIcon(for: DoNotDisturbManager.shared.currentFocusModeName) + return FocusMetadataReader.shared.getIcon( + for: DoNotDisturbManager.shared.currentFocusModeName, + identifier: DoNotDisturbManager.shared.currentFocusModeIdentifier + ) } func getCustomAccentColorFromFile() -> Color { - return FocusMetadataReader.shared - .getAccentColor(for: DoNotDisturbManager.shared.currentFocusModeName) + return FocusMetadataReader.shared.getAccentColor( + for: DoNotDisturbManager.shared.currentFocusModeName, + identifier: DoNotDisturbManager.shared.currentFocusModeIdentifier + ) } } @@ -815,6 +898,7 @@ private final class FocusLogStream { private var pipe: Pipe? private var buffer = Data() private var isRunning = false + private var didTerminate = false private let metadataLock = NSLock() private var lastIdentifier: String? @@ -831,12 +915,13 @@ private final class FocusLogStream { process.executableURL = URL(fileURLWithPath: "/usr/bin/log") process.arguments = [ "stream", + "--no-backtrace", "--style", "compact", "--level", "info", "--predicate", - "subsystem == \"com.apple.focus\"" + "process == \"duetexpertd\" AND eventMessage CONTAINS \"semanticModeIdentifier\"" ] let pipe = Pipe() @@ -870,7 +955,8 @@ private final class FocusLogStream { self.process = process self.pipe = pipe 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 { debugPrint("[FocusLogStream] Failed to start log stream: \(error)") pipe.fileHandleForReading.readabilityHandler = nil @@ -937,7 +1023,12 @@ private final class FocusLogStream { 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() return } @@ -945,11 +1036,26 @@ private final class FocusLogStream { var updatedIdentifier: 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: + if trimmed.contains(" 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[.. DNDMode? { + + private func getModeConfig(for focusName: String, identifier: String? = nil) -> DNDMode? { guard FullDiskAccessAuthorization.hasPermission() else { return nil } + + let trimmedName = focusName.trimmingCharacters(in: .whitespacesAndNewlines) + let trimmedIdentifier = identifier?.trimmingCharacters(in: .whitespacesAndNewlines) + do { let data = try Data(contentsOf: pathToDatabase) let root = try JSONDecoder().decode(DNDConfigRoot.self, from: data) - + for entry in root.data { for wrapper in entry.modeConfigurations.values { - if wrapper.mode.name - .localizedCaseInsensitiveCompare(focusName) == .orderedSame { - return wrapper.mode + let mode = 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 { - print("JSON Error: \(error)") + print("ModeConfigurations.json decode error: \(error)") } + 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` /// - Returns A string representing the sfSymbol of the current focus - func getIcon(for focus: String) -> String { - guard let mode = getModeConfig(for: focus) else { return "app.badge" } + func getIcon(for focus: String, identifier: String? = nil) -> String { + guard let mode = getModeConfig(for: focus, identifier: identifier) else { return "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` /// - Returns A Color representing the accent color for the current focus - func getAccentColor(for focus: String) -> Color { - guard let mode = getModeConfig(for: focus), + func getAccentColor(for focus: String, identifier: String? = nil) -> Color { + guard let mode = getModeConfig(for: focus, identifier: identifier), let colorName = mode.tintColorName else { return .indigo } - + return Color.stringToColor(for: colorName) } - } extension Color { diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index bce1462..23ae426 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -794,10 +794,10 @@ extension Defaults.Keys { static let lockScreenWeatherProviderSource = Key("lockScreenWeatherProviderSource", default: .openMeteo) static let lockScreenWeatherVerticalOffset = Key("lockScreenWeatherVerticalOffset", default: 0) static let lockScreenMusicVerticalOffset = Key("lockScreenMusicVerticalOffset", default: 0) - static let lockScreenMusicPanelWidth = Key("lockScreenMusicPanelWidth", default: 420) + static let lockScreenMusicPanelWidth = Key("lockScreenMusicPanelWidth", default: 350) static let lockScreenMusicAlbumParallaxEnabled = Key("lockScreenMusicAlbumParallaxEnabled", default: false) static let lockScreenTimerVerticalOffset = Key("lockScreenTimerVerticalOffset", default: 0) - static let lockScreenTimerWidgetWidth = Key("lockScreenTimerWidgetWidth", default: 420) + static let lockScreenTimerWidgetWidth = Key("lockScreenTimerWidgetWidth", default: 350) static let lockScreenGlassStyle = Key("lockScreenGlassStyle", default: .liquid) static let lockScreenGlassCustomizationMode = Key( "lockScreenGlassCustomizationMode", @@ -903,13 +903,14 @@ extension Defaults.Keys { // MARK: Stats Feature static let enableStatsFeature = Key("enableStatsFeature", default: false) static let autoStartStatsMonitoring = Key("autoStartStatsMonitoring", default: true) - static let statsStopWhenNotchCloses = Key("statsStopWhenNotchCloses", default: false) + static let statsStopWhenNotchCloses = Key("statsStopWhenNotchCloses", default: true) static let statsUpdateInterval = Key("statsUpdateInterval", default: 1.0) static let showCpuGraph = Key("showCpuGraph", default: true) static let showMemoryGraph = Key("showMemoryGraph", default: true) static let showGpuGraph = Key("showGpuGraph", default: true) static let showNetworkGraph = Key("showNetworkGraph", default: false) static let showDiskGraph = Key("showDiskGraph", default: false) + static let cpuTemperatureUnit = Key("cpuTemperatureUnit", default: .celsius) // MARK: Terminal Feature static let enableTerminalFeature = Key("enableTerminalFeature", default: false) @@ -1069,7 +1070,7 @@ extension Defaults.Keys { static let capsLockIndicatorUseGreenColor = Key("capsLockIndicatorUseGreenColor", default: false) // Legacy toggle static let capsLockIndicatorTintMode = Key("capsLockIndicatorTintMode", default: .white) static let didMigrateCapsLockTintMode = Key("didMigrateCapsLockTintMode", default: false) - static let showCapsLockLabel = Key("showCapsLockLabel", default: true) + static let showCapsLockLabel = Key("showCapsLockLabel", default: false) // MARK: ImageService static let didClearLegacyURLCacheV1 = Key("didClearLegacyURLCacheV1", default: false) diff --git a/DynamicIsland/utils/CPUSensorCollector.swift b/DynamicIsland/utils/CPUSensorCollector.swift index dddd5ff..bf1aa04 100644 --- a/DynamicIsland/utils/CPUSensorCollector.swift +++ b/DynamicIsland/utils/CPUSensorCollector.swift @@ -30,6 +30,11 @@ struct CPUFrequencyMetrics: Equatable { struct CPUTemperatureMetrics: Equatable { let celsius: Double? + + var fahrenheit: Double? { + guard let celsius else { return nil } + return celsius * 9.0 / 5.0 + 32.0 + } } final class CPUSensorCollector {