From 3fd190f24a9de347e05bf73b8ad6ccaf03299278 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Sat, 13 Dec 2025 23:29:35 +0530 Subject: [PATCH] Lock Screen indicator for focus mode --- DynamicIsland/Localizable.xcstrings | 10 ++ .../LockScreen/LockScreenWeatherWidget.swift | 91 +++++++++++++++++-- .../components/Settings/SettingsView.swift | 10 ++ DynamicIsland/models/Constants.swift | 1 + 4 files changed, 103 insertions(+), 9 deletions(-) diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index f72d62b..f5b5b24 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -7084,6 +7084,9 @@ }, "Displays play/pause and skip buttons beside the notch while music is active. Disabled by default." : { + }, + "Displays the current Focus state above the weather capsule whenever Focus detection is enabled." : { + }, "Do Not Disturb" : { @@ -8847,9 +8850,16 @@ }, "Fit to Notch" : { + }, + "Focus active: %@" : { + "comment" : "An accessibility label for the focus widget, describing the currently active focus mode.", + "isCommentAutoGenerated" : true }, "Focus Status" : { + }, + "Focus Widget" : { + }, "Free ยท %@" : { "comment" : "A label displaying the amount of free storage on a disk device. The argument is the number of free bytes on the disk.", diff --git a/DynamicIsland/components/LockScreen/LockScreenWeatherWidget.swift b/DynamicIsland/components/LockScreen/LockScreenWeatherWidget.swift index 59af845..edfe261 100644 --- a/DynamicIsland/components/LockScreen/LockScreenWeatherWidget.swift +++ b/DynamicIsland/components/LockScreen/LockScreenWeatherWidget.swift @@ -1,8 +1,13 @@ import SwiftUI +import Defaults struct LockScreenWeatherWidget: View { let snapshot: LockScreenWeatherSnapshot + @ObservedObject private var focusManager = DoNotDisturbManager.shared + @Default(.enableDoNotDisturbDetection) private var focusDetectionEnabled + @Default(.showDoNotDisturbIndicator) private var focusIndicatorEnabled + @Default(.enableLockScreenFocusWidget) private var lockScreenFocusWidgetEnabled private let inlinePrimaryFont = Font.system(size: 22, weight: .semibold, design: .rounded) private let inlineSecondaryFont = Font.system(size: 13, weight: .medium, design: .rounded) @@ -20,6 +25,24 @@ struct LockScreenWeatherWidget: View { } var body: some View { + VStack(alignment: .leading, spacing: focusWidgetSpacing) { + if shouldShowFocusWidget { + focusWidget + } + mainWidgetRow + } + .frame(maxWidth: .infinity, alignment: .leading) + .foregroundStyle(Color.white.opacity(0.65)) + .padding(.horizontal, 10) + .padding(.top, topPadding) + .padding(.bottom, bottomPadding) + .background(Color.clear) + .shadow(color: .black.opacity(0.35), radius: 8, x: 0, y: 3) + .accessibilityElement(children: .ignore) + .accessibilityLabel(accessibilityLabel) + } + + private var mainWidgetRow: some View { HStack(alignment: stackAlignment, spacing: stackSpacing) { if let charging = snapshot.charging { chargingSegment(for: charging) @@ -43,15 +66,6 @@ struct LockScreenWeatherWidget: View { locationSegment } } - .frame(maxWidth: .infinity, alignment: .leading) - .foregroundStyle(Color.white.opacity(0.65)) - .padding(.horizontal, 10) - .padding(.top, topPadding) - .padding(.bottom, bottomPadding) - .background(Color.clear) - .shadow(color: .black.opacity(0.35), radius: 8, x: 0, y: 3) - .accessibilityElement(children: .ignore) - .accessibilityLabel(accessibilityLabel) } @ViewBuilder @@ -336,6 +350,61 @@ struct LockScreenWeatherWidget: View { snapshot.showsLocation && (snapshot.locationName?.isEmpty == false) } + private var focusWidgetSpacing: CGFloat { + guard shouldShowFocusWidget else { return 0 } + return isInline ? 14 : 20 + } + + private var shouldShowFocusWidget: Bool { + lockScreenFocusWidgetEnabled && + focusDetectionEnabled && + focusIndicatorEnabled && + focusManager.isDoNotDisturbActive && + !focusDisplayName.isEmpty + } + + private var focusDisplayName: String { + let trimmed = focusManager.currentFocusModeName.trimmingCharacters(in: .whitespacesAndNewlines) + if !trimmed.isEmpty { + return trimmed + } + if focusMode == .doNotDisturb { + return "Do Not Disturb" + } + let fallback = focusMode.displayName + return fallback.isEmpty ? "Focus" : fallback + } + + private var focusMode: FocusModeType { + FocusModeType.resolve( + identifier: focusManager.currentFocusModeIdentifier, + name: focusManager.currentFocusModeName + ) + } + + private var focusIcon: Image { + focusMode + .resolvedActiveIcon(usePrivateSymbol: true) + .renderingMode(.template) + } + + private var focusWidget: some View { + HStack(alignment: .center, spacing: 8) { + focusIcon + .font(.system(size: 20, weight: .semibold)) + .frame(width: 26, height: 26) + + Text(focusDisplayName) + .font(inlinePrimaryFont) + .lineLimit(1) + .minimumScaleFactor(0.85) + } + .frame(maxWidth: .infinity, alignment: .center) + .multilineTextAlignment(.center) + .padding(.horizontal, 2) + .accessibilityLabel("Focus active: \(focusDisplayName)") + } + private func chargingIconName(for info: LockScreenWeatherSnapshot.ChargingInfo) -> String? { let icon = info.iconName return icon.isEmpty ? nil : icon @@ -549,6 +618,10 @@ struct LockScreenWeatherWidget: View { components.append(accessibilityBatteryText(for: battery)) } + if shouldShowFocusWidget { + components.append("Focus active: \(focusDisplayName)") + } + return components.joined(separator: ". ") } diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 94d4a5c..c3808a4 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -2583,6 +2583,7 @@ struct LockScreenSettings: View { @Default(.enableLockScreenMediaWidget) private var enableLockScreenMediaWidget @Default(.enableLockScreenTimerWidget) private var enableLockScreenTimerWidget @Default(.enableLockScreenWeatherWidget) private var enableLockScreenWeatherWidget + @Default(.enableLockScreenFocusWidget) private var enableLockScreenFocusWidget @Default(.lockScreenWeatherWidgetStyle) private var lockScreenWeatherWidgetStyle @Default(.lockScreenWeatherProviderSource) private var lockScreenWeatherProviderSource @Default(.lockScreenWeatherTemperatureUnit) private var lockScreenWeatherTemperatureUnit @@ -2738,6 +2739,15 @@ struct LockScreenSettings: View { Text("Enable the weather capsule and configure its layout, provider, units, and optional battery/AQI indicators.") } + Section { + Defaults.Toggle("Show focus widget", key: .enableLockScreenFocusWidget) + .settingsHighlight(id: highlightID("Show focus widget")) + } header: { + Text("Focus Widget") + } footer: { + Text("Displays the current Focus state above the weather capsule whenever Focus detection is enabled.") + } + LockScreenPositioningControls() Section { diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index e696e87..2320c2e 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -452,6 +452,7 @@ extension Defaults.Keys { // Enable lock screen media widget (shows the standalone panel when screen is locked) static let enableLockScreenMediaWidget = Key("enableLockScreenMediaWidget", default: true) static let enableLockScreenWeatherWidget = Key("enableLockScreenWeatherWidget", default: true) + static let enableLockScreenFocusWidget = Key("enableLockScreenFocusWidget", default: true) static let enableLockScreenReminderWidget = Key("enableLockScreenReminderWidget", default: true) static let enableLockScreenTimerWidget = Key("enableLockScreenTimerWidget", default: true) static let lockScreenWeatherRefreshInterval = Key("lockScreenWeatherRefreshInterval", default: 30 * 60)