mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 01:39:46 +00:00
Yattee v2 rewrite
This commit is contained in:
693
Yattee/Views/Player/QualitySelectorView+Sections.swift
Normal file
693
Yattee/Views/Player/QualitySelectorView+Sections.swift
Normal file
@@ -0,0 +1,693 @@
|
||||
//
|
||||
// QualitySelectorView+Sections.swift
|
||||
// Yattee
|
||||
//
|
||||
// Section content views for QualitySelectorView.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension QualitySelectorView {
|
||||
// MARK: - Main Content Views
|
||||
|
||||
@ViewBuilder
|
||||
var loadingContent: some View {
|
||||
VStack(spacing: 16) {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
.controlSize(.large)
|
||||
Text(String(localized: "player.quality.loading"))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var emptyContent: some View {
|
||||
ContentUnavailableView(
|
||||
String(localized: "player.quality.unavailable"),
|
||||
systemImage: "film.stack",
|
||||
description: Text(String(localized: "player.quality.unavailable.description"))
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var downloadedContent: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
if !hasOnlineStreams {
|
||||
downloadInfoSection
|
||||
loadOnlineStreamsButton
|
||||
} else {
|
||||
onlineStreamsAfterDownload
|
||||
}
|
||||
|
||||
if showTabPicker {
|
||||
generalSectionContent
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var streamsContent: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
if showTabPicker && availableTabs.count > 1 {
|
||||
mediaSelectionRows
|
||||
} else {
|
||||
tabContent
|
||||
}
|
||||
|
||||
if showTabPicker {
|
||||
generalSectionContent
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.onAppear {
|
||||
selectedTab = initialTab
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Tab Content
|
||||
|
||||
@ViewBuilder
|
||||
private var mediaSelectionRows: some View {
|
||||
VStack(spacing: 0) {
|
||||
NavigationLink(value: QualitySelectorDestination.video) {
|
||||
HStack {
|
||||
Label(String(localized: "player.quality.video"), systemImage: "film")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Text(currentVideoDisplayValue)
|
||||
.foregroundStyle(.secondary)
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 12)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
if availableTabs.contains(.audio) {
|
||||
Divider()
|
||||
NavigationLink(value: QualitySelectorDestination.audio) {
|
||||
HStack {
|
||||
Label(String(localized: "stream.audio"), systemImage: "speaker.wave.2")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Text(currentAudioDisplayValue)
|
||||
.foregroundStyle(.secondary)
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 12)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
if availableTabs.contains(.subtitles) {
|
||||
Divider()
|
||||
NavigationLink(value: QualitySelectorDestination.subtitles) {
|
||||
HStack {
|
||||
Label(String(localized: "stream.subtitles"), systemImage: "captions.bubble")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Text(currentSubtitlesDisplayValue)
|
||||
.foregroundStyle(.secondary)
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 12)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.cardBackground()
|
||||
}
|
||||
|
||||
// MARK: - Display Value Computed Properties
|
||||
|
||||
private var currentVideoDisplayValue: String {
|
||||
guard let stream = currentStream else {
|
||||
return String(localized: "stream.subtitles.none")
|
||||
}
|
||||
let format = StreamFormat.detect(from: stream)
|
||||
if format == .hls || format == .dash {
|
||||
return format == .hls ? "HLS" : "DASH"
|
||||
}
|
||||
return stream.qualityLabel
|
||||
}
|
||||
|
||||
private var currentAudioDisplayValue: String {
|
||||
if isCurrentStreamMuxed {
|
||||
return String(localized: "player.quality.audioFromVideo.short")
|
||||
}
|
||||
if let audio = selectedAudioStream ?? currentAudioStream {
|
||||
return parseAudioTrackName(audio).language
|
||||
}
|
||||
return String(localized: "stream.audio.default")
|
||||
}
|
||||
|
||||
private var currentSubtitlesDisplayValue: String {
|
||||
currentCaption?.displayName ?? String(localized: "stream.subtitles.off")
|
||||
}
|
||||
|
||||
// MARK: - Detail Content Views
|
||||
|
||||
@ViewBuilder
|
||||
var videoDetailContent: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
if !adaptiveStreams.isEmpty {
|
||||
adaptiveSectionContent
|
||||
}
|
||||
if !videoStreams.isEmpty {
|
||||
videoSectionContent
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.background(ListBackgroundStyle.grouped.color)
|
||||
.navigationTitle(String(localized: "player.quality.video"))
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var audioDetailContent: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
audioSectionContent
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.background(ListBackgroundStyle.grouped.color)
|
||||
.navigationTitle(String(localized: "stream.audio"))
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var subtitlesDetailContent: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
subtitlesSectionContent
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.background(ListBackgroundStyle.grouped.color)
|
||||
.navigationTitle(String(localized: "stream.subtitles"))
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var tabContent: some View {
|
||||
if selectedTab == .video && !adaptiveStreams.isEmpty {
|
||||
adaptiveSectionContent
|
||||
}
|
||||
|
||||
switch selectedTab {
|
||||
case .video:
|
||||
if !videoStreams.isEmpty {
|
||||
videoSectionContent
|
||||
}
|
||||
case .audio:
|
||||
audioSectionContent
|
||||
case .subtitles:
|
||||
subtitlesSectionContent
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var onlineStreamsAfterDownload: some View {
|
||||
if availableTabs.count > 1 {
|
||||
mediaSelectionRows
|
||||
} else {
|
||||
if selectedTab == .video && !adaptiveStreams.isEmpty {
|
||||
adaptiveSectionContent
|
||||
}
|
||||
|
||||
switch selectedTab {
|
||||
case .video:
|
||||
if !videoStreams.isEmpty {
|
||||
videoSectionContent
|
||||
}
|
||||
case .audio:
|
||||
audioSectionContent
|
||||
case .subtitles:
|
||||
subtitlesSectionContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - General Section
|
||||
|
||||
@ViewBuilder
|
||||
var generalSectionContent: some View {
|
||||
VStack(spacing: 0) {
|
||||
playbackSpeedRow
|
||||
|
||||
Divider()
|
||||
|
||||
#if !os(tvOS)
|
||||
lockControlsRow
|
||||
#endif
|
||||
}
|
||||
.cardBackground()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var playbackSpeedRow: some View {
|
||||
HStack {
|
||||
Label(String(localized: "player.quality.playbackSpeed"), systemImage: "gauge.with.needle")
|
||||
.font(.headline)
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Button {
|
||||
if let newRate = previousRate() {
|
||||
onRateChanged?(newRate)
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "minus")
|
||||
.font(.body.weight(.medium))
|
||||
.frame(minWidth: 18, minHeight: 18)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.disabled(previousRate() == nil)
|
||||
|
||||
Menu {
|
||||
ForEach(PlaybackRate.allCases) { rate in
|
||||
Button {
|
||||
onRateChanged?(rate)
|
||||
} label: {
|
||||
if currentRate == rate {
|
||||
Label(rate.displayText, systemImage: "checkmark")
|
||||
} else {
|
||||
Text(rate.displayText)
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(currentRate.displayText)
|
||||
.font(.body.weight(.medium))
|
||||
.frame(minWidth: 60)
|
||||
}
|
||||
.menuStyle(.borderlessButton)
|
||||
|
||||
Button {
|
||||
if let newRate = nextRate() {
|
||||
onRateChanged?(newRate)
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
.font(.body.weight(.medium))
|
||||
.frame(minWidth: 18, minHeight: 18)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.disabled(nextRate() == nil)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
@ViewBuilder
|
||||
private var lockControlsRow: some View {
|
||||
HStack {
|
||||
Label(String(localized: "player.quality.lockControls"), systemImage: "lock")
|
||||
.font(.headline)
|
||||
|
||||
Spacer()
|
||||
|
||||
Toggle("", isOn: Binding(
|
||||
get: { isControlsLocked },
|
||||
set: { onLockToggled?($0) }
|
||||
))
|
||||
.labelsHidden()
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - Download Info Section
|
||||
|
||||
@ViewBuilder
|
||||
var downloadInfoSection: some View {
|
||||
if let download = currentDownload {
|
||||
// Video section
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Label(String(localized: "player.quality.video"), systemImage: "film")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
DownloadedVideoRowView(download: download, showAdvancedDetails: showAdvancedStreamDetails)
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.cardBackground()
|
||||
}
|
||||
|
||||
// Audio section (if separate audio track)
|
||||
if download.localAudioPath != nil {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Label(String(localized: "stream.audio"), systemImage: "speaker.wave.2")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
DownloadedAudioRowView(download: download, showAdvancedDetails: showAdvancedStreamDetails)
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.cardBackground()
|
||||
}
|
||||
}
|
||||
|
||||
// Subtitles section (if downloaded)
|
||||
if download.localCaptionPath != nil {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Label(String(localized: "stream.subtitles"), systemImage: "captions.bubble")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
CaptionRowView(
|
||||
caption: nil,
|
||||
isSelected: currentCaption == nil,
|
||||
isPreferred: false,
|
||||
onTap: {
|
||||
onCaptionSelected(nil)
|
||||
dismiss()
|
||||
}
|
||||
)
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, 12)
|
||||
|
||||
Divider()
|
||||
|
||||
DownloadedCaptionRowView(
|
||||
download: download,
|
||||
localCaptionURL: localCaptionURL,
|
||||
currentCaption: currentCaption,
|
||||
onCaptionSelected: onCaptionSelected,
|
||||
onDismiss: { dismiss() }
|
||||
)
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.cardBackground()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var loadOnlineStreamsButton: some View {
|
||||
Button {
|
||||
onLoadOnlineStreams()
|
||||
} label: {
|
||||
HStack {
|
||||
if isLoadingOnlineStreams {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
} else {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
}
|
||||
Text(String(localized: "player.quality.loadOnline"))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.disabled(isLoadingOnlineStreams)
|
||||
}
|
||||
|
||||
// MARK: - Adaptive Section
|
||||
|
||||
@ViewBuilder
|
||||
var adaptiveSectionContent: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Label(String(localized: "stream.adaptive"), systemImage: "antenna.radiowaves.left.and.right")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
ForEach(Array(adaptiveStreams.enumerated()), id: \.element.url) { index, stream in
|
||||
if index > 0 {
|
||||
Divider()
|
||||
}
|
||||
AdaptiveStreamRowView(
|
||||
stream: stream,
|
||||
isSelected: stream.url == currentStream?.url,
|
||||
onTap: {
|
||||
handleAdaptiveStreamTap(stream)
|
||||
}
|
||||
)
|
||||
.padding(.vertical, 10)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
}
|
||||
.cardBackground()
|
||||
}
|
||||
}
|
||||
|
||||
private func handleAdaptiveStreamTap(_ stream: Stream) {
|
||||
if isPlayingDownloadedContent {
|
||||
onSwitchToOnlineStream(stream, nil)
|
||||
} else {
|
||||
onStreamSelected(stream, nil)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
|
||||
// MARK: - Video Section
|
||||
|
||||
@ViewBuilder
|
||||
var videoSectionContent: some View {
|
||||
VStack(spacing: 16) {
|
||||
// Recommended section
|
||||
if !recommendedVideoStreams.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Label(String(localized: "player.quality.recommended"), systemImage: "bolt.fill")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
ForEach(Array(recommendedVideoStreams.enumerated()), id: \.element.url) { index, stream in
|
||||
if index > 0 {
|
||||
Divider()
|
||||
}
|
||||
videoStreamRow(stream)
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
}
|
||||
.cardBackground()
|
||||
}
|
||||
}
|
||||
|
||||
// Other section (software decode)
|
||||
if showAdvancedStreamDetails && !otherVideoStreams.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Label(String(localized: "player.quality.other"), systemImage: "cpu")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
ForEach(Array(otherVideoStreams.enumerated()), id: \.element.url) { index, stream in
|
||||
if index > 0 {
|
||||
Divider()
|
||||
}
|
||||
videoStreamRow(stream)
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
}
|
||||
.cardBackground()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func videoStreamRow(_ stream: Stream) -> some View {
|
||||
let isDownloadedStream: Bool = stream.url.isFileURL
|
||||
let isSelected: Bool = stream.isMuxed
|
||||
? stream.url == currentStream?.url
|
||||
: stream.url == selectedVideoStream?.url
|
||||
let isPreferredQuality: Bool = stream.resolution == preferredQuality.maxResolution
|
||||
|
||||
VideoStreamRowView(
|
||||
stream: stream,
|
||||
isSelected: isSelected,
|
||||
isPreferredQuality: isPreferredQuality,
|
||||
isDownloaded: isDownloadedStream,
|
||||
showAdvancedDetails: showAdvancedStreamDetails,
|
||||
requiresSoftwareDecode: !stream.isMuxed && requiresSoftwareDecode(stream.videoCodec),
|
||||
onTap: {
|
||||
handleVideoStreamTap(stream, isDownloaded: isDownloadedStream)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func handleVideoStreamTap(_ stream: Stream, isDownloaded: Bool) {
|
||||
if isDownloaded {
|
||||
if stream.isMuxed {
|
||||
onStreamSelected(stream, nil)
|
||||
dismiss()
|
||||
} else {
|
||||
selectedVideoStream = stream
|
||||
if let audio = selectedAudioStream {
|
||||
onStreamSelected(stream, audio)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
} else if isPlayingDownloadedContent {
|
||||
let audioStream: Stream? = stream.isVideoOnly ? (selectedAudioStream ?? defaultAudioStream) : nil
|
||||
onSwitchToOnlineStream(stream, audioStream)
|
||||
dismiss()
|
||||
} else {
|
||||
if stream.isMuxed {
|
||||
onStreamSelected(stream, nil)
|
||||
dismiss()
|
||||
} else {
|
||||
selectedVideoStream = stream
|
||||
if let audio = selectedAudioStream {
|
||||
onStreamSelected(stream, audio)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Audio Section
|
||||
|
||||
@ViewBuilder
|
||||
var audioSectionContent: some View {
|
||||
if isCurrentStreamMuxed {
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundStyle(.secondary)
|
||||
Text(String(localized: "player.quality.audioFromVideo"))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.cardBackground()
|
||||
} else {
|
||||
VStack(spacing: 0) {
|
||||
ForEach(Array(audioStreams.enumerated()), id: \.element.url) { index, stream in
|
||||
if index > 0 {
|
||||
Divider()
|
||||
}
|
||||
audioStreamRow(stream)
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
}
|
||||
.cardBackground()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func audioStreamRow(_ stream: Stream) -> some View {
|
||||
let isSelected: Bool = stream.url == selectedAudioStream?.url
|
||||
let isPreferred: Bool = preferredAudioLanguage.map { (stream.audioLanguage ?? "").hasPrefix($0) } ?? false
|
||||
let trackInfo: AudioTrackInfo = parseAudioTrackName(stream)
|
||||
|
||||
AudioStreamRowView(
|
||||
stream: stream,
|
||||
isSelected: isSelected,
|
||||
isPreferred: isPreferred,
|
||||
showAdvancedDetails: showAdvancedStreamDetails,
|
||||
trackInfo: trackInfo,
|
||||
onTap: {
|
||||
handleAudioStreamTap(stream)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func handleAudioStreamTap(_ stream: Stream) {
|
||||
selectedAudioStream = stream
|
||||
if let video = selectedVideoStream, video.isVideoOnly {
|
||||
onStreamSelected(video, stream)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Subtitles Section
|
||||
|
||||
@ViewBuilder
|
||||
var subtitlesSectionContent: some View {
|
||||
VStack(spacing: 0) {
|
||||
CaptionRowView(
|
||||
caption: nil,
|
||||
isSelected: currentCaption == nil,
|
||||
isPreferred: false,
|
||||
onTap: {
|
||||
handleCaptionTap(nil)
|
||||
}
|
||||
)
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, 12)
|
||||
|
||||
ForEach(sortedCaptions) { caption in
|
||||
Divider()
|
||||
|
||||
CaptionRowView(
|
||||
caption: caption,
|
||||
isSelected: caption.id == currentCaption?.id,
|
||||
isPreferred: isCaptionPreferred(caption),
|
||||
onTap: {
|
||||
handleCaptionTap(caption)
|
||||
}
|
||||
)
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
}
|
||||
.cardBackground()
|
||||
}
|
||||
|
||||
private func isCaptionPreferred(_ caption: Caption) -> Bool {
|
||||
guard let preferred = preferredSubtitlesLanguage else { return false }
|
||||
return caption.baseLanguageCode == preferred || caption.languageCode.hasPrefix(preferred)
|
||||
}
|
||||
|
||||
private func handleCaptionTap(_ caption: Caption?) {
|
||||
if caption?.id == currentCaption?.id && caption != nil {
|
||||
onCaptionSelected(nil)
|
||||
} else {
|
||||
onCaptionSelected(caption)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user