From c60ec26bd249897cc016e5332c463278306c43e2 Mon Sep 17 00:00:00 2001 From: Axel Courty Date: Sun, 17 May 2026 19:21:06 +0200 Subject: [PATCH 1/2] Add Focus sneak peek on activation/deactivation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quand l'utilisateur active ou désactive un mode Focus (Travail, Sommeil, Ne pas déranger…), un peek 2.5s s'affiche sous l'encoche avec l'icône du mode + label « X activé » ou « Focus désactivé ». Comparable au HUD volume mais déclenché par changement d'état, pas par interaction utilisateur. Implémentation : - NotchIAViewCoordinator : `case focus` ajouté à SneakContentType, exempté du gate hudReplacement (c'est une notification d'état, pas un HUD) - FocusModeManager : observer `\$isActive.dropFirst().removeDuplicates()` qui déclenche `coordinator.toggleSneakPeek(type: .focus, duration: 2.5)` - ContentView : branche de rendu pour `.focus` — capsule noire stroke blanc avec icône + label, padding 10×4 - Localizable.xcstrings : « %@ activé » et « Focus désactivé » en EN/FR/ES/DE Le toggle existant `enableFocusModeDetection` gate aussi le peek (cohérent — si l'utilisateur a désactivé toute la détection Focus, pas de peek non plus). Version bump 2.8.0 → 2.8.1 (build 20801, formule monotone). Co-Authored-By: Claude Opus 4.7 --- NotchIA/ContentView.swift | 24 ++++++++++ NotchIA/Localizable.xcstrings | 58 ++++++++++++++++++++++++- NotchIA/NotchIAViewCoordinator.swift | 8 +++- NotchIA/managers/FocusModeManager.swift | 19 ++++++++ 4 files changed, 107 insertions(+), 2 deletions(-) diff --git a/NotchIA/ContentView.swift b/NotchIA/ContentView.swift index 58bd590..7622b94 100644 --- a/NotchIA/ContentView.swift +++ b/NotchIA/ContentView.swift @@ -607,6 +607,30 @@ struct ContentView: View { } .foregroundStyle(.gray) .padding(.bottom, 10) + } else if coordinator.sneakPeek.type == .focus { + // Peek 2.5s à l'activation/désactivation d'un Mode Focus. + // value == 1.0 → activé (icône Focus + label), value == 0 → désactivé. + HStack(spacing: 6) { + Image(systemName: coordinator.sneakPeek.icon.isEmpty ? "moon.fill" : coordinator.sneakPeek.icon) + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(coordinator.sneakPeek.value > 0 ? .purple : .gray) + Text( + coordinator.sneakPeek.value > 0 + ? String(localized: "\(focusModeManager.focusLabel) activé") + : String(localized: "Focus désactivé") + ) + .font(.system(size: 11, weight: .medium, design: .rounded)) + .foregroundStyle(.white.opacity(0.9)) + .lineLimit(1) + } + .padding(.horizontal, 10) + .padding(.vertical, 4) + .background( + Capsule() + .fill(Color.black) + .stroke(Color.white.opacity(0.1), lineWidth: 1) + ) + .padding(.bottom, 10) } } } diff --git a/NotchIA/Localizable.xcstrings b/NotchIA/Localizable.xcstrings index 9811dd6..b006f46 100644 --- a/NotchIA/Localizable.xcstrings +++ b/NotchIA/Localizable.xcstrings @@ -42695,7 +42695,63 @@ } } }, - "Σ": {} + "Σ": {}, + "%@ activé": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ on" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ activé" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ activado" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ aktiv" + } + } + } + }, + "Focus désactivé": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Focus off" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Focus désactivé" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Focus desactivado" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Focus aus" + } + } + } + } }, "version": "1.0" } diff --git a/NotchIA/NotchIAViewCoordinator.swift b/NotchIA/NotchIAViewCoordinator.swift index 8b500b2..5a2b98c 100644 --- a/NotchIA/NotchIAViewCoordinator.swift +++ b/NotchIA/NotchIAViewCoordinator.swift @@ -23,6 +23,9 @@ enum SneakContentType { case download case pomodoro case clipboard + /// Focus / Ne pas déranger activé ou désactivé. Déclenché par + /// `FocusModeManager` quand `isActive` change. Durée du peek 2.5s. + case focus } struct sneakPeek { @@ -251,7 +254,10 @@ class NotchIAViewCoordinator: ObservableObject { icon: String = "" ) { sneakPeekDuration = duration - if type != .music && type != .pomodoro && type != .screenRecording { + // Le peek `.focus` est une notification d'état (activation/désactivation + // d'un Mode Focus), pas un HUD de remplacement — il s'affiche peu importe + // le réglage hudReplacement, comme music/pomodoro/screenRecording. + if type != .music && type != .pomodoro && type != .screenRecording && type != .focus { // close() if !Defaults[.hudReplacement] { return diff --git a/NotchIA/managers/FocusModeManager.swift b/NotchIA/managers/FocusModeManager.swift index cab1282..517593a 100644 --- a/NotchIA/managers/FocusModeManager.swift +++ b/NotchIA/managers/FocusModeManager.swift @@ -136,6 +136,25 @@ final class FocusModeManager: ObservableObject { } } .store(in: &cancellables) + + // Peek 2.5s quand le mode Focus change (on/off). dropFirst() évite + // de fire au boot quand l'état initial est calculé. removeDuplicates + // dédoublonne les notifications privées qui se déclenchent en rafale. + $isActive + .dropFirst() + .removeDuplicates() + .sink { [weak self] active in + guard let self else { return } + guard Defaults[.enableFocusModeDetection] else { return } + NotchIAViewCoordinator.shared.toggleSneakPeek( + status: true, + type: .focus, + duration: 2.5, + value: active ? 1.0 : 0.0, + icon: active ? self.focusIcon : "moon.slash.fill" + ) + } + .store(in: &cancellables) } private func startMonitoring() { From e44b9f36d1e46e814b1064c2f30e75f50e23831c Mon Sep 17 00:00:00 2001 From: Anka Date: Sun, 17 May 2026 17:22:44 +0000 Subject: [PATCH 2/2] Set version to v2.8.1 (build 20801) --- NotchIA.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/NotchIA.xcodeproj/project.pbxproj b/NotchIA.xcodeproj/project.pbxproj index 31e0426..dd71f50 100644 --- a/NotchIA.xcodeproj/project.pbxproj +++ b/NotchIA.xcodeproj/project.pbxproj @@ -1277,7 +1277,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 20800; + CURRENT_PROJECT_VERSION = 20801; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1285,7 +1285,7 @@ INFOPLIST_KEY_CFBundleDisplayName = NotchIAXPCHelper; INFOPLIST_KEY_NSHumanReadableCopyright = ""; MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 2.8.0; + MARKETING_VERSION = 2.8.1; PRODUCT_BUNDLE_IDENTIFIER = com.coaxel2.notchia.NotchIAXPCHelper; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -1303,7 +1303,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 20800; + CURRENT_PROJECT_VERSION = 20801; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1311,7 +1311,7 @@ INFOPLIST_KEY_CFBundleDisplayName = NotchIAXPCHelper; INFOPLIST_KEY_NSHumanReadableCopyright = ""; MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 2.8.0; + MARKETING_VERSION = 2.8.1; PRODUCT_BUNDLE_IDENTIFIER = com.coaxel2.notchia.NotchIAXPCHelper; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -1462,7 +1462,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 20800; + CURRENT_PROJECT_VERSION = 20801; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"NotchIA/Preview Content\""; DEVELOPMENT_TEAM = ""; @@ -1490,7 +1490,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 2.8.0; + MARKETING_VERSION = 2.8.1; PRODUCT_BUNDLE_IDENTIFIER = com.coaxel2.notchia; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1518,7 +1518,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = YES; - CURRENT_PROJECT_VERSION = 20800; + CURRENT_PROJECT_VERSION = 20801; DEAD_CODE_STRIPPING = YES; DEPLOYMENT_POSTPROCESSING = YES; DEVELOPMENT_ASSET_PATHS = "\"NotchIA/Preview Content\""; @@ -1546,7 +1546,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 2.8.0; + MARKETING_VERSION = 2.8.1; PRODUCT_BUNDLE_IDENTIFIER = com.coaxel2.notchia; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "";