mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Merge pull request #46 from Ebullioscopic/feature/lock-screen
Feature/lock screen
This commit is contained in:
commit
34f60d4447
24 changed files with 1441 additions and 48 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -85,4 +85,9 @@ comparison_report.json
|
||||||
ANIMATION_DEBUG_GUIDE.md
|
ANIMATION_DEBUG_GUIDE.md
|
||||||
MIGRATION_SUMMARY.md
|
MIGRATION_SUMMARY.md
|
||||||
ONBOARDING_ENHANCEMENT.md
|
ONBOARDING_ENHANCEMENT.md
|
||||||
SHEET_PRESENTATION_FIX.md
|
SHEET_PRESENTATION_FIX.md
|
||||||
|
lock_mode_reference.md
|
||||||
|
liquid_glass.md
|
||||||
|
|
||||||
|
MoveToSky.xcodeproj
|
||||||
|
/MoveToSky/
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
7E1ADF1D2EA209B1001DA867 /* SkyLightWindow in Frameworks */ = {isa = PBXBuildFile; productRef = 7E61181E2E24071E0050006C /* SkyLightWindow */; };
|
||||||
7E25ECA12E6C6C3800700E72 /* CoreBrightness.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7E25ECA02E6C6C3800700E72 /* CoreBrightness.framework */; };
|
7E25ECA12E6C6C3800700E72 /* CoreBrightness.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7E25ECA02E6C6C3800700E72 /* CoreBrightness.framework */; };
|
||||||
7E25ECA32E6C6E2600700E72 /* OSD.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7E25ECA22E6C6E2600700E72 /* OSD.framework */; };
|
7E25ECA32E6C6E2600700E72 /* OSD.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7E25ECA22E6C6E2600700E72 /* OSD.framework */; };
|
||||||
7E25ECA52E6C6E7800700E72 /* DisplayServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7E25ECA42E6C6E7800700E72 /* DisplayServices.framework */; };
|
7E25ECA52E6C6E7800700E72 /* DisplayServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7E25ECA42E6C6E7800700E72 /* DisplayServices.framework */; };
|
||||||
|
|
@ -72,6 +73,7 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
7E1ADF1D2EA209B1001DA867 /* SkyLightWindow in Frameworks */,
|
||||||
7E25ECA12E6C6C3800700E72 /* CoreBrightness.framework in Frameworks */,
|
7E25ECA12E6C6C3800700E72 /* CoreBrightness.framework in Frameworks */,
|
||||||
7EE1D73F2E6B1E1200997219 /* IOKit.framework in Frameworks */,
|
7EE1D73F2E6B1E1200997219 /* IOKit.framework in Frameworks */,
|
||||||
7EE1D73D2E6B1E0B00997219 /* ApplicationServices.framework in Frameworks */,
|
7EE1D73D2E6B1E0B00997219 /* ApplicationServices.framework in Frameworks */,
|
||||||
|
|
@ -174,6 +176,7 @@
|
||||||
B1628B912CC260C0003D8DF3 /* SwiftUIIntrospect */,
|
B1628B912CC260C0003D8DF3 /* SwiftUIIntrospect */,
|
||||||
B194240A2CD10017003E5DC2 /* LottieUI */,
|
B194240A2CD10017003E5DC2 /* LottieUI */,
|
||||||
146BC4362D366E9B0047C66A /* MacroVisionKit */,
|
146BC4362D366E9B0047C66A /* MacroVisionKit */,
|
||||||
|
7E61181E2E24071E0050006C /* SkyLightWindow */,
|
||||||
);
|
);
|
||||||
productName = DynamicIsland;
|
productName = DynamicIsland;
|
||||||
productReference = F54406E72CD4987A00C7AFD9 /* DynamicIsland.app */;
|
productReference = F54406E72CD4987A00C7AFD9 /* DynamicIsland.app */;
|
||||||
|
|
@ -214,6 +217,7 @@
|
||||||
B1628B902CC260C0003D8DF3 /* XCRemoteSwiftPackageReference "swiftui-introspect" */,
|
B1628B902CC260C0003D8DF3 /* XCRemoteSwiftPackageReference "swiftui-introspect" */,
|
||||||
B19424072CD0DB52003E5DC2 /* XCRemoteSwiftPackageReference "LottieUI" */,
|
B19424072CD0DB52003E5DC2 /* XCRemoteSwiftPackageReference "LottieUI" */,
|
||||||
146BC4352D366E9B0047C66A /* XCRemoteSwiftPackageReference "MacroVisionKit" */,
|
146BC4352D366E9B0047C66A /* XCRemoteSwiftPackageReference "MacroVisionKit" */,
|
||||||
|
7E61181F2E24071E0050006C /* XCRemoteSwiftPackageReference "SkyLightWindow" */,
|
||||||
);
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = F54406E82CD4987A00C7AFD9 /* Products */;
|
productRefGroup = F54406E82CD4987A00C7AFD9 /* Products */;
|
||||||
|
|
@ -517,6 +521,14 @@
|
||||||
minimumVersion = 2.6.4;
|
minimumVersion = 2.6.4;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
7E61181F2E24071E0050006C /* XCRemoteSwiftPackageReference "SkyLightWindow" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/Lakr233/SkyLightWindow";
|
||||||
|
requirement = {
|
||||||
|
branch = main;
|
||||||
|
kind = branch;
|
||||||
|
};
|
||||||
|
};
|
||||||
9A987A112C73CAA1005CA465 /* XCRemoteSwiftPackageReference "Pow" */ = {
|
9A987A112C73CAA1005CA465 /* XCRemoteSwiftPackageReference "Pow" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/EmergeTools/Pow";
|
repositoryURL = "https://github.com/EmergeTools/Pow";
|
||||||
|
|
@ -588,6 +600,11 @@
|
||||||
package = 14D0321B2C68F3350096E6A1 /* XCRemoteSwiftPackageReference "Sparkle" */;
|
package = 14D0321B2C68F3350096E6A1 /* XCRemoteSwiftPackageReference "Sparkle" */;
|
||||||
productName = Sparkle;
|
productName = Sparkle;
|
||||||
};
|
};
|
||||||
|
7E61181E2E24071E0050006C /* SkyLightWindow */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 7E61181F2E24071E0050006C /* XCRemoteSwiftPackageReference "SkyLightWindow" */;
|
||||||
|
productName = SkyLightWindow;
|
||||||
|
};
|
||||||
9A987A0F2C73CA8D005CA465 /* Collections */ = {
|
9A987A0F2C73CA8D005CA465 /* Collections */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = F54407082CD4A23F00C7AFD9 /* XCRemoteSwiftPackageReference "swift-collections" */;
|
package = F54407082CD4A23F00C7AFD9 /* XCRemoteSwiftPackageReference "swift-collections" */;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"originHash" : "2741287b65cf0758abd89baadf314f5f8cbfcb46cb8d236a83f98731e5501ee3",
|
"originHash" : "7d8482ed9f8bfd7995906cd0663db3b8e6656cebc1c013800b81bccf13756944",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "defaults",
|
"identity" : "defaults",
|
||||||
|
|
@ -64,6 +64,15 @@
|
||||||
"version" : "1.0.5"
|
"version" : "1.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "skylightwindow",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/Lakr233/SkyLightWindow",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "main",
|
||||||
|
"revision" : "4a7e8628c85e6b2ec429b57657a556a2171a4864"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "sparkle",
|
"identity" : "sparkle",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ struct ContentView: View {
|
||||||
@ObservedObject var statsManager = StatsManager.shared
|
@ObservedObject var statsManager = StatsManager.shared
|
||||||
@ObservedObject var recordingManager = ScreenRecordingManager.shared
|
@ObservedObject var recordingManager = ScreenRecordingManager.shared
|
||||||
@ObservedObject var privacyManager = PrivacyIndicatorManager.shared
|
@ObservedObject var privacyManager = PrivacyIndicatorManager.shared
|
||||||
|
@ObservedObject var lockScreenManager = LockScreenManager.shared
|
||||||
|
|
||||||
@Default(.enableStatsFeature) var enableStatsFeature
|
@Default(.enableStatsFeature) var enableStatsFeature
|
||||||
@Default(.showCpuGraph) var showCpuGraph
|
@Default(.showCpuGraph) var showCpuGraph
|
||||||
|
|
@ -33,7 +34,7 @@ struct ContentView: View {
|
||||||
@Default(.showNetworkGraph) var showNetworkGraph
|
@Default(.showNetworkGraph) var showNetworkGraph
|
||||||
@Default(.showDiskGraph) var showDiskGraph
|
@Default(.showDiskGraph) var showDiskGraph
|
||||||
|
|
||||||
// Dynamic sizing based on view type and graph count with smooth transitions
|
// Dynamic sizing based on view type and graph count with smooth transitions
|
||||||
var dynamicNotchSize: CGSize {
|
var dynamicNotchSize: CGSize {
|
||||||
// Use minimalistic or normal size based on settings
|
// Use minimalistic or normal size based on settings
|
||||||
let baseSize = Defaults[.enableMinimalisticUI] ? minimalisticOpenNotchSize : openNotchSize
|
let baseSize = Defaults[.enableMinimalisticUI] ? minimalisticOpenNotchSize : openNotchSize
|
||||||
|
|
@ -106,6 +107,8 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
let interactionsEnabled = !lockScreenManager.isLocked
|
||||||
|
|
||||||
ZStack(alignment: .top) {
|
ZStack(alignment: .top) {
|
||||||
let mainLayout = NotchLayout()
|
let mainLayout = NotchLayout()
|
||||||
.frame(alignment: .top)
|
.frame(alignment: .top)
|
||||||
|
|
@ -148,12 +151,12 @@ struct ContentView: View {
|
||||||
.animation(notchStateAnimation, value: vm.notchState)
|
.animation(notchStateAnimation, value: vm.notchState)
|
||||||
.animation(viewTransitionAnimation, value: coordinator.currentView)
|
.animation(viewTransitionAnimation, value: coordinator.currentView)
|
||||||
}
|
}
|
||||||
.conditionalModifier(Defaults[.openNotchOnHover]) { view in
|
.conditionalModifier(Defaults[.openNotchOnHover] && interactionsEnabled) { view in
|
||||||
view.onHover { hovering in
|
view.onHover { hovering in
|
||||||
handleHover(hovering)
|
handleHover(hovering)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.conditionalModifier(!Defaults[.openNotchOnHover]) { view in
|
.conditionalModifier(!Defaults[.openNotchOnHover] && interactionsEnabled) { view in
|
||||||
view
|
view
|
||||||
.onHover { hovering in
|
.onHover { hovering in
|
||||||
handleSimpleHover(hovering)
|
handleSimpleHover(hovering)
|
||||||
|
|
@ -164,14 +167,14 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
doOpen()
|
doOpen()
|
||||||
}
|
}
|
||||||
.conditionalModifier(Defaults[.enableGestures]) { view in
|
.conditionalModifier(Defaults[.enableGestures] && interactionsEnabled) { view in
|
||||||
view
|
view
|
||||||
.panGesture(direction: .down) { translation, phase in
|
.panGesture(direction: .down) { translation, phase in
|
||||||
handleDownGesture(translation: translation, phase: phase)
|
handleDownGesture(translation: translation, phase: phase)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.conditionalModifier(Defaults[.closeGestureEnabled] && Defaults[.enableGestures]) { view in
|
.conditionalModifier(Defaults[.closeGestureEnabled] && Defaults[.enableGestures] && interactionsEnabled) { view in
|
||||||
view
|
view
|
||||||
.panGesture(direction: .up) { translation, phase in
|
.panGesture(direction: .up) { translation, phase in
|
||||||
handleUpGesture(translation: translation, phase: phase)
|
handleUpGesture(translation: translation, phase: phase)
|
||||||
|
|
@ -416,12 +419,14 @@ struct ContentView: View {
|
||||||
} else if coordinator.sneakPeek.show && Defaults[.inlineHUD] && (coordinator.sneakPeek.type != .music) && (coordinator.sneakPeek.type != .battery) {
|
} else if coordinator.sneakPeek.show && Defaults[.inlineHUD] && (coordinator.sneakPeek.type != .music) && (coordinator.sneakPeek.type != .battery) {
|
||||||
InlineHUD(type: $coordinator.sneakPeek.type, value: $coordinator.sneakPeek.value, icon: $coordinator.sneakPeek.icon, hoverAnimation: $isHovering, gestureProgress: $gestureProgress)
|
InlineHUD(type: $coordinator.sneakPeek.type, value: $coordinator.sneakPeek.value, icon: $coordinator.sneakPeek.icon, hoverAnimation: $isHovering, gestureProgress: $gestureProgress)
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .music) && vm.notchState == .closed && (musicManager.isPlaying || !musicManager.isPlayerIdle) && coordinator.musicLiveActivityEnabled && !vm.hideOnClosed {
|
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .music) && vm.notchState == .closed && (musicManager.isPlaying || !musicManager.isPlayerIdle) && coordinator.musicLiveActivityEnabled && !vm.hideOnClosed && !lockScreenManager.isLocked {
|
||||||
MusicLiveActivity()
|
MusicLiveActivity()
|
||||||
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .timer) && vm.notchState == .closed && timerManager.isTimerActive && coordinator.timerLiveActivityEnabled && !vm.hideOnClosed {
|
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .timer) && vm.notchState == .closed && timerManager.isTimerActive && coordinator.timerLiveActivityEnabled && !vm.hideOnClosed {
|
||||||
TimerLiveActivity()
|
TimerLiveActivity()
|
||||||
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .recording) && vm.notchState == .closed && (recordingManager.isRecording || !recordingManager.isRecorderIdle) && Defaults[.enableScreenRecordingDetection] && !vm.hideOnClosed {
|
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .recording) && vm.notchState == .closed && (recordingManager.isRecording || !recordingManager.isRecorderIdle) && Defaults[.enableScreenRecordingDetection] && !vm.hideOnClosed {
|
||||||
RecordingLiveActivity()
|
RecordingLiveActivity()
|
||||||
|
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .lockScreen) && vm.notchState == .closed && (lockScreenManager.isLocked || !lockScreenManager.isLockIdle) && Defaults[.enableLockScreenLiveActivity] && !vm.hideOnClosed {
|
||||||
|
LockScreenLiveActivity()
|
||||||
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .privacy) && vm.notchState == .closed && privacyManager.hasAnyIndicator && (Defaults[.enableCameraDetection] || Defaults[.enableMicrophoneDetection]) && !vm.hideOnClosed {
|
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .privacy) && vm.notchState == .closed && privacyManager.hasAnyIndicator && (Defaults[.enableCameraDetection] || Defaults[.enableMicrophoneDetection]) && !vm.hideOnClosed {
|
||||||
PrivacyLiveActivity()
|
PrivacyLiveActivity()
|
||||||
} else if !coordinator.expandingView.show && vm.notchState == .closed && (!musicManager.isPlaying && musicManager.isPlayerIdle) && Defaults[.showNotHumanFace] && !vm.hideOnClosed {
|
} else if !coordinator.expandingView.show && vm.notchState == .closed && (!musicManager.isPlaying && musicManager.isPlayerIdle) && Defaults[.showNotHumanFace] && !vm.hideOnClosed {
|
||||||
|
|
@ -539,6 +544,7 @@ struct ContentView: View {
|
||||||
.clipped()
|
.clipped()
|
||||||
.clipShape(RoundedRectangle(cornerRadius: MusicPlayerImageSizes.cornerRadiusInset.closed))
|
.clipShape(RoundedRectangle(cornerRadius: MusicPlayerImageSizes.cornerRadiusInset.closed))
|
||||||
.matchedGeometryEffect(id: "albumArt", in: albumArtNamespace)
|
.matchedGeometryEffect(id: "albumArt", in: albumArtNamespace)
|
||||||
|
.albumArtFlip(angle: musicManager.flipAngle)
|
||||||
.frame(width: max(0, vm.effectiveClosedNotchHeight - 12), height: max(0, vm.effectiveClosedNotchHeight - 12))
|
.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)))
|
.frame(width: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12) + gestureProgress / 2), height: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)))
|
||||||
|
|
@ -608,7 +614,9 @@ struct ContentView: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var dragDetector: some View {
|
var dragDetector: some View {
|
||||||
if Defaults[.dynamicShelf] && !Defaults[.enableMinimalisticUI] {
|
if lockScreenManager.isLocked {
|
||||||
|
EmptyView()
|
||||||
|
} else if Defaults[.dynamicShelf] && !Defaults[.enableMinimalisticUI] {
|
||||||
Color.clear
|
Color.clear
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import Defaults
|
||||||
import KeyboardShortcuts
|
import KeyboardShortcuts
|
||||||
import Sparkle
|
import Sparkle
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import SkyLightWindow
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct DynamicNotchApp: App {
|
struct DynamicNotchApp: App {
|
||||||
|
|
@ -85,10 +86,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
let dndManager = DoNotDisturbManager.shared // NEW: DND detection
|
let dndManager = DoNotDisturbManager.shared // NEW: DND detection
|
||||||
let bluetoothAudioManager = BluetoothAudioManager.shared // NEW: Bluetooth audio detection
|
let bluetoothAudioManager = BluetoothAudioManager.shared // NEW: Bluetooth audio detection
|
||||||
let idleAnimationManager = IdleAnimationManager.shared // NEW: Custom idle animations
|
let idleAnimationManager = IdleAnimationManager.shared // NEW: Custom idle animations
|
||||||
|
let lockScreenPanelManager = LockScreenPanelManager.shared // NEW: Lock screen music panel
|
||||||
var closeNotchWorkItem: DispatchWorkItem?
|
var closeNotchWorkItem: DispatchWorkItem?
|
||||||
private var previousScreens: [NSScreen]?
|
private var previousScreens: [NSScreen]?
|
||||||
private var onboardingWindowController: NSWindowController?
|
private var onboardingWindowController: NSWindowController?
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
private var windowsHiddenForLock = false
|
||||||
|
|
||||||
// Media key monitoring
|
// Media key monitoring
|
||||||
private var globalEventMonitor: Any?
|
private var globalEventMonitor: Any?
|
||||||
|
|
@ -207,14 +210,45 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
@objc func onScreenLocked(_: Notification) {
|
@objc func onScreenLocked(_: Notification) {
|
||||||
print("Screen locked")
|
print("Screen locked")
|
||||||
cleanupWindows()
|
hideWindowsForLock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func onScreenUnlocked(_: Notification) {
|
@objc func onScreenUnlocked(_: Notification) {
|
||||||
print("Screen unlocked")
|
print("Screen unlocked")
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
|
||||||
self?.cleanupWindows()
|
guard let self = self else { return }
|
||||||
self?.adjustWindowPosition(changeAlpha: true)
|
self.restoreWindowsAfterLock()
|
||||||
|
self.adjustWindowPosition(changeAlpha: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hideWindowsForLock() {
|
||||||
|
guard !windowsHiddenForLock else { return }
|
||||||
|
windowsHiddenForLock = true
|
||||||
|
|
||||||
|
if Defaults[.showOnAllDisplays] {
|
||||||
|
for window in windows.values {
|
||||||
|
window.alphaValue = 0
|
||||||
|
window.orderOut(nil)
|
||||||
|
}
|
||||||
|
} else if let window = window {
|
||||||
|
window.alphaValue = 0
|
||||||
|
window.orderOut(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func restoreWindowsAfterLock() {
|
||||||
|
guard windowsHiddenForLock else { return }
|
||||||
|
windowsHiddenForLock = false
|
||||||
|
|
||||||
|
if Defaults[.showOnAllDisplays] {
|
||||||
|
for window in windows.values {
|
||||||
|
window.orderFrontRegardless()
|
||||||
|
window.alphaValue = 1
|
||||||
|
}
|
||||||
|
} else if let window = window {
|
||||||
|
window.orderFrontRegardless()
|
||||||
|
window.alphaValue = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -251,11 +285,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
rootView: ContentView()
|
rootView: ContentView()
|
||||||
.environmentObject(viewModel)
|
.environmentObject(viewModel)
|
||||||
.environmentObject(webcamManager)
|
.environmentObject(webcamManager)
|
||||||
|
//.moveToSky()
|
||||||
)
|
)
|
||||||
|
|
||||||
window.orderFrontRegardless()
|
window.orderFrontRegardless()
|
||||||
NotchSpaceManager.shared.notchSpace.windows.insert(window)
|
NotchSpaceManager.shared.notchSpace.windows.insert(window)
|
||||||
|
//SkyLightOperator.shared.delegateWindow(window)
|
||||||
return window
|
return window
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -399,6 +434,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||||
|
|
||||||
coordinator.setupWorkersNotificationObservers()
|
coordinator.setupWorkersNotificationObservers()
|
||||||
|
LockScreenLiveActivityWindowManager.shared.configure(viewModel: vm)
|
||||||
|
LockScreenManager.shared.configure(viewModel: vm)
|
||||||
|
|
||||||
// Migrate legacy progress bar settings
|
// Migrate legacy progress bar settings
|
||||||
Defaults.Keys.migrateProgressBarStyle()
|
Defaults.Keys.migrateProgressBarStyle()
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ enum SneakContentType {
|
||||||
case recording
|
case recording
|
||||||
case bluetoothAudio
|
case bluetoothAudio
|
||||||
case privacy
|
case privacy
|
||||||
|
case lockScreen
|
||||||
}
|
}
|
||||||
|
|
||||||
struct sneakPeek {
|
struct sneakPeek {
|
||||||
|
|
|
||||||
|
|
@ -4816,6 +4816,9 @@
|
||||||
},
|
},
|
||||||
"Controls" : {
|
"Controls" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Copy Latest Crash Report" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Copy something to get started" : {
|
"Copy something to get started" : {
|
||||||
|
|
||||||
|
|
@ -6662,6 +6665,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Enable lock screen media widget" : {
|
||||||
|
"comment" : "A toggle that enables or disables the lock screen media widget.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Enable music live activity" : {
|
"Enable music live activity" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
@ -9182,12 +9189,21 @@
|
||||||
},
|
},
|
||||||
"LinkedIn" : {
|
"LinkedIn" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Liquid Glass requires macOS 26 or later." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Live Monitoring" : {
|
"Live Monitoring" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Live Performance Data" : {
|
"Live Performance Data" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Lock Screen" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Lock screen material" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Loop Mode" : {
|
"Loop Mode" : {
|
||||||
|
|
||||||
|
|
@ -15106,6 +15122,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Shows a lock icon in the Dynamic Island when the screen is locked. The lock screen music panel can be toggled separately in Media settings." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Shows green camera icon and yellow microphone icon when in use. Uses event-driven CoreAudio and CoreMediaIO APIs." : {
|
"Shows green camera icon and yellow microphone icon when in use. Uses event-driven CoreAudio and CoreMediaIO APIs." : {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
// Legacy placeholder file superseded by LockScreenLiveActivityOverlay.swift.
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
//
|
||||||
|
// LockScreenLiveActivity.swift
|
||||||
|
// DynamicIsland
|
||||||
|
//
|
||||||
|
// Created for lock screen live activity
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Defaults
|
||||||
|
|
||||||
|
struct LockScreenLiveActivity: View {
|
||||||
|
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||||
|
@ObservedObject private var lockScreenManager = LockScreenManager.shared
|
||||||
|
@State private var isHovering: Bool = false
|
||||||
|
@State private var gestureProgress: CGFloat = 0
|
||||||
|
@State private var isExpanded: Bool = false
|
||||||
|
@Namespace private var lockIconSpace
|
||||||
|
|
||||||
|
private var iconName: String {
|
||||||
|
lockScreenManager.isLocked ? "lock.fill" : "lock.open.fill"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var iconColor: Color {
|
||||||
|
.white
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
// Left - Lock icon with subtle glow
|
||||||
|
Color.clear
|
||||||
|
.overlay(alignment: .leading) {
|
||||||
|
if isExpanded {
|
||||||
|
Image(systemName: iconName)
|
||||||
|
.font(.system(size: 12, weight: .semibold))
|
||||||
|
.foregroundStyle(iconColor)
|
||||||
|
.matchedGeometryEffect(id: "lock-icon", in: lockIconSpace)
|
||||||
|
.frame(width: vm.effectiveClosedNotchHeight - 12, height: vm.effectiveClosedNotchHeight - 12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: isExpanded ? max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12) + gestureProgress / 2) : 0, height: vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12))
|
||||||
|
|
||||||
|
// Center - Black fill
|
||||||
|
Rectangle()
|
||||||
|
.fill(.black)
|
||||||
|
.frame(width: vm.closedNotchSize.width + (isHovering ? 8 : 0))
|
||||||
|
|
||||||
|
// Right - Empty for symmetry with animation
|
||||||
|
Color.clear
|
||||||
|
.frame(width: isExpanded ? max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12) + gestureProgress / 2) : 0, height: vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12))
|
||||||
|
}
|
||||||
|
.frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0))
|
||||||
|
.onAppear {
|
||||||
|
// Expand immediately without animation to avoid conflicts
|
||||||
|
withAnimation(.smooth(duration: 0.4)) {
|
||||||
|
isExpanded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
// Collapse immediately when removed from hierarchy
|
||||||
|
isExpanded = false
|
||||||
|
}
|
||||||
|
.onChange(of: lockScreenManager.isLockIdle) { _, newValue in
|
||||||
|
if newValue {
|
||||||
|
withAnimation(.smooth(duration: 0.4)) {
|
||||||
|
isExpanded = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
withAnimation(.smooth(duration: 0.4)) {
|
||||||
|
isExpanded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.animation(.spring(response: 0.5, dampingFraction: 0.85), value: lockScreenManager.isLocked)
|
||||||
|
.animation(.easeOut(duration: 0.25), value: isExpanded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
final class LockScreenLiveActivityOverlayModel: ObservableObject {
|
||||||
|
@Published var iconName: String = "lock.fill"
|
||||||
|
@Published var scale: CGFloat = 0.6
|
||||||
|
@Published var opacity: Double = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LockScreenLiveActivityOverlay: View {
|
||||||
|
@ObservedObject var model: LockScreenLiveActivityOverlayModel
|
||||||
|
let notchSize: CGSize
|
||||||
|
|
||||||
|
private var indicatorSize: CGFloat {
|
||||||
|
max(0, notchSize.height - 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var horizontalPadding: CGFloat {
|
||||||
|
cornerRadiusInsets.closed.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
private var totalWidth: CGFloat {
|
||||||
|
notchSize.width + (indicatorSize * 2) + (horizontalPadding * 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Color.clear
|
||||||
|
.overlay(alignment: .leading) {
|
||||||
|
Image(systemName: model.iconName)
|
||||||
|
.font(.system(size: 12, weight: .semibold))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.frame(width: indicatorSize, height: indicatorSize)
|
||||||
|
}
|
||||||
|
.frame(width: indicatorSize, height: notchSize.height)
|
||||||
|
|
||||||
|
Rectangle()
|
||||||
|
.fill(.black)
|
||||||
|
.frame(width: notchSize.width, height: notchSize.height)
|
||||||
|
|
||||||
|
Color.clear
|
||||||
|
.frame(width: indicatorSize, height: notchSize.height)
|
||||||
|
}
|
||||||
|
.frame(width: notchSize.width + (indicatorSize * 2), height: notchSize.height)
|
||||||
|
.padding(.horizontal, horizontalPadding)
|
||||||
|
.background(Color.black)
|
||||||
|
.clipShape(
|
||||||
|
NotchShape(
|
||||||
|
topCornerRadius: cornerRadiusInsets.closed.top,
|
||||||
|
bottomCornerRadius: cornerRadiusInsets.closed.bottom
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.frame(width: totalWidth, height: notchSize.height)
|
||||||
|
.scaleEffect(x: model.scale, y: 1, anchor: .center)
|
||||||
|
.opacity(model.opacity)
|
||||||
|
.drawingGroup()
|
||||||
|
}
|
||||||
|
}
|
||||||
492
DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift
Normal file
492
DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift
Normal file
|
|
@ -0,0 +1,492 @@
|
||||||
|
//
|
||||||
|
// LockScreenMusicPanel.swift
|
||||||
|
// DynamicIsland
|
||||||
|
//
|
||||||
|
// Created for lock screen music panel with liquid glass effect
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Defaults
|
||||||
|
|
||||||
|
struct LockScreenMusicPanel: View {
|
||||||
|
static let collapsedSize = CGSize(width: 420, height: 180)
|
||||||
|
static let expandedSize = CGSize(width: 720, height: 340)
|
||||||
|
|
||||||
|
@ObservedObject var musicManager = MusicManager.shared
|
||||||
|
@State private var sliderValue: Double = 0
|
||||||
|
@State private var dragging: Bool = false
|
||||||
|
@State private var isActive = true
|
||||||
|
@State private var isExpanded = false
|
||||||
|
@State private var collapseWorkItem: DispatchWorkItem?
|
||||||
|
@Default(.lockScreenGlassStyle) var lockScreenGlassStyle
|
||||||
|
@Default(.lockScreenShowAppIcon) var showAppIcon
|
||||||
|
@Default(.lockScreenPanelShowsBorder) var showPanelBorder
|
||||||
|
@Default(.lockScreenPanelUsesBlur) var enableBlur
|
||||||
|
|
||||||
|
private let collapsedPanelCornerRadius: CGFloat = 28
|
||||||
|
private let expandedPanelCornerRadius: CGFloat = 52
|
||||||
|
private let collapsedAlbumArtCornerRadius: CGFloat = 16
|
||||||
|
private let expandedAlbumArtCornerRadius: CGFloat = 60
|
||||||
|
private let expandedContentSpacing: CGFloat = 40
|
||||||
|
private let collapseTimeout: TimeInterval = 5
|
||||||
|
|
||||||
|
private var currentSize: CGSize {
|
||||||
|
isExpanded ? Self.expandedSize : Self.collapsedSize
|
||||||
|
}
|
||||||
|
|
||||||
|
private var panelCornerRadius: CGFloat {
|
||||||
|
isExpanded ? expandedPanelCornerRadius : collapsedPanelCornerRadius
|
||||||
|
}
|
||||||
|
|
||||||
|
private var usesLiquidGlass: Bool {
|
||||||
|
if #available(macOS 26.0, *) {
|
||||||
|
return lockScreenGlassStyle == .liquid
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if isActive {
|
||||||
|
panelContent
|
||||||
|
} else {
|
||||||
|
Color.clear
|
||||||
|
.frame(width: Self.collapsedSize.width, height: Self.collapsedSize.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var panelContent: some View {
|
||||||
|
panelCore
|
||||||
|
.frame(width: currentSize.width, height: currentSize.height)
|
||||||
|
.animation(.spring(response: 0.48, dampingFraction: 0.82, blendDuration: 0.18), value: isExpanded)
|
||||||
|
.onAppear {
|
||||||
|
sliderValue = musicManager.elapsedTime
|
||||||
|
isActive = true
|
||||||
|
logPanelAppearance()
|
||||||
|
LockScreenPanelManager.shared.updatePanelSize(expanded: false, animated: false)
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
isActive = false
|
||||||
|
cancelCollapseTimer()
|
||||||
|
}
|
||||||
|
.onChange(of: isExpanded) { _, expanded in
|
||||||
|
LockScreenPanelManager.shared.updatePanelSize(expanded: expanded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var panelCore: some View {
|
||||||
|
Group {
|
||||||
|
if isExpanded {
|
||||||
|
expandedLayout
|
||||||
|
} else {
|
||||||
|
collapsedLayout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, isExpanded ? 24 : 20)
|
||||||
|
.padding(.vertical, isExpanded ? 22 : 16)
|
||||||
|
.frame(width: currentSize.width, height: currentSize.height, alignment: .topLeading)
|
||||||
|
.background(panelBackground)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: panelCornerRadius, style: .continuous))
|
||||||
|
.overlay {
|
||||||
|
if showPanelBorder {
|
||||||
|
RoundedRectangle(cornerRadius: panelCornerRadius)
|
||||||
|
.stroke(Color.white.opacity(0.35), lineWidth: 1.4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.shadow(color: Color.black.opacity(0.3), radius: 20, x: 0, y: 10)
|
||||||
|
.contentShape(RoundedRectangle(cornerRadius: panelCornerRadius, style: .continuous))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var collapsedLayout: some View {
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
collapsedHeader
|
||||||
|
progressBar
|
||||||
|
.padding(.top, 4)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
playbackControls(alignment: .center)
|
||||||
|
.padding(.top, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var expandedLayout: some View {
|
||||||
|
HStack(alignment: .center, spacing: expandedContentSpacing) {
|
||||||
|
albumArtButton(size: 230, cornerRadius: expandedAlbumArtCornerRadius)
|
||||||
|
.frame(width: 230, height: 230)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
|
expandedHeader
|
||||||
|
progressBar
|
||||||
|
.padding(.top, 10)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
playbackControls(alignment: .leading)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
.frame(maxHeight: .infinity, alignment: .center)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var collapsedHeader: some View {
|
||||||
|
HStack(alignment: .center, spacing: 16) {
|
||||||
|
albumArtButton(size: 60, cornerRadius: collapsedAlbumArtCornerRadius)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 1) {
|
||||||
|
Text(musicManager.songTitle.isEmpty ? "No Music Playing" : musicManager.songTitle)
|
||||||
|
.font(.system(size: 12, weight: .semibold))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.lineLimit(1)
|
||||||
|
|
||||||
|
Text(musicManager.artistName.isEmpty ? "Unknown Artist" : musicManager.artistName)
|
||||||
|
.font(.system(size: 10, weight: .regular))
|
||||||
|
.foregroundColor(Defaults[.playerColorTinting] ? Color(nsColor: musicManager.avgColor).ensureMinimumBrightness(factor: 0.6) : .gray)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
visualizer(width: 20, height: 16)
|
||||||
|
}
|
||||||
|
.frame(height: 60)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var expandedHeader: some View {
|
||||||
|
HStack(alignment: .top, spacing: 16) {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
Text(musicManager.songTitle.isEmpty ? "No Music Playing" : musicManager.songTitle)
|
||||||
|
.font(.system(size: 18, weight: .semibold))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.lineLimit(2)
|
||||||
|
|
||||||
|
Text(musicManager.artistName.isEmpty ? "Unknown Artist" : musicManager.artistName)
|
||||||
|
.font(.system(size: 13, weight: .medium))
|
||||||
|
.foregroundColor(Defaults[.playerColorTinting] ? Color(nsColor: musicManager.avgColor).ensureMinimumBrightness(factor: 0.7) : .gray)
|
||||||
|
.lineLimit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
visualizer(width: 24, height: 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func albumArtButton(size: CGFloat, cornerRadius: CGFloat) -> some View {
|
||||||
|
Button(action: toggleExpanded) {
|
||||||
|
ZStack(alignment: .bottomTrailing) {
|
||||||
|
albumArtImage(size: size, cornerRadius: cornerRadius)
|
||||||
|
if showAppIcon, let icon = lockScreenAppIcon {
|
||||||
|
icon
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fill)
|
||||||
|
.frame(width: appIconSize, height: appIconSize)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: appIconCornerRadius, style: .continuous))
|
||||||
|
.shadow(color: Color.black.opacity(0.35), radius: 6, x: 0, y: 4)
|
||||||
|
.offset(x: appIconOffset, y: appIconOffset)
|
||||||
|
.transition(.scale.combined(with: .opacity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.albumArtFlip(angle: musicManager.flipAngle)
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.background(albumArtBackground(cornerRadius: cornerRadius))
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
.opacity(musicManager.isPlaying ? 1 : 0.4)
|
||||||
|
.scaleEffect(musicManager.isPlaying ? 1 : 0.85)
|
||||||
|
.animation(.easeInOut(duration: 0.2), value: musicManager.isPlaying)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func visualizer(width: CGFloat, height: CGFloat) -> some View {
|
||||||
|
if Defaults[.useMusicVisualizer] {
|
||||||
|
Rectangle()
|
||||||
|
.fill(Defaults[.coloredSpectrogram] ? Color(nsColor: musicManager.avgColor).gradient : Color.gray.gradient)
|
||||||
|
.mask {
|
||||||
|
AudioSpectrumView(isPlaying: .constant(musicManager.isPlaying))
|
||||||
|
.frame(width: width, height: height)
|
||||||
|
}
|
||||||
|
.frame(width: width, height: height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func toggleExpanded() {
|
||||||
|
let newState = !isExpanded
|
||||||
|
withAnimation(.spring(response: 0.5, dampingFraction: 0.82)) {
|
||||||
|
isExpanded = newState
|
||||||
|
}
|
||||||
|
|
||||||
|
if newState {
|
||||||
|
registerInteraction()
|
||||||
|
logPanelAppearance(event: "🔍 Expanded")
|
||||||
|
} else {
|
||||||
|
logPanelAppearance(event: "⬇️ Collapsed")
|
||||||
|
cancelCollapseTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func registerInteraction() {
|
||||||
|
cancelCollapseTimer()
|
||||||
|
guard isExpanded else { return }
|
||||||
|
|
||||||
|
let workItem = DispatchWorkItem {
|
||||||
|
withAnimation(.spring(response: 0.5, dampingFraction: 0.85)) {
|
||||||
|
isExpanded = false
|
||||||
|
}
|
||||||
|
logPanelAppearance(event: "⏱️ Auto-collapsed")
|
||||||
|
}
|
||||||
|
|
||||||
|
collapseWorkItem = workItem
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + collapseTimeout, execute: workItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cancelCollapseTimer() {
|
||||||
|
collapseWorkItem?.cancel()
|
||||||
|
collapseWorkItem = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Progress Bar
|
||||||
|
|
||||||
|
private var progressBar: some View {
|
||||||
|
TimelineView(.animation(minimumInterval: musicManager.playbackRate > 0 ? 0.1 : nil)) { timeline in
|
||||||
|
let currentElapsed = currentSliderValue(timeline.date)
|
||||||
|
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
// Elapsed time
|
||||||
|
Text(formatTime(dragging ? sliderValue : currentElapsed))
|
||||||
|
.font(.system(size: 11, weight: .medium, design: .monospaced))
|
||||||
|
.foregroundColor(.white.opacity(0.7))
|
||||||
|
.frame(width: 42, alignment: .leading)
|
||||||
|
|
||||||
|
// Progress bar
|
||||||
|
GeometryReader { geometry in
|
||||||
|
ZStack(alignment: .leading) {
|
||||||
|
// Background track
|
||||||
|
RoundedRectangle(cornerRadius: 3)
|
||||||
|
.fill(Color.white.opacity(0.2))
|
||||||
|
.frame(height: 6)
|
||||||
|
|
||||||
|
// Filled portion
|
||||||
|
RoundedRectangle(cornerRadius: 3)
|
||||||
|
.fill(sliderColor)
|
||||||
|
.frame(width: max(0, geometry.size.width * (currentSliderValue(timeline.date) / max(musicManager.songDuration, 1))), height: 6)
|
||||||
|
}
|
||||||
|
.gesture(
|
||||||
|
DragGesture(minimumDistance: 0)
|
||||||
|
.onChanged { value in
|
||||||
|
registerInteraction()
|
||||||
|
dragging = true
|
||||||
|
let newValue = min(max(0, Double(value.location.x / geometry.size.width) * musicManager.songDuration), musicManager.songDuration)
|
||||||
|
sliderValue = newValue
|
||||||
|
}
|
||||||
|
.onEnded { _ in
|
||||||
|
registerInteraction()
|
||||||
|
musicManager.seek(to: sliderValue)
|
||||||
|
dragging = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.frame(height: 6)
|
||||||
|
|
||||||
|
// Time remaining
|
||||||
|
Text("-\(formatTime(musicManager.songDuration - (dragging ? sliderValue : currentElapsed)))")
|
||||||
|
.font(.system(size: 11, weight: .medium, design: .monospaced))
|
||||||
|
.foregroundColor(.white.opacity(0.7))
|
||||||
|
.frame(width: 48, alignment: .trailing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
sliderValue = musicManager.elapsedTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func currentSliderValue(_ date: Date) -> Double {
|
||||||
|
if dragging {
|
||||||
|
return sliderValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if musicManager.isPlaying {
|
||||||
|
let timeSinceLastUpdate = date.timeIntervalSince(musicManager.timestampDate)
|
||||||
|
let estimatedElapsed = musicManager.elapsedTime + (timeSinceLastUpdate * musicManager.playbackRate)
|
||||||
|
return min(estimatedElapsed, musicManager.songDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
return musicManager.elapsedTime
|
||||||
|
}
|
||||||
|
|
||||||
|
private var sliderColor: Color {
|
||||||
|
switch Defaults[.sliderColor] {
|
||||||
|
case .white:
|
||||||
|
return .white
|
||||||
|
case .albumArt:
|
||||||
|
return Color(nsColor: musicManager.avgColor)
|
||||||
|
case .accent:
|
||||||
|
return .accentColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formatTime(_ time: TimeInterval) -> String {
|
||||||
|
let minutes = Int(time) / 60
|
||||||
|
let seconds = Int(time) % 60
|
||||||
|
return String(format: "%d:%02d", minutes, seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Playback Controls
|
||||||
|
|
||||||
|
private func playbackControls(alignment: Alignment) -> some View {
|
||||||
|
let spacing: CGFloat = isExpanded ? 24 : 20
|
||||||
|
|
||||||
|
return HStack(spacing: spacing) {
|
||||||
|
// Always show shuffle on lock screen
|
||||||
|
controlButton(icon: "shuffle", isActive: musicManager.isShuffled) {
|
||||||
|
musicManager.toggleShuffle()
|
||||||
|
}
|
||||||
|
|
||||||
|
controlButton(icon: "backward.fill", size: 18) {
|
||||||
|
musicManager.previousTrack()
|
||||||
|
}
|
||||||
|
|
||||||
|
playPauseButton
|
||||||
|
|
||||||
|
controlButton(icon: "forward.fill", size: 18) {
|
||||||
|
musicManager.nextTrack()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always show repeat on lock screen
|
||||||
|
controlButton(icon: repeatIcon, isActive: musicManager.repeatMode != .off) {
|
||||||
|
musicManager.toggleRepeat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: alignment)
|
||||||
|
.padding(.top, isExpanded ? 6 : 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var playPauseButton: some View {
|
||||||
|
let frameSize: CGFloat = isExpanded ? 72 : 48
|
||||||
|
let symbolSize: CGFloat = isExpanded ? 30 : 24
|
||||||
|
|
||||||
|
return Button(action: {
|
||||||
|
registerInteraction()
|
||||||
|
musicManager.togglePlay()
|
||||||
|
}) {
|
||||||
|
Image(systemName: musicManager.isPlaying ? "pause.fill" : "play.fill")
|
||||||
|
.font(.system(size: symbolSize, weight: .semibold))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.frame(width: frameSize, height: frameSize)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
private func controlButton(icon: String, size: CGFloat = 18, isActive: Bool = false, action: @escaping () -> Void) -> some View {
|
||||||
|
let frameSize: CGFloat = isExpanded ? 56 : 32
|
||||||
|
let iconSize: CGFloat = isExpanded ? max(size, 24) : size
|
||||||
|
|
||||||
|
return Button(action: {
|
||||||
|
registerInteraction()
|
||||||
|
action()
|
||||||
|
}) {
|
||||||
|
Image(systemName: icon)
|
||||||
|
.font(.system(size: iconSize, weight: .medium))
|
||||||
|
.foregroundColor(isActive ? .red : .white.opacity(0.8))
|
||||||
|
.frame(width: frameSize, height: frameSize)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
private var repeatIcon: String {
|
||||||
|
switch musicManager.repeatMode {
|
||||||
|
case .off: return "repeat"
|
||||||
|
case .all: return "repeat"
|
||||||
|
case .one: return "repeat.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var panelBackground: some View {
|
||||||
|
if enableBlur {
|
||||||
|
if usesLiquidGlass {
|
||||||
|
liquidPanelBackground
|
||||||
|
} else {
|
||||||
|
frostedPanelBackground
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RoundedRectangle(cornerRadius: panelCornerRadius)
|
||||||
|
.fill(Color.black.opacity(0.45))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var liquidPanelBackground: some View {
|
||||||
|
if #available(macOS 26.0, *) {
|
||||||
|
RoundedRectangle(cornerRadius: panelCornerRadius)
|
||||||
|
.glassEffect(
|
||||||
|
.regular
|
||||||
|
.tint(Color.white.opacity(0.12))
|
||||||
|
.interactive(),
|
||||||
|
in: .rect(cornerRadius: panelCornerRadius)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var frostedPanelBackground: some View {
|
||||||
|
RoundedRectangle(cornerRadius: panelCornerRadius)
|
||||||
|
.fill(.ultraThinMaterial)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func albumArtImage(size: CGFloat, cornerRadius: CGFloat) -> some View {
|
||||||
|
Image(nsImage: musicManager.albumArt)
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fill)
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func albumArtBackground(cornerRadius: CGFloat) -> some View {
|
||||||
|
if enableBlur {
|
||||||
|
if usesLiquidGlass {
|
||||||
|
if #available(macOS 26.0, *) {
|
||||||
|
RoundedRectangle(cornerRadius: cornerRadius)
|
||||||
|
.glassEffect(
|
||||||
|
.regular
|
||||||
|
.tint(Color.white.opacity(0.16))
|
||||||
|
.interactive(),
|
||||||
|
in: .rect(cornerRadius: cornerRadius)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
RoundedRectangle(cornerRadius: cornerRadius)
|
||||||
|
.fill(.ultraThinMaterial)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RoundedRectangle(cornerRadius: cornerRadius)
|
||||||
|
.fill(.ultraThinMaterial)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RoundedRectangle(cornerRadius: cornerRadius)
|
||||||
|
.fill(Color.black.opacity(0.35))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var lockScreenAppIcon: Image? {
|
||||||
|
guard showAppIcon, !musicManager.usingAppIconForArtwork else { return nil }
|
||||||
|
let bundleIdentifier = musicManager.bundleIdentifier ?? "com.apple.Music"
|
||||||
|
return AppIcon(for: bundleIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var appIconSize: CGFloat {
|
||||||
|
isExpanded ? 58 : 34
|
||||||
|
}
|
||||||
|
|
||||||
|
private var appIconCornerRadius: CGFloat {
|
||||||
|
isExpanded ? 18 : 10
|
||||||
|
}
|
||||||
|
|
||||||
|
private var appIconOffset: CGFloat {
|
||||||
|
isExpanded ? 18 : 12
|
||||||
|
}
|
||||||
|
|
||||||
|
private func logPanelAppearance(event: String = "✅ View appeared") {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateFormat = "HH:mm:ss.SSS"
|
||||||
|
let styleDescriptor = usesLiquidGlass ? "Liquid Glass" : "Frosted"
|
||||||
|
print("[\(formatter.string(from: Date()))] LockScreenMusicPanel: \(event) – \(styleDescriptor)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -58,6 +58,7 @@ struct MinimalisticMusicView: View {
|
||||||
)
|
)
|
||||||
.clipped()
|
.clipped()
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 18)) // Dramatically increased corner radius for minimalistic mode
|
.clipShape(RoundedRectangle(cornerRadius: 18)) // Dramatically increased corner radius for minimalistic mode
|
||||||
|
.albumArtFlip(angle: musicManager.flipAngle)
|
||||||
.frame(width: max(0, vm.effectiveClosedNotchHeight - 12), height: max(0, vm.effectiveClosedNotchHeight - 12))
|
.frame(width: max(0, vm.effectiveClosedNotchHeight - 12), height: max(0, vm.effectiveClosedNotchHeight - 12))
|
||||||
}
|
}
|
||||||
.frame(width: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)), height: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)))
|
.frame(width: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)), height: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)))
|
||||||
|
|
|
||||||
|
|
@ -192,24 +192,29 @@ struct MinimalisticMusicPlayerView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var playPauseButton: some View {
|
private var playPauseButton: some View {
|
||||||
Button(action: {
|
MinimalisticSquircircleButton(
|
||||||
Task { await musicManager.togglePlay() }
|
icon: musicManager.isPlaying ? "pause.fill" : "play.fill",
|
||||||
}) {
|
fontSize: 24,
|
||||||
Image(systemName: musicManager.isPlaying ? "pause.fill" : "play.fill")
|
fontWeight: .semibold,
|
||||||
.font(.system(size: 24, weight: .semibold))
|
frameSize: CGSize(width: 54, height: 54),
|
||||||
.foregroundColor(.white)
|
cornerRadius: 20,
|
||||||
.frame(width: 50, height: 50)
|
foregroundColor: .white,
|
||||||
}
|
action: {
|
||||||
.buttonStyle(PlainButtonStyle())
|
Task { await musicManager.togglePlay() }
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func controlButton(icon: String, size: CGFloat = 18, isActive: Bool = false, action: @escaping () -> Void) -> some View {
|
private func controlButton(icon: String, size: CGFloat = 18, isActive: Bool = false, action: @escaping () -> Void) -> some View {
|
||||||
Button(action: action) {
|
MinimalisticSquircircleButton(
|
||||||
Image(systemName: icon)
|
icon: icon,
|
||||||
.font(.system(size: size, weight: .medium))
|
fontSize: size,
|
||||||
.foregroundColor(isActive ? .red : .white.opacity(0.8))
|
fontWeight: .medium,
|
||||||
}
|
frameSize: CGSize(width: 40, height: 40),
|
||||||
.buttonStyle(PlainButtonStyle())
|
cornerRadius: 16,
|
||||||
|
foregroundColor: isActive ? .red : .white.opacity(0.85),
|
||||||
|
action: action
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var repeatIcon: String {
|
private var repeatIcon: String {
|
||||||
|
|
@ -263,14 +268,48 @@ struct MinimalisticAlbumArtView: View {
|
||||||
Image(nsImage: musicManager.albumArt)
|
Image(nsImage: musicManager.albumArt)
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fill)
|
.aspectRatio(contentMode: .fill)
|
||||||
.animation(.spring(response: 0.4, dampingFraction: 0.8), value: musicManager.isFlipping)
|
|
||||||
)
|
)
|
||||||
.clipped()
|
.clipped()
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||||
.matchedGeometryEffect(id: "albumArt", in: albumArtNamespace)
|
.matchedGeometryEffect(id: "albumArt", in: albumArtNamespace)
|
||||||
|
.albumArtFlip(angle: musicManager.flipAngle)
|
||||||
}
|
}
|
||||||
.buttonStyle(PlainButtonStyle())
|
.buttonStyle(PlainButtonStyle())
|
||||||
.opacity(musicManager.isPlaying ? 1 : 0.4)
|
.opacity(musicManager.isPlaying ? 1 : 0.4)
|
||||||
.scaleEffect(musicManager.isPlaying ? 1 : 0.85)
|
.scaleEffect(musicManager.isPlaying ? 1 : 0.85)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Hover-highlighted control button
|
||||||
|
|
||||||
|
private struct MinimalisticSquircircleButton: View {
|
||||||
|
let icon: String
|
||||||
|
let fontSize: CGFloat
|
||||||
|
let fontWeight: Font.Weight
|
||||||
|
let frameSize: CGSize
|
||||||
|
let cornerRadius: CGFloat
|
||||||
|
let foregroundColor: Color
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
@State private var isHovering = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: action) {
|
||||||
|
Image(systemName: icon)
|
||||||
|
.font(.system(size: fontSize, weight: fontWeight))
|
||||||
|
.foregroundColor(foregroundColor)
|
||||||
|
.frame(width: frameSize.width, height: frameSize.height)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
|
||||||
|
.fill(isHovering ? Color.white.opacity(0.18) : .clear)
|
||||||
|
)
|
||||||
|
.contentShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
.onHover { hovering in
|
||||||
|
withAnimation(.easeOut(duration: 0.18)) {
|
||||||
|
isHovering = hovering
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ struct AlbumArtView: View {
|
||||||
albumArtImage
|
albumArtImage
|
||||||
appIconOverlay
|
appIconOverlay
|
||||||
}
|
}
|
||||||
|
.albumArtFlip(angle: musicManager.flipAngle)
|
||||||
}
|
}
|
||||||
.buttonStyle(PlainButtonStyle())
|
.buttonStyle(PlainButtonStyle())
|
||||||
.scaleEffect(musicManager.isPlaying ? 1 : 0.85)
|
.scaleEffect(musicManager.isPlaying ? 1 : 0.85)
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,10 @@ struct GeneralSettings: View {
|
||||||
@Default(.enableScreenRecordingDetection) var enableScreenRecordingDetection
|
@Default(.enableScreenRecordingDetection) var enableScreenRecordingDetection
|
||||||
@Default(.showRecordingIndicator) var showRecordingIndicator
|
@Default(.showRecordingIndicator) var showRecordingIndicator
|
||||||
@Default(.enableMinimalisticUI) var enableMinimalisticUI
|
@Default(.enableMinimalisticUI) var enableMinimalisticUI
|
||||||
|
@Default(.lockScreenGlassStyle) var lockScreenGlassStyle
|
||||||
|
@Default(.lockScreenShowAppIcon) var lockScreenShowAppIcon
|
||||||
|
@Default(.lockScreenPanelShowsBorder) var lockScreenPanelShowsBorder
|
||||||
|
@Default(.lockScreenPanelUsesBlur) var lockScreenPanelUsesBlur
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
|
|
@ -295,6 +299,38 @@ struct GeneralSettings: View {
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("Shows green camera icon and yellow microphone icon when in use. Uses event-driven CoreAudio and CoreMediaIO APIs.")
|
Text("Shows green camera icon and yellow microphone icon when in use. Uses event-driven CoreAudio and CoreMediaIO APIs.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
Defaults.Toggle("Enable Lock Screen Live Activity", key: .enableLockScreenLiveActivity)
|
||||||
|
if #available(macOS 26.0, *) {
|
||||||
|
Picker("Lock screen material", selection: $lockScreenGlassStyle) {
|
||||||
|
ForEach(LockScreenGlassStyle.allCases) { style in
|
||||||
|
Text(style.rawValue).tag(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Picker("Lock screen material", selection: $lockScreenGlassStyle) {
|
||||||
|
ForEach(LockScreenGlassStyle.allCases) { style in
|
||||||
|
Text(style.rawValue).tag(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disabled(true)
|
||||||
|
Text("Liquid Glass requires macOS 26 or later.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
Defaults.Toggle("Show media app icon", key: .lockScreenShowAppIcon)
|
||||||
|
Defaults.Toggle("Show panel border", key: .lockScreenPanelShowsBorder)
|
||||||
|
Defaults.Toggle("Enable blur", key: .lockScreenPanelUsesBlur)
|
||||||
|
|
||||||
|
Button("Copy Latest Crash Report") {
|
||||||
|
copyLatestCrashReport()
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Lock Screen")
|
||||||
|
} footer: {
|
||||||
|
Text("Shows a lock icon in the Dynamic Island when the screen is locked. The lock screen music panel can be toggled separately in Media settings.")
|
||||||
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Picker(selection: $notchHeightMode, label:
|
Picker(selection: $notchHeightMode, label:
|
||||||
|
|
@ -373,6 +409,43 @@ struct GeneralSettings: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func copyLatestCrashReport() {
|
||||||
|
let crashReportsPath = NSString(string: "~/Library/Logs/DiagnosticReports").expandingTildeInPath
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
|
||||||
|
do {
|
||||||
|
let files = try fileManager.contentsOfDirectory(atPath: crashReportsPath)
|
||||||
|
let crashFiles = files.filter { $0.contains("DynamicIsland") && $0.hasSuffix(".crash") }
|
||||||
|
|
||||||
|
guard let latestCrash = crashFiles.sorted(by: >).first else {
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = "No Crash Reports Found"
|
||||||
|
alert.informativeText = "No crash reports found for DynamicIsland"
|
||||||
|
alert.alertStyle = .informational
|
||||||
|
alert.runModal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let crashPath = (crashReportsPath as NSString).appendingPathComponent(latestCrash)
|
||||||
|
let crashContent = try String(contentsOfFile: crashPath, encoding: .utf8)
|
||||||
|
|
||||||
|
NSPasteboard.general.clearContents()
|
||||||
|
NSPasteboard.general.setString(crashContent, forType: .string)
|
||||||
|
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = "Crash Report Copied"
|
||||||
|
alert.informativeText = "Crash report '\(latestCrash)' has been copied to clipboard"
|
||||||
|
alert.alertStyle = .informational
|
||||||
|
alert.runModal()
|
||||||
|
} catch {
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = "Error"
|
||||||
|
alert.informativeText = "Failed to read crash reports: \(error.localizedDescription)"
|
||||||
|
alert.alertStyle = .warning
|
||||||
|
alert.runModal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func gestureControls() -> some View {
|
func gestureControls() -> some View {
|
||||||
|
|
@ -709,6 +782,12 @@ struct Media: View {
|
||||||
customBadge(text: "Beta")
|
customBadge(text: "Beta")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Enable/disable the lock screen media widget
|
||||||
|
Defaults.Toggle(key: .enableLockScreenMediaWidget) {
|
||||||
|
HStack {
|
||||||
|
Text("Enable lock screen media widget")
|
||||||
|
}
|
||||||
|
}
|
||||||
} header: {
|
} header: {
|
||||||
Text("Media controls")
|
Text("Media controls")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,3 +70,10 @@ enum SliderColorEnum: String, CaseIterable, Defaults.Serializable {
|
||||||
case albumArt = "Match album art"
|
case albumArt = "Match album art"
|
||||||
case accent = "Accent color"
|
case accent = "Accent color"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum LockScreenGlassStyle: String, CaseIterable, Defaults.Serializable, Identifiable {
|
||||||
|
case liquid = "Liquid Glass"
|
||||||
|
case frosted = "Frosted Glass"
|
||||||
|
|
||||||
|
var id: String { rawValue }
|
||||||
|
}
|
||||||
|
|
|
||||||
1
DynamicIsland/extensions/AlbumArtFlipModifier.swift
Normal file
1
DynamicIsland/extensions/AlbumArtFlipModifier.swift
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
// Deprecated placeholder: albumArtFlip modifier now lives alongside MusicManager.
|
||||||
213
DynamicIsland/managers/LockScreenLiveActivityWindowManager.swift
Normal file
213
DynamicIsland/managers/LockScreenLiveActivityWindowManager.swift
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
//
|
||||||
|
// LockScreenLiveActivityWindowManager.swift
|
||||||
|
// DynamicIsland
|
||||||
|
//
|
||||||
|
// Delegates the lock screen live activity layout to SkyLight so it remains visible when the display is locked.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AppKit
|
||||||
|
import Defaults
|
||||||
|
import SkyLightWindow
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class LockScreenLiveActivityWindowManager {
|
||||||
|
static let shared = LockScreenLiveActivityWindowManager()
|
||||||
|
|
||||||
|
private var window: NSWindow?
|
||||||
|
private var hasDelegated = false
|
||||||
|
private var hideTask: Task<Void, Never>?
|
||||||
|
private var hostingView: NSHostingView<LockScreenLiveActivityOverlay>?
|
||||||
|
private let overlayModel = LockScreenLiveActivityOverlayModel()
|
||||||
|
private weak var viewModel: DynamicIslandViewModel?
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
private func timestamp() -> String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateFormat = "HH:mm:ss.SSS"
|
||||||
|
return formatter.string(from: Date())
|
||||||
|
}
|
||||||
|
|
||||||
|
private func windowSize(for notchSize: CGSize) -> CGSize {
|
||||||
|
let indicatorWidth = max(0, notchSize.height - 12)
|
||||||
|
let horizontalPadding = cornerRadiusInsets.closed.bottom
|
||||||
|
|
||||||
|
let totalWidth = notchSize.width + (indicatorWidth * 2) + (horizontalPadding * 2)
|
||||||
|
|
||||||
|
return CGSize(width: totalWidth, height: notchSize.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func frame(for windowSize: CGSize, on screen: NSScreen) -> NSRect {
|
||||||
|
let screenFrame = screen.frame
|
||||||
|
let originX = screenFrame.origin.x + (screenFrame.width / 2) - (windowSize.width / 2)
|
||||||
|
let originY = screenFrame.origin.y + screenFrame.height - windowSize.height
|
||||||
|
|
||||||
|
return NSRect(x: originX, y: originY, width: windowSize.width, height: windowSize.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func ensureWindow(windowSize: CGSize, screen: NSScreen) -> NSWindow {
|
||||||
|
if let window {
|
||||||
|
return window
|
||||||
|
}
|
||||||
|
|
||||||
|
let window = NSWindow(
|
||||||
|
contentRect: frame(for: windowSize, on: screen),
|
||||||
|
styleMask: [.borderless, .nonactivatingPanel],
|
||||||
|
backing: .buffered,
|
||||||
|
defer: false
|
||||||
|
)
|
||||||
|
|
||||||
|
window.isOpaque = false
|
||||||
|
window.backgroundColor = .clear
|
||||||
|
window.isReleasedWhenClosed = false
|
||||||
|
window.ignoresMouseEvents = true
|
||||||
|
window.hasShadow = false
|
||||||
|
window.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()))
|
||||||
|
window.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
|
||||||
|
window.alphaValue = 0
|
||||||
|
window.animationBehavior = .none
|
||||||
|
|
||||||
|
self.window = window
|
||||||
|
self.hasDelegated = false
|
||||||
|
return window
|
||||||
|
}
|
||||||
|
|
||||||
|
private func lockContext() -> (notchSize: CGSize, screen: NSScreen)? {
|
||||||
|
guard let screen = NSScreen.main else {
|
||||||
|
print("[\(timestamp())] LockScreenLiveActivityWindowManager: no main screen available")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let viewModel else {
|
||||||
|
print("[\(timestamp())] LockScreenLiveActivityWindowManager: no view model configured")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var notchSize = viewModel.closedNotchSize
|
||||||
|
if notchSize.width <= 0 || notchSize.height <= 0 {
|
||||||
|
notchSize = getClosedNotchSize(screen: screen.localizedName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (notchSize, screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func present(notchSize: CGSize, on screen: NSScreen) {
|
||||||
|
guard Defaults[.enableLockScreenLiveActivity] else {
|
||||||
|
hideImmediately()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let windowSize = windowSize(for: notchSize)
|
||||||
|
let window = ensureWindow(windowSize: windowSize, screen: screen)
|
||||||
|
let targetFrame = frame(for: windowSize, on: screen)
|
||||||
|
window.setFrame(targetFrame, display: true)
|
||||||
|
|
||||||
|
let overlayView = LockScreenLiveActivityOverlay(model: overlayModel, notchSize: notchSize)
|
||||||
|
|
||||||
|
if let hostingView {
|
||||||
|
hostingView.rootView = overlayView
|
||||||
|
hostingView.frame = CGRect(origin: .zero, size: targetFrame.size)
|
||||||
|
} else {
|
||||||
|
let view = NSHostingView(rootView: overlayView)
|
||||||
|
view.frame = CGRect(origin: .zero, size: targetFrame.size)
|
||||||
|
hostingView = view
|
||||||
|
window.contentView = view
|
||||||
|
}
|
||||||
|
|
||||||
|
if window.contentView !== hostingView {
|
||||||
|
window.contentView = hostingView
|
||||||
|
}
|
||||||
|
|
||||||
|
window.displayIfNeeded()
|
||||||
|
|
||||||
|
if !hasDelegated {
|
||||||
|
SkyLightOperator.shared.delegateWindow(window)
|
||||||
|
hasDelegated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
window.orderFrontRegardless()
|
||||||
|
window.alphaValue = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func showLocked() {
|
||||||
|
hideTask?.cancel()
|
||||||
|
guard let context = lockContext() else { return }
|
||||||
|
|
||||||
|
overlayModel.iconName = "lock.fill"
|
||||||
|
overlayModel.scale = 0.6
|
||||||
|
overlayModel.opacity = 0
|
||||||
|
|
||||||
|
present(notchSize: context.notchSize, on: context.screen)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
withAnimation(.spring(response: 0.45, dampingFraction: 0.82)) {
|
||||||
|
self.overlayModel.scale = 1
|
||||||
|
}
|
||||||
|
withAnimation(.easeOut(duration: 0.18)) {
|
||||||
|
self.overlayModel.opacity = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print("[\(timestamp())] LockScreenLiveActivityWindowManager: showing locked state")
|
||||||
|
}
|
||||||
|
|
||||||
|
func showUnlockAndScheduleHide() {
|
||||||
|
hideTask?.cancel()
|
||||||
|
guard let context = lockContext() else { return }
|
||||||
|
|
||||||
|
overlayModel.iconName = "lock.open.fill"
|
||||||
|
overlayModel.scale = 0.7
|
||||||
|
overlayModel.opacity = 0
|
||||||
|
|
||||||
|
present(notchSize: context.notchSize, on: context.screen)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
withAnimation(.spring(response: 0.45, dampingFraction: 0.85)) {
|
||||||
|
self.overlayModel.scale = 1
|
||||||
|
}
|
||||||
|
withAnimation(.easeOut(duration: 0.16)) {
|
||||||
|
self.overlayModel.opacity = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideTask = Task { [weak self] in
|
||||||
|
try? await Task.sleep(for: .seconds(max(Defaults[.waitInterval], 1.0)))
|
||||||
|
guard let self, !Task.isCancelled else { return }
|
||||||
|
await MainActor.run {
|
||||||
|
self.hideWithAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hideImmediately() {
|
||||||
|
hideTask?.cancel()
|
||||||
|
hideTask = nil
|
||||||
|
|
||||||
|
hideWithAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hideWithAnimation() {
|
||||||
|
guard let window else { return }
|
||||||
|
|
||||||
|
withAnimation(.easeInOut(duration: 0.18)) {
|
||||||
|
overlayModel.opacity = 0
|
||||||
|
overlayModel.scale = 0.7
|
||||||
|
}
|
||||||
|
|
||||||
|
NSAnimationContext.runAnimationGroup { context in
|
||||||
|
context.duration = 0.2
|
||||||
|
window.animator().alphaValue = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.22) {
|
||||||
|
window.orderOut(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
print("[\(timestamp())] LockScreenLiveActivityWindowManager: HUD hidden")
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure(viewModel: DynamicIslandViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
171
DynamicIsland/managers/LockScreenManager.swift
Normal file
171
DynamicIsland/managers/LockScreenManager.swift
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
private weak var viewModel: DynamicIslandViewModel?
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
viewModel?.closeForLockScreen()
|
||||||
|
|
||||||
|
if coordinator.expandingView.show {
|
||||||
|
let currentType = coordinator.expandingView.type
|
||||||
|
coordinator.toggleExpandingView(status: false, type: currentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if coordinator.sneakPeek.show {
|
||||||
|
coordinator.toggleSneakPeek(status: false, type: coordinator.sneakPeek.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show panel FIRST (creates and shows window on lock screen)
|
||||||
|
print("[\(timestamp())] LockScreenManager: 🎵 Showing lock screen panel")
|
||||||
|
LockScreenPanelManager.shared.showPanel()
|
||||||
|
LockScreenLiveActivityWindowManager.shared.showLocked()
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
LockScreenLiveActivityWindowManager.shared.showUnlockAndScheduleHide()
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
func configure(viewModel: DynamicIslandViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
122
DynamicIsland/managers/LockScreenPanelManager.swift
Normal file
122
DynamicIsland/managers/LockScreenPanelManager.swift
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
//
|
||||||
|
// 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 var collapsedFrame: NSRect?
|
||||||
|
|
||||||
|
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 collapsedSize = LockScreenMusicPanel.collapsedSize
|
||||||
|
let screenFrame = screen.frame
|
||||||
|
let centerX = screenFrame.origin.x + (screenFrame.width / 2)
|
||||||
|
let originX = centerX - (collapsedSize.width / 2)
|
||||||
|
let originY = screenFrame.origin.y + (screenFrame.height / 2) - collapsedSize.height - 32
|
||||||
|
let targetFrame = NSRect(x: originX, y: originY, width: collapsedSize.width, height: collapsedSize.height)
|
||||||
|
collapsedFrame = targetFrame
|
||||||
|
|
||||||
|
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 updatePanelSize(expanded: Bool, animated: Bool = true) {
|
||||||
|
guard let window = panelWindow, let baseFrame = collapsedFrame else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetSize = expanded ? LockScreenMusicPanel.expandedSize : LockScreenMusicPanel.collapsedSize
|
||||||
|
let originX = baseFrame.midX - (targetSize.width / 2)
|
||||||
|
let originY = baseFrame.origin.y
|
||||||
|
let targetFrame = NSRect(x: originX, y: originY, width: targetSize.width, height: targetSize.height)
|
||||||
|
|
||||||
|
if animated {
|
||||||
|
window.animator().setFrame(targetFrame, display: true)
|
||||||
|
} else {
|
||||||
|
window.setFrame(targetFrame, display: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -55,8 +55,8 @@ class MusicManager: ObservableObject {
|
||||||
private var lastArtworkAlbum: String = "Self Love"
|
private var lastArtworkAlbum: String = "Self Love"
|
||||||
private var lastArtworkBundleIdentifier: String? = nil
|
private var lastArtworkBundleIdentifier: String? = nil
|
||||||
|
|
||||||
@Published var isFlipping: Bool = false
|
@Published var flipAngle: Double = 0
|
||||||
private var flipWorkItem: DispatchWorkItem?
|
private let flipAnimationDuration: TimeInterval = 0.45
|
||||||
|
|
||||||
@Published var isTransitioning: Bool = false
|
@Published var isTransitioning: Bool = false
|
||||||
private var transitionWorkItem: DispatchWorkItem?
|
private var transitionWorkItem: DispatchWorkItem?
|
||||||
|
|
@ -93,7 +93,6 @@ class MusicManager: ObservableObject {
|
||||||
debounceIdleTask?.cancel()
|
debounceIdleTask?.cancel()
|
||||||
cancellables.removeAll()
|
cancellables.removeAll()
|
||||||
controllerCancellables.removeAll()
|
controllerCancellables.removeAll()
|
||||||
flipWorkItem?.cancel()
|
|
||||||
transitionWorkItem?.cancel()
|
transitionWorkItem?.cancel()
|
||||||
|
|
||||||
// Release active controller
|
// Release active controller
|
||||||
|
|
@ -159,9 +158,6 @@ class MusicManager: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setActiveController(_ controller: any MediaControllerProtocol) {
|
private func setActiveController(_ controller: any MediaControllerProtocol) {
|
||||||
// Cancel any existing flip animation
|
|
||||||
flipWorkItem?.cancel()
|
|
||||||
|
|
||||||
// Set new active controller
|
// Set new active controller
|
||||||
activeController = controller
|
activeController = controller
|
||||||
|
|
||||||
|
|
@ -270,19 +266,9 @@ class MusicManager: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func triggerFlipAnimation() {
|
private func triggerFlipAnimation() {
|
||||||
// Cancel any existing animation
|
withAnimation(.easeInOut(duration: flipAnimationDuration)) {
|
||||||
flipWorkItem?.cancel()
|
flipAngle += 180
|
||||||
|
|
||||||
// Create a new animation
|
|
||||||
let workItem = DispatchWorkItem { [weak self] in
|
|
||||||
self?.isFlipping = true
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
|
||||||
self?.isFlipping = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flipWorkItem = workItem
|
|
||||||
DispatchQueue.main.async(execute: workItem)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateArtwork(_ artworkData: Data) {
|
private func updateArtwork(_ artworkData: Data) {
|
||||||
|
|
@ -447,3 +433,35 @@ class MusicManager: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Album Art Flip Helper
|
||||||
|
|
||||||
|
private struct AlbumArtFlipModifier: ViewModifier {
|
||||||
|
let angle: Double
|
||||||
|
|
||||||
|
private var normalizedAngle: Double {
|
||||||
|
var value = angle.truncatingRemainder(dividingBy: 360)
|
||||||
|
if value < 0 { value += 360 }
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mirrorScale: CGFloat {
|
||||||
|
(normalizedAngle > 90 && normalizedAngle < 270) ? -1 : 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
.rotation3DEffect(
|
||||||
|
.degrees(angle),
|
||||||
|
axis: (x: 0, y: 1, z: 0),
|
||||||
|
perspective: 0.65
|
||||||
|
)
|
||||||
|
.scaleEffect(x: mirrorScale, y: 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
func albumArtFlip(angle: Double) -> some View {
|
||||||
|
modifier(AlbumArtFlipModifier(angle: angle))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -334,6 +334,12 @@ extension Defaults.Keys {
|
||||||
static let enableFullscreenMediaDetection = Key<Bool>("enableFullscreenMediaDetection", default: true)
|
static let enableFullscreenMediaDetection = Key<Bool>("enableFullscreenMediaDetection", default: true)
|
||||||
static let waitInterval = Key<Double>("waitInterval", default: 3)
|
static let waitInterval = Key<Double>("waitInterval", default: 3)
|
||||||
static let showShuffleAndRepeat = Key<Bool>("showShuffleAndRepeat", default: false)
|
static let showShuffleAndRepeat = Key<Bool>("showShuffleAndRepeat", default: false)
|
||||||
|
// Enable lock screen media widget (shows the standalone panel when screen is locked)
|
||||||
|
static let enableLockScreenMediaWidget = Key<Bool>("enableLockScreenMediaWidget", default: true)
|
||||||
|
static let lockScreenGlassStyle = Key<LockScreenGlassStyle>("lockScreenGlassStyle", default: .liquid)
|
||||||
|
static let lockScreenShowAppIcon = Key<Bool>("lockScreenShowAppIcon", default: false)
|
||||||
|
static let lockScreenPanelShowsBorder = Key<Bool>("lockScreenPanelShowsBorder", default: true)
|
||||||
|
static let lockScreenPanelUsesBlur = Key<Bool>("lockScreenPanelUsesBlur", default: true)
|
||||||
|
|
||||||
// MARK: Battery
|
// MARK: Battery
|
||||||
static let showPowerStatusNotifications = Key<Bool>("showPowerStatusNotifications", default: true)
|
static let showPowerStatusNotifications = Key<Bool>("showPowerStatusNotifications", default: true)
|
||||||
|
|
@ -436,6 +442,9 @@ extension Defaults.Keys {
|
||||||
static let enableCameraDetection = Key<Bool>("enableCameraDetection", default: true)
|
static let enableCameraDetection = Key<Bool>("enableCameraDetection", default: true)
|
||||||
static let enableMicrophoneDetection = Key<Bool>("enableMicrophoneDetection", default: true)
|
static let enableMicrophoneDetection = Key<Bool>("enableMicrophoneDetection", default: true)
|
||||||
|
|
||||||
|
// MARK: Lock Screen Features
|
||||||
|
static let enableLockScreenLiveActivity = Key<Bool>("enableLockScreenLiveActivity", default: true)
|
||||||
|
|
||||||
// MARK: ImageService
|
// MARK: ImageService
|
||||||
static let didClearLegacyURLCacheV1 = Key<Bool>("didClearLegacyURLCacheV1", default: false)
|
static let didClearLegacyURLCacheV1 = Key<Bool>("didClearLegacyURLCacheV1", default: false)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,15 @@ class DynamicIslandViewModel: NSObject, ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func closeForLockScreen() {
|
||||||
|
let targetSize = getClosedNotchSize(screen: screen)
|
||||||
|
withAnimation(.none) {
|
||||||
|
notchSize = targetSize
|
||||||
|
closedNotchSize = targetSize
|
||||||
|
notchState = .closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func closeHello() {
|
func closeHello() {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) { [weak self] in
|
||||||
self?.coordinator.firstLaunch = false
|
self?.coordinator.firstLaunch = false
|
||||||
|
|
|
||||||
1
DynamicIsland/utils/Environment+LockScreenReplica.swift
Normal file
1
DynamicIsland/utils/Environment+LockScreenReplica.swift
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
// Deprecated: lockScreenReplica environment key now lives in ContentView.swift.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue