mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 01:39:46 +00:00
396 lines
11 KiB
Swift
396 lines
11 KiB
Swift
//
|
|
// PlaybackSettingsView.swift
|
|
// Yattee
|
|
//
|
|
// Playback settings view with quality and behavior preferences.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct PlaybackSettingsView: View {
|
|
@Environment(\.appEnvironment) private var appEnvironment
|
|
|
|
var body: some View {
|
|
Form {
|
|
if let settings = appEnvironment?.settingsManager {
|
|
QualitySection(settings: settings)
|
|
AudioSection(settings: settings)
|
|
SubtitlesSection(settings: settings)
|
|
BehaviorSection(settings: settings)
|
|
QueueSection(settings: settings)
|
|
#if os(iOS)
|
|
OrientationSection(settings: settings)
|
|
#endif
|
|
#if os(macOS)
|
|
MacOSSection(settings: settings)
|
|
#endif
|
|
}
|
|
}
|
|
.navigationTitle(String(localized: "settings.playback.title"))
|
|
#if os(iOS)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// MARK: - Quality Section
|
|
|
|
private struct QualitySection: View {
|
|
@Bindable var settings: SettingsManager
|
|
|
|
var body: some View {
|
|
Section {
|
|
Picker(
|
|
String(localized: "settings.playback.quality.preferred"),
|
|
selection: $settings.preferredQuality
|
|
) {
|
|
ForEach(VideoQuality.allCases, id: \.self) { quality in
|
|
Text(quality.displayName).tag(quality)
|
|
}
|
|
}
|
|
|
|
#if os(iOS)
|
|
Picker(
|
|
String(localized: "settings.playback.quality.cellular"),
|
|
selection: $settings.cellularQuality
|
|
) {
|
|
ForEach(VideoQuality.allCases, id: \.self) { quality in
|
|
Text(quality.displayName).tag(quality)
|
|
}
|
|
}
|
|
#endif
|
|
} header: {
|
|
Text(String(localized: "settings.playback.video.header"))
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Audio Section
|
|
|
|
private struct AudioSection: View {
|
|
@Bindable var settings: SettingsManager
|
|
|
|
// All YouTube-supported language codes, sorted alphabetically by localized name
|
|
static let languageCodes: [String] = [
|
|
"af", // Afrikaans
|
|
"am", // Amharic
|
|
"ar", // Arabic
|
|
"az", // Azerbaijani
|
|
"be", // Belarusian
|
|
"bg", // Bulgarian
|
|
"bn", // Bengali
|
|
"bs", // Bosnian
|
|
"ca", // Catalan
|
|
"cs", // Czech
|
|
"cy", // Welsh
|
|
"da", // Danish
|
|
"de", // German
|
|
"el", // Greek
|
|
"en", // English
|
|
"es", // Spanish
|
|
"et", // Estonian
|
|
"eu", // Basque
|
|
"fa", // Persian
|
|
"fi", // Finnish
|
|
"fil", // Filipino
|
|
"fr", // French
|
|
"gl", // Galician
|
|
"gu", // Gujarati
|
|
"he", // Hebrew
|
|
"hi", // Hindi
|
|
"hr", // Croatian
|
|
"hu", // Hungarian
|
|
"hy", // Armenian
|
|
"id", // Indonesian
|
|
"is", // Icelandic
|
|
"it", // Italian
|
|
"ja", // Japanese
|
|
"ka", // Georgian
|
|
"kk", // Kazakh
|
|
"km", // Khmer
|
|
"kn", // Kannada
|
|
"ko", // Korean
|
|
"ky", // Kyrgyz
|
|
"lo", // Lao
|
|
"lt", // Lithuanian
|
|
"lv", // Latvian
|
|
"mk", // Macedonian
|
|
"ml", // Malayalam
|
|
"mn", // Mongolian
|
|
"mr", // Marathi
|
|
"ms", // Malay
|
|
"my", // Burmese
|
|
"ne", // Nepali
|
|
"nl", // Dutch
|
|
"no", // Norwegian
|
|
"or", // Odia
|
|
"pa", // Punjabi
|
|
"pl", // Polish
|
|
"pt", // Portuguese
|
|
"ro", // Romanian
|
|
"ru", // Russian
|
|
"si", // Sinhala
|
|
"sk", // Slovak
|
|
"sl", // Slovenian
|
|
"sq", // Albanian
|
|
"sr", // Serbian
|
|
"sv", // Swedish
|
|
"sw", // Swahili
|
|
"ta", // Tamil
|
|
"te", // Telugu
|
|
"th", // Thai
|
|
"tr", // Turkish
|
|
"uk", // Ukrainian
|
|
"ur", // Urdu
|
|
"uz", // Uzbek
|
|
"vi", // Vietnamese
|
|
"zh", // Chinese
|
|
"zu", // Zulu
|
|
]
|
|
|
|
// Generate sorted list of (code, localizedName) tuples
|
|
private var sortedLanguages: [(code: String, name: String)] {
|
|
Self.languageCodes
|
|
.map { code in
|
|
let name = Locale.current.localizedString(forLanguageCode: code) ?? code.uppercased()
|
|
return (code: code, name: name)
|
|
}
|
|
.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
|
|
}
|
|
|
|
var body: some View {
|
|
Section {
|
|
Picker(
|
|
String(localized: "settings.playback.audio.preferredLanguage"),
|
|
selection: Binding(
|
|
get: { settings.preferredAudioLanguage ?? "" },
|
|
set: { settings.preferredAudioLanguage = $0.isEmpty ? nil : $0 }
|
|
)
|
|
) {
|
|
Text(String(localized: "settings.playback.audio.original"))
|
|
.tag("")
|
|
|
|
Divider()
|
|
|
|
ForEach(sortedLanguages, id: \.code) { language in
|
|
Text(language.name).tag(language.code)
|
|
}
|
|
}
|
|
} header: {
|
|
Text(String(localized: "settings.playback.audio.header"))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Volume and System Controls settings are in Player Controls settings
|
|
|
|
// MARK: - Subtitles Section
|
|
|
|
private struct SubtitlesSection: View {
|
|
@Bindable var settings: SettingsManager
|
|
|
|
// Same language codes as audio
|
|
private static let languageCodes = AudioSection.languageCodes
|
|
|
|
// Generate sorted list of (code, localizedName) tuples
|
|
private var sortedLanguages: [(code: String, name: String)] {
|
|
Self.languageCodes
|
|
.map { code in
|
|
let name = Locale.current.localizedString(forLanguageCode: code) ?? code.uppercased()
|
|
return (code: code, name: name)
|
|
}
|
|
.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
|
|
}
|
|
|
|
var body: some View {
|
|
Section {
|
|
Picker(
|
|
String(localized: "settings.playback.subtitles.preferredLanguage"),
|
|
selection: Binding(
|
|
get: { settings.preferredSubtitlesLanguage ?? "" },
|
|
set: { settings.preferredSubtitlesLanguage = $0.isEmpty ? nil : $0 }
|
|
)
|
|
) {
|
|
Text(String(localized: "settings.playback.subtitles.off"))
|
|
.tag("")
|
|
|
|
Divider()
|
|
|
|
ForEach(sortedLanguages, id: \.code) { language in
|
|
Text(language.name).tag(language.code)
|
|
}
|
|
}
|
|
|
|
NavigationLink {
|
|
SubtitlesSettingsView()
|
|
} label: {
|
|
Label(String(localized: "settings.playback.subtitles.appearance"), systemImage: "textformat.size")
|
|
}
|
|
} header: {
|
|
Text(String(localized: "settings.playback.subtitles.header"))
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Behavior Section
|
|
|
|
private struct BehaviorSection: View {
|
|
@Bindable var settings: SettingsManager
|
|
|
|
var body: some View {
|
|
Section(String(localized: "settings.playback.behavior.header")) {
|
|
Picker(
|
|
String(localized: "settings.playback.resumeAction"),
|
|
selection: $settings.resumeAction
|
|
) {
|
|
ForEach(ResumeAction.allCases, id: \.self) { action in
|
|
Text(action.displayName).tag(action)
|
|
}
|
|
}
|
|
|
|
#if os(iOS) || os(macOS)
|
|
Toggle(
|
|
String(localized: "settings.playback.backgroundPlayback"),
|
|
isOn: $settings.backgroundPlaybackEnabled
|
|
)
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Queue Section
|
|
|
|
private struct QueueSection: View {
|
|
@Bindable var settings: SettingsManager
|
|
|
|
var body: some View {
|
|
Section {
|
|
Toggle(
|
|
String(localized: "settings.playback.queue.enabled"),
|
|
isOn: $settings.queueEnabled
|
|
)
|
|
|
|
#if os(tvOS)
|
|
Picker(
|
|
String(localized: "settings.playback.queue.autoPlayCountdown"),
|
|
selection: $settings.queueAutoPlayCountdown
|
|
) {
|
|
ForEach(1...15, id: \.self) { value in
|
|
Text("\(value)s").tag(value)
|
|
}
|
|
}
|
|
.disabled(!settings.queueEnabled)
|
|
#elseif os(macOS)
|
|
// macOS: Use simple string label (custom HStack label breaks Form rendering)
|
|
Stepper(
|
|
"\(String(localized: "settings.playback.queue.autoPlayCountdown")): \(settings.queueAutoPlayCountdown)s",
|
|
value: $settings.queueAutoPlayCountdown,
|
|
in: 1...15
|
|
)
|
|
.disabled(!settings.queueEnabled)
|
|
#else
|
|
Stepper(
|
|
value: $settings.queueAutoPlayCountdown,
|
|
in: 1...15
|
|
) {
|
|
HStack {
|
|
Text(String(localized: "settings.playback.queue.autoPlayCountdown"))
|
|
Spacer()
|
|
Text("\(settings.queueAutoPlayCountdown)s")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.disabled(!settings.queueEnabled)
|
|
#endif
|
|
|
|
|
|
} header: {
|
|
Text(String(localized: "settings.playback.queue.header"))
|
|
} footer: {
|
|
Text(String(localized: "settings.playback.queue.footer"))
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Orientation Section (iOS)
|
|
|
|
#if os(iOS)
|
|
private struct OrientationSection: View {
|
|
@Bindable var settings: SettingsManager
|
|
|
|
private var isPhone: Bool {
|
|
UIDevice.current.userInterfaceIdiom == .phone
|
|
}
|
|
|
|
var body: some View {
|
|
Section {
|
|
Toggle(
|
|
String(localized: "settings.playback.orientation.rotateToMatchAspectRatio"),
|
|
isOn: $settings.rotateToMatchAspectRatio
|
|
)
|
|
|
|
if isPhone {
|
|
Toggle(
|
|
String(localized: "settings.playback.orientation.preferPortraitBrowsing"),
|
|
isOn: $settings.preferPortraitBrowsing
|
|
)
|
|
}
|
|
} header: {
|
|
Text(String(localized: "settings.playback.orientation.header"))
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// MARK: - macOS Section
|
|
|
|
#if os(macOS)
|
|
private struct MacOSSection: View {
|
|
@Bindable var settings: SettingsManager
|
|
|
|
var body: some View {
|
|
Section(String(localized: "settings.playback.macOS.header")) {
|
|
Picker(
|
|
String(localized: "settings.playback.macOS.playerMode"),
|
|
selection: $settings.macPlayerMode
|
|
) {
|
|
ForEach(MacPlayerMode.allCases, id: \.self) { mode in
|
|
Text(mode.displayName).tag(mode)
|
|
}
|
|
}
|
|
.pickerStyle(.segmented)
|
|
|
|
Toggle(
|
|
String(localized: "settings.playback.macOS.autoResizePlayer"),
|
|
isOn: $settings.playerSheetAutoResize
|
|
)
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// MARK: - VideoQuality Extension
|
|
|
|
extension VideoQuality {
|
|
var displayName: String {
|
|
switch self {
|
|
case .auto: return String(localized: "settings.playback.quality.best")
|
|
case .hd4k: return "4K"
|
|
case .hd1440p: return "1440p"
|
|
case .hd1080p: return "1080p"
|
|
case .hd720p: return "720p"
|
|
case .sd480p: return "480p"
|
|
case .sd360p: return "360p"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview
|
|
|
|
#Preview {
|
|
NavigationStack {
|
|
PlaybackSettingsView()
|
|
}
|
|
.appEnvironment(.preview)
|
|
}
|