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

View File

@@ -23,9 +23,11 @@ struct MacOSControlBar: View {
var isFullscreen: Bool = false var isFullscreen: Bool = false
var onTogglePiP: (() -> Void)? = nil var onTogglePiP: (() -> Void)? = nil
var onPlayNext: (() async -> Void)? = nil var onPlayNext: (() async -> Void)? = nil
var onPlayPrevious: (() async -> Void)? = nil
var onVolumeChanged: ((Float) -> Void)? = nil var onVolumeChanged: ((Float) -> Void)? = nil
var onMuteToggled: (() -> Void)? = nil var onMuteToggled: (() -> Void)? = nil
var onShowSettings: (() -> Void)? = nil var onShowSettings: (() -> Void)? = nil
var onShowQueue: (() -> Void)? = nil
/// Whether to show chapter markers on the progress bar (default: true) /// Whether to show chapter markers on the progress bar (default: true)
var showChapters: Bool = true var showChapters: Bool = true
/// SponsorBlock segments to display on the progress bar. /// SponsorBlock segments to display on the progress bar.
@@ -47,8 +49,7 @@ struct MacOSControlBar: View {
@State private var isHoveringProgress = false @State private var isHoveringProgress = false
@State private var hoverProgress: Double = 0 @State private var hoverProgress: Double = 0
@State private var playNextTapCount = 0 @State private var playNextTapCount = 0
@State private var seekBackwardTrigger = 0 @State private var playPreviousTapCount = 0
@State private var seekForwardTrigger = 0
// MARK: - Computed Properties // MARK: - Computed Properties
@@ -149,19 +150,21 @@ struct MacOSControlBar: View {
private var transportControls: some View { private var transportControls: some View {
HStack(spacing: 4) { HStack(spacing: 4) {
// Skip backward // Play previous (disabled when no previous in queue)
if let onPlayPrevious {
Button { Button {
seekBackwardTrigger += 1 playPreviousTapCount += 1
Task { await onSeekBackward(10) } Task { await onPlayPrevious() }
} label: { } label: {
Image(systemName: "10.arrow.trianglehead.counterclockwise") Image(systemName: "backward.fill")
.font(.system(size: 14, weight: .medium)) .font(.system(size: 12, weight: .medium))
.frame(width: 28, height: 28) .frame(width: 28, height: 28)
.contentShape(Rectangle()) .contentShape(Rectangle())
.symbolEffect(.rotate.byLayer, options: .speed(2).nonRepeating, value: seekBackwardTrigger) .symbolEffect(.bounce.down.byLayer, options: .nonRepeating, value: playPreviousTapCount)
} }
.buttonStyle(MacOSControlButtonStyle()) .buttonStyle(MacOSControlButtonStyle())
.disabled(isTransportDisabled) .disabled(!playerState.hasPrevious)
}
// Play/Pause // Play/Pause
Button { Button {
@@ -177,20 +180,6 @@ struct MacOSControlBar: View {
.disabled(isTransportDisabled) .disabled(isTransportDisabled)
.opacity(isTransportDisabled ? 0.3 : 1.0) .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) // Play next (if queue has items)
if let onPlayNext, playerState.hasNext { if let onPlayNext, playerState.hasNext {
Button { Button {
@@ -337,6 +326,32 @@ struct MacOSControlBar: View {
private var trailingActionControls: some View { private var trailingActionControls: some View {
HStack(spacing: 4) { 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 // Settings
if let onShowSettings { if let onShowSettings {
Button { Button {
@@ -362,19 +377,6 @@ struct MacOSControlBar: View {
} }
.buttonStyle(MacOSControlButtonStyle()) .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 onClose: (() -> Void)? = nil
var onTogglePiP: (() -> Void)? = nil var onTogglePiP: (() -> Void)? = nil
var onPlayNext: (() async -> Void)? = nil var onPlayNext: (() async -> Void)? = nil
var onPlayPrevious: (() async -> Void)? = nil
var onVolumeChanged: ((Float) -> Void)? = nil var onVolumeChanged: ((Float) -> Void)? = nil
var onMuteToggled: (() -> Void)? = nil var onMuteToggled: (() -> Void)? = nil
var onShowSettings: (() -> Void)? = nil var onShowSettings: (() -> Void)? = nil
var onShowQueue: (() -> Void)? = nil
// MARK: - State // MARK: - State
@@ -91,9 +93,11 @@ struct MacOSPlayerControlsView: View {
isFullscreen: isFullscreen, isFullscreen: isFullscreen,
onTogglePiP: onTogglePiP, onTogglePiP: onTogglePiP,
onPlayNext: onPlayNext, onPlayNext: onPlayNext,
onPlayPrevious: onPlayPrevious,
onVolumeChanged: onVolumeChanged, onVolumeChanged: onVolumeChanged,
onMuteToggled: onMuteToggled, onMuteToggled: onMuteToggled,
onShowSettings: onShowSettings, onShowSettings: onShowSettings,
onShowQueue: onShowQueue,
sponsorSegments: playerState.sponsorSegments, sponsorSegments: playerState.sponsorSegments,
onInteractionStarted: { onInteractionStarted: {
isInteracting = true isInteracting = true