diff --git a/Yattee/Core/Settings/SettingsManager+MPV.swift b/Yattee/Core/Settings/SettingsManager+MPV.swift index 81c5eb75..3794c970 100644 --- a/Yattee/Core/Settings/SettingsManager+MPV.swift +++ b/Yattee/Core/Settings/SettingsManager+MPV.swift @@ -16,6 +16,7 @@ extension SettingsManager { /// NOT synced to iCloud - local-only storage. var customMPVOptions: [String: String] { get { + if let cached = _customMPVOptions { return cached } guard let data = localDefaults.data(forKey: "customMPVOptions"), let options = try? JSONDecoder().decode([String: String].self, from: data) else { return [:] @@ -23,6 +24,7 @@ extension SettingsManager { return options } set { + _customMPVOptions = newValue if let data = try? JSONEncoder().encode(newValue) { localDefaults.set(data, forKey: "customMPVOptions") } diff --git a/Yattee/Core/SettingsManager.swift b/Yattee/Core/SettingsManager.swift index 519e8e1e..7548842c 100644 --- a/Yattee/Core/SettingsManager.swift +++ b/Yattee/Core/SettingsManager.swift @@ -117,6 +117,9 @@ final class SettingsManager { // Top Shelf (tvOS) var _topShelfSections: [TopShelfSection]? + // Custom MPV options (local-only) + var _customMPVOptions: [String: String]? + // Tab bar settings (compact size class only - iOS) var _tabBarItemOrder: [TabBarItem]? var _tabBarItemVisibility: [TabBarItem: Bool]? diff --git a/Yattee/Views/Settings/AdvancedSettingsView.swift b/Yattee/Views/Settings/AdvancedSettingsView.swift index 7ab499d5..5f219d49 100644 --- a/Yattee/Views/Settings/AdvancedSettingsView.swift +++ b/Yattee/Views/Settings/AdvancedSettingsView.swift @@ -212,7 +212,12 @@ struct AdvancedSettingsView: View { #if os(tvOS) NavigationLink { - MPVOptionsSettingsView() + TVSidebarDetailContainer( + systemImage: "slider.horizontal.3", + title: String(localized: "settings.advanced.mpv.options") + ) { + MPVOptionsSettingsView() + } } label: { Label(String(localized: "settings.advanced.mpv.options"), systemImage: "slider.horizontal.3") } diff --git a/Yattee/Views/Settings/MPVOptionsSettingsView.swift b/Yattee/Views/Settings/MPVOptionsSettingsView.swift index 8f59b751..1c4fcd95 100644 --- a/Yattee/Views/Settings/MPVOptionsSettingsView.swift +++ b/Yattee/Views/Settings/MPVOptionsSettingsView.swift @@ -16,12 +16,21 @@ struct MPVOptionsSettingsView: View { var body: some View { SettingsFormContainer { if let settings = appEnvironment?.settingsManager { + #if os(tvOS) + CustomOptionsSection( + settings: settings, + showingAddSheet: $showingAddSheet, + editingOption: $editingOption + ) + DefaultOptionsSection() + #else DefaultOptionsSection() CustomOptionsSection( settings: settings, showingAddSheet: $showingAddSheet, editingOption: $editingOption ) + #endif } } .navigationTitle(String(localized: "settings.mpvOptions.title")) @@ -69,6 +78,7 @@ private struct DefaultOptionsSection: View { Text(option.value) .foregroundStyle(.secondary) } + .focusable() } } #else @@ -263,6 +273,44 @@ private struct AddMPVOptionSheet: View { } .padding(20) .frame(minWidth: 420) + #elseif os(tvOS) + VStack(spacing: 30) { + Text(String(localized: "settings.mpvOptions.addOption.title")) + .font(.title2) + .fontWeight(.semibold) + + VStack(spacing: 20) { + TextField( + String(localized: "settings.mpvOptions.optionName"), + text: $optionName + ) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + + TextField( + String(localized: "settings.mpvOptions.optionValue"), + text: $optionValue + ) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + } + + Button { + save() + } label: { + Text(String(localized: "common.add")) + .frame(maxWidth: .infinity) + } + .disabled(!canSave) + + Text(String(localized: "settings.mpvOptions.addOption.footer")) + .font(.callout) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + } + .padding(60) + .frame(maxWidth: 900) #else NavigationStack { Form { @@ -400,6 +448,59 @@ private struct EditMPVOptionSheet: View { .onAppear { optionValue = initialValue } + #elseif os(tvOS) + VStack(spacing: 30) { + Text(String(localized: "settings.mpvOptions.editOption.title")) + .font(.title2) + .fontWeight(.semibold) + + VStack(spacing: 20) { + HStack { + Text(String(localized: "settings.mpvOptions.optionName")) + Spacer() + Text(originalName) + .foregroundStyle(.secondary) + } + .padding(.horizontal, 24) + + TextField( + String(localized: "settings.mpvOptions.optionValue"), + text: $optionValue + ) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + } + + Button { + save() + } label: { + Text(String(localized: "common.save")) + .frame(maxWidth: .infinity) + } + .disabled(!canSave) + + Button(role: .destructive) { + showingDeleteConfirmation = true + } label: { + Label(String(localized: "settings.mpvOptions.deleteOption"), systemImage: "trash") + .frame(maxWidth: .infinity) + } + } + .padding(60) + .frame(maxWidth: 900) + .confirmationDialog( + String(localized: "settings.mpvOptions.deleteOption.confirmation"), + isPresented: $showingDeleteConfirmation, + titleVisibility: .visible + ) { + Button(String(localized: "common.delete"), role: .destructive) { + delete() + } + Button(String(localized: "common.cancel"), role: .cancel) {} + } + .onAppear { + optionValue = initialValue + } #else NavigationStack { Form {