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:
Hariharan Mudaliar 2026-05-18 10:07:54 +05:30 • committed by GitHub
commit bc858401c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 80 additions and 22 deletions

View file

@ -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
) )

View file

@ -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 {

View file

@ -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