From 2ece36fe09cdceed4868238a0e5e662e8cd29472 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Wed, 15 Oct 2025 23:55:50 +0530 Subject: [PATCH] Final Fix: Working with Animation --- DynamicIsland/ContentView.swift | 2 +- DynamicIsland/Localizable.xcstrings | 30 +++-- .../Privacy/PrivacyLiveActivity.swift | 117 +++++++++--------- .../components/Settings/SettingsView.swift | 50 +++++++- .../managers/PrivacyIndicatorManager.swift | 29 +++-- DynamicIsland/models/Constants.swift | 4 + 6 files changed, 158 insertions(+), 74 deletions(-) diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 8b1d5cb..aad7e35 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -422,7 +422,7 @@ struct ContentView: View { TimerLiveActivity() } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .recording) && vm.notchState == .closed && (recordingManager.isRecording || !recordingManager.isRecorderIdle) && Defaults[.enableScreenRecordingDetection] && !vm.hideOnClosed { RecordingLiveActivity() - } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .privacy) && vm.notchState == .closed && privacyManager.hasAnyIndicator && !vm.hideOnClosed { + } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .privacy) && vm.notchState == .closed && privacyManager.hasAnyIndicator && (Defaults[.enableCameraDetection] || Defaults[.enableMicrophoneDetection]) && !vm.hideOnClosed { PrivacyLiveActivity() } else if !coordinator.expandingView.show && vm.notchState == .closed && (!musicManager.isPlaying && musicManager.isPlayerIdle) && Defaults[.showNotHumanFace] && !vm.hideOnClosed { DynamicIslandFaceAnimation().animation(.interactiveSpring, value: musicManager.isPlayerIdle) diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 5d07b92..6cad043 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -3792,6 +3792,12 @@ "Camera" : { "comment" : "A label for the camera privacy indicator.", "isCommentAutoGenerated" : true + }, + "Camera Active" : { + + }, + "Camera Status" : { + }, "Cancel" : { "localizations" : { @@ -8847,6 +8853,9 @@ } } } + }, + "Inactive" : { + }, "Inline" : { "localizations" : { @@ -10799,6 +10808,12 @@ "Microphone" : { "comment" : "A label displayed next to the microphone icon in the privacy indicator detail view.", "isCommentAutoGenerated" : true + }, + "Microphone Active" : { + + }, + "Microphone Status" : { + }, "min" : { @@ -13097,10 +13112,6 @@ }, "Preview Zoom:" : { - }, - "Privacy & Security" : { - "comment" : "The title of the privacy and security settings screen.", - "isCommentAutoGenerated" : true }, "Privacy Indicators" : { "comment" : "A heading for the section that lists privacy indicators.", @@ -13908,9 +13919,8 @@ "Screen Assistant:" : { }, - "Screen recording detection shows a red indicator when your screen is being captured. Detection uses minimal system resources and can be disabled at any time." : { - "comment" : "A footer description for the \"Privacy & Security\" section of the settings.", - "isCommentAutoGenerated" : true + "Screen Recording Detection" : { + }, "Screenshot options" : { @@ -15096,6 +15106,9 @@ } } } + }, + "Shows green camera icon and yellow microphone icon when in use. Uses event-driven CoreAudio and CoreMediaIO APIs." : { + }, "Shows the model's reasoning process before the final answer" : { @@ -17031,6 +17044,9 @@ } } } + }, + "Uses event-driven private API for real-time screen recording detection" : { + }, "Version" : { "localizations" : { diff --git a/DynamicIsland/components/Privacy/PrivacyLiveActivity.swift b/DynamicIsland/components/Privacy/PrivacyLiveActivity.swift index c2a3785..380c6f6 100644 --- a/DynamicIsland/components/Privacy/PrivacyLiveActivity.swift +++ b/DynamicIsland/components/Privacy/PrivacyLiveActivity.swift @@ -16,56 +16,76 @@ struct PrivacyLiveActivity: View { @State private var gestureProgress: CGFloat = 0 @State private var isExpanded: Bool = false + // Calculate if both camera and mic are active + private var bothIndicatorsActive: Bool { + privacyManager.indicatorLayout.showsCameraIndicator && + privacyManager.indicatorLayout.showsMicrophoneIndicator + } + var body: some View { HStack(spacing: 0) { - // Left side - Recording pulsator (if recording is active) + // Left side - Recording pulsator OR microphone (if both indicators active without recording) Color.clear .background { - if recordingManager.isRecording && isExpanded { + if isExpanded { HStack { - ZStack { - RoundedRectangle(cornerRadius: 6) - .fill(Color.red.opacity(0.15)) - - Circle() - .fill(Color.red) - .frame(width: 10, height: 10) - .modifier(PulsingModifier()) + if recordingManager.isRecording { + // Recording pulsator + ZStack { + RoundedRectangle(cornerRadius: 6) + .fill(Color.red.opacity(0.15)) + + Circle() + .fill(Color.red) + .frame(width: 10, height: 10) + .modifier(PulsingModifier()) + } + .frame(width: vm.effectiveClosedNotchHeight - 12, height: vm.effectiveClosedNotchHeight - 12) + } else if bothIndicatorsActive { + // Microphone on left when both active and no recording + PrivacyIcon(type: .microphone) } - .frame(width: vm.effectiveClosedNotchHeight - 12, height: vm.effectiveClosedNotchHeight - 12) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) } } - .frame(width: recordingManager.isRecording && isExpanded ? max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12) + gestureProgress / 2) : 0, height: vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)) + .frame(width: isExpanded && (recordingManager.isRecording || bothIndicatorsActive) ? max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12) + gestureProgress / 2 + 20) : 0, height: vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)) // Center - Black fill Rectangle() .fill(.black) .frame(width: vm.closedNotchSize.width + (isHovering ? 8 : 0)) - // Right side - Privacy indicators (camera and/or microphone) + // Right side - Privacy indicators Color.clear .background { if isExpanded { - HStack(spacing: 8) { - // Microphone indicator (shows first if both active) - if privacyManager.indicatorLayout.showsMicrophoneIndicator { - PrivacyIcon(type: .microphone) - .transition(.scale.combined(with: .opacity)) - } + HStack(spacing: 4) { + // When both active without recording: only camera on right (mic is on left) + // When recording + both: show both on right + // When only one active: show that one - // Camera indicator - if privacyManager.indicatorLayout.showsCameraIndicator { + if bothIndicatorsActive && recordingManager.isRecording { + // Both indicators on right when recording + PrivacyIcon(type: .microphone) PrivacyIcon(type: .camera) - .transition(.scale.combined(with: .opacity)) + } else if bothIndicatorsActive { + // Only camera on right when both active (mic is on left) + PrivacyIcon(type: .camera) + } else { + // Single indicator + if privacyManager.indicatorLayout.showsMicrophoneIndicator { + PrivacyIcon(type: .microphone) + } + if privacyManager.indicatorLayout.showsCameraIndicator { + PrivacyIcon(type: .camera) + } } } - .padding(.trailing, 8) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing) } } - .frame(width: isExpanded ? max(0, vm.effectiveClosedNotchHeight + 30 + gestureProgress / 2) : 0, height: vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)) + .frame(width: isExpanded ? max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12) + gestureProgress / 2 + 20) : 0, height: vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)) } .frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0)) .onAppear { @@ -92,40 +112,23 @@ struct PrivacyIcon: View { let type: PrivacyIndicatorType @State private var isPulsing = false + // Color based on type: camera = #26CC41, mic = #FF9402 + private var iconColor: Color { + type == .camera ? Color(red: 0.152, green: 0.804, blue: 0.256) : Color(red: 1.000, green: 0.584, blue: 0.010) + } + var body: some View { - ZStack { - // Glow effect - Circle() - .fill( - RadialGradient( - colors: [ - Color.orange.opacity(0.4), - Color.orange.opacity(0.2), - Color.clear - ], - center: .center, - startRadius: 0, - endRadius: 10 - ) - ) - .frame(width: 20, height: 20) - .opacity(isPulsing ? 0.8 : 0.5) - - // Icon background - Circle() - .fill(Color.orange.opacity(0.2)) - .frame(width: 16, height: 16) - - // Icon - Image(systemName: type.icon) - .font(.system(size: 8, weight: .semibold)) - .foregroundColor(.orange) - } - .frame(width: 20, height: 20) - .onAppear { - withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) { - isPulsing = true + // Simple icon without blur/glow effects + Image(systemName: type.icon) + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(iconColor) + .frame(width: 24, height: 24) + .scaleEffect(isPulsing ? 1.1 : 1.0) + .opacity(isPulsing ? 1.0 : 0.85) + .onAppear { + withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) { + isPulsing = true + } } - } } } diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index c03aae3..0fcc1a4 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -157,6 +157,7 @@ struct GeneralSettings: View { @EnvironmentObject var vm: DynamicIslandViewModel @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared @ObservedObject var recordingManager = ScreenRecordingManager.shared + @ObservedObject var privacyManager = PrivacyIndicatorManager.shared @Default(.mirrorShape) var mirrorShape @Default(.showEmojis) var showEmojis @@ -245,9 +246,54 @@ struct GeneralSettings: View { } } } header: { - Text("Privacy & Security") + Text("Screen Recording Detection") } footer: { - Text("Screen recording detection shows a red indicator when your screen is being captured. Detection uses minimal system resources and can be disabled at any time.") + Text("Uses event-driven private API for real-time screen recording detection") + } + + Section { + Defaults.Toggle("Enable Camera Detection", key: .enableCameraDetection) + Defaults.Toggle("Enable Microphone Detection", key: .enableMicrophoneDetection) + + if privacyManager.isMonitoring { + HStack { + Text("Camera Status") + Spacer() + if privacyManager.cameraActive { + HStack(spacing: 4) { + Circle() + .fill(Color.green) + .frame(width: 8, height: 8) + Text("Camera Active") + .foregroundColor(.green) + } + } else { + Text("Inactive") + .foregroundColor(.secondary) + } + } + + HStack { + Text("Microphone Status") + Spacer() + if privacyManager.microphoneActive { + HStack(spacing: 4) { + Circle() + .fill(Color.yellow) + .frame(width: 8, height: 8) + Text("Microphone Active") + .foregroundColor(.yellow) + } + } else { + Text("Inactive") + .foregroundColor(.secondary) + } + } + } + } header: { + Text("Privacy Indicators") + } footer: { + Text("Shows green camera icon and yellow microphone icon when in use. Uses event-driven CoreAudio and CoreMediaIO APIs.") } Section { diff --git a/DynamicIsland/managers/PrivacyIndicatorManager.swift b/DynamicIsland/managers/PrivacyIndicatorManager.swift index a02fb9a..0367481 100644 --- a/DynamicIsland/managers/PrivacyIndicatorManager.swift +++ b/DynamicIsland/managers/PrivacyIndicatorManager.swift @@ -9,6 +9,7 @@ import Foundation import SwiftUI import Combine +import Defaults // MARK: - Indicator Layout Enum enum IndicatorLayout { @@ -74,6 +75,7 @@ class PrivacyIndicatorManager: ObservableObject { @Published var cameraActive: Bool = false @Published var microphoneActive: Bool = false @Published var screenRecordingActive: Bool = false + @Published var isMonitoring: Bool = false // MARK: - Child Monitors private let cameraMonitor = CameraMonitor() @@ -87,8 +89,9 @@ class PrivacyIndicatorManager: ObservableObject { /// Current indicator layout based on active states var indicatorLayout: IndicatorLayout { - let camera = cameraActive - let mic = microphoneActive + // Respect user settings + let camera = cameraActive && Defaults[.enableCameraDetection] + let mic = microphoneActive && Defaults[.enableMicrophoneDetection] let recording = screenRecordingActive // 8 possible combinations @@ -112,9 +115,11 @@ class PrivacyIndicatorManager: ObservableObject { } } - /// Check if any indicator is active + /// Check if any indicator is active (respecting user settings) var hasAnyIndicator: Bool { - return cameraActive || microphoneActive || screenRecordingActive + let showCamera = cameraActive && Defaults[.enableCameraDetection] + let showMic = microphoneActive && Defaults[.enableMicrophoneDetection] + return showCamera || showMic || screenRecordingActive } // MARK: - Initialization @@ -134,7 +139,9 @@ class PrivacyIndicatorManager: ObservableObject { guard let self = self else { return } if self.cameraActive != isActive { print("PrivacyIndicatorManager: 📷 Camera state: \(isActive)") - self.cameraActive = isActive + withAnimation(.smooth) { + self.cameraActive = isActive + } self.logLayoutChange() } } @@ -147,7 +154,9 @@ class PrivacyIndicatorManager: ObservableObject { guard let self = self else { return } if self.microphoneActive != isActive { print("PrivacyIndicatorManager: 🎤 Microphone state: \(isActive)") - self.microphoneActive = isActive + withAnimation(.smooth) { + self.microphoneActive = isActive + } self.logLayoutChange() } } @@ -163,7 +172,9 @@ class PrivacyIndicatorManager: ObservableObject { guard let self = self else { return } if self.screenRecordingActive != isRecording { print("PrivacyIndicatorManager: 📹 Screen recording state: \(isRecording)") - self.screenRecordingActive = isRecording + withAnimation(.smooth) { + self.screenRecordingActive = isRecording + } self.logLayoutChange() } } @@ -182,6 +193,8 @@ class PrivacyIndicatorManager: ObservableObject { func startMonitoring() { print("PrivacyIndicatorManager: 🟢 Starting all monitors...") + isMonitoring = true + // Start camera monitoring if cameraMonitor.isMonitoringAvailable { cameraMonitor.startMonitoring() @@ -204,6 +217,8 @@ class PrivacyIndicatorManager: ObservableObject { func stopMonitoring() { print("PrivacyIndicatorManager: 🛑 Stopping all monitors...") + isMonitoring = false + cameraMonitor.stopMonitoring() microphoneMonitor.stopMonitoring() diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 7539643..4d5c6ff 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -432,6 +432,10 @@ extension Defaults.Keys { // Polling removed - now uses event-driven private API detection (CGSIsScreenWatcherPresent) // static let enableScreenRecordingPolling = Key("enableScreenRecordingPolling", default: false) + // MARK: Privacy Indicators (Camera & Microphone Detection) + static let enableCameraDetection = Key("enableCameraDetection", default: true) + static let enableMicrophoneDetection = Key("enableMicrophoneDetection", default: true) + // MARK: ImageService static let didClearLegacyURLCacheV1 = Key("didClearLegacyURLCacheV1", default: false)