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()
} 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)

View file

@ -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" : {

View file

@ -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
}
}
}
}
}

View file

@ -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 {

View file

@ -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()

View file

@ -432,6 +432,10 @@ extension Defaults.Keys {
// Polling removed - now uses event-driven private API detection (CGSIsScreenWatcherPresent)
// 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
static let didClearLegacyURLCacheV1 = Key<Bool>("didClearLegacyURLCacheV1", default: false)