Transient DND Mode

This commit is contained in:
Hariharan Mudaliar 2025-12-12 13:43:16 +05:30
commit 2ce6a3ae96
No known key found for this signature in database
GPG key ID: D04F1C2B9194DDFC
5 changed files with 83 additions and 16 deletions

View file

@ -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 { } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .download) && vm.notchState == .closed && downloadManager.isDownloading && Defaults[.enableDownloadListener] && !vm.hideOnClosed {
DownloadLiveActivity() DownloadLiveActivity()
.transition(.blurReplace.animation(.interactiveSpring(dampingFraction: 1.2))) .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() DoNotDisturbLiveActivity()
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .lockScreen) && vm.notchState == .closed && (lockScreenManager.isLocked || !lockScreenManager.isLockIdle) && Defaults[.enableLockScreenLiveActivity] && !vm.hideOnClosed { } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .lockScreen) && vm.notchState == .closed && (lockScreenManager.isLocked || !lockScreenManager.isLockIdle) && Defaults[.enableLockScreenLiveActivity] && !vm.hideOnClosed {
LockScreenLiveActivity() LockScreenLiveActivity()

View file

@ -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" : { "Last Updated" : {
"localizations" : { "localizations" : {
"fr" : { "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" : { "Show timer name" : {
}, },
@ -20378,6 +20386,10 @@
}, },
"When disabled, all keyboard shortcuts will be inactive. You can still use the UI controls." : { "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." : { "When enabled, stats monitoring stops a few seconds after the notch closes." : {

View file

@ -13,6 +13,7 @@ struct DoNotDisturbLiveActivity: View {
@EnvironmentObject var vm: DynamicIslandViewModel @EnvironmentObject var vm: DynamicIslandViewModel
@ObservedObject var manager = DoNotDisturbManager.shared @ObservedObject var manager = DoNotDisturbManager.shared
@Default(.showDoNotDisturbLabel) private var showLabelSetting @Default(.showDoNotDisturbLabel) private var showLabelSetting
@Default(.focusIndicatorNonPersistent) private var focusToastMode
@State private var isExpanded = false @State private var isExpanded = false
@State private var showInactiveIcon = false @State private var showInactiveIcon = false
@ -53,10 +54,15 @@ struct DoNotDisturbLiveActivity: View {
} }
private var labelWingWidth: CGFloat { private var labelWingWidth: CGFloat {
if shouldShowLabel { guard shouldShowLabel else {
return max(desiredLabelWidth, minimalWingWidth) 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 { private var minimalWingWidth: CGFloat {
@ -76,7 +82,7 @@ struct DoNotDisturbLiveActivity: View {
} }
private var shouldShowLabel: Bool { private var shouldShowLabel: Bool {
showLabelSetting && isExpanded && !labelText.isEmpty focusToastMode ? (isExpanded && !labelText.isEmpty) : (showLabelSetting && isExpanded && !labelText.isEmpty)
} }
// MARK: - Focus metadata // MARK: - Focus metadata
@ -93,16 +99,23 @@ struct DoNotDisturbLiveActivity: View {
} }
private var labelText: String { private var labelText: String {
let trimmed = manager.currentFocusModeName.trimmingCharacters(in: .whitespacesAndNewlines) if focusToastMode {
if !trimmed.isEmpty { return manager.isDoNotDisturbActive ? "On" : "Off"
return trimmed
} }
let fallback = focusMode.displayName let trimmed = manager.currentFocusModeName.trimmingCharacters(in: .whitespacesAndNewlines)
if focusMode == .doNotDisturb { if showLabelSetting {
return "Do Not Disturb" 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 { private var accessibilityDescription: String {
@ -158,6 +171,7 @@ struct DoNotDisturbLiveActivity: View {
.font(.system(size: 12, weight: .semibold, design: .rounded)) .font(.system(size: 12, weight: .semibold, design: .rounded))
.foregroundColor(activeAccentColor) .foregroundColor(activeAccentColor)
.lineLimit(1) .lineLimit(1)
.minimumScaleFactor(0.8)
.contentTransition(.opacity) .contentTransition(.opacity)
.background( .background(
GeometryReader { proxy in GeometryReader { proxy in
@ -181,12 +195,15 @@ struct DoNotDisturbLiveActivity: View {
withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) { withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) {
isExpanded = true isExpanded = true
} }
if focusToastMode {
scheduleTransientCollapse()
}
} }
} }
private func handleFocusStateChange(_ oldValue: Bool, _ isActive: Bool) { private func handleFocusStateChange(_ oldValue: Bool, _ isActive: Bool) {
cancelPendingTasks()
if isActive { if isActive {
cancelPendingTasks()
withAnimation(.smooth(duration: 0.2)) { withAnimation(.smooth(duration: 0.2)) {
showInactiveIcon = false showInactiveIcon = false
} }
@ -194,12 +211,18 @@ struct DoNotDisturbLiveActivity: View {
iconScale = 1.0 iconScale = 1.0
isExpanded = true isExpanded = true
} }
if focusToastMode {
scheduleTransientCollapse()
}
} else { } else {
triggerInactiveAnimation() triggerInactiveAnimation()
} }
} }
private func triggerInactiveAnimation() { private func triggerInactiveAnimation() {
withAnimation(.spring(response: 0.3, dampingFraction: 0.9)) {
isExpanded = true
}
withAnimation(.smooth(duration: 0.2)) { withAnimation(.smooth(duration: 0.2)) {
showInactiveIcon = true showInactiveIcon = true
} }
@ -208,9 +231,16 @@ struct DoNotDisturbLiveActivity: View {
iconScale = 1.2 iconScale = 1.2
} }
let toastMode = focusToastMode
scaleResetTask?.cancel() scaleResetTask?.cancel()
scaleResetTask = Task { @MainActor in 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)) { withAnimation(.smooth(duration: 0.2)) {
showInactiveIcon = false showInactiveIcon = false
} }
@ -218,13 +248,17 @@ struct DoNotDisturbLiveActivity: View {
collapseTask?.cancel() collapseTask?.cancel()
collapseTask = Task { @MainActor in 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)) { withAnimation(.smooth(duration: 0.32)) {
isExpanded = false isExpanded = false
if focusToastMode {
showInactiveIcon = false
}
} }
} }
cleanupTask?.cancel() cleanupTask?.cancel()
guard !focusToastMode else { return }
cleanupTask = Task { @MainActor in cleanupTask = Task { @MainActor in
try? await Task.sleep(for: .milliseconds(650)) try? await Task.sleep(for: .milliseconds(650))
withAnimation(.smooth(duration: 0.2)) { 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() { private func cancelPendingTasks() {
scaleResetTask?.cancel() scaleResetTask?.cancel()
collapseTask?.cancel() collapseTask?.cancel()

View file

@ -2056,6 +2056,7 @@ struct LiveActivitiesSettings: View {
@Default(.enableScreenRecordingDetection) var enableScreenRecordingDetection @Default(.enableScreenRecordingDetection) var enableScreenRecordingDetection
@Default(.enableDoNotDisturbDetection) var enableDoNotDisturbDetection @Default(.enableDoNotDisturbDetection) var enableDoNotDisturbDetection
@Default(.focusIndicatorNonPersistent) var focusIndicatorNonPersistent
private func highlightID(_ title: String) -> String { private func highlightID(_ title: String) -> String {
SettingsTab.liveActivities.highlightID(for: title) SettingsTab.liveActivities.highlightID(for: title)
@ -2104,9 +2105,15 @@ struct LiveActivitiesSettings: View {
.settingsHighlight(id: highlightID("Show Focus Indicator")) .settingsHighlight(id: highlightID("Show Focus Indicator"))
Defaults.Toggle("Show Focus Label", key: .showDoNotDisturbLabel) 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")) .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 { if doNotDisturbManager.isMonitoring {
HStack { HStack {
Text("Focus Status") Text("Focus Status")

View file

@ -616,6 +616,7 @@ extension Defaults.Keys {
static let enableDoNotDisturbDetection = Key<Bool>("enableDoNotDisturbDetection", default: true) static let enableDoNotDisturbDetection = Key<Bool>("enableDoNotDisturbDetection", default: true)
static let showDoNotDisturbIndicator = Key<Bool>("showDoNotDisturbIndicator", default: true) static let showDoNotDisturbIndicator = Key<Bool>("showDoNotDisturbIndicator", default: true)
static let showDoNotDisturbLabel = Key<Bool>("showDoNotDisturbLabel", default: true) static let showDoNotDisturbLabel = Key<Bool>("showDoNotDisturbLabel", default: true)
static let focusIndicatorNonPersistent = Key<Bool>("focusIndicatorNonPersistent", default: false)
// MARK: Privacy Indicators (Camera & Microphone Detection) // MARK: Privacy Indicators (Camera & Microphone Detection)
static let enableCameraDetection = Key<Bool>("enableCameraDetection", default: true) static let enableCameraDetection = Key<Bool>("enableCameraDetection", default: true)