Files
yattee/Yattee/Views/Player/QualitySelectorView.swift
Arkadiusz Fal 612dce6b9f Refactor views
2026-02-09 01:13:02 +01:00

250 lines
7.7 KiB
Swift

//
// QualitySelectorView.swift
// Yattee
//
// Sheet for selecting video quality, audio track, and subtitles.
//
import SwiftUI
struct QualitySelectorView: View {
// MARK: - Environment
@Environment(\.dismiss) var dismiss
@Environment(\.appEnvironment) private var appEnvironment
// MARK: - Properties
let streams: [Stream]
let captions: [Caption]
let currentStream: Stream?
let currentAudioStream: Stream?
let currentCaption: Caption?
let isLoading: Bool
let currentDownload: Download?
let isLoadingOnlineStreams: Bool
let localCaptionURL: URL?
let onStreamSelected: (Stream, Stream?) -> Void
let onCaptionSelected: (Caption?) -> Void
let onLoadOnlineStreams: () -> Void
let onSwitchToOnlineStream: (Stream, Stream?) -> Void
/// Current playback rate
var currentRate: PlaybackRate = .x1
/// Callback when playback rate changes
var onRateChanged: ((PlaybackRate) -> Void)?
/// Whether controls are locked
var isControlsLocked: Bool = false
/// Callback when lock state changes
var onLockToggled: ((Bool) -> Void)?
/// Initial tab to show when view appears
var initialTab: QualitySelectorTab = .video
/// Whether to show the segmented tab picker (false for focused single-tab mode)
var showTabPicker: Bool = true
// MARK: - State
@State var selectedTab: QualitySelectorTab = .video
@State var selectedVideoStream: Stream?
@State var selectedAudioStream: Stream?
// MARK: - Settings Access
/// The preferred audio language from settings
var preferredAudioLanguage: String? {
appEnvironment?.settingsManager.preferredAudioLanguage
}
/// The preferred subtitles language from settings
var preferredSubtitlesLanguage: String? {
appEnvironment?.settingsManager.preferredSubtitlesLanguage
}
/// The preferred video quality from settings
var preferredQuality: VideoQuality {
appEnvironment?.settingsManager.preferredQuality ?? .auto
}
/// Whether to show advanced stream details (codec, bitrate, size)
var showAdvancedStreamDetails: Bool {
appEnvironment?.settingsManager.showAdvancedStreamDetails ?? false
}
// MARK: - Computed Properties
/// Available tabs based on streams
var availableTabs: [QualitySelectorTab] {
var tabs: [QualitySelectorTab] = [.video]
if hasVideoOnlyStreams && !audioStreams.isEmpty {
tabs.append(.audio)
}
if !captions.isEmpty {
tabs.append(.subtitles)
}
return tabs
}
/// Navigation title based on mode
var navigationTitle: String {
if showTabPicker {
return String(localized: "player.quality.settings")
} else {
switch initialTab {
case .video:
return String(localized: "player.quality.video")
case .audio:
return String(localized: "stream.audio")
case .subtitles:
return String(localized: "stream.subtitles")
}
}
}
/// Whether we're playing downloaded content
var isPlayingDownloadedContent: Bool {
currentDownload != nil
}
/// Whether streams are empty (not loading, but no streams available)
var hasNoStreams: Bool {
if !showTabPicker && initialTab == .subtitles {
return !isLoading && captions.isEmpty && !isPlayingDownloadedContent
}
return !isLoading && streams.isEmpty && !isPlayingDownloadedContent
}
/// Whether online streams have been loaded
var hasOnlineStreams: Bool {
streams.contains { !$0.url.isFileURL }
}
/// Whether the current or selected video stream is muxed
var isCurrentStreamMuxed: Bool {
if let selected = selectedVideoStream {
return selected.isMuxed
}
return currentStream?.isMuxed ?? false
}
// MARK: - Initialization
init(
streams: [Stream],
captions: [Caption] = [],
currentStream: Stream?,
currentAudioStream: Stream? = nil,
currentCaption: Caption? = nil,
isLoading: Bool = false,
currentDownload: Download? = nil,
isLoadingOnlineStreams: Bool = false,
localCaptionURL: URL? = nil,
currentRate: PlaybackRate = .x1,
isControlsLocked: Bool = false,
initialTab: QualitySelectorTab = .video,
showTabPicker: Bool = true,
onStreamSelected: @escaping (Stream, Stream?) -> Void,
onCaptionSelected: @escaping (Caption?) -> Void = { _ in },
onLoadOnlineStreams: @escaping () -> Void = {},
onSwitchToOnlineStream: @escaping (Stream, Stream?) -> Void = { _, _ in },
onRateChanged: ((PlaybackRate) -> Void)? = nil,
onLockToggled: ((Bool) -> Void)? = nil
) {
self.streams = streams
self.captions = captions
self.currentStream = currentStream
self.currentAudioStream = currentAudioStream
self.currentCaption = currentCaption
self.isLoading = isLoading
self.currentDownload = currentDownload
self.isLoadingOnlineStreams = isLoadingOnlineStreams
self.localCaptionURL = localCaptionURL
self.initialTab = initialTab
self.showTabPicker = showTabPicker
self.currentRate = currentRate
self.isControlsLocked = isControlsLocked
self.onStreamSelected = onStreamSelected
self.onCaptionSelected = onCaptionSelected
self.onLoadOnlineStreams = onLoadOnlineStreams
self.onSwitchToOnlineStream = onSwitchToOnlineStream
self.onRateChanged = onRateChanged
self.onLockToggled = onLockToggled
}
// MARK: - Body
var body: some View {
NavigationStack {
Group {
if isLoading {
loadingContent
} else if isPlayingDownloadedContent {
downloadedContent
} else if hasNoStreams {
emptyContent
} else {
streamsContent
}
}
.background(ListBackgroundStyle.grouped.color)
.navigationTitle(navigationTitle)
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button(role: .cancel) {
dismiss()
} label: {
Label(String(localized: "common.close"), systemImage: "xmark")
.labelStyle(.iconOnly)
}
}
}
.navigationDestination(for: QualitySelectorDestination.self) { destination in
switch destination {
case .video:
videoDetailContent
case .audio:
audioDetailContent
case .subtitles:
subtitlesDetailContent
}
}
.onAppear {
selectedVideoStream = currentStream
selectedAudioStream = currentAudioStream ?? defaultAudioStream
}
}
.presentationDetents([.medium, .large])
}
}
// MARK: - Preview
#Preview {
QualitySelectorView(
streams: [.preview, .videoOnlyPreview, .audioPreview],
captions: [.preview, .autoGeneratedPreview],
currentStream: .preview,
onStreamSelected: { _, _ in }
)
}
#Preview("Loading") {
QualitySelectorView(
streams: [],
currentStream: nil,
isLoading: true,
onStreamSelected: { _, _ in }
)
}
#Preview("Empty") {
QualitySelectorView(
streams: [],
currentStream: nil,
onStreamSelected: { _, _ in }
)
}