mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Still working on the janky animation
This commit is contained in:
parent
44569d36b5
commit
c5186c47f8
10 changed files with 628 additions and 1 deletions
|
|
@ -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 {
|
||||
|
|
@ -584,6 +588,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 {
|
||||
if Defaults[.dynamicShelf] {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
@ -417,6 +422,15 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
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,
|
||||
selector: #selector(screenConfigurationDidChange),
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ enum SneakContentType {
|
|||
case battery
|
||||
case download
|
||||
case timer
|
||||
case recording
|
||||
}
|
||||
|
||||
struct sneakPeek {
|
||||
|
|
|
|||
|
|
@ -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" : {
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ enum ActivityType {
|
|||
case mediaPlayback
|
||||
case charging
|
||||
case download
|
||||
case recording
|
||||
}
|
||||
|
||||
struct LiveActivityModifier<Left: View, Right: View>: ViewModifier {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -193,6 +196,36 @@ struct GeneralSettings: View {
|
|||
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:
|
||||
Text("Notch display height")) {
|
||||
|
|
|
|||
172
DynamicIsland/components/UI/RecordingIndicator.swift
Normal file
172
DynamicIsland/components/UI/RecordingIndicator.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
322
DynamicIsland/managers/ScreenRecordingManager.swift
Normal file
322
DynamicIsland/managers/ScreenRecordingManager.swift
Normal file
|
|
@ -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<AnyCancellable>()
|
||||
private var recordingStartTime: Date?
|
||||
private var durationTimer: Timer?
|
||||
private var debounceIdleTask: Task<Void, Never>?
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -343,6 +343,10 @@ extension Defaults.Keys {
|
|||
static let enableKeyboardBacklightHUD = Key<Bool>("enableKeyboardBacklightHUD", default: true)
|
||||
static let systemHUDSensitivity = Key<Int>("systemHUDSensitivity", default: 5)
|
||||
|
||||
// MARK: Screen Recording Detection Feature
|
||||
static let enableScreenRecordingDetection = Key<Bool>("enableScreenRecordingDetection", default: true)
|
||||
static let showRecordingIndicator = Key<Bool>("showRecordingIndicator", default: true)
|
||||
|
||||
// Helper to determine the default media controller based on macOS version
|
||||
static var defaultMediaController: MediaControllerType {
|
||||
if #available(macOS 15.4, *) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue