mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 09:49:46 +00:00
Yattee v2 rewrite
This commit is contained in:
247
Yattee/Views/Player/QualitySelectorView.swift
Normal file
247
Yattee/Views/Player/QualitySelectorView.swift
Normal file
@@ -0,0 +1,247 @@
|
||||
//
|
||||
// QualitySelectorView.swift
|
||||
// Yattee
|
||||
//
|
||||
// Sheet for selecting video quality, audio track, and subtitles.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct QualitySelectorView: View {
|
||||
// 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: - Environment & State
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.appEnvironment) private var appEnvironment
|
||||
@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("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 }
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user