From 93f9a382b04c415a52886bca43ef96455e4ec8f9 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Thu, 21 May 2026 21:36:09 +0200 Subject: [PATCH] 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. --- .../Player/ExpandedPlayerSheet+Layouts.swift | 18 ++++ .../Views/Player/macOS/MacOSControlBar.swift | 84 ++++++++++--------- .../macOS/MacOSPlayerControlsView.swift | 4 + 3 files changed, 65 insertions(+), 41 deletions(-) diff --git a/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift b/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift index 66f4bd64..16fe7bb5 100644 --- a/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift +++ b/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift @@ -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) diff --git a/Yattee/Views/Player/macOS/MacOSControlBar.swift b/Yattee/Views/Player/macOS/MacOSControlBar.swift index a7262ae3..246fd369 100644 --- a/Yattee/Views/Player/macOS/MacOSControlBar.swift +++ b/Yattee/Views/Player/macOS/MacOSControlBar.swift @@ -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()) - } } } diff --git a/Yattee/Views/Player/macOS/MacOSPlayerControlsView.swift b/Yattee/Views/Player/macOS/MacOSPlayerControlsView.swift index c8a07ea2..49ead00f 100644 --- a/Yattee/Views/Player/macOS/MacOSPlayerControlsView.swift +++ b/Yattee/Views/Player/macOS/MacOSPlayerControlsView.swift @@ -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