mirror of
https://github.com/yattee/yattee.git
synced 2026-06-07 15:24:21 +00:00
Convert tvOS settings and queue overlays to half-screen panels
The Settings (quality/audio/subtitles) and Queue panels now slide in from the right and occupy the right half of the screen, matching the info/comments details panel introduced in 92cc8b79f. Video stays visible on the left so the user retains visual context while browsing. Both panels supply their own ultraThinMaterial backdrop and use a custom title bar (replacing NavigationStack's auto-title on tvOS) so the title styling and symmetric padding match across panels and across pushed destination screens. The Menu button now pops the quality panel's pushed Video/Audio/Subtitles detail screens before dismissing the panel itself. Removes the background Button from the focus tree while either panel is open so D-pad left/right inside a row no longer escapes focus into the player and triggers a seek. Initial focus is steered into the first row programmatically since tvOS doesn't auto-focus inline overlays the way it does for fullScreenCover. Doubles the queue thumbnail size on tvOS (160x90) for readability at the half-screen panel width.
This commit is contained in:
@@ -66,7 +66,16 @@ extension QualitySelectorView {
|
||||
generalSectionContent
|
||||
}
|
||||
}
|
||||
#if os(tvOS)
|
||||
// Explicit symmetric padding so rows are evenly inset from the
|
||||
// half-screen panel's edges; matches the queue panel's 80pt
|
||||
// breathing room so the focus halo around row buttons doesn't
|
||||
// crowd the panel boundaries.
|
||||
.padding(.horizontal, 80)
|
||||
.padding(.vertical, 24)
|
||||
#else
|
||||
.padding()
|
||||
#endif
|
||||
}
|
||||
.onAppear {
|
||||
selectedTab = initialTab
|
||||
@@ -87,6 +96,7 @@ extension QualitySelectorView {
|
||||
systemImage: "film",
|
||||
value: currentVideoDisplayValue
|
||||
)
|
||||
.focused($inlinePanelInitialFocus)
|
||||
if availableTabs.contains(.audio) {
|
||||
mediaSelectionRow(
|
||||
destination: .audio,
|
||||
@@ -195,64 +205,77 @@ extension QualitySelectorView {
|
||||
|
||||
@ViewBuilder
|
||||
var videoDetailContent: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
if !adaptiveStreams.isEmpty {
|
||||
adaptiveSectionContent
|
||||
}
|
||||
if !videoStreams.isEmpty {
|
||||
videoSectionContent
|
||||
}
|
||||
detailContent(title: String(localized: "player.quality.video")) {
|
||||
if !adaptiveStreams.isEmpty {
|
||||
adaptiveSectionContent
|
||||
}
|
||||
if !videoStreams.isEmpty {
|
||||
videoSectionContent
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
#if os(tvOS)
|
||||
.background(Color.clear)
|
||||
#else
|
||||
.background(ListBackgroundStyle.grouped.color)
|
||||
#endif
|
||||
.navigationTitle(String(localized: "player.quality.video"))
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var audioDetailContent: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
audioSectionContent
|
||||
}
|
||||
.padding()
|
||||
detailContent(title: String(localized: "stream.audio")) {
|
||||
audioSectionContent
|
||||
}
|
||||
#if os(tvOS)
|
||||
.background(Color.clear)
|
||||
#else
|
||||
.background(ListBackgroundStyle.grouped.color)
|
||||
#endif
|
||||
.navigationTitle(String(localized: "stream.audio"))
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var subtitlesDetailContent: some View {
|
||||
detailContent(title: String(localized: "stream.subtitles")) {
|
||||
subtitlesSectionContent
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared layout for the pushed detail screens. On tvOS the destination
|
||||
/// gets the same glass backdrop + custom title bar as the panel root so
|
||||
/// the visual treatment is continuous as the user navigates in.
|
||||
@ViewBuilder
|
||||
private func detailContent<Content: View>(
|
||||
title: String,
|
||||
@ViewBuilder content: () -> Content
|
||||
) -> some View {
|
||||
#if os(tvOS)
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
ZStack {
|
||||
Text(title)
|
||||
.font(.system(size: 32, weight: .semibold))
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.horizontal, 80)
|
||||
.padding(.top, 32)
|
||||
.padding(.bottom, 16)
|
||||
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
content()
|
||||
}
|
||||
.padding(.horizontal, 80)
|
||||
.padding(.vertical, 24)
|
||||
}
|
||||
}
|
||||
.background(
|
||||
Rectangle()
|
||||
.fill(.ultraThinMaterial)
|
||||
.ignoresSafeArea()
|
||||
)
|
||||
.ignoresSafeArea(.container, edges: .horizontal)
|
||||
#else
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
subtitlesSectionContent
|
||||
content()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
#if os(tvOS)
|
||||
.background(Color.clear)
|
||||
#else
|
||||
.background(ListBackgroundStyle.grouped.color)
|
||||
#endif
|
||||
.navigationTitle(String(localized: "stream.subtitles"))
|
||||
.navigationTitle(title)
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -454,7 +477,7 @@ extension QualitySelectorView {
|
||||
isPreferred: false,
|
||||
onTap: {
|
||||
onCaptionSelected(nil)
|
||||
dismiss()
|
||||
performDismiss()
|
||||
}
|
||||
)
|
||||
.padding(.vertical, 8)
|
||||
@@ -467,7 +490,7 @@ extension QualitySelectorView {
|
||||
localCaptionURL: localCaptionURL,
|
||||
currentCaption: currentCaption,
|
||||
onCaptionSelected: onCaptionSelected,
|
||||
onDismiss: { dismiss() }
|
||||
onDismiss: { performDismiss() }
|
||||
)
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, 12)
|
||||
@@ -548,7 +571,7 @@ extension QualitySelectorView {
|
||||
} else {
|
||||
onStreamSelected(stream, nil)
|
||||
}
|
||||
dismiss()
|
||||
performDismiss()
|
||||
}
|
||||
|
||||
// MARK: - Video Section
|
||||
@@ -641,27 +664,27 @@ extension QualitySelectorView {
|
||||
if isDownloaded {
|
||||
if stream.isMuxed {
|
||||
onStreamSelected(stream, nil)
|
||||
dismiss()
|
||||
performDismiss()
|
||||
} else {
|
||||
selectedVideoStream = stream
|
||||
if let audio = selectedAudioStream {
|
||||
onStreamSelected(stream, audio)
|
||||
dismiss()
|
||||
performDismiss()
|
||||
}
|
||||
}
|
||||
} else if isPlayingDownloadedContent {
|
||||
let audioStream: Stream? = stream.isVideoOnly ? (selectedAudioStream ?? defaultAudioStream) : nil
|
||||
onSwitchToOnlineStream(stream, audioStream)
|
||||
dismiss()
|
||||
performDismiss()
|
||||
} else {
|
||||
if stream.isMuxed {
|
||||
onStreamSelected(stream, nil)
|
||||
dismiss()
|
||||
performDismiss()
|
||||
} else {
|
||||
selectedVideoStream = stream
|
||||
if let audio = selectedAudioStream {
|
||||
onStreamSelected(stream, audio)
|
||||
dismiss()
|
||||
performDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -730,7 +753,7 @@ extension QualitySelectorView {
|
||||
selectedAudioStream = stream
|
||||
if let video = selectedVideoStream, video.isVideoOnly {
|
||||
onStreamSelected(video, stream)
|
||||
dismiss()
|
||||
performDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -803,7 +826,7 @@ extension QualitySelectorView {
|
||||
} else {
|
||||
onCaptionSelected(caption)
|
||||
}
|
||||
dismiss()
|
||||
performDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user