mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Final Fix: Working with Animation
This commit is contained in:
parent
357335d35e
commit
2ece36fe09
6 changed files with 158 additions and 74 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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" : {
|
||||
|
|
|
|||
|
|
@ -16,13 +16,21 @@ 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 {
|
||||
if recordingManager.isRecording {
|
||||
// Recording pulsator
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(Color.red.opacity(0.15))
|
||||
|
|
@ -33,39 +41,51 @@ struct PrivacyLiveActivity: View {
|
|||
.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(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)
|
||||
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
|
||||
|
||||
if bothIndicatorsActive && recordingManager.isRecording {
|
||||
// Both indicators on right when recording
|
||||
PrivacyIcon(type: .microphone)
|
||||
PrivacyIcon(type: .camera)
|
||||
} 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)
|
||||
.transition(.scale.combined(with: .opacity))
|
||||
}
|
||||
|
||||
// Camera indicator
|
||||
if privacyManager.indicatorLayout.showsCameraIndicator {
|
||||
PrivacyIcon(type: .camera)
|
||||
.transition(.scale.combined(with: .opacity))
|
||||
}
|
||||
}
|
||||
.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,36 +112,19 @@ struct PrivacyIcon: View {
|
|||
let type: PrivacyIndicatorType
|
||||
@State private var isPulsing = false
|
||||
|
||||
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)
|
||||
// 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)
|
||||
}
|
||||
.frame(width: 20, height: 20)
|
||||
|
||||
var body: some View {
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
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)")
|
||||
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)")
|
||||
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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue