Remove the tracks details panel page

The quality selector sheet already covers quality, audio, subtitles and
rate, so the embedded tracks page duplicated it. Drop the page type, its
per-page settings, the editor toggles, and the visibleSections plumbing
in QualitySelectorView.
This commit is contained in:
Arkadiusz Fal
2026-09-25 20:26:09 +02:00
parent 1fe1c2cd74
commit f6ba35c1f5
11 changed files with 15 additions and 262 deletions

View File

@@ -7241,17 +7241,6 @@
}
}
},
"panel.page.tracks" : {
"comment" : "Details panel page name",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Tracks"
}
}
}
},
"panel.region.belowVideo" : {
"comment" : "Corner layout region option",
"localizations" : {

View File

@@ -17,7 +17,6 @@ enum PanelPageType: String, Codable, CaseIterable, Sendable {
case related
case chapters
case queue
case tracks
/// Localized display name shown in the page bar and settings.
var displayName: String {
@@ -32,8 +31,6 @@ enum PanelPageType: String, Codable, CaseIterable, Sendable {
return String(localized: "player.chapters")
case .queue:
return String(localized: "panel.page.queue")
case .tracks:
return String(localized: "panel.page.tracks")
}
}
@@ -66,8 +63,6 @@ enum PanelPageType: String, Codable, CaseIterable, Sendable {
return "list.bullet.rectangle"
case .queue:
return "list.bullet"
case .tracks:
return "slider.horizontal.3"
}
}
}
@@ -206,19 +201,6 @@ struct ChaptersPageSettings: Codable, Hashable, Sendable {
var effectiveShowThumbnails: Bool { showThumbnails ?? true }
}
/// Element toggles for the tracks page.
struct TracksPageSettings: Codable, Hashable, Sendable {
var showQuality: Bool?
var showAudio: Bool?
var showSubtitles: Bool?
var showRate: Bool?
var effectiveShowQuality: Bool { showQuality ?? true }
var effectiveShowAudio: Bool { showAudio ?? true }
var effectiveShowSubtitles: Bool { showSubtitles ?? true }
var effectiveShowRate: Bool { showRate ?? true }
}
/// Typed per-page settings payload. Uses the same discriminator pattern as
/// `ButtonSettings`; an unknown discriminator from a future app version
/// decodes to `nil` at the configuration level so the pages array never
@@ -229,7 +211,6 @@ enum PanelPageSettings: Codable, Hashable, Sendable {
case related(RelatedPageSettings)
case chapters(ChaptersPageSettings)
case queue(QueuePageSettings)
case tracks(TracksPageSettings)
private enum CodingKeys: String, CodingKey {
case type
@@ -238,7 +219,6 @@ enum PanelPageSettings: Codable, Hashable, Sendable {
case related
case chapters
case queue
case tracks
}
private enum SettingsType: String, Codable {
@@ -247,7 +227,6 @@ enum PanelPageSettings: Codable, Hashable, Sendable {
case related
case chapters
case queue
case tracks
}
init(from decoder: Decoder) throws {
@@ -265,8 +244,6 @@ enum PanelPageSettings: Codable, Hashable, Sendable {
self = .chapters(try container.decode(ChaptersPageSettings.self, forKey: .chapters))
case .queue:
self = .queue(try container.decode(QueuePageSettings.self, forKey: .queue))
case .tracks:
self = .tracks(try container.decode(TracksPageSettings.self, forKey: .tracks))
}
}
@@ -289,9 +266,6 @@ enum PanelPageSettings: Codable, Hashable, Sendable {
case .queue(let settings):
try container.encode(SettingsType.queue, forKey: .type)
try container.encode(settings, forKey: .queue)
case .tracks(let settings):
try container.encode(SettingsType.tracks, forKey: .type)
try container.encode(settings, forKey: .tracks)
}
}
}
@@ -349,12 +323,6 @@ struct PanelPageConfiguration: Codable, Hashable, Sendable, Identifiable {
return QueuePageSettings()
}
/// Tracks page settings when set, defaults otherwise.
var tracksSettings: TracksPageSettings {
if case .tracks(let settings) = settings { return settings }
return TracksPageSettings()
}
private enum CodingKeys: String, CodingKey {
case pageType
case isEnabled
@@ -415,7 +383,6 @@ struct PanelSettings: Codable, Hashable, Sendable {
PanelPageConfiguration(pageType: .related, region: .primary),
PanelPageConfiguration(pageType: .chapters, region: .primary),
PanelPageConfiguration(pageType: .queue, region: .primary),
PanelPageConfiguration(pageType: .tracks, isEnabled: false, region: .primary),
])
var effectiveLayoutMode: PanelLayoutMode { layoutMode ?? .stacked }

View File

@@ -891,15 +891,6 @@ final class PlayerControlsSettingsViewModel {
}
}
/// Synchronously updates the tracks page element toggles.
func updatePanelTracksSettingsSync(_ mutation: (inout TracksPageSettings) -> Void) {
updatePanelSettingsSync { settings in
var tracks = settings.configuration(for: .tracks)?.tracksSettings ?? TracksPageSettings()
mutation(&tracks)
settings.updateSettings(.tracks(tracks), for: .tracks)
}
}
/// Whether the page bar shows labels next to icons.
var panelShowPageBarLabels: Bool {
panelSettings.effectiveShowPageBarLabels

View File

@@ -63,8 +63,6 @@ struct PlayerPanelContentView: View {
return !(playerState?.chapters ?? []).isEmpty
case .queue:
return appEnvironment?.settingsManager.queueEnabled ?? true
case .tracks:
return true
}
}
@@ -178,9 +176,6 @@ struct PlayerPanelContentView: View {
case .queue:
QueuePanelPage(context: context, settings: page.queueSettings, bottomInset: scrollConfig.bottomInset)
.id(PanelPageType.queue)
case .tracks:
TracksPanelPage(context: context, settings: page.tracksSettings, bottomInset: scrollConfig.bottomInset)
.id(PanelPageType.tracks)
}
}
}

View File

@@ -1,97 +0,0 @@
//
// TracksPanelPage.swift
// Yattee
//
// Details panel page embedding the quality/audio/subtitles/rate selector.
//
import SwiftUI
#if os(iOS) || os(macOS)
struct TracksPanelPage: View {
let context: PanelPageContext
var settings: TracksPageSettings = TracksPageSettings()
var bottomInset: CGFloat = 0
private var appEnvironment: AppEnvironment? { context.appEnvironment }
private var playerService: PlayerService? { appEnvironment?.playerService }
private var playerState: PlayerState? { playerService?.state }
var body: some View {
if let playerService {
let supportedFormats = playerService.currentBackendType.supportedFormats
QualitySelectorView(
streams: playerService.availableStreams.filter { stream in
let format = StreamFormat.detect(from: stream)
// DASH is never offered for manual selection
if format == .dash {
return false
}
return supportedFormats.contains(format)
},
captions: playerService.availableCaptions,
currentStream: playerState?.currentStream,
currentAudioStream: playerState?.currentAudioStream,
currentCaption: playerService.currentCaption,
isLoading: playerService.availableStreams.isEmpty && 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,
isControlsLocked: playerState?.isControlsLocked ?? false,
isAudioMode: appEnvironment?.settingsManager.audioOnlyModeEnabled ?? false,
embeddedAudioTracks: playerService.embeddedAudioTracks,
embeddedSubtitleTracks: playerService.embeddedSubtitleTracks,
currentEmbeddedAudioTrackID: playerService.selectedEmbeddedAudioTrackID,
currentEmbeddedSubtitleTrackID: playerService.selectedEmbeddedSubtitleTrackID,
embeddedVideoTrack: playerService.primaryEmbeddedVideoTrack,
onStreamSelected: { stream, audioStream in
Task {
await playerService.selectStreamManually(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)
}
},
onEmbeddedAudioTrackSelected: { trackID in
playerService.selectEmbeddedAudioTrack(trackID)
},
onEmbeddedSubtitleTrackSelected: { trackID in
playerService.selectEmbeddedSubtitleTrack(trackID)
},
onRateChanged: { rate in
playerState?.rate = rate
playerService.currentBackend?.rate = Float(rate.rawValue)
},
onLockToggled: { locked in
playerState?.isControlsLocked = locked
},
onAudioModeToggled: { enabled in
Task {
await playerService.setAudioMode(enabled)
}
},
isEmbedded: true,
visibleSections: settings
)
.safeAreaPadding(.bottom, bottomInset)
.panelListLeadingMargin()
}
}
}
#endif

View File

@@ -346,19 +346,15 @@ extension QualitySelectorView {
#if os(tvOS)
// tvOS rows are styled individually to match the Settings rows.
VStack(spacing: 8) {
if visibleSections.effectiveShowRate {
playbackSpeedRow
}
playbackSpeedRow
audioModeRow
}
#else
VStack(spacing: 0) {
if visibleSections.effectiveShowRate {
playbackSpeedRow
playbackSpeedRow
Divider()
}
Divider()
audioModeRow

View File

@@ -64,14 +64,10 @@ struct QualitySelectorView: View {
/// the tvOS half-screen panel). When nil, falls back to `@Environment(\.dismiss)`.
var onDismiss: (() -> Void)?
/// Embedded inside a container (details panel tracks page): no close
/// toolbar item, no sheet detents, and no minimum frame.
/// Embedded inside a container: no close toolbar item, no sheet detents,
/// and no minimum frame.
var isEmbedded: Bool = false
/// Section visibility from the tracks page preset settings. Defaults show
/// everything, so the standalone sheet is unaffected.
var visibleSections: TracksPageSettings = TracksPageSettings()
#if os(tvOS)
/// Bound to the first focusable row so we can programmatically pull focus
/// into the panel on appear (the system doesn't auto-focus an inline
@@ -119,18 +115,13 @@ struct QualitySelectorView: View {
// MARK: - Computed Properties
/// Available tabs based on streams and the tracks page section settings
/// Available tabs based on streams
var availableTabs: [QualitySelectorTab] {
var tabs: [QualitySelectorTab] = []
if visibleSections.effectiveShowQuality {
tabs.append(.video)
}
if visibleSections.effectiveShowAudio,
(hasVideoOnlyStreams && !audioStreams.isEmpty) || embeddedAudioTracks.count > 1 {
var tabs: [QualitySelectorTab] = [.video]
if (hasVideoOnlyStreams && !audioStreams.isEmpty) || embeddedAudioTracks.count > 1 {
tabs.append(.audio)
}
if visibleSections.effectiveShowSubtitles,
!captions.isEmpty || !embeddedSubtitleTracks.isEmpty || canLoadExternalSubtitles {
if !captions.isEmpty || !embeddedSubtitleTracks.isEmpty || canLoadExternalSubtitles {
tabs.append(.subtitles)
}
// The selector never renders with zero tabs; fall back to video.
@@ -222,8 +213,7 @@ struct QualitySelectorView: View {
onLockToggled: ((Bool) -> Void)? = nil,
onAudioModeToggled: ((Bool) -> Void)? = nil,
onDismiss: (() -> Void)? = nil,
isEmbedded: Bool = false,
visibleSections: TracksPageSettings = TracksPageSettings()
isEmbedded: Bool = false
) {
self.streams = streams
self.captions = captions
@@ -255,7 +245,6 @@ struct QualitySelectorView: View {
self.onAudioModeToggled = onAudioModeToggled
self.onDismiss = onDismiss
self.isEmbedded = isEmbedded
self.visibleSections = visibleSections
}
// MARK: - Body

View File

@@ -56,9 +56,6 @@ struct TVDetailsPanel: View {
return !(playerState?.chapters ?? []).isEmpty
case .queue:
return appEnvironment?.settingsManager.queueEnabled ?? true
case .tracks:
// The tvOS quality panel covers tracks; never offered here.
return false
}
}
@@ -90,7 +87,7 @@ struct TVDetailsPanel: View {
// Content for the selected page
Group {
switch effectivePage {
case .info, .tracks:
case .info:
infoContent
case .comments:
commentsContent

View File

@@ -24,10 +24,6 @@ struct PanelPageEditorView: View {
@State private var showRelatedThumbnails: Bool = true
@State private var showChapterThumbnails: Bool = true
@State private var showHistory: Bool = true
@State private var showQuality: Bool = true
@State private var showAudio: Bool = true
@State private var showSubtitles: Bool = true
@State private var showRate: Bool = true
private var configuration: PanelPageConfiguration? {
viewModel.panelSettings.configuration(for: pageType)
@@ -57,8 +53,6 @@ struct PanelPageEditorView: View {
chaptersElementsSection
case .queue:
queueElementsSection
case .tracks:
tracksElementsSection
}
}
.navigationTitle(pageType.displayName)
@@ -243,51 +237,6 @@ struct PanelPageEditorView: View {
.disabled(!viewModel.canEditActivePreset)
}
// MARK: - Tracks Elements Section
private var tracksElementsSection: some View {
Section {
Toggle(
String(localized: "player.quality.video"),
isOn: $showQuality
)
.onChange(of: showQuality) { _, newValue in
guard newValue != tracksSettings.effectiveShowQuality else { return }
viewModel.updatePanelTracksSettingsSync { $0.showQuality = newValue }
}
Toggle(
String(localized: "stream.audio"),
isOn: $showAudio
)
.onChange(of: showAudio) { _, newValue in
guard newValue != tracksSettings.effectiveShowAudio else { return }
viewModel.updatePanelTracksSettingsSync { $0.showAudio = newValue }
}
Toggle(
String(localized: "stream.subtitles"),
isOn: $showSubtitles
)
.onChange(of: showSubtitles) { _, newValue in
guard newValue != tracksSettings.effectiveShowSubtitles else { return }
viewModel.updatePanelTracksSettingsSync { $0.showSubtitles = newValue }
}
Toggle(
String(localized: "controls.button.speed"),
isOn: $showRate
)
.onChange(of: showRate) { _, newValue in
guard newValue != tracksSettings.effectiveShowRate else { return }
viewModel.updatePanelTracksSettingsSync { $0.showRate = newValue }
}
} header: {
Text(String(localized: "panel.settings.elements.header"))
}
.disabled(!viewModel.canEditActivePreset)
}
// MARK: - Helpers
private var infoSettings: InfoPageSettings {
@@ -310,10 +259,6 @@ struct PanelPageEditorView: View {
configuration?.queueSettings ?? QueuePageSettings()
}
private var tracksSettings: TracksPageSettings {
configuration?.tracksSettings ?? TracksPageSettings()
}
private func syncLocalState() {
isEnabled = configuration?.isEnabled ?? true
region = configuration?.region ?? .primary
@@ -326,11 +271,6 @@ struct PanelPageEditorView: View {
showRelatedThumbnails = relatedSettings.effectiveShowThumbnails
showChapterThumbnails = chaptersSettings.effectiveShowThumbnails
showHistory = queueSettings.effectiveShowHistory
let tracks = tracksSettings
showQuality = tracks.effectiveShowQuality
showAudio = tracks.effectiveShowAudio
showSubtitles = tracks.effectiveShowSubtitles
showRate = tracks.effectiveShowRate
}
}

View File

@@ -42,9 +42,9 @@ struct TVPlayerPanelsSettingsView: View {
private struct TVPlayerPanelsContent: View {
@Bindable var viewModel: PlayerControlsSettingsViewModel
/// Pages offered on tvOS; tracks is covered by the quality panel.
/// Pages offered on tvOS.
private var tvOSPages: [PanelPageConfiguration] {
viewModel.panelPages.filter { $0.pageType != .tracks }
viewModel.panelPages
}
var body: some View {
@@ -143,9 +143,9 @@ private struct TVPanelPageEditorView: View {
isEnabled && viewModel.enabledPanelPageCount <= 1
}
/// Pages as displayed in the tvOS list (tracks hidden).
/// Pages as displayed in the tvOS list.
private var displayedPages: [PanelPageConfiguration] {
viewModel.panelPages.filter { $0.pageType != .tracks }
viewModel.panelPages
}
private var displayedIndex: Int? {

View File

@@ -57,10 +57,6 @@ struct PanelSettingsTests {
settings.updateSettings(.related(RelatedPageSettings(showThumbnails: false)), for: .related)
settings.updateSettings(.chapters(ChaptersPageSettings(showThumbnails: false)), for: .chapters)
settings.updateSettings(.queue(QueuePageSettings(showHistory: false)), for: .queue)
settings.updateSettings(
.tracks(TracksPageSettings(showQuality: false, showSubtitles: false)),
for: .tracks
)
let decoded = try roundTrip(settings)
#expect(decoded == settings)
@@ -68,13 +64,6 @@ struct PanelSettingsTests {
#expect(decoded.configuration(for: .related)?.relatedSettings.effectiveShowThumbnails == false)
#expect(decoded.configuration(for: .chapters)?.chaptersSettings.effectiveShowThumbnails == false)
#expect(decoded.configuration(for: .queue)?.queueSettings.effectiveShowHistory == false)
let tracks = try #require(decoded.configuration(for: .tracks)?.tracksSettings)
#expect(tracks.effectiveShowQuality == false)
#expect(tracks.effectiveShowSubtitles == false)
// Unset fields keep their effective defaults
#expect(tracks.effectiveShowAudio == true)
#expect(tracks.effectiveShowRate == true)
}
@Test("Typed accessors fall back to defaults for mismatched payloads")
@@ -86,7 +75,6 @@ struct PanelSettingsTests {
)
#expect(page.commentsSettings.effectiveShowAvatars == true)
#expect(page.queueSettings.effectiveShowHistory == true)
#expect(page.tracksSettings.effectiveShowQuality == true)
}
// MARK: - Backward Compatibility
@@ -185,8 +173,6 @@ struct PanelSettingsTests {
#expect(primary == [.related, .chapters, .queue])
#expect(belowVideo == [.info, .comments])
#expect(all == [.info, .comments, .related, .chapters, .queue])
// Tracks is disabled by default
#expect(!all.contains(.tracks))
}
// MARK: - Mutations