From c5186c47f8712bfd1daa8717bdee5b4e2c791410 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Thu, 2 Oct 2025 23:18:58 +0530 Subject: [PATCH] Still working on the janky animation --- DynamicIsland/ContentView.swift | 51 ++- DynamicIsland/DynamicIslandApp.swift | 14 + .../DynamicIslandViewCoordinator.swift | 1 + DynamicIsland/Localizable.xcstrings | 24 ++ .../LiveActivityModifier.swift | 1 + .../Notch/DynamicIslandHeader.swift | 7 + .../components/Settings/SettingsView.swift | 33 ++ .../components/UI/RecordingIndicator.swift | 172 ++++++++++ .../managers/ScreenRecordingManager.swift | 322 ++++++++++++++++++ DynamicIsland/models/Constants.swift | 4 + 10 files changed, 628 insertions(+), 1 deletion(-) create mode 100644 DynamicIsland/components/UI/RecordingIndicator.swift create mode 100644 DynamicIsland/managers/ScreenRecordingManager.swift diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 372f6bd..bc4ec20 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -22,6 +22,7 @@ struct ContentView: View { @ObservedObject var timerManager = TimerManager.shared @ObservedObject var batteryModel = BatteryStatusViewModel.shared @ObservedObject var statsManager = StatsManager.shared + @ObservedObject var recordingManager = ScreenRecordingManager.shared @Default(.enableStatsFeature) var enableStatsFeature @Default(.showCpuGraph) var showCpuGraph @@ -75,6 +76,7 @@ struct ContentView: View { @State private var lastHapticTime: Date = Date() @State private var gestureProgress: CGFloat = .zero + @State private var isPulsing: Bool = false @State private var haptics: Bool = false @@ -402,6 +404,8 @@ struct ContentView: View { MusicLiveActivity() } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .timer) && vm.notchState == .closed && timerManager.isTimerActive && coordinator.timerLiveActivityEnabled && !vm.hideOnClosed { 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 && vm.notchState == .closed && (!musicManager.isPlaying && musicManager.isPlayerIdle) && Defaults[.showNotHumanFace] && !vm.hideOnClosed { DynamicIslandFaceAnimation().animation(.interactiveSpring, value: musicManager.isPlayerIdle) } else if vm.notchState == .open { @@ -558,7 +562,7 @@ struct ContentView: View { } } ) - .frame(width: (coordinator.expandingView.show && (coordinator.expandingView.type == .music || coordinator.expandingView.type == .timer) && Defaults[.enableSneakPeek] && Defaults[.sneakPeekStyles] == .inline) ? 380 : vm.closedNotchSize.width + (isHovering ? 8 : 0)) + .frame(width: (coordinator.expandingView.show && (coordinator.expandingView.type == .music || coordinator.expandingView.type == .timer || coordinator.expandingView.type == .recording) && Defaults[.enableSneakPeek] && Defaults[.sneakPeekStyles] == .inline) ? 380 : vm.closedNotchSize.width + (isHovering ? 8 : 0)) HStack { @@ -583,6 +587,51 @@ struct ContentView: View { } .frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center) } + + func RecordingLiveActivity() -> some View { + HStack { + // Left side - Recording icon with record.circle (same pattern as album art) + HStack { + ZStack { + // Background circle matching notch style + RoundedRectangle(cornerRadius: MusicPlayerImageSizes.cornerRadiusInset.closed) + .fill(Color.red.opacity(0.1)) + + // Circle.fill icon with pulsing animation + Image(systemName: "circle.fill") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(.red) + .scaleEffect(isPulsing ? 1.2 : 1.0) + .opacity(isPulsing ? 0.7 : 1.0) + } + .onAppear { + withAnimation(.easeInOut(duration: 0.8).repeatForever(autoreverses: true)) { + isPulsing = true + } + } + .onDisappear { + isPulsing = false + } + .frame(width: max(0, vm.effectiveClosedNotchHeight - 12), height: max(0, vm.effectiveClosedNotchHeight - 12)) + } + .frame(width: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12) + gestureProgress / 2), + height: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12))) + + // Center - Recording indicator (no expansion needed since no text) + Rectangle() + .fill(.black) + .frame(width: vm.closedNotchSize.width + (isHovering ? 8 : 0)) + + // Right side - Empty (as requested) + HStack { + // Empty space + } + .frame(width: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12) + gestureProgress / 2), + height: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)), alignment: .center) + } + .frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center) + .animation(.easeInOut(duration: 0.3), value: isPulsing) + } @ViewBuilder var dragDetector: some View { diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index 444a14d..ad27d3c 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -384,6 +384,11 @@ class AppDelegate: NSObject, NSApplicationDelegate { // Setup SystemHUD Manager SystemHUDManager.shared.setup(coordinator: coordinator) + // Setup ScreenRecording Manager + if Defaults[.enableScreenRecordingDetection] { + ScreenRecordingManager.shared.startMonitoring() + } + // Setup media key monitoring setupMediaKeyMonitoring() @@ -416,6 +421,15 @@ class AppDelegate: NSObject, NSApplicationDelegate { Defaults.publisher(.showDiskGraph, options: []).sink { [weak self] _ in self?.debouncedUpdateWindowSize() }.store(in: &cancellables) + + // Observe screen recording settings changes + Defaults.publisher(.enableScreenRecordingDetection, options: []).sink { _ in + if Defaults[.enableScreenRecordingDetection] { + ScreenRecordingManager.shared.startMonitoring() + } else { + ScreenRecordingManager.shared.stopMonitoring() + } + }.store(in: &cancellables) NotificationCenter.default.addObserver( self, diff --git a/DynamicIsland/DynamicIslandViewCoordinator.swift b/DynamicIsland/DynamicIslandViewCoordinator.swift index 6147995..b8275fe 100644 --- a/DynamicIsland/DynamicIslandViewCoordinator.swift +++ b/DynamicIsland/DynamicIslandViewCoordinator.swift @@ -19,6 +19,7 @@ enum SneakContentType { case battery case download case timer + case recording } struct sneakPeek { diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 655bb42..caed19b 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -135,6 +135,10 @@ }, "Active" : { + }, + "Active - No Recording" : { + "comment" : "A description of the recording detection feature, indicating that recording is not currently active.", + "isCommentAutoGenerated" : true }, "Add" : { @@ -476,6 +480,10 @@ }, "Description" : { + }, + "Detection Status" : { + "comment" : "The header for a section that displays whether the screen recording detection feature is currently active or inactive.", + "isCommentAutoGenerated" : true }, "Disable" : { @@ -1094,6 +1102,14 @@ }, "Popover mode shows the assistant as a dropdown attached to the AI button. Panel mode shows the assistant in a floating window near the notch." : { + }, + "Privacy & Security" : { + "comment" : "The title of the privacy and security settings screen.", + "isCommentAutoGenerated" : true + }, + "Privacy Alert" : { + "comment" : "A label displayed below the recording icon, indicating privacy settings.", + "isCommentAutoGenerated" : true }, "Progress bar" : { "localizations" : { @@ -1146,6 +1162,10 @@ }, "Recording" : { + }, + "Recording Detected" : { + "comment" : "A text description of a red dot displayed in the Dynamic Island header when screen recording is active.", + "isCommentAutoGenerated" : true }, "Recording Status" : { @@ -1199,6 +1219,10 @@ }, "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 }, "Screenshot options" : { diff --git a/DynamicIsland/components/Live activities/LiveActivityModifier.swift b/DynamicIsland/components/Live activities/LiveActivityModifier.swift index 051b779..3af0829 100644 --- a/DynamicIsland/components/Live activities/LiveActivityModifier.swift +++ b/DynamicIsland/components/Live activities/LiveActivityModifier.swift @@ -11,6 +11,7 @@ enum ActivityType { case mediaPlayback case charging case download + case recording } struct LiveActivityModifier: ViewModifier { diff --git a/DynamicIsland/components/Notch/DynamicIslandHeader.swift b/DynamicIsland/components/Notch/DynamicIslandHeader.swift index 2e6fea2..c0c0af2 100644 --- a/DynamicIsland/components/Notch/DynamicIslandHeader.swift +++ b/DynamicIsland/components/Notch/DynamicIslandHeader.swift @@ -155,6 +155,13 @@ struct DynamicIslandHeader: View { } .buttonStyle(PlainButtonStyle()) } + + // Screen Recording Indicator + if Defaults[.enableScreenRecordingDetection] && Defaults[.showRecordingIndicator] { + RecordingIndicator() + .frame(width: 30, height: 30) // Same size as other header elements + } + if Defaults[.showBatteryIndicator] { DynamicIslandBatteryView( batteryWidth: 30, diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 07fd21c..518b32c 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -149,6 +149,7 @@ struct GeneralSettings: View { @State private var screens: [String] = NSScreen.screens.compactMap { $0.localizedName } @EnvironmentObject var vm: DynamicIslandViewModel @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared + @ObservedObject var recordingManager = ScreenRecordingManager.shared @Default(.mirrorShape) var mirrorShape @Default(.showEmojis) var showEmojis @@ -162,6 +163,8 @@ struct GeneralSettings: View { @Default(.automaticallySwitchDisplay) var automaticallySwitchDisplay @Default(.enableGestures) var enableGestures @Default(.openNotchOnHover) var openNotchOnHover + @Default(.enableScreenRecordingDetection) var enableScreenRecordingDetection + @Default(.showRecordingIndicator) var showRecordingIndicator var body: some View { Form { @@ -192,6 +195,36 @@ struct GeneralSettings: View { } header: { Text("System features") } + + Section { + Defaults.Toggle("Enable Screen Recording Detection", key: .enableScreenRecordingDetection) + + Defaults.Toggle("Show Recording Indicator", key: .showRecordingIndicator) + .disabled(!enableScreenRecordingDetection) + + if recordingManager.isMonitoring { + HStack { + Text("Detection Status") + Spacer() + if recordingManager.isRecording { + HStack(spacing: 4) { + Circle() + .fill(Color.red) + .frame(width: 8, height: 8) + Text("Recording Detected") + .foregroundColor(.red) + } + } else { + Text("Active - No Recording") + .foregroundColor(.green) + } + } + } + } header: { + Text("Privacy & Security") + } 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.") + } Section { Picker(selection: $notchHeightMode, label: diff --git a/DynamicIsland/components/UI/RecordingIndicator.swift b/DynamicIsland/components/UI/RecordingIndicator.swift new file mode 100644 index 0000000..9b3edaa --- /dev/null +++ b/DynamicIsland/components/UI/RecordingIndicator.swift @@ -0,0 +1,172 @@ +// +// RecordingIndicator.swift +// DynamicIsland +// +// Created for screen recording detection feature +// Displays a red pulsing dot when screen recording is active + +import SwiftUI + +struct RecordingIndicator: View { + @ObservedObject var recordingManager = ScreenRecordingManager.shared + @State private var isPulsing = false + + // MARK: - Configuration + private let indicatorSize: CGFloat = 6 + private let glowSize: CGFloat = 2 + private let animationDuration: Double = 0.8 + + var body: some View { + Group { + if recordingManager.isRecording { + ZStack { + // Glow effect + Circle() + .fill(Color.red.opacity(0.3)) + .frame(width: indicatorSize + glowSize * 2, height: indicatorSize + glowSize * 2) + .blur(radius: glowSize) + .scaleEffect(isPulsing ? 1.2 : 1.0) + + // Main indicator dot + Circle() + .fill(Color.red) + .frame(width: indicatorSize, height: indicatorSize) + .opacity(isPulsing ? 1.0 : 0.6) + } + .onAppear { + startPulsingAnimation() + } + .onDisappear { + stopPulsingAnimation() + } + .transition(.asymmetric( + insertion: .scale.combined(with: .opacity), + removal: .scale.combined(with: .opacity) + )) + } + } + .animation(.easeInOut(duration: 0.3), value: recordingManager.isRecording) + } + + // MARK: - Private Methods + + private func startPulsingAnimation() { + isPulsing = false + withAnimation(.easeInOut(duration: animationDuration).repeatForever(autoreverses: true)) { + isPulsing = true + } + } + + private func stopPulsingAnimation() { + withAnimation(.easeInOut(duration: 0.2)) { + isPulsing = false + } + } +} + +// MARK: - Preview + +struct RecordingIndicator_Previews: PreviewProvider { + static var previews: some View { + Group { + // Recording state preview + VStack { + Text("Recording Active") + .font(.caption) + RecordingIndicator() + .onAppear { + ScreenRecordingManager.shared.isRecording = true + } + } + .padding() + .background(Color.black) + .previewDisplayName("Recording Active") + + // Non-recording state preview + VStack { + Text("Not Recording") + .font(.caption) + RecordingIndicator() + .onAppear { + ScreenRecordingManager.shared.isRecording = false + } + } + .padding() + .background(Color.black) + .previewDisplayName("Not Recording") + } + } +} + +// MARK: - Alternative Indicator Styles + +struct RecordingIndicatorLarge: View { + @ObservedObject var recordingManager = ScreenRecordingManager.shared + @State private var isPulsing = false + + private let indicatorSize: CGFloat = 10 + private let glowSize: CGFloat = 3 + + var body: some View { + Group { + if recordingManager.isRecording { + ZStack { + // Outer glow + Circle() + .fill(Color.red.opacity(0.2)) + .frame(width: indicatorSize + glowSize * 4, height: indicatorSize + glowSize * 4) + .blur(radius: glowSize) + .scaleEffect(isPulsing ? 1.3 : 1.0) + + // Inner glow + Circle() + .fill(Color.red.opacity(0.4)) + .frame(width: indicatorSize + glowSize * 2, height: indicatorSize + glowSize * 2) + .blur(radius: glowSize / 2) + .scaleEffect(isPulsing ? 1.1 : 1.0) + + // Main indicator + Circle() + .fill(Color.red) + .frame(width: indicatorSize, height: indicatorSize) + .opacity(isPulsing ? 1.0 : 0.7) + } + .onAppear { + withAnimation(.easeInOut(duration: 0.8).repeatForever(autoreverses: true)) { + isPulsing = true + } + } + .transition(.asymmetric( + insertion: .scale.combined(with: .opacity), + removal: .scale.combined(with: .opacity) + )) + } + } + .animation(.easeInOut(duration: 0.3), value: recordingManager.isRecording) + } +} + +// MARK: - Subtle Indicator for Closed State + +struct RecordingIndicatorSubtle: View { + @ObservedObject var recordingManager = ScreenRecordingManager.shared + @State private var opacity: Double = 0.5 + + var body: some View { + Group { + if recordingManager.isRecording { + Circle() + .fill(Color.red) + .frame(width: 4, height: 4) + .opacity(opacity) + .onAppear { + withAnimation(.easeInOut(duration: 1.0).repeatForever(autoreverses: true)) { + opacity = 1.0 + } + } + .transition(.opacity) + } + } + .animation(.easeInOut(duration: 0.2), value: recordingManager.isRecording) + } +} \ No newline at end of file diff --git a/DynamicIsland/managers/ScreenRecordingManager.swift b/DynamicIsland/managers/ScreenRecordingManager.swift new file mode 100644 index 0000000..1082f45 --- /dev/null +++ b/DynamicIsland/managers/ScreenRecordingManager.swift @@ -0,0 +1,322 @@ +// +// ScreenRecordingManager.swift +// DynamicIsland +// +// Created for screen recording detection feature +// Monitors system for active screen recording and provides real-time status updates + +import Foundation +import Combine +import ScreenCaptureKit +import AppKit +import Defaults +import SwiftUI + +@MainActor +class ScreenRecordingManager: ObservableObject { + static let shared = ScreenRecordingManager() + + // MARK: - Coordinator + private let coordinator = DynamicIslandViewCoordinator.shared + + // MARK: - Published Properties + @Published var isRecording: Bool = false + @Published var isMonitoring: Bool = false + @Published var recordingDuration: TimeInterval = 0 + @Published var isRecorderIdle: Bool = true + @Published var lastUpdated: Date = .distantPast + + // MARK: - Private Properties + private var monitoringTimer: Timer? + private var lastScreensHaveSeparateSpaces: Bool = false + private var cancellables = Set() + private var recordingStartTime: Date? + private var durationTimer: Timer? + private var debounceIdleTask: Task? + + // MARK: - Configuration + private let monitoringInterval: TimeInterval = 1.0 // Check every second + private let debounceDelay: TimeInterval = 0.5 // Debounce rapid changes + + // MARK: - Initialization + private init() { + setupInitialState() + } + + deinit { + Task { @MainActor in + stopMonitoring() + debounceIdleTask?.cancel() + } + } + + // MARK: - Public Methods + + /// Start monitoring for screen recording activity + func startMonitoring() { + guard !isMonitoring else { + print("ScreenRecordingManager: Already monitoring, skipping start") + return + } + + isMonitoring = true + lastScreensHaveSeparateSpaces = NSScreen.screensHaveSeparateSpaces + + print("ScreenRecordingManager: đŸŸĸ Starting monitoring...") + print("ScreenRecordingManager: Initial screensHaveSeparateSpaces = \(lastScreensHaveSeparateSpaces)") + + // Start timer-based monitoring + monitoringTimer = Timer.scheduledTimer(withTimeInterval: monitoringInterval, repeats: true) { [weak self] _ in + Task { @MainActor in + await self?.checkRecordingStatus() + } + } + + print("ScreenRecordingManager: ✅ Started monitoring with \(monitoringInterval)s interval") + } + + /// Stop monitoring for screen recording activity + func stopMonitoring() { + guard isMonitoring else { + print("ScreenRecordingManager: Not monitoring, skipping stop") + return + } + + print("ScreenRecordingManager: 🛑 Stopping monitoring...") + + isMonitoring = false + monitoringTimer?.invalidate() + monitoringTimer = nil + + // Stop duration tracking + stopDurationTracking() + + // Reset recording state when stopping + if isRecording { + print("ScreenRecordingManager: Resetting isRecording from true to false") + } + isRecording = false + + print("ScreenRecordingManager: ✅ Stopped monitoring") + } + + /// Toggle monitoring state + func toggleMonitoring() { + if isMonitoring { + stopMonitoring() + } else { + startMonitoring() + } + } + + // MARK: - Private Methods + + private func setupInitialState() { + lastScreensHaveSeparateSpaces = NSScreen.screensHaveSeparateSpaces + } + + /// Check current recording status using multiple detection methods + private func checkRecordingStatus() async { + let currentRecordingState = await detectScreenRecording() + + // Debug: Always log current check + print("ScreenRecordingManager: 🔍 Checking... current=\(isRecording), detected=\(currentRecordingState)") + + // Debounce changes to avoid flickering + if currentRecordingState != isRecording { + print("ScreenRecordingManager: 🔄 State change detected (\(isRecording) -> \(currentRecordingState)), debouncing...") + + // Add a small delay to confirm the state change + try? await Task.sleep(nanoseconds: UInt64(debounceDelay * 1_000_000_000)) + + // Double-check the state after debounce + let confirmedState = await detectScreenRecording() + print("ScreenRecordingManager: 🔍 After debounce: confirmed=\(confirmedState), original=\(currentRecordingState)") + + if confirmedState == currentRecordingState { + if confirmedState && !isRecording { + // Started recording + lastUpdated = Date() + startDurationTracking() + updateIdleState(recording: true) + // Trigger expanding view like music activity + coordinator.toggleExpandingView(status: true, type: .recording) + } else if !confirmedState && isRecording { + // Stopped recording - let expanding view auto-collapse naturally (like music) + lastUpdated = Date() + stopDurationTracking() + updateIdleState(recording: false) + } + + isRecording = confirmedState + print("ScreenRecordingManager: ✅ Recording state changed to \(confirmedState)") + } else { + print("ScreenRecordingManager: ❌ State not confirmed, keeping \(isRecording)") + } + } + } + + /// Detect screen recording using hybrid approach + private func detectScreenRecording() async -> Bool { + print("ScreenRecordingManager: 🔎 Starting detection...") + + // Method 1: NSScreen.screensHaveSeparateSpaces detection + let screensHaveSeparateSpaces = NSScreen.screensHaveSeparateSpaces + let screenSpacesChanged = screensHaveSeparateSpaces != lastScreensHaveSeparateSpaces + + print("ScreenRecordingManager: Method 1 - screensHaveSeparateSpaces: \(screensHaveSeparateSpaces), last: \(lastScreensHaveSeparateSpaces), changed: \(screenSpacesChanged)") + + // Update last known state + lastScreensHaveSeparateSpaces = screensHaveSeparateSpaces + + // Method 2: ScreenCaptureKit detection (if available) + let screencaptureKitDetection = await detectWithScreenCaptureKit() + print("ScreenRecordingManager: Method 2 - ScreenCaptureKit: \(screencaptureKitDetection)") + + // Method 3: Process-based detection for common recording apps + let processBasedDetection = detectRecordingProcesses() + print("ScreenRecordingManager: Method 3 - Process detection: \(processBasedDetection)") + + // Combine detection methods (any positive result indicates recording) + let isCurrentlyRecording = screenSpacesChanged || screencaptureKitDetection || processBasedDetection + + print("ScreenRecordingManager: đŸŽ¯ Final result: \(isCurrentlyRecording) (spaces:\(screenSpacesChanged) || kit:\(screencaptureKitDetection) || process:\(processBasedDetection))") + + return isCurrentlyRecording + } + + /// Detect recording using ScreenCaptureKit (requires macOS 12.3+) + private func detectWithScreenCaptureKit() async -> Bool { + guard #available(macOS 12.3, *) else { return false } + + do { + // Check if there are any active capture sessions + let content = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true) + + // If we can successfully get content and there are running applications with capture capabilities, + // it might indicate active screen capture + let runningApps = content.applications.filter { $0.applicationName.lowercased().contains("screen") || + $0.applicationName.lowercased().contains("record") || + $0.applicationName.lowercased().contains("capture") } + + return !runningApps.isEmpty + } catch { + // If ScreenCaptureKit fails, fall back to other detection methods + return false + } + } + + /// Detect recording by checking for known recording processes + private func detectRecordingProcesses() -> Bool { + let recordingProcessNames = [ + "QuickTime Player", + "ScreenSearch", + "OBS", + "Camtasia", + "ScreenFlow", + "CleanMyMac", + "Screenshot", + "screencapture" + ] + + let runningApps = NSWorkspace.shared.runningApplications + + print("ScreenRecordingManager: Checking \(runningApps.count) running apps for recording processes...") + + for app in runningApps { + if let appName = app.localizedName { + for processName in recordingProcessNames { + if appName.lowercased().contains(processName.lowercased()) { + print("ScreenRecordingManager: đŸŽŦ Found recording app: \(appName)") + return true + } + } + } + } + + print("ScreenRecordingManager: No recording processes found") + return false + } + + /// Start tracking recording duration + private func startDurationTracking() { + recordingStartTime = Date() + recordingDuration = 0 + + durationTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in + Task { @MainActor in + self?.updateDuration() + } + } + + print("ScreenRecordingManager: âąī¸ Started duration tracking") + } + + /// Stop tracking recording duration + private func stopDurationTracking() { + durationTimer?.invalidate() + durationTimer = nil + recordingStartTime = nil + + // Keep the last duration for a moment before resetting + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in + self?.recordingDuration = 0 + } + + print("ScreenRecordingManager: âšī¸ Stopped duration tracking") + } + + /// Update the current recording duration + private func updateDuration() { + guard let startTime = recordingStartTime else { return } + recordingDuration = Date().timeIntervalSince(startTime) + } + + /// Copy EXACT music idle state logic + private func updateIdleState(recording: Bool) { + if recording { + isRecorderIdle = false + debounceIdleTask?.cancel() + } else { + debounceIdleTask?.cancel() + debounceIdleTask = Task { [weak self] in + try? await Task.sleep(for: .seconds(Defaults[.waitInterval])) + guard let self = self, !Task.isCancelled else { return } + await MainActor.run { + if self.lastUpdated.timeIntervalSinceNow < -Defaults[.waitInterval] { + withAnimation { + self.isRecorderIdle = !self.isRecording + } + } + } + } + } + } +} + +// MARK: - Extensions + +extension ScreenRecordingManager { + /// Get current recording status without async + var currentRecordingStatus: Bool { + return isRecording + } + + /// Check if monitoring is available (for settings UI) + var isMonitoringAvailable: Bool { + return true // Basic monitoring is always available + } + + /// Get current monitoring interval for debugging + var currentMonitoringInterval: TimeInterval { + return monitoringInterval + } + + /// Get formatted recording duration string + var formattedDuration: String { + let totalSeconds = Int(recordingDuration) + let minutes = totalSeconds / 60 + let seconds = totalSeconds % 60 + return String(format: "%d:%02d", minutes, seconds) + } +} \ No newline at end of file diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index c30515d..b4481f1 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -343,6 +343,10 @@ extension Defaults.Keys { static let enableKeyboardBacklightHUD = Key("enableKeyboardBacklightHUD", default: true) static let systemHUDSensitivity = Key("systemHUDSensitivity", default: 5) + // MARK: Screen Recording Detection Feature + static let enableScreenRecordingDetection = Key("enableScreenRecordingDetection", default: true) + static let showRecordingIndicator = Key("showRecordingIndicator", default: true) + // Helper to determine the default media controller based on macOS version static var defaultMediaController: MediaControllerType { if #available(macOS 15.4, *) {