Working lock screen widget

This commit is contained in:
Hariharan Mudaliar 2025-10-17 17:26:46 +05:30
commit 44b18513f2
13 changed files with 705 additions and 3 deletions

View file

@ -0,0 +1,153 @@
//
// LockScreenManager.swift
// DynamicIsland
//
// Created for lock screen detection feature
// Monitors system lock/unlock events and provides real-time status updates
//
import Foundation
import Combine
import AppKit
import Defaults
import SwiftUI
@MainActor
class LockScreenManager: ObservableObject {
static let shared = LockScreenManager()
// MARK: - Coordinator
private let coordinator = DynamicIslandViewCoordinator.shared
// MARK: - Published Properties
@Published var isLocked: Bool = false
@Published var isLockIdle: Bool = true
@Published var lastUpdated: Date = .distantPast
// MARK: - Private Properties
private var debounceIdleTask: Task<Void, Never>?
// MARK: - Helpers
private func timestamp() -> String {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss.SSS"
return formatter.string(from: Date())
}
// MARK: - Initialization
private init() {
setupObservers()
print("LockScreenManager: 🔒 Initialized")
}
deinit {
DistributedNotificationCenter.default().removeObserver(self)
debounceIdleTask?.cancel()
}
// MARK: - Setup
private func setupObservers() {
// Observe screen locked event
DistributedNotificationCenter.default().addObserver(
self,
selector: #selector(screenLocked),
name: .init("com.apple.screenIsLocked"),
object: nil
)
// Observe screen unlocked event
DistributedNotificationCenter.default().addObserver(
self,
selector: #selector(screenUnlocked),
name: .init("com.apple.screenIsUnlocked"),
object: nil
)
print("LockScreenManager: ✅ Observers registered for lock/unlock events")
}
// MARK: - Event Handlers
@objc private func screenLocked() {
print("[\(timestamp())] LockScreenManager: 🔒 Screen LOCKED event received")
// Update state SYNCHRONOUSLY without Task/await to avoid any delay
lastUpdated = Date()
updateIdleState(locked: true)
// Set locked state immediately without animation wrapper
isLocked = true
// Show panel FIRST (creates and shows window on lock screen)
print("[\(timestamp())] LockScreenManager: 🎵 Showing lock screen panel")
LockScreenPanelManager.shared.showPanel()
// THEN trigger lock icon in Dynamic Island (only if enabled in settings)
if Defaults[.enableLockScreenLiveActivity] {
print("[\(timestamp())] LockScreenManager: 🔴 Starting lock icon live activity")
coordinator.toggleExpandingView(status: true, type: .lockScreen)
} else {
print("[\(timestamp())] LockScreenManager: ⏭️ Lock icon disabled in settings")
}
print("[\(timestamp())] LockScreenManager: ✅ Lock screen activated")
}
@objc private func screenUnlocked() {
print("[\(timestamp())] LockScreenManager: 🔓 Screen UNLOCKED event received")
// Hide panel window immediately and synchronously
print("[\(timestamp())] LockScreenManager: 🚪 Hiding panel window")
LockScreenPanelManager.shared.hidePanel()
// Update state immediately
if Defaults[.enableLockScreenLiveActivity] {
self.coordinator.toggleExpandingView(status: false, type: .lockScreen)
}
self.lastUpdated = Date()
self.updateIdleState(locked: false)
self.isLocked = false
print("[\(self.timestamp())] LockScreenManager: ✅ Lock screen deactivated")
}
// MARK: - Idle State Management
/// Copy EXACT logic from ScreenRecordingManager
private func updateIdleState(locked: Bool) {
if locked {
isLockIdle = 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.isLockIdle = !self.isLocked
}
}
}
}
}
}
}
// MARK: - Extensions
extension LockScreenManager {
/// Get current lock status without async
var currentLockStatus: Bool {
return isLocked
}
/// Check if monitoring is available (for settings UI)
var isMonitoringAvailable: Bool {
return true // Always available on macOS
}
}

View file

@ -0,0 +1,103 @@
//
// LockScreenPanelManager.swift
// DynamicIsland
//
// Manages the lock screen music panel window.
//
import SwiftUI
import AppKit
import SkyLightWindow
import Defaults
@MainActor
class LockScreenPanelManager {
static let shared = LockScreenPanelManager()
private var panelWindow: NSWindow?
private var hasDelegated = false
private init() {
print("[\(timestamp())] LockScreenPanelManager: initialized")
}
private func timestamp() -> String {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss.SSS"
return formatter.string(from: Date())
}
func showPanel() {
print("[\(timestamp())] LockScreenPanelManager: showPanel")
guard Defaults[.enableLockScreenMediaWidget] else {
print("[\(timestamp())] LockScreenPanelManager: widget disabled")
hidePanel()
return
}
guard let screen = NSScreen.main else {
print("[\(timestamp())] LockScreenPanelManager: no main screen available")
return
}
let panelWidth: CGFloat = 420
let panelHeight: CGFloat = 150
let screenFrame = screen.frame
let originX = screenFrame.origin.x + (screenFrame.width / 2) - (panelWidth / 2)
let originY = screenFrame.origin.y + (screenFrame.height / 2) - panelHeight - 40
let targetFrame = NSRect(x: originX, y: originY, width: panelWidth, height: panelHeight)
let window: NSWindow
if let existingWindow = panelWindow {
window = existingWindow
} else {
let newWindow = NSWindow(
contentRect: targetFrame,
styleMask: [.borderless, .nonactivatingPanel],
backing: .buffered,
defer: false
)
newWindow.isReleasedWhenClosed = false
newWindow.isOpaque = false
newWindow.backgroundColor = .clear
newWindow.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()))
newWindow.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
newWindow.isMovable = false
newWindow.hasShadow = false
panelWindow = newWindow
window = newWindow
hasDelegated = false
}
window.setFrame(targetFrame, display: true)
window.contentView = NSHostingView(rootView: LockScreenMusicPanel())
if !hasDelegated {
SkyLightOperator.shared.delegateWindow(window)
hasDelegated = true
}
// Keep the window alive and simply order it out on unlock to avoid SkyLight crashes.
window.orderFrontRegardless()
print("[\(timestamp())] LockScreenPanelManager: panel visible")
}
func hidePanel() {
print("[\(timestamp())] LockScreenPanelManager: hidePanel")
guard let window = panelWindow else {
print("LockScreenPanelManager: no panel to hide")
return
}
window.orderOut(nil)
window.contentView = nil
print("[\(timestamp())] LockScreenPanelManager: panel hidden")
}
}