Open queue sheet from tvOS player controls

Turn the tvOS bottom-row queue count indicator into a focusable button
that opens QueueManagementSheet in a fullScreenCover with an
ultraThinMaterial backdrop, matching the Settings sheet pattern. Hide
the sheet's close toolbar button on tvOS (Menu button dismisses) and
replace the unusable Menu-based queue mode picker with an icon-only
tap-to-cycle button.
This commit is contained in:
Arkadiusz Fal
2026-04-15 03:56:38 +02:00
parent 9aeb329b64
commit 29782035f7
3 changed files with 50 additions and 7 deletions

View File

@@ -41,6 +41,7 @@ struct QueueManagementSheet: View {
queueModeMenu
}
#if !os(tvOS)
ToolbarItem(placement: .confirmationAction) {
Button(role: .cancel) {
dismiss()
@@ -49,6 +50,7 @@ struct QueueManagementSheet: View {
.labelStyle(.iconOnly)
}
}
#endif
}
}
.presentationDragIndicator(.visible)
@@ -209,6 +211,14 @@ struct QueueManagementSheet: View {
/// Menu for selecting queue mode (shuffle, repeat, etc.)
@ViewBuilder
private var queueModeMenu: some View {
#if os(tvOS)
Button {
cycleQueueMode()
} label: {
Image(systemName: playerState?.queueMode.icon ?? "list.bullet")
.foregroundStyle(.tint)
}
#else
Menu {
ForEach(QueueMode.allCases, id: \.self) { mode in
Button {
@@ -226,6 +236,15 @@ struct QueueManagementSheet: View {
}
.foregroundStyle(.tint)
}
#endif
}
private func cycleQueueMode() {
guard let playerState else { return }
let modes = QueueMode.allCases
let currentIndex = modes.firstIndex(of: playerState.queueMode) ?? 0
let nextIndex = (currentIndex + 1) % modes.count
playerState.queueMode = modes[nextIndex]
}
// MARK: - Actions

View File

@@ -16,6 +16,7 @@ struct TVPlayerControlsView: View {
@FocusState.Binding var focusedControl: TVPlayerFocusTarget?
let onShowSettings: () -> Void
let onShowQueue: () -> Void
let onShowDetails: () -> Void
let onShowComments: () -> Void
let onShowDebug: () -> Void
@@ -241,15 +242,20 @@ struct TVPlayerControlsView: View {
Spacer()
// Queue indicator (if videos in queue)
// Queue button (if videos in queue)
if let state = playerState, state.hasNext {
HStack(spacing: 8) {
Button {
onShowQueue()
} label: {
VStack(spacing: 6) {
Image(systemName: "list.bullet")
.font(.system(size: 20))
.font(.system(size: 28))
Text(String(localized: "queue.section.count \(state.queue.count)"))
.font(.subheadline)
.font(.caption)
}
.foregroundStyle(.white.opacity(0.6))
}
.buttonStyle(TVActionButtonStyle())
.focused($focusedControl, equals: .queueButton)
}
}
}

View File

@@ -18,6 +18,7 @@ enum TVPlayerFocusTarget: Hashable {
case debugButton
case playNext
case closeButton
case queueButton
}
/// Main tvOS fullscreen player view.
@@ -45,6 +46,9 @@ struct TVPlayerView: View {
/// Whether the quality sheet is shown.
@State private var showingQualitySheet = false
/// Whether the queue sheet is shown.
@State private var showingQueueSheet = false
/// Whether the debug overlay is shown.
@State private var isDebugOverlayVisible = false
@@ -109,6 +113,14 @@ struct TVPlayerView: View {
.fullScreenCover(isPresented: $showingQualitySheet) {
qualitySheetContent
}
.fullScreenCover(isPresented: $showingQueueSheet) {
ZStack {
Rectangle()
.fill(.ultraThinMaterial)
.ignoresSafeArea()
QueueManagementSheet()
}
}
}
// MARK: - Quality Sheet Content
@@ -195,6 +207,7 @@ struct TVPlayerView: View {
playerService: playerService,
focusedControl: $focusedControl,
onShowSettings: { showQualitySheet() },
onShowQueue: { showQueueSheet() },
onShowDetails: { showDetailsPanel(tab: .info) },
onShowComments: { showDetailsPanel(tab: .comments) },
onShowDebug: { showDebugOverlay() },
@@ -419,6 +432,11 @@ struct TVPlayerView: View {
showingQualitySheet = true
}
private func showQueueSheet() {
stopControlsTimer()
showingQueueSheet = true
}
private func switchToStream(_ stream: Stream, audioStream: Stream? = nil) {
guard let video = playerState?.currentVideo else { return }