mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Merge pull request #458 from StudioKeys/bugfix/minimalist-notch
Atoll: Fix layout issues on Minimalist Notch UI and improve widgets upon unlock
This commit is contained in:
commit
bc858401c7
3 changed files with 80 additions and 22 deletions
|
|
@ -113,6 +113,7 @@ struct MinimalisticMusicPlayerView: View {
|
||||||
// Compact progress bar
|
// Compact progress bar
|
||||||
progressBar
|
progressBar
|
||||||
.padding(.top, 6)
|
.padding(.top, 6)
|
||||||
|
.clipped()
|
||||||
|
|
||||||
// Compact playback controls
|
// Compact playback controls
|
||||||
if shouldShowControlHUDRow {
|
if shouldShowControlHUDRow {
|
||||||
|
|
@ -133,7 +134,7 @@ struct MinimalisticMusicPlayerView: View {
|
||||||
reminderList
|
reminderList
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
.padding(.top, -15)
|
.padding(.top, 12)
|
||||||
.padding(.bottom, ReminderLiveActivityManager.baselineMinimalisticBottomPadding)
|
.padding(.bottom, ReminderLiveActivityManager.baselineMinimalisticBottomPadding)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.frame(height: calculateDynamicHeight(), alignment: .top)
|
.frame(height: calculateDynamicHeight(), alignment: .top)
|
||||||
|
|
@ -254,7 +255,7 @@ struct MinimalisticMusicPlayerView: View {
|
||||||
height += 6 + 4 // progress bar + top padding
|
height += 6 + 4 // progress bar + top padding
|
||||||
|
|
||||||
// Add playback controls height
|
// Add playback controls height
|
||||||
height += 40 + 2 // controls + top padding
|
height += 54 + 2 // controls + top padding
|
||||||
|
|
||||||
// Add lyrics height if enabled in settings (reserve space even while loading)
|
// Add lyrics height if enabled in settings (reserve space even while loading)
|
||||||
if enableLyrics {
|
if enableLyrics {
|
||||||
|
|
@ -706,7 +707,7 @@ private struct MinimalisticReminderDetailsView: View {
|
||||||
// MARK: - Playback Controls (Larger)
|
// MARK: - Playback Controls (Larger)
|
||||||
|
|
||||||
private var playbackControls: some View {
|
private var playbackControls: some View {
|
||||||
HStack(spacing: 16) {
|
HStack(spacing: 10) {
|
||||||
ForEach(Array(displayedSlots.enumerated()), id: \.offset) { _, slot in
|
ForEach(Array(displayedSlots.enumerated()), id: \.offset) { _, slot in
|
||||||
slotView(for: slot)
|
slotView(for: slot)
|
||||||
}
|
}
|
||||||
|
|
@ -834,10 +835,10 @@ private struct MinimalisticReminderDetailsView: View {
|
||||||
private var playPauseButton: some View {
|
private var playPauseButton: some View {
|
||||||
MinimalisticSquircircleButton(
|
MinimalisticSquircircleButton(
|
||||||
icon: musicManager.isPlaying ? (musicManager.isLiveStream ? "stop.fill" : "pause.fill") : "play.fill",
|
icon: musicManager.isPlaying ? (musicManager.isLiveStream ? "stop.fill" : "pause.fill") : "play.fill",
|
||||||
fontSize: 28,
|
fontSize: 26,
|
||||||
fontWeight: .semibold,
|
fontWeight: .semibold,
|
||||||
frameSize: CGSize(width: 60, height: 60),
|
frameSize: CGSize(width: 54, height: 54),
|
||||||
cornerRadius: 24,
|
cornerRadius: 22,
|
||||||
foregroundColor: .white,
|
foregroundColor: .white,
|
||||||
pressEffect: .none,
|
pressEffect: .none,
|
||||||
symbolEffectStyle: .replace,
|
symbolEffectStyle: .replace,
|
||||||
|
|
@ -867,8 +868,8 @@ private struct MinimalisticReminderDetailsView: View {
|
||||||
icon: icon,
|
icon: icon,
|
||||||
fontSize: size,
|
fontSize: size,
|
||||||
fontWeight: .medium,
|
fontWeight: .medium,
|
||||||
frameSize: CGSize(width: 40, height: 40),
|
frameSize: CGSize(width: 36, height: 36),
|
||||||
cornerRadius: 16,
|
cornerRadius: 14,
|
||||||
foregroundColor: isActive ? resolvedActiveColor : .white.opacity(0.85),
|
foregroundColor: isActive ? resolvedActiveColor : .white.opacity(0.85),
|
||||||
pressEffect: pressEffect,
|
pressEffect: pressEffect,
|
||||||
symbolEffectStyle: symbolEffect,
|
symbolEffectStyle: symbolEffect,
|
||||||
|
|
@ -991,8 +992,8 @@ private struct MinimalisticReminderDetailsView: View {
|
||||||
icon: routeManager.activeDevice?.iconName ?? "speaker.wave.2",
|
icon: routeManager.activeDevice?.iconName ?? "speaker.wave.2",
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: .medium,
|
fontWeight: .medium,
|
||||||
frameSize: CGSize(width: 40, height: 40),
|
frameSize: CGSize(width: 36, height: 36),
|
||||||
cornerRadius: 16,
|
cornerRadius: 14,
|
||||||
foregroundColor: .white.opacity(0.85),
|
foregroundColor: .white.opacity(0.85),
|
||||||
symbolEffectStyle: .replace
|
symbolEffectStyle: .replace
|
||||||
) {
|
) {
|
||||||
|
|
@ -1051,8 +1052,8 @@ private struct MinimalisticReminderDetailsView: View {
|
||||||
icon: "airplayaudio",
|
icon: "airplayaudio",
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: .medium,
|
fontWeight: .medium,
|
||||||
frameSize: CGSize(width: 40, height: 40),
|
frameSize: CGSize(width: 36, height: 36),
|
||||||
cornerRadius: 16,
|
cornerRadius: 14,
|
||||||
foregroundColor: .white.opacity(0.85),
|
foregroundColor: .white.opacity(0.85),
|
||||||
symbolEffectStyle: .replace
|
symbolEffectStyle: .replace
|
||||||
) {
|
) {
|
||||||
|
|
@ -1144,17 +1145,17 @@ struct MinimalisticAlbumArtView: View {
|
||||||
)
|
)
|
||||||
.clipped()
|
.clipped()
|
||||||
.clipShape(RoundedRectangle(cornerRadius: albumArtCornerRadius))
|
.clipShape(RoundedRectangle(cornerRadius: albumArtCornerRadius))
|
||||||
.scaleEffect(x: 1.3, y: 1.4)
|
.scaleEffect(x: 1.08, y: 1.1)
|
||||||
.rotationEffect(.degrees(92))
|
.rotationEffect(.degrees(92))
|
||||||
.blur(radius: 35)
|
.blur(radius: 16)
|
||||||
.opacity(
|
.opacity(
|
||||||
usesLiveCanvasArtwork
|
usesLiveCanvasArtwork
|
||||||
? (musicManager.isPlaying ? 0.68 : 0.22)
|
? (musicManager.isPlaying ? 0.55 : 0.18)
|
||||||
: min(0.6, 1 - max(musicManager.albumArt.getBrightness(), 0.3))
|
: min(0.45, 1 - max(musicManager.albumArt.getBrightness(), 0.3))
|
||||||
)
|
)
|
||||||
.shadow(
|
.shadow(
|
||||||
color: Color(nsColor: musicManager.avgColor).opacity(usesLiveCanvasArtwork ? 0.28 : 0.16),
|
color: Color(nsColor: musicManager.avgColor).opacity(usesLiveCanvasArtwork ? 0.22 : 0.12),
|
||||||
radius: usesLiveCanvasArtwork ? 18 : 12,
|
radius: usesLiveCanvasArtwork ? 10 : 7,
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0
|
y: 0
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ class LockScreenManager: ObservableObject {
|
||||||
private var debounceIdleTask: Task<Void, Never>?
|
private var debounceIdleTask: Task<Void, Never>?
|
||||||
private var collapseTask: Task<Void, Never>?
|
private var collapseTask: Task<Void, Never>?
|
||||||
private var postUnlockMusicHUDTask: Task<Void, Never>?
|
private var postUnlockMusicHUDTask: Task<Void, Never>?
|
||||||
|
private var lockStatePollTask: Task<Void, Never>?
|
||||||
|
|
||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
|
@ -65,9 +66,11 @@ class LockScreenManager: ObservableObject {
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
DistributedNotificationCenter.default().removeObserver(self)
|
DistributedNotificationCenter.default().removeObserver(self)
|
||||||
|
NSWorkspace.shared.notificationCenter.removeObserver(self)
|
||||||
debounceIdleTask?.cancel()
|
debounceIdleTask?.cancel()
|
||||||
collapseTask?.cancel()
|
collapseTask?.cancel()
|
||||||
postUnlockMusicHUDTask?.cancel()
|
postUnlockMusicHUDTask?.cancel()
|
||||||
|
lockStatePollTask?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Setup
|
// MARK: - Setup
|
||||||
|
|
@ -80,7 +83,7 @@ class LockScreenManager: ObservableObject {
|
||||||
name: .init("com.apple.screenIsLocked"),
|
name: .init("com.apple.screenIsLocked"),
|
||||||
object: nil
|
object: nil
|
||||||
)
|
)
|
||||||
|
|
||||||
// Observe screen unlocked event
|
// Observe screen unlocked event
|
||||||
DistributedNotificationCenter.default().addObserver(
|
DistributedNotificationCenter.default().addObserver(
|
||||||
self,
|
self,
|
||||||
|
|
@ -88,7 +91,18 @@ class LockScreenManager: ObservableObject {
|
||||||
name: .init("com.apple.screenIsUnlocked"),
|
name: .init("com.apple.screenIsUnlocked"),
|
||||||
object: nil
|
object: nil
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Fallback: macOS sometimes delays `com.apple.screenIsUnlocked`, leaving
|
||||||
|
// lock-screen widgets visible after the user-perceived unlock.
|
||||||
|
// The workspace session-active notification typically fires earlier; the
|
||||||
|
// guard at the top of `screenUnlocked` makes the call idempotent.
|
||||||
|
NSWorkspace.shared.notificationCenter.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(screenUnlocked),
|
||||||
|
name: NSWorkspace.sessionDidBecomeActiveNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
|
||||||
print("LockScreenManager: ✅ Observers registered for lock/unlock events")
|
print("LockScreenManager: ✅ Observers registered for lock/unlock events")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,9 +155,11 @@ class LockScreenManager: ObservableObject {
|
||||||
print("[\(timestamp())] LockScreenManager: ⏭️ Lock icon disabled in settings")
|
print("[\(timestamp())] LockScreenManager: ⏭️ Lock icon disabled in settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startLockStatePolling()
|
||||||
|
|
||||||
print("[\(timestamp())] LockScreenManager: ✅ Lock screen activated")
|
print("[\(timestamp())] LockScreenManager: ✅ Lock screen activated")
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func screenUnlocked() {
|
@objc private func screenUnlocked() {
|
||||||
guard isLocked else {
|
guard isLocked else {
|
||||||
print("[\(timestamp())] LockScreenManager: 🔁 Unlock event ignored (already unlocked)")
|
print("[\(timestamp())] LockScreenManager: 🔁 Unlock event ignored (already unlocked)")
|
||||||
|
|
@ -156,6 +172,7 @@ class LockScreenManager: ObservableObject {
|
||||||
lastUpdated = Date()
|
lastUpdated = Date()
|
||||||
updateIdleState(locked: false)
|
updateIdleState(locked: false)
|
||||||
isLocked = false
|
isLocked = false
|
||||||
|
stopLockStatePolling()
|
||||||
postUnlockMusicHUDTask?.cancel()
|
postUnlockMusicHUDTask?.cancel()
|
||||||
shouldDelayPostUnlockMusicHUD = Defaults[.enableLockScreenLiveActivity]
|
shouldDelayPostUnlockMusicHUD = Defaults[.enableLockScreenLiveActivity]
|
||||||
|
|
||||||
|
|
@ -207,8 +224,46 @@ class LockScreenManager: ObservableObject {
|
||||||
print("[\(self.timestamp())] LockScreenManager: ✅ Lock screen deactivated")
|
print("[\(self.timestamp())] LockScreenManager: ✅ Lock screen deactivated")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Lock State Polling
|
||||||
|
|
||||||
|
// Defensive fallback against late/missed `com.apple.screenIsUnlocked` and
|
||||||
|
// `NSWorkspace.sessionDidBecomeActiveNotification` notifications, which
|
||||||
|
// macOS sometimes delivers well after the user-perceived unlock — leaving
|
||||||
|
// lock-screen widgets visible for an extra moment. While we believe we are
|
||||||
|
// locked, poll the canonical session-lock state and fire `screenUnlocked()`
|
||||||
|
// the moment the OS flips. The handler's duplicate-event guard makes this
|
||||||
|
// safe to call alongside any later-arriving notification.
|
||||||
|
private static func isSessionScreenLocked() -> Bool {
|
||||||
|
guard let session = CGSessionCopyCurrentDictionary() as? [String: Any] else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return session["CGSSessionScreenIsLocked"] as? Bool ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startLockStatePolling() {
|
||||||
|
lockStatePollTask?.cancel()
|
||||||
|
lockStatePollTask = Task { [weak self] in
|
||||||
|
while !Task.isCancelled {
|
||||||
|
try? await Task.sleep(for: .milliseconds(500))
|
||||||
|
if Task.isCancelled { return }
|
||||||
|
await MainActor.run {
|
||||||
|
guard let self, self.isLocked else { return }
|
||||||
|
if !Self.isSessionScreenLocked() {
|
||||||
|
print("[\(self.timestamp())] LockScreenManager: 🔓 Polling detected unlock ahead of notification")
|
||||||
|
self.screenUnlocked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopLockStatePolling() {
|
||||||
|
lockStatePollTask?.cancel()
|
||||||
|
lockStatePollTask = nil
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Idle State Management
|
// MARK: - Idle State Management
|
||||||
|
|
||||||
/// Copy EXACT logic from ScreenRecordingManager
|
/// Copy EXACT logic from ScreenRecordingManager
|
||||||
private func updateIdleState(locked: Bool) {
|
private func updateIdleState(locked: Bool) {
|
||||||
if locked {
|
if locked {
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,10 @@ final class LockScreenWeatherPanelManager {
|
||||||
|
|
||||||
func hide() {
|
func hide() {
|
||||||
guard let window else { return }
|
guard let window else { return }
|
||||||
|
window.alphaValue = 0
|
||||||
window.orderOut(nil)
|
window.orderOut(nil)
|
||||||
window.contentView = nil
|
window.contentView = nil
|
||||||
|
window.alphaValue = 1
|
||||||
latestFrame = nil
|
latestFrame = nil
|
||||||
lastSnapshot = nil
|
lastSnapshot = nil
|
||||||
lastContentSize = nil
|
lastContentSize = nil
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue