Refine macOS player controls: add queue, more, previous; drop seek/fullscreen buttons

Add Queue (list.bullet), More (VideoContextMenuView ellipsis), and Play
Previous buttons to the macOS control bar. Play Previous is shown always and
disabled when the queue has no previous item. Remove the skip-back/forward 10s
buttons and the fullscreen button from the bar (arrow-key seek and F-key
fullscreen shortcuts are retained).

Queue reuses the existing cross-platform queue sheet; More reuses the shared
VideoContextMenuView (already !os(tvOS)), styled borderless to match the flat
bar buttons.
This commit is contained in:
Arkadiusz Fal
2026-05-21 21:36:09 +02:00
parent 64171a9cfb
commit 93f9a382b0
3 changed files with 65 additions and 41 deletions

View File

@@ -904,6 +904,9 @@ extension ExpandedPlayerSheet {
onPlayNext: {
await playerService.playNext()
},
onPlayPrevious: {
await playerService.playPrevious()
},
onVolumeChanged: { [weak appEnvironment] volume in
playerService.currentBackend?.volume = volume
appEnvironment?.settingsManager.playerVolume = volume
@@ -917,6 +920,9 @@ extension ExpandedPlayerSheet {
},
onShowSettings: { [self] in
showingQualitySheet = true
},
onShowQueue: { [self] in
showingQueueSheet = true
}
)
.frame(width: controlsWidth, height: controlsHeight)
@@ -1160,6 +1166,9 @@ extension ExpandedPlayerSheet {
onPlayNext: {
await playerService.playNext()
},
onPlayPrevious: {
await playerService.playPrevious()
},
onVolumeChanged: { [weak appEnvironment] volume in
playerService.currentBackend?.volume = volume
appEnvironment?.settingsManager.playerVolume = volume
@@ -1173,6 +1182,9 @@ extension ExpandedPlayerSheet {
},
onShowSettings: { [self] in
showingQualitySheet = true
},
onShowQueue: { [self] in
showingQueueSheet = true
}
)
.frame(width: controlsWidth, height: controlsHeight)
@@ -1560,6 +1572,9 @@ extension ExpandedPlayerSheet {
onPlayNext: {
await playerService.playNext()
},
onPlayPrevious: {
await playerService.playPrevious()
},
onVolumeChanged: { [weak appEnvironment] volume in
playerService.currentBackend?.volume = volume
appEnvironment?.settingsManager.playerVolume = volume
@@ -1573,6 +1588,9 @@ extension ExpandedPlayerSheet {
},
onShowSettings: { [self] in
showingQualitySheet = true
},
onShowQueue: { [self] in
showingQueueSheet = true
}
)
.frame(width: availableWidth, height: availableHeight)

View File

@@ -23,9 +23,11 @@ struct MacOSControlBar: View {
var isFullscreen: Bool = false
var onTogglePiP: (() -> Void)? = nil
var onPlayNext: (() async -> Void)? = nil
var onPlayPrevious: (() async -> Void)? = nil
var onVolumeChanged: ((Float) -> Void)? = nil
var onMuteToggled: (() -> Void)? = nil
var onShowSettings: (() -> Void)? = nil
var onShowQueue: (() -> Void)? = nil
/// Whether to show chapter markers on the progress bar (default: true)
var showChapters: Bool = true
/// SponsorBlock segments to display on the progress bar.
@@ -47,8 +49,7 @@ struct MacOSControlBar: View {
@State private var isHoveringProgress = false
@State private var hoverProgress: Double = 0
@State private var playNextTapCount = 0
@State private var seekBackwardTrigger = 0
@State private var seekForwardTrigger = 0
@State private var playPreviousTapCount = 0
// MARK: - Computed Properties
@@ -149,19 +150,21 @@ struct MacOSControlBar: View {
private var transportControls: some View {
HStack(spacing: 4) {
// Skip backward
Button {
seekBackwardTrigger += 1
Task { await onSeekBackward(10) }
} label: {
Image(systemName: "10.arrow.trianglehead.counterclockwise")
.font(.system(size: 14, weight: .medium))
.frame(width: 28, height: 28)
.contentShape(Rectangle())
.symbolEffect(.rotate.byLayer, options: .speed(2).nonRepeating, value: seekBackwardTrigger)
// Play previous (disabled when no previous in queue)
if let onPlayPrevious {
Button {
playPreviousTapCount += 1
Task { await onPlayPrevious() }
} label: {
Image(systemName: "backward.fill")
.font(.system(size: 12, weight: .medium))
.frame(width: 28, height: 28)
.contentShape(Rectangle())
.symbolEffect(.bounce.down.byLayer, options: .nonRepeating, value: playPreviousTapCount)
}
.buttonStyle(MacOSControlButtonStyle())
.disabled(!playerState.hasPrevious)
}
.buttonStyle(MacOSControlButtonStyle())
.disabled(isTransportDisabled)
// Play/Pause
Button {
@@ -177,20 +180,6 @@ struct MacOSControlBar: View {
.disabled(isTransportDisabled)
.opacity(isTransportDisabled ? 0.3 : 1.0)
// Skip forward
Button {
seekForwardTrigger += 1
Task { await onSeekForward(10) }
} label: {
Image(systemName: "10.arrow.trianglehead.clockwise")
.font(.system(size: 14, weight: .medium))
.frame(width: 28, height: 28)
.contentShape(Rectangle())
.symbolEffect(.rotate.byLayer, options: .speed(2).nonRepeating, value: seekForwardTrigger)
}
.buttonStyle(MacOSControlButtonStyle())
.disabled(isTransportDisabled)
// Play next (if queue has items)
if let onPlayNext, playerState.hasNext {
Button {
@@ -337,6 +326,32 @@ struct MacOSControlBar: View {
private var trailingActionControls: some View {
HStack(spacing: 4) {
// Queue
if let onShowQueue {
Button {
onShowQueue()
} label: {
Image(systemName: "list.bullet")
.font(.system(size: 13, weight: .medium))
.frame(width: 28, height: 28)
.contentShape(Rectangle())
}
.buttonStyle(MacOSControlButtonStyle())
}
// More (context menu)
if let video = playerState.currentVideo {
VideoContextMenuView(
video: video,
accentColor: .primary,
buttonSize: 28,
buttonBackgroundStyle: .none,
theme: .dark
)
.menuStyle(.borderlessButton)
.fixedSize()
}
// Settings
if let onShowSettings {
Button {
@@ -362,19 +377,6 @@ struct MacOSControlBar: View {
}
.buttonStyle(MacOSControlButtonStyle())
}
// Fullscreen
if let onToggleFullscreen {
Button {
onToggleFullscreen()
} label: {
Image(systemName: isFullscreen ? "arrow.down.right.and.arrow.up.left" : "arrow.up.left.and.arrow.down.right")
.font(.system(size: 13, weight: .medium))
.frame(width: 28, height: 28)
.contentShape(Rectangle())
}
.buttonStyle(MacOSControlButtonStyle())
}
}
}

View File

@@ -27,9 +27,11 @@ struct MacOSPlayerControlsView: View {
var onClose: (() -> Void)? = nil
var onTogglePiP: (() -> Void)? = nil
var onPlayNext: (() async -> Void)? = nil
var onPlayPrevious: (() async -> Void)? = nil
var onVolumeChanged: ((Float) -> Void)? = nil
var onMuteToggled: (() -> Void)? = nil
var onShowSettings: (() -> Void)? = nil
var onShowQueue: (() -> Void)? = nil
// MARK: - State
@@ -91,9 +93,11 @@ struct MacOSPlayerControlsView: View {
isFullscreen: isFullscreen,
onTogglePiP: onTogglePiP,
onPlayNext: onPlayNext,
onPlayPrevious: onPlayPrevious,
onVolumeChanged: onVolumeChanged,
onMuteToggled: onMuteToggled,
onShowSettings: onShowSettings,
onShowQueue: onShowQueue,
sponsorSegments: playerState.sponsorSegments,
onInteractionStarted: {
isInteracting = true