Final Fix: Working with Animation

This commit is contained in:
Hariharan Mudaliar 2025-10-15 23:55:50 +05:30
commit 2ece36fe09
6 changed files with 158 additions and 74 deletions

View file

@ -422,7 +422,7 @@ struct ContentView: View {
TimerLiveActivity() TimerLiveActivity()
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .recording) && vm.notchState == .closed && (recordingManager.isRecording || !recordingManager.isRecorderIdle) && Defaults[.enableScreenRecordingDetection] && !vm.hideOnClosed { } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .recording) && vm.notchState == .closed && (recordingManager.isRecording || !recordingManager.isRecorderIdle) && Defaults[.enableScreenRecordingDetection] && !vm.hideOnClosed {
RecordingLiveActivity() 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() PrivacyLiveActivity()
} else if !coordinator.expandingView.show && vm.notchState == .closed && (!musicManager.isPlaying && musicManager.isPlayerIdle) && Defaults[.showNotHumanFace] && !vm.hideOnClosed { } else if !coordinator.expandingView.show && vm.notchState == .closed && (!musicManager.isPlaying && musicManager.isPlayerIdle) && Defaults[.showNotHumanFace] && !vm.hideOnClosed {
DynamicIslandFaceAnimation().animation(.interactiveSpring, value: musicManager.isPlayerIdle) DynamicIslandFaceAnimation().animation(.interactiveSpring, value: musicManager.isPlayerIdle)

View file

@ -3792,6 +3792,12 @@
"Camera" : { "Camera" : {
"comment" : "A label for the camera privacy indicator.", "comment" : "A label for the camera privacy indicator.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
},
"Camera Active" : {
},
"Camera Status" : {
}, },
"Cancel" : { "Cancel" : {
"localizations" : { "localizations" : {
@ -8847,6 +8853,9 @@
} }
} }
} }
},
"Inactive" : {
}, },
"Inline" : { "Inline" : {
"localizations" : { "localizations" : {
@ -10799,6 +10808,12 @@
"Microphone" : { "Microphone" : {
"comment" : "A label displayed next to the microphone icon in the privacy indicator detail view.", "comment" : "A label displayed next to the microphone icon in the privacy indicator detail view.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
},
"Microphone Active" : {
},
"Microphone Status" : {
}, },
"min" : { "min" : {
@ -13097,10 +13112,6 @@
}, },
"Preview Zoom:" : { "Preview Zoom:" : {
},
"Privacy & Security" : {
"comment" : "The title of the privacy and security settings screen.",
"isCommentAutoGenerated" : true
}, },
"Privacy Indicators" : { "Privacy Indicators" : {
"comment" : "A heading for the section that lists privacy indicators.", "comment" : "A heading for the section that lists privacy indicators.",
@ -13908,9 +13919,8 @@
"Screen Assistant:" : { "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." : { "Screen Recording Detection" : {
"comment" : "A footer description for the \"Privacy & Security\" section of the settings.",
"isCommentAutoGenerated" : true
}, },
"Screenshot options" : { "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" : { "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" : { "Version" : {
"localizations" : { "localizations" : {

View file

@ -16,56 +16,76 @@ struct PrivacyLiveActivity: View {
@State private var gestureProgress: CGFloat = 0 @State private var gestureProgress: CGFloat = 0
@State private var isExpanded: Bool = false @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 { var body: some View {
HStack(spacing: 0) { 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 Color.clear
.background { .background {
if recordingManager.isRecording && isExpanded { if isExpanded {
HStack { HStack {
ZStack { if recordingManager.isRecording {
RoundedRectangle(cornerRadius: 6) // Recording pulsator
.fill(Color.red.opacity(0.15)) ZStack {
RoundedRectangle(cornerRadius: 6)
Circle() .fill(Color.red.opacity(0.15))
.fill(Color.red)
.frame(width: 10, height: 10) Circle()
.modifier(PulsingModifier()) .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(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 // Center - Black fill
Rectangle() Rectangle()
.fill(.black) .fill(.black)
.frame(width: vm.closedNotchSize.width + (isHovering ? 8 : 0)) .frame(width: vm.closedNotchSize.width + (isHovering ? 8 : 0))
// Right side - Privacy indicators (camera and/or microphone) // Right side - Privacy indicators
Color.clear Color.clear
.background { .background {
if isExpanded { if isExpanded {
HStack(spacing: 8) { HStack(spacing: 4) {
// Microphone indicator (shows first if both active) // When both active without recording: only camera on right (mic is on left)
if privacyManager.indicatorLayout.showsMicrophoneIndicator { // When recording + both: show both on right
PrivacyIcon(type: .microphone) // When only one active: show that one
.transition(.scale.combined(with: .opacity))
}
// Camera indicator if bothIndicatorsActive && recordingManager.isRecording {
if privacyManager.indicatorLayout.showsCameraIndicator { // Both indicators on right when recording
PrivacyIcon(type: .microphone)
PrivacyIcon(type: .camera) 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(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)) .frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0))
.onAppear { .onAppear {
@ -92,40 +112,23 @@ struct PrivacyIcon: View {
let type: PrivacyIndicatorType let type: PrivacyIndicatorType
@State private var isPulsing = false @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 { var body: some View {
ZStack { // Simple icon without blur/glow effects
// Glow effect Image(systemName: type.icon)
Circle() .font(.system(size: 13, weight: .semibold))
.fill( .foregroundColor(iconColor)
RadialGradient( .frame(width: 24, height: 24)
colors: [ .scaleEffect(isPulsing ? 1.1 : 1.0)
Color.orange.opacity(0.4), .opacity(isPulsing ? 1.0 : 0.85)
Color.orange.opacity(0.2), .onAppear {
Color.clear withAnimation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true)) {
], isPulsing = true
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
} }
}
} }
} }

View file

@ -157,6 +157,7 @@ struct GeneralSettings: View {
@EnvironmentObject var vm: DynamicIslandViewModel @EnvironmentObject var vm: DynamicIslandViewModel
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
@ObservedObject var recordingManager = ScreenRecordingManager.shared @ObservedObject var recordingManager = ScreenRecordingManager.shared
@ObservedObject var privacyManager = PrivacyIndicatorManager.shared
@Default(.mirrorShape) var mirrorShape @Default(.mirrorShape) var mirrorShape
@Default(.showEmojis) var showEmojis @Default(.showEmojis) var showEmojis
@ -245,9 +246,54 @@ struct GeneralSettings: View {
} }
} }
} header: { } header: {
Text("Privacy & Security") Text("Screen Recording Detection")
} footer: { } 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 { Section {

View file

@ -9,6 +9,7 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import Combine import Combine
import Defaults
// MARK: - Indicator Layout Enum // MARK: - Indicator Layout Enum
enum IndicatorLayout { enum IndicatorLayout {
@ -74,6 +75,7 @@ class PrivacyIndicatorManager: ObservableObject {
@Published var cameraActive: Bool = false @Published var cameraActive: Bool = false
@Published var microphoneActive: Bool = false @Published var microphoneActive: Bool = false
@Published var screenRecordingActive: Bool = false @Published var screenRecordingActive: Bool = false
@Published var isMonitoring: Bool = false
// MARK: - Child Monitors // MARK: - Child Monitors
private let cameraMonitor = CameraMonitor() private let cameraMonitor = CameraMonitor()
@ -87,8 +89,9 @@ class PrivacyIndicatorManager: ObservableObject {
/// Current indicator layout based on active states /// Current indicator layout based on active states
var indicatorLayout: IndicatorLayout { var indicatorLayout: IndicatorLayout {
let camera = cameraActive // Respect user settings
let mic = microphoneActive let camera = cameraActive && Defaults[.enableCameraDetection]
let mic = microphoneActive && Defaults[.enableMicrophoneDetection]
let recording = screenRecordingActive let recording = screenRecordingActive
// 8 possible combinations // 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 { var hasAnyIndicator: Bool {
return cameraActive || microphoneActive || screenRecordingActive let showCamera = cameraActive && Defaults[.enableCameraDetection]
let showMic = microphoneActive && Defaults[.enableMicrophoneDetection]
return showCamera || showMic || screenRecordingActive
} }
// MARK: - Initialization // MARK: - Initialization
@ -134,7 +139,9 @@ class PrivacyIndicatorManager: ObservableObject {
guard let self = self else { return } guard let self = self else { return }
if self.cameraActive != isActive { if self.cameraActive != isActive {
print("PrivacyIndicatorManager: 📷 Camera state: \(isActive)") print("PrivacyIndicatorManager: 📷 Camera state: \(isActive)")
self.cameraActive = isActive withAnimation(.smooth) {
self.cameraActive = isActive
}
self.logLayoutChange() self.logLayoutChange()
} }
} }
@ -147,7 +154,9 @@ class PrivacyIndicatorManager: ObservableObject {
guard let self = self else { return } guard let self = self else { return }
if self.microphoneActive != isActive { if self.microphoneActive != isActive {
print("PrivacyIndicatorManager: 🎤 Microphone state: \(isActive)") print("PrivacyIndicatorManager: 🎤 Microphone state: \(isActive)")
self.microphoneActive = isActive withAnimation(.smooth) {
self.microphoneActive = isActive
}
self.logLayoutChange() self.logLayoutChange()
} }
} }
@ -163,7 +172,9 @@ class PrivacyIndicatorManager: ObservableObject {
guard let self = self else { return } guard let self = self else { return }
if self.screenRecordingActive != isRecording { if self.screenRecordingActive != isRecording {
print("PrivacyIndicatorManager: 📹 Screen recording state: \(isRecording)") print("PrivacyIndicatorManager: 📹 Screen recording state: \(isRecording)")
self.screenRecordingActive = isRecording withAnimation(.smooth) {
self.screenRecordingActive = isRecording
}
self.logLayoutChange() self.logLayoutChange()
} }
} }
@ -182,6 +193,8 @@ class PrivacyIndicatorManager: ObservableObject {
func startMonitoring() { func startMonitoring() {
print("PrivacyIndicatorManager: 🟢 Starting all monitors...") print("PrivacyIndicatorManager: 🟢 Starting all monitors...")
isMonitoring = true
// Start camera monitoring // Start camera monitoring
if cameraMonitor.isMonitoringAvailable { if cameraMonitor.isMonitoringAvailable {
cameraMonitor.startMonitoring() cameraMonitor.startMonitoring()
@ -204,6 +217,8 @@ class PrivacyIndicatorManager: ObservableObject {
func stopMonitoring() { func stopMonitoring() {
print("PrivacyIndicatorManager: 🛑 Stopping all monitors...") print("PrivacyIndicatorManager: 🛑 Stopping all monitors...")
isMonitoring = false
cameraMonitor.stopMonitoring() cameraMonitor.stopMonitoring()
microphoneMonitor.stopMonitoring() microphoneMonitor.stopMonitoring()

View file

@ -432,6 +432,10 @@ extension Defaults.Keys {
// Polling removed - now uses event-driven private API detection (CGSIsScreenWatcherPresent) // Polling removed - now uses event-driven private API detection (CGSIsScreenWatcherPresent)
// static let enableScreenRecordingPolling = Key<Bool>("enableScreenRecordingPolling", default: false) // static let enableScreenRecordingPolling = Key<Bool>("enableScreenRecordingPolling", default: false)
// MARK: Privacy Indicators (Camera & Microphone Detection)
static let enableCameraDetection = Key<Bool>("enableCameraDetection", default: true)
static let enableMicrophoneDetection = Key<Bool>("enableMicrophoneDetection", default: true)
// MARK: ImageService // MARK: ImageService
static let didClearLegacyURLCacheV1 = Key<Bool>("didClearLegacyURLCacheV1", default: false) static let didClearLegacyURLCacheV1 = Key<Bool>("didClearLegacyURLCacheV1", default: false)