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:
395
Yattee/Views/Settings/PlaybackSettingsView.swift
Normal file
395
Yattee/Views/Settings/PlaybackSettingsView.swift
Normal file
@@ -0,0 +1,395 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user