mirror of
https://github.com/yattee/yattee.git
synced 2026-06-23 15:14:19 +00:00
Rework tvOS player controls and settings sheet
Replace the tvOS bottom action bar with Settings / Info / Comments / Next / Close. Settings reuses QualitySelectorView (video, audio, subtitles, speed); Comments opens TVDetailsPanel directly on the comments tab; Close stops playback and dismisses. Debug button is hidden by default and can be re-enabled via a new tvOS-only Advanced Settings > Developer toggle. Present the settings sheet as a fullScreenCover with a centered material card, fix the "Normal" hyphenation, and restyle row selection throughout the quality selector on tvOS: per-row rounded backgrounds with focus tint + stroke, vertical spacing instead of dividers, and a focusable speed-rate menu.
This commit is contained in:
@@ -15,13 +15,12 @@ enum TVPlayerFocusTarget: Hashable {
|
||||
case playPause
|
||||
case skipForward
|
||||
case progressBar
|
||||
case qualityButton
|
||||
case captionsButton
|
||||
case debugButton
|
||||
case settingsButton
|
||||
case infoButton
|
||||
case volumeDown
|
||||
case volumeUp
|
||||
case commentsButton
|
||||
case debugButton
|
||||
case playNext
|
||||
case closeButton
|
||||
}
|
||||
|
||||
/// Main tvOS fullscreen player view.
|
||||
@@ -40,6 +39,9 @@ struct TVPlayerView: View {
|
||||
/// Whether the details panel is shown.
|
||||
@State private var isDetailsPanelVisible = false
|
||||
|
||||
/// Initial tab for the details panel when opened.
|
||||
@State private var detailsPanelInitialTab: TVDetailsTab = .info
|
||||
|
||||
/// Whether user is scrubbing the progress bar.
|
||||
@State private var isScrubbing = false
|
||||
|
||||
@@ -83,56 +85,77 @@ struct TVPlayerView: View {
|
||||
mpvPlayerContent
|
||||
.ignoresSafeArea()
|
||||
.playerToastOverlay()
|
||||
// Quality selector sheet
|
||||
.sheet(isPresented: $showingQualitySheet) {
|
||||
if let playerService {
|
||||
let dashEnabled = appEnvironment?.settingsManager.dashEnabled ?? false
|
||||
let supportedFormats = playerService.currentBackendType.supportedFormats
|
||||
QualitySelectorView(
|
||||
streams: playerService.availableStreams.filter { stream in
|
||||
let format = StreamFormat.detect(from: stream)
|
||||
if format == .dash && !dashEnabled {
|
||||
return false
|
||||
}
|
||||
return supportedFormats.contains(format)
|
||||
},
|
||||
captions: playerService.availableCaptions,
|
||||
currentStream: playerState?.currentStream,
|
||||
currentAudioStream: playerState?.currentAudioStream,
|
||||
currentCaption: playerService.currentCaption,
|
||||
isLoading: playerState?.playbackState == .loading,
|
||||
currentDownload: playerService.currentDownload,
|
||||
isLoadingOnlineStreams: playerService.isLoadingOnlineStreams,
|
||||
localCaptionURL: playerService.currentDownload.flatMap { download in
|
||||
guard let path = download.localCaptionPath else { return nil }
|
||||
return appEnvironment?.downloadManager.downloadsDirectory().appendingPathComponent(path)
|
||||
},
|
||||
currentRate: playerState?.rate ?? .x1,
|
||||
onStreamSelected: { stream, audioStream in
|
||||
switchToStream(stream, audioStream: audioStream)
|
||||
},
|
||||
onCaptionSelected: { caption in
|
||||
playerService.loadCaption(caption)
|
||||
},
|
||||
onLoadOnlineStreams: {
|
||||
Task {
|
||||
await playerService.loadOnlineStreams()
|
||||
}
|
||||
},
|
||||
onSwitchToOnlineStream: { stream, audioStream in
|
||||
Task {
|
||||
await playerService.switchToOnlineStream(stream, audioStream: audioStream)
|
||||
}
|
||||
},
|
||||
onRateChanged: { rate in
|
||||
playerState?.rate = rate
|
||||
playerService.currentBackend?.rate = Float(rate.rawValue)
|
||||
}
|
||||
)
|
||||
}
|
||||
// Quality / Settings selector (fullscreen cover gives tvOS enough room)
|
||||
.fullScreenCover(isPresented: $showingQualitySheet) {
|
||||
qualitySheetContent
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Quality Sheet Content
|
||||
|
||||
@ViewBuilder
|
||||
private var qualitySheetContent: some View {
|
||||
if let playerService {
|
||||
let dashEnabled = appEnvironment?.settingsManager.dashEnabled ?? false
|
||||
let supportedFormats = playerService.currentBackendType.supportedFormats
|
||||
|
||||
ZStack {
|
||||
// Dimmed backdrop over the video
|
||||
Color.black.opacity(0.7)
|
||||
.ignoresSafeArea()
|
||||
|
||||
QualitySelectorView(
|
||||
streams: playerService.availableStreams.filter { stream in
|
||||
let format = StreamFormat.detect(from: stream)
|
||||
if format == .dash && !dashEnabled {
|
||||
return false
|
||||
}
|
||||
return supportedFormats.contains(format)
|
||||
},
|
||||
captions: playerService.availableCaptions,
|
||||
currentStream: playerState?.currentStream,
|
||||
currentAudioStream: playerState?.currentAudioStream,
|
||||
currentCaption: playerService.currentCaption,
|
||||
isLoading: playerState?.playbackState == .loading,
|
||||
currentDownload: playerService.currentDownload,
|
||||
isLoadingOnlineStreams: playerService.isLoadingOnlineStreams,
|
||||
localCaptionURL: playerService.currentDownload.flatMap { download in
|
||||
guard let path = download.localCaptionPath else { return nil }
|
||||
return appEnvironment?.downloadManager.downloadsDirectory().appendingPathComponent(path)
|
||||
},
|
||||
currentRate: playerState?.rate ?? .x1,
|
||||
onStreamSelected: { stream, audioStream in
|
||||
switchToStream(stream, audioStream: audioStream)
|
||||
},
|
||||
onCaptionSelected: { caption in
|
||||
playerService.loadCaption(caption)
|
||||
},
|
||||
onLoadOnlineStreams: {
|
||||
Task {
|
||||
await playerService.loadOnlineStreams()
|
||||
}
|
||||
},
|
||||
onSwitchToOnlineStream: { stream, audioStream in
|
||||
Task {
|
||||
await playerService.switchToOnlineStream(stream, audioStream: audioStream)
|
||||
}
|
||||
},
|
||||
onRateChanged: { rate in
|
||||
playerState?.rate = rate
|
||||
playerService.currentBackend?.rate = Float(rate.rawValue)
|
||||
}
|
||||
)
|
||||
.frame(maxWidth: 900, maxHeight: 700)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 24, style: .continuous)
|
||||
.fill(.ultraThinMaterial)
|
||||
)
|
||||
.padding(.horizontal, 200)
|
||||
.padding(.vertical, 80)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MPV Content
|
||||
|
||||
/// Custom MPV player view with custom controls.
|
||||
@@ -151,10 +174,11 @@ struct TVPlayerView: View {
|
||||
playerState: playerState,
|
||||
playerService: playerService,
|
||||
focusedControl: $focusedControl,
|
||||
onShowDetails: { showDetailsPanel() },
|
||||
onShowQuality: { showQualitySheet() },
|
||||
onShowSettings: { showQualitySheet() },
|
||||
onShowDetails: { showDetailsPanel(tab: .info) },
|
||||
onShowComments: { showDetailsPanel(tab: .comments) },
|
||||
onShowDebug: { showDebugOverlay() },
|
||||
onDismiss: { dismissPlayer() },
|
||||
onClose: { closeVideo() },
|
||||
onScrubbingChanged: { scrubbing in
|
||||
isScrubbing = scrubbing
|
||||
if scrubbing {
|
||||
@@ -171,6 +195,7 @@ struct TVPlayerView: View {
|
||||
if isDetailsPanelVisible {
|
||||
TVDetailsPanel(
|
||||
video: playerState?.currentVideo,
|
||||
initialTab: detailsPanelInitialTab,
|
||||
onDismiss: { hideDetailsPanel() }
|
||||
)
|
||||
.transition(.move(edge: .bottom).combined(with: .opacity))
|
||||
@@ -336,8 +361,9 @@ struct TVPlayerView: View {
|
||||
|
||||
// MARK: - Details Panel
|
||||
|
||||
private func showDetailsPanel() {
|
||||
private func showDetailsPanel(tab: TVDetailsTab = .info) {
|
||||
stopControlsTimer()
|
||||
detailsPanelInitialTab = tab
|
||||
withAnimation(.spring(response: 0.4, dampingFraction: 0.85)) {
|
||||
isDetailsPanelVisible = true
|
||||
controlsVisible = false
|
||||
@@ -466,6 +492,14 @@ struct TVPlayerView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func closeVideo() {
|
||||
playerState?.isClosingVideo = true
|
||||
appEnvironment?.queueManager.clearQueue()
|
||||
playerService?.stop()
|
||||
appEnvironment?.navigationCoordinator.isPlayerExpanded = false
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private func dismissPlayer() {
|
||||
// Collapse the player but keep it alive so audio continues in the background
|
||||
// and the "Now Playing" sidebar entry can restore the session. Matches the
|
||||
|
||||
Reference in New Issue
Block a user