Honor Lock Controls on macOS player

The macOS controls stack (MacOSPlayerControlsView + MacOSControlBar)
never read playerState.isControlsLocked, so locking via the settings
sheet had no effect: buttons, the scrubber, the volume slider, and the
AppKit keyboard shortcuts all kept working.

When locked, dim and disable transport, volume, the progress bar,
queue, the more menu, PiP, the pin/floating toggle, and the title-tap
details button; ignore playback keyboard shortcuts (space/arrows/mute)
while keeping F/Esc for fullscreen. Settings and Close stay enabled so
the user can reopen Settings to unlock.
This commit is contained in:
Arkadiusz Fal
2026-05-30 22:22:18 +02:00
parent 2f2e436fe2
commit 5950dbb22a
2 changed files with 41 additions and 2 deletions

View File

@@ -58,6 +58,11 @@ struct MacOSControlBar: View {
playerState.isTransportDisabled playerState.isTransportDisabled
} }
/// Whether the controls are locked (playback buttons/gestures disabled except Settings).
private var isLocked: Bool {
playerState.isControlsLocked
}
private var playPauseIcon: String { private var playPauseIcon: String {
switch playerState.playbackState { switch playerState.playbackState {
case .playing: case .playing:
@@ -133,11 +138,15 @@ struct MacOSControlBar: View {
HStack(spacing: 0) { HStack(spacing: 0) {
// Left: Volume controls // Left: Volume controls
volumeControls volumeControls
.disabled(isLocked)
.opacity(isLocked ? 0.5 : 1.0)
Spacer() Spacer()
// Center: Transport controls // Center: Transport controls
transportControls transportControls
.disabled(isLocked)
.opacity(isLocked ? 0.5 : 1.0)
Spacer() Spacer()
@@ -264,6 +273,7 @@ struct MacOSControlBar: View {
.gesture( .gesture(
DragGesture(minimumDistance: 0) DragGesture(minimumDistance: 0)
.onChanged { value in .onChanged { value in
guard !isLocked else { return }
if !isDragging { if !isDragging {
isDragging = true isDragging = true
onInteractionStarted?() onInteractionStarted?()
@@ -272,6 +282,7 @@ struct MacOSControlBar: View {
dragProgress = progress dragProgress = progress
} }
.onEnded { value in .onEnded { value in
guard !isLocked else { return }
isDragging = false isDragging = false
let progress = max(0, min(1, value.location.x / geometry.size.width)) let progress = max(0, min(1, value.location.x / geometry.size.width))
let seekTime = progress * playerState.duration let seekTime = progress * playerState.duration
@@ -288,8 +299,10 @@ struct MacOSControlBar: View {
isHoveringProgress = false isHoveringProgress = false
} }
} }
.allowsHitTesting(!isLocked)
} }
.frame(height: 20) .frame(height: 20)
.opacity(isLocked ? 0.5 : 1.0)
} }
private func seekPreviewPosition( private func seekPreviewPosition(
@@ -338,6 +351,8 @@ struct MacOSControlBar: View {
.contentShape(Rectangle()) .contentShape(Rectangle())
} }
.buttonStyle(MacOSControlButtonStyle()) .buttonStyle(MacOSControlButtonStyle())
.disabled(isLocked)
.opacity(isLocked ? 0.5 : 1.0)
} }
// More (context menu) // More (context menu)
@@ -351,6 +366,8 @@ struct MacOSControlBar: View {
) )
.menuStyle(.borderlessButton) .menuStyle(.borderlessButton)
.fixedSize() .fixedSize()
.disabled(isLocked)
.opacity(isLocked ? 0.5 : 1.0)
} }
// Settings // Settings
@@ -377,7 +394,8 @@ struct MacOSControlBar: View {
.contentShape(Rectangle()) .contentShape(Rectangle())
} }
.buttonStyle(MacOSControlButtonStyle()) .buttonStyle(MacOSControlButtonStyle())
.disabled(!playerState.isPiPPossible) .disabled(isLocked || !playerState.isPiPPossible)
.opacity(isLocked ? 0.5 : 1.0)
} }
} }
} }

View File

@@ -107,7 +107,8 @@ struct MacOSPlayerControlsView: View {
.contentShape(Rectangle()) .contentShape(Rectangle())
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.disabled(onTitleTap == nil) .disabled(onTitleTap == nil || playerState.isControlsLocked)
.opacity(playerState.isControlsLocked ? 0.5 : 1.0)
.help(Text("player.controls.info")) .help(Text("player.controls.info"))
} }
@@ -129,6 +130,8 @@ struct MacOSPlayerControlsView: View {
.buttonStyle(.plain) .buttonStyle(.plain)
.help(Text("player.controls.keepOnTop")) .help(Text("player.controls.keepOnTop"))
.accessibilityLabel(Text("player.controls.keepOnTop")) .accessibilityLabel(Text("player.controls.keepOnTop"))
.disabled(playerState.isControlsLocked)
.opacity(playerState.isControlsLocked ? 0.5 : 1.0)
} }
if onClose != nil { if onClose != nil {
@@ -273,6 +276,24 @@ struct MacOSPlayerControlsView: View {
// Only handle events when the player window is key (active) // Only handle events when the player window is key (active)
guard NSApp.keyWindow != nil else { return event } guard NSApp.keyWindow != nil else { return event }
// When controls are locked, ignore playback keys (space/arrows/mute) and
// only keep fullscreen (F) and exit-fullscreen (Esc) working.
if playerState.isControlsLocked {
switch event.keyCode {
case 3: // F
onToggleFullscreen?()
return nil
case 53: // Escape
if isFullscreen {
onToggleFullscreen?()
return nil
}
return event
default:
return event // Pass through space/arrows/M while locked
}
}
// Handle keyboard shortcuts // Handle keyboard shortcuts
switch event.keyCode { switch event.keyCode {
case 49: // Space case 49: // Space