From 2ce6a3ae9660f709b8c57aef9a8258ef5d929660 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Fri, 12 Dec 2025 13:43:16 +0530 Subject: [PATCH] Transient DND Mode --- DynamicIsland/ContentView.swift | 2 +- DynamicIsland/Localizable.xcstrings | 12 +++ .../Focus/DoNotDisturbLiveActivity.swift | 75 +++++++++++++++---- .../components/Settings/SettingsView.swift | 9 ++- DynamicIsland/models/Constants.swift | 1 + 5 files changed, 83 insertions(+), 16 deletions(-) diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index c8347ed..60fd3ea 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -471,7 +471,7 @@ struct ContentView: View { } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .download) && vm.notchState == .closed && downloadManager.isDownloading && Defaults[.enableDownloadListener] && !vm.hideOnClosed { DownloadLiveActivity() .transition(.blurReplace.animation(.interactiveSpring(dampingFraction: 1.2))) - } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .doNotDisturb) && vm.notchState == .closed && doNotDisturbManager.isDoNotDisturbActive && Defaults[.enableDoNotDisturbDetection] && Defaults[.showDoNotDisturbIndicator] && !vm.hideOnClosed && !lockScreenManager.isLocked { + } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .doNotDisturb) && vm.notchState == .closed && Defaults[.enableDoNotDisturbDetection] && Defaults[.showDoNotDisturbIndicator] && (doNotDisturbManager.isDoNotDisturbActive || Defaults[.focusIndicatorNonPersistent]) && !vm.hideOnClosed && !lockScreenManager.isLocked { DoNotDisturbLiveActivity() } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .lockScreen) && vm.notchState == .closed && (lockScreenManager.isLocked || !lockScreenManager.isLockIdle) && Defaults[.enableLockScreenLiveActivity] && !vm.hideOnClosed { LockScreenLiveActivity() diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 07f523e..f72d62b 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -10731,6 +10731,10 @@ } } }, + "Labels are forced to compact on/off text while brief toast mode is enabled." : { + "comment" : "A help text explaining why the \"Show Focus Label\" toggle is disabled when the \"Show Focus as brief toast\" toggle is enabled.", + "isCommentAutoGenerated" : true + }, "Last Updated" : { "localizations" : { "fr" : { @@ -17451,6 +17455,10 @@ } } }, + "Show the active Focus name inside the indicator." : { + "comment" : "A description of why the \"Show Focus Label\" toggle is disabled when the \"Show Focus as brief toast\" toggle is enabled.", + "isCommentAutoGenerated" : true + }, "Show timer name" : { }, @@ -20378,6 +20386,10 @@ }, "When disabled, all keyboard shortcuts will be inactive. You can still use the UI controls." : { + }, + "When enabled, Focus appears briefly (on/off) and then collapses instead of staying visible." : { + "comment" : "A help text for the setting that allows the focus indicator to collapse instead of staying visible.", + "isCommentAutoGenerated" : true }, "When enabled, stats monitoring stops a few seconds after the notch closes." : { diff --git a/DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift b/DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift index a4b3f9e..ef430b9 100644 --- a/DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift +++ b/DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift @@ -13,6 +13,7 @@ struct DoNotDisturbLiveActivity: View { @EnvironmentObject var vm: DynamicIslandViewModel @ObservedObject var manager = DoNotDisturbManager.shared @Default(.showDoNotDisturbLabel) private var showLabelSetting + @Default(.focusIndicatorNonPersistent) private var focusToastMode @State private var isExpanded = false @State private var showInactiveIcon = false @@ -53,10 +54,15 @@ struct DoNotDisturbLiveActivity: View { } private var labelWingWidth: CGFloat { - if shouldShowLabel { - return max(desiredLabelWidth, minimalWingWidth) + guard shouldShowLabel else { + return focusToastMode ? 0 : ((isExpanded || showInactiveIcon) ? minimalWingWidth : 0) } - return (isExpanded || showInactiveIcon) ? minimalWingWidth : 0 + + if focusToastMode { + return max(labelIntrinsicWidth + 26, minimalWingWidth) + } + + return max(desiredLabelWidth, minimalWingWidth) } private var minimalWingWidth: CGFloat { @@ -76,7 +82,7 @@ struct DoNotDisturbLiveActivity: View { } private var shouldShowLabel: Bool { - showLabelSetting && isExpanded && !labelText.isEmpty + focusToastMode ? (isExpanded && !labelText.isEmpty) : (showLabelSetting && isExpanded && !labelText.isEmpty) } // MARK: - Focus metadata @@ -93,16 +99,23 @@ struct DoNotDisturbLiveActivity: View { } private var labelText: String { - let trimmed = manager.currentFocusModeName.trimmingCharacters(in: .whitespacesAndNewlines) - if !trimmed.isEmpty { - return trimmed + if focusToastMode { + return manager.isDoNotDisturbActive ? "On" : "Off" } - let fallback = focusMode.displayName - if focusMode == .doNotDisturb { - return "Do Not Disturb" + let trimmed = manager.currentFocusModeName.trimmingCharacters(in: .whitespacesAndNewlines) + if showLabelSetting { + if !trimmed.isEmpty { + return trimmed + } else if focusMode == .doNotDisturb { + return "Do Not Disturb" + } else { + let fallback = focusMode.displayName + return fallback.isEmpty ? "Focus" : fallback + } } - return fallback.isEmpty ? "Focus" : fallback + + return "" } private var accessibilityDescription: String { @@ -158,6 +171,7 @@ struct DoNotDisturbLiveActivity: View { .font(.system(size: 12, weight: .semibold, design: .rounded)) .foregroundColor(activeAccentColor) .lineLimit(1) + .minimumScaleFactor(0.8) .contentTransition(.opacity) .background( GeometryReader { proxy in @@ -181,12 +195,15 @@ struct DoNotDisturbLiveActivity: View { withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) { isExpanded = true } + if focusToastMode { + scheduleTransientCollapse() + } } } private func handleFocusStateChange(_ oldValue: Bool, _ isActive: Bool) { + cancelPendingTasks() if isActive { - cancelPendingTasks() withAnimation(.smooth(duration: 0.2)) { showInactiveIcon = false } @@ -194,12 +211,18 @@ struct DoNotDisturbLiveActivity: View { iconScale = 1.0 isExpanded = true } + if focusToastMode { + scheduleTransientCollapse() + } } else { triggerInactiveAnimation() } } private func triggerInactiveAnimation() { + withAnimation(.spring(response: 0.3, dampingFraction: 0.9)) { + isExpanded = true + } withAnimation(.smooth(duration: 0.2)) { showInactiveIcon = true } @@ -208,9 +231,16 @@ struct DoNotDisturbLiveActivity: View { iconScale = 1.2 } + let toastMode = focusToastMode scaleResetTask?.cancel() scaleResetTask = Task { @MainActor in - try? await Task.sleep(for: .milliseconds(650)) + try? await Task.sleep(for: .milliseconds(toastMode ? 650 : 450)) + withAnimation(.interpolatingSpring(stiffness: 180, damping: 18)) { + iconScale = 1.0 + } + if toastMode { + return + } withAnimation(.smooth(duration: 0.2)) { showInactiveIcon = false } @@ -218,13 +248,17 @@ struct DoNotDisturbLiveActivity: View { collapseTask?.cancel() collapseTask = Task { @MainActor in - try? await Task.sleep(for: .milliseconds(320)) + try? await Task.sleep(for: .milliseconds(focusToastMode ? 900 : 320)) withAnimation(.smooth(duration: 0.32)) { isExpanded = false + if focusToastMode { + showInactiveIcon = false + } } } cleanupTask?.cancel() + guard !focusToastMode else { return } cleanupTask = Task { @MainActor in try? await Task.sleep(for: .milliseconds(650)) withAnimation(.smooth(duration: 0.2)) { @@ -233,6 +267,19 @@ struct DoNotDisturbLiveActivity: View { } } + private func scheduleTransientCollapse() { + collapseTask?.cancel() + cleanupTask?.cancel() + + collapseTask = Task { @MainActor in + try? await Task.sleep(for: .milliseconds(1150)) + withAnimation(.smooth(duration: 0.32)) { + isExpanded = false + showInactiveIcon = false + } + } + } + private func cancelPendingTasks() { scaleResetTask?.cancel() collapseTask?.cancel() diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 7e3aa50..94d4a5c 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -2056,6 +2056,7 @@ struct LiveActivitiesSettings: View { @Default(.enableScreenRecordingDetection) var enableScreenRecordingDetection @Default(.enableDoNotDisturbDetection) var enableDoNotDisturbDetection + @Default(.focusIndicatorNonPersistent) var focusIndicatorNonPersistent private func highlightID(_ title: String) -> String { SettingsTab.liveActivities.highlightID(for: title) @@ -2104,9 +2105,15 @@ struct LiveActivitiesSettings: View { .settingsHighlight(id: highlightID("Show Focus Indicator")) Defaults.Toggle("Show Focus Label", key: .showDoNotDisturbLabel) - .disabled(!enableDoNotDisturbDetection) + .disabled(!enableDoNotDisturbDetection || focusIndicatorNonPersistent) + .help(focusIndicatorNonPersistent ? "Labels are forced to compact on/off text while brief toast mode is enabled." : "Show the active Focus name inside the indicator.") .settingsHighlight(id: highlightID("Show Focus Label")) + Defaults.Toggle("Show Focus as brief toast", key: .focusIndicatorNonPersistent) + .disabled(!enableDoNotDisturbDetection) + .settingsHighlight(id: highlightID("Show Focus as brief toast")) + .help("When enabled, Focus appears briefly (on/off) and then collapses instead of staying visible.") + if doNotDisturbManager.isMonitoring { HStack { Text("Focus Status") diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 37b9ae1..e696e87 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -616,6 +616,7 @@ extension Defaults.Keys { static let enableDoNotDisturbDetection = Key("enableDoNotDisturbDetection", default: true) static let showDoNotDisturbIndicator = Key("showDoNotDisturbIndicator", default: true) static let showDoNotDisturbLabel = Key("showDoNotDisturbLabel", default: true) + static let focusIndicatorNonPersistent = Key("focusIndicatorNonPersistent", default: false) // MARK: Privacy Indicators (Camera & Microphone Detection) static let enableCameraDetection = Key("enableCameraDetection", default: true)