From 4444df4cb4e4a8eb3d442e65d12638e53fc70f17 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Mon, 10 Nov 2025 03:00:10 +0530 Subject: [PATCH] Somewhat working --- .../Focus/DoNotDisturbLiveActivity.swift | 90 +++++++++----- .../components/UI/FocusIndicator.swift | 21 ++-- .../managers/DoNotDisturbManager.swift | 110 +++++++++++++++--- 3 files changed, 164 insertions(+), 57 deletions(-) diff --git a/DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift b/DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift index 6f1ffb0..aaeb179 100644 --- a/DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift +++ b/DynamicIsland/components/Focus/DoNotDisturbLiveActivity.swift @@ -20,6 +20,7 @@ struct DoNotDisturbLiveActivity: View { @State private var scaleResetTask: Task? @State private var collapseTask: Task? @State private var cleanupTask: Task? + @State private var labelIntrinsicWidth: CGFloat = 0 var body: some View { HStack(spacing: 0) { @@ -48,11 +49,24 @@ struct DoNotDisturbLiveActivity: View { } private var iconWingWidth: CGFloat { - (isExpanded || showInactiveIcon) ? max(vm.effectiveClosedNotchHeight - 12, 24) : 0 + (isExpanded || showInactiveIcon) ? minimalWingWidth : 0 } private var labelWingWidth: CGFloat { - shouldShowLabel ? max(vm.closedNotchSize.width * 0.45, 110) : 0 + if shouldShowLabel { + return max(desiredLabelWidth, minimalWingWidth) + } + return (isExpanded || showInactiveIcon) ? minimalWingWidth : 0 + } + + private var minimalWingWidth: CGFloat { + max(vm.effectiveClosedNotchHeight - 12, 24) + } + + private var desiredLabelWidth: CGFloat { + let measuredWidth = labelIntrinsicWidth + 8 // horizontal padding inside the label + let fallbackWidth = max(vm.closedNotchSize.width * 0.52, 136) + return max(measuredWidth, fallbackWidth) } private var shouldShowLabel: Bool { @@ -62,7 +76,10 @@ struct DoNotDisturbLiveActivity: View { // MARK: - Focus metadata private var focusMode: FocusModeType { - FocusModeType(rawValue: manager.currentFocusModeIdentifier) ?? .doNotDisturb + FocusModeType.resolve( + identifier: manager.currentFocusModeIdentifier, + name: manager.currentFocusModeName + ) } private var activeAccentColor: Color { @@ -71,7 +88,12 @@ struct DoNotDisturbLiveActivity: View { private var labelText: String { let trimmed = manager.currentFocusModeName.trimmingCharacters(in: .whitespacesAndNewlines) - return trimmed.isEmpty ? "Focus" : trimmed + if !trimmed.isEmpty { + return trimmed + } + + let fallback = focusMode.displayName + return fallback.isEmpty ? "Focus" : fallback } private var accessibilityDescription: String { @@ -102,31 +124,18 @@ struct DoNotDisturbLiveActivity: View { } } - private var iconBackground: Color { - if manager.isDoNotDisturbActive { - return activeAccentColor.opacity(0.18) - } else if showInactiveIcon { - return Color.white.opacity(0.10) - } else { - return Color.black.opacity(0.001) - } - } - // MARK: - Subviews private var iconWing: some View { Color.clear .overlay(alignment: .center) { if iconWingWidth > 0 { - RoundedRectangle(cornerRadius: 6, style: .continuous) - .fill(iconBackground) - .overlay { - Image(systemName: currentIconName) - .font(.system(size: 13, weight: .semibold)) - .foregroundColor(currentIconColor) - .scaleEffect(iconScale) - .animation(.none, value: iconScale) - } + Image(systemName: currentIconName) + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(currentIconColor) + .contentTransition(.opacity) + .scaleEffect(iconScale) + .animation(.none, value: iconScale) } } .animation(.smooth(duration: 0.3), value: iconWingWidth) @@ -141,10 +150,19 @@ struct DoNotDisturbLiveActivity: View { .foregroundColor(activeAccentColor) .lineLimit(1) .contentTransition(.opacity) + .background( + GeometryReader { proxy in + Color.clear + .preference(key: FocusLabelWidthPreferenceKey.self, value: proxy.size.width) + } + ) .padding(.horizontal, 4) } } .animation(.smooth(duration: 0.3), value: shouldShowLabel) + .onPreferenceChange(FocusLabelWidthPreferenceKey.self) { value in + labelIntrinsicWidth = value + } } // MARK: - State transitions @@ -160,7 +178,9 @@ struct DoNotDisturbLiveActivity: View { private func handleFocusStateChange(_ oldValue: Bool, _ isActive: Bool) { if isActive { cancelPendingTasks() - showInactiveIcon = false + withAnimation(.smooth(duration: 0.2)) { + showInactiveIcon = false + } withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) { iconScale = 1.0 isExpanded = true @@ -171,7 +191,9 @@ struct DoNotDisturbLiveActivity: View { } private func triggerInactiveAnimation() { - showInactiveIcon = true + withAnimation(.smooth(duration: 0.2)) { + showInactiveIcon = true + } withAnimation(.interpolatingSpring(stiffness: 220, damping: 12)) { iconScale = 1.2 @@ -179,9 +201,9 @@ struct DoNotDisturbLiveActivity: View { scaleResetTask?.cancel() scaleResetTask = Task { @MainActor in - try? await Task.sleep(for: .milliseconds(180)) - withAnimation(.spring(response: 0.28, dampingFraction: 0.75)) { - iconScale = 1.0 + try? await Task.sleep(for: .milliseconds(650)) + withAnimation(.smooth(duration: 0.2)) { + showInactiveIcon = false } } @@ -196,7 +218,9 @@ struct DoNotDisturbLiveActivity: View { cleanupTask?.cancel() cleanupTask = Task { @MainActor in try? await Task.sleep(for: .milliseconds(650)) - showInactiveIcon = false + withAnimation(.smooth(duration: 0.2)) { + showInactiveIcon = false + } } } @@ -216,3 +240,11 @@ struct DoNotDisturbLiveActivity: View { .frame(width: 320, height: 54) .background(Color.black) } + +private struct FocusLabelWidthPreferenceKey: PreferenceKey { + static var defaultValue: CGFloat = 0 + + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = max(value, nextValue()) + } +} diff --git a/DynamicIsland/components/UI/FocusIndicator.swift b/DynamicIsland/components/UI/FocusIndicator.swift index ec13c8a..4bcb5e8 100644 --- a/DynamicIsland/components/UI/FocusIndicator.swift +++ b/DynamicIsland/components/UI/FocusIndicator.swift @@ -14,22 +14,20 @@ struct FocusIndicator: View { Capsule() .fill(Color.black) .overlay { - ZStack { - Circle() - .fill(accentColor.opacity(0.2)) - .frame(width: 20, height: 20) - - Image(systemName: focusSymbol) - .font(.system(size: 12, weight: .semibold)) - .foregroundStyle(accentColor) - } + Image(systemName: focusSymbol) + .font(.system(size: 12, weight: .semibold)) + .foregroundStyle(accentColor) + .frame(width: 24, height: 24) } .accessibilityElement(children: .ignore) .accessibilityLabel(accessibilityLabel) } private var focusMode: FocusModeType { - FocusModeType(rawValue: manager.currentFocusModeIdentifier) ?? .doNotDisturb + FocusModeType.resolve( + identifier: manager.currentFocusModeIdentifier, + name: manager.currentFocusModeName + ) } private var focusSymbol: String { @@ -41,7 +39,8 @@ struct FocusIndicator: View { } private var accessibilityLabel: String { - let name = manager.currentFocusModeName.isEmpty ? focusMode.displayName : manager.currentFocusModeName + let trimmedName = manager.currentFocusModeName.trimmingCharacters(in: .whitespacesAndNewlines) + let name = trimmedName.isEmpty ? focusMode.displayName : trimmedName return "Focus active: \(name)" } } diff --git a/DynamicIsland/managers/DoNotDisturbManager.swift b/DynamicIsland/managers/DoNotDisturbManager.swift index 9087389..f659e93 100644 --- a/DynamicIsland/managers/DoNotDisturbManager.swift +++ b/DynamicIsland/managers/DoNotDisturbManager.swift @@ -82,19 +82,34 @@ final class DoNotDisturbManager: ObservableObject { let metadata = self.extractMetadata(from: notification) DispatchQueue.main.async { - if let identifier = metadata.identifier, identifier != self.currentFocusModeIdentifier { - self.currentFocusModeIdentifier = identifier + let trimmedIdentifier = metadata.identifier?.trimmingCharacters(in: .whitespacesAndNewlines) + let trimmedName = metadata.name?.trimmingCharacters(in: .whitespacesAndNewlines) + + let incomingIdentifier = (trimmedIdentifier?.isEmpty == false ? trimmedIdentifier! : self.currentFocusModeIdentifier) + let incomingName = (trimmedName?.isEmpty == false ? trimmedName! : self.currentFocusModeName) + + let resolvedMode = FocusModeType.resolve(identifier: incomingIdentifier, name: incomingName) + + debugPrint("[DoNotDisturbManager] Focus update -> identifier: \(trimmedIdentifier ?? "") (incoming: \(incomingIdentifier.isEmpty ? "" : incomingIdentifier)) | name: \(trimmedName ?? "") | resolved: \(resolvedMode.rawValue)") + + let finalIdentifier = resolvedMode.rawValue + if finalIdentifier != self.currentFocusModeIdentifier { + self.currentFocusModeIdentifier = finalIdentifier } - if let name = metadata.name { - if name != self.currentFocusModeName { - self.currentFocusModeName = name - } - } else if !self.currentFocusModeIdentifier.isEmpty { - let derivedName = FocusModeType(rawValue: self.currentFocusModeIdentifier)?.displayName ?? "" - if !derivedName.isEmpty { - self.currentFocusModeName = derivedName - } + let finalName: String + if let name = trimmedName, !name.isEmpty { + finalName = name + } else if !resolvedMode.displayName.isEmpty { + finalName = resolvedMode.displayName + } else if !incomingName.isEmpty { + finalName = incomingName + } else { + finalName = "Focus" + } + + if finalName != self.currentFocusModeName { + self.currentFocusModeName = finalName } guard self.isDoNotDisturbActive != isActive else { return } @@ -117,6 +132,10 @@ final class DoNotDisturbManager: ObservableObject { let identifierKeys = [ "FocusModeIdentifier", "focusModeIdentifier", + "FocusModeUUID", + "focusModeUUID", + "UUID", + "uuid", "identifier", "Identifier" ] @@ -124,12 +143,21 @@ final class DoNotDisturbManager: ObservableObject { let nameKeys = [ "FocusModeName", "focusModeName", + "FocusMode", + "focusMode", "name", "Name" ] - let identifier = identifierKeys.compactMap { userInfo[$0] as? String }.first - let name = nameKeys.compactMap { userInfo[$0] as? String }.first + let identifier = identifierKeys + .compactMap { userInfo[$0] as? String } + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .first { !$0.isEmpty } + + let name = nameKeys + .compactMap { userInfo[$0] as? String } + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .first { !$0.isEmpty } return (name, identifier) } @@ -143,7 +171,7 @@ private extension Notification.Name { // MARK: - Focus Mode Types -enum FocusModeType: String { +enum FocusModeType: String, CaseIterable { case doNotDisturb = "com.apple.donotdisturb.mode" case work = "com.apple.focus.work" case personal = "com.apple.focus.personal" @@ -158,7 +186,7 @@ enum FocusModeType: String { var displayName: String { switch self { - case .doNotDisturb: return "Do Not Disturb" + case .doNotDisturb: return "DnD" case .work: return "Work" case .personal: return "Personal" case .sleep: return "Sleep" @@ -184,7 +212,7 @@ enum FocusModeType: String { case .mindfulness: return "brain.head.profile" case .reading: return "book.fill" case .custom: return "app.badge" - case .unknown: return "moon.zzz.fill" + case .unknown: return "moon.fill" } } @@ -218,9 +246,57 @@ enum FocusModeType: String { var inactiveSymbol: String { switch self { case .doNotDisturb: - return "moon.circle" + return "moon.circle.fill" default: return sfSymbol } } } + +extension FocusModeType { + init(identifier: String) { + let normalized = identifier.trimmingCharacters(in: .whitespacesAndNewlines) + let normalizedLowercased = normalized.lowercased() + + guard !normalized.isEmpty else { + self = .doNotDisturb + return + } + + if let direct = FocusModeType(rawValue: normalized) ?? FocusModeType(rawValue: normalizedLowercased) { + self = direct + return + } + + if let resolved = FocusModeType.allCases.first(where: { + guard !$0.rawValue.isEmpty else { return false } + return normalized.hasPrefix($0.rawValue) || normalizedLowercased.hasPrefix($0.rawValue) + }) { + self = resolved + return + } + + if normalizedLowercased.hasPrefix("com.apple.focus.") { + self = .custom + return + } + + self = .doNotDisturb + } + + static func resolve(identifier: String?, name: String?) -> FocusModeType { + let trimmedIdentifier = identifier?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + if !trimmedIdentifier.isEmpty { + return FocusModeType(identifier: trimmedIdentifier) + } + + let trimmedName = name?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + if !trimmedName.isEmpty { + if let match = FocusModeType.allCases.first(where: { !$0.displayName.isEmpty && $0.displayName.compare(trimmedName, options: [.caseInsensitive, .diacriticInsensitive]) == .orderedSame }) { + return match + } + } + + return .doNotDisturb + } +}