mirror of
https://github.com/yattee/yattee.git
synced 2026-06-05 06:14:18 +00:00
Convert the main view to shared SettingsFormContainer/Section helpers, drop the DisclosureGroup in favor of a single always-visible Default Options section with monospaced trailing values, and redesign the Add/Edit MPV option sheets on macOS as native dialogs with a Grid layout, clearer footers, and keyboard-shortcut actions.
471 lines
16 KiB
Swift
471 lines
16 KiB
Swift
//
|
|
// MPVOptionsSettingsView.swift
|
|
// Yattee
|
|
//
|
|
// Settings view for displaying and managing MPV options.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct MPVOptionsSettingsView: View {
|
|
@Environment(\.appEnvironment) private var appEnvironment
|
|
|
|
@State private var showingAddSheet = false
|
|
@State private var editingOption: (name: String, value: String)?
|
|
|
|
var body: some View {
|
|
SettingsFormContainer {
|
|
if let settings = appEnvironment?.settingsManager {
|
|
DefaultOptionsSection()
|
|
CustomOptionsSection(
|
|
settings: settings,
|
|
showingAddSheet: $showingAddSheet,
|
|
editingOption: $editingOption
|
|
)
|
|
}
|
|
}
|
|
.navigationTitle(String(localized: "settings.mpvOptions.title"))
|
|
#if os(iOS)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
#endif
|
|
.sheet(isPresented: $showingAddSheet) {
|
|
if let settings = appEnvironment?.settingsManager {
|
|
AddMPVOptionSheet(settings: settings)
|
|
}
|
|
}
|
|
.sheet(item: Binding(
|
|
get: { editingOption.map { EditableOption(name: $0.name, value: $0.value) } },
|
|
set: { editingOption = $0.map { ($0.name, $0.value) } }
|
|
)) { option in
|
|
if let settings = appEnvironment?.settingsManager {
|
|
EditMPVOptionSheet(
|
|
settings: settings,
|
|
originalName: option.name,
|
|
initialValue: option.value
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Identifiable wrapper for editing
|
|
|
|
private struct EditableOption: Identifiable {
|
|
let name: String
|
|
let value: String
|
|
var id: String { name }
|
|
}
|
|
|
|
// MARK: - Default Options Section
|
|
|
|
private struct DefaultOptionsSection: View {
|
|
var body: some View {
|
|
#if os(tvOS)
|
|
SettingsFormSection(footer: "settings.mpvOptions.defaultOptions.footer") {
|
|
Text(String(localized: "settings.mpvOptions.defaultOptions"))
|
|
.font(.headline)
|
|
ForEach(Self.defaultOptions, id: \.name) { option in
|
|
LabeledContent(option.name) {
|
|
Text(option.value)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
#else
|
|
SettingsFormSection("settings.mpvOptions.defaultOptions", footer: "settings.mpvOptions.defaultOptions.footer") {
|
|
ForEach(Self.defaultOptions, id: \.name) { option in
|
|
HStack(alignment: .firstTextBaseline) {
|
|
Text(option.name)
|
|
.monospaced()
|
|
Spacer()
|
|
Text(option.value)
|
|
.monospaced()
|
|
.foregroundStyle(.secondary)
|
|
.multilineTextAlignment(.trailing)
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/// Default MPV options from MPVClient.configureDefaultOptions()
|
|
private static let defaultOptions: [(name: String, value: String)] = {
|
|
var options: [(name: String, value: String)] = []
|
|
|
|
options.append(("vo", "libmpv"))
|
|
|
|
#if targetEnvironment(simulator)
|
|
options.append(("hwdec", "no"))
|
|
options.append(("sw-fast", "yes"))
|
|
#else
|
|
options.append(("hwdec", "videotoolbox-copy"))
|
|
options.append(("hwdec-codecs", "h264,hevc,mpeg1video,mpeg2video,mpeg4,vp9,av1,prores"))
|
|
#endif
|
|
|
|
options.append(("keep-open", "yes"))
|
|
options.append(("pause", "yes"))
|
|
options.append(("target-prim", "bt.709"))
|
|
options.append(("target-trc", "srgb"))
|
|
options.append(("video-sync", "display-vdrop"))
|
|
options.append(("framedrop", "decoder+vo"))
|
|
options.append(("audio-client-name", "Yattee"))
|
|
|
|
#if os(iOS) || os(tvOS)
|
|
options.append(("ao", "audiounit"))
|
|
#else
|
|
options.append(("ao", "coreaudio"))
|
|
#endif
|
|
|
|
options.append(("cache", "yes"))
|
|
options.append(("demuxer-max-bytes", "50MiB"))
|
|
options.append(("demuxer-max-back-bytes", "25MiB"))
|
|
|
|
return options
|
|
}()
|
|
}
|
|
|
|
// MARK: - Custom Options Section
|
|
|
|
private struct CustomOptionsSection: View {
|
|
@Bindable var settings: SettingsManager
|
|
@Binding var showingAddSheet: Bool
|
|
@Binding var editingOption: (name: String, value: String)?
|
|
|
|
var body: some View {
|
|
SettingsFormSection("settings.mpvOptions.customOptions", footer: "settings.mpvOptions.customOptions.footer") {
|
|
let sortedOptions = settings.customMPVOptions.sorted { $0.key < $1.key }
|
|
|
|
if sortedOptions.isEmpty {
|
|
Text(String(localized: "settings.mpvOptions.customOptions.empty"))
|
|
.foregroundStyle(.secondary)
|
|
} else {
|
|
ForEach(sortedOptions, id: \.key) { name, value in
|
|
Button {
|
|
editingOption = (name, value)
|
|
} label: {
|
|
HStack {
|
|
Text(name)
|
|
.foregroundStyle(.primary)
|
|
Spacer()
|
|
Text(value)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.contentShape(Rectangle())
|
|
}
|
|
#if os(macOS)
|
|
.buttonStyle(.plain)
|
|
#endif
|
|
#if os(iOS)
|
|
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
|
Button(role: .destructive) {
|
|
removeOption(named: name)
|
|
} label: {
|
|
Label(String(localized: "common.delete"), systemImage: "trash")
|
|
}
|
|
}
|
|
#endif
|
|
.contextMenu {
|
|
Button(role: .destructive) {
|
|
removeOption(named: name)
|
|
} label: {
|
|
Label(String(localized: "common.delete"), systemImage: "trash")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
HStack {
|
|
Button {
|
|
showingAddSheet = true
|
|
} label: {
|
|
Label(String(localized: "settings.mpvOptions.addOption"), systemImage: "plus")
|
|
}
|
|
Spacer()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func removeOption(named name: String) {
|
|
var options = settings.customMPVOptions
|
|
options.removeValue(forKey: name)
|
|
settings.customMPVOptions = options
|
|
}
|
|
}
|
|
|
|
// MARK: - Add MPV Option Sheet
|
|
|
|
private struct AddMPVOptionSheet: View {
|
|
@Environment(\.dismiss) private var dismiss
|
|
@Bindable var settings: SettingsManager
|
|
|
|
@State private var optionName = ""
|
|
@State private var optionValue = ""
|
|
|
|
private var trimmedName: String {
|
|
optionName.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
}
|
|
|
|
private var trimmedValue: String {
|
|
optionValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
}
|
|
|
|
private var canSave: Bool { !trimmedName.isEmpty && !trimmedValue.isEmpty }
|
|
|
|
private func save() {
|
|
if canSave {
|
|
var options = settings.customMPVOptions
|
|
options[trimmedName] = trimmedValue
|
|
settings.customMPVOptions = options
|
|
}
|
|
dismiss()
|
|
}
|
|
|
|
var body: some View {
|
|
#if os(macOS)
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
Text(String(localized: "settings.mpvOptions.addOption.title"))
|
|
.font(.headline)
|
|
|
|
Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 12, verticalSpacing: 10) {
|
|
GridRow {
|
|
Text(String(localized: "settings.mpvOptions.optionName"))
|
|
.gridColumnAlignment(.trailing)
|
|
TextField("", text: $optionName)
|
|
.textFieldStyle(.roundedBorder)
|
|
.autocorrectionDisabled()
|
|
}
|
|
GridRow {
|
|
Text(String(localized: "settings.mpvOptions.optionValue"))
|
|
.gridColumnAlignment(.trailing)
|
|
TextField("", text: $optionValue)
|
|
.textFieldStyle(.roundedBorder)
|
|
.autocorrectionDisabled()
|
|
}
|
|
}
|
|
|
|
Text(String(localized: "settings.mpvOptions.addOption.footer"))
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
|
|
HStack {
|
|
Spacer()
|
|
Button(String(localized: "common.cancel")) {
|
|
dismiss()
|
|
}
|
|
.keyboardShortcut(.cancelAction)
|
|
Button(String(localized: "common.add")) {
|
|
save()
|
|
}
|
|
.keyboardShortcut(.defaultAction)
|
|
.disabled(!canSave)
|
|
}
|
|
}
|
|
.padding(20)
|
|
.frame(minWidth: 420)
|
|
#else
|
|
NavigationStack {
|
|
Form {
|
|
Section {
|
|
TextField(
|
|
String(localized: "settings.mpvOptions.optionName"),
|
|
text: $optionName
|
|
)
|
|
.textInputAutocapitalization(.never)
|
|
.autocorrectionDisabled()
|
|
|
|
TextField(
|
|
String(localized: "settings.mpvOptions.optionValue"),
|
|
text: $optionValue
|
|
)
|
|
.textInputAutocapitalization(.never)
|
|
.autocorrectionDisabled()
|
|
} footer: {
|
|
Text(String(localized: "settings.mpvOptions.addOption.footer"))
|
|
}
|
|
}
|
|
.navigationTitle(String(localized: "settings.mpvOptions.addOption.title"))
|
|
#if os(iOS)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
#endif
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button(String(localized: "common.cancel")) {
|
|
dismiss()
|
|
}
|
|
}
|
|
ToolbarItem(placement: .confirmationAction) {
|
|
Button(String(localized: "common.add")) {
|
|
save()
|
|
}
|
|
.disabled(!canSave)
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// MARK: - Edit MPV Option Sheet
|
|
|
|
private struct EditMPVOptionSheet: View {
|
|
@Environment(\.dismiss) private var dismiss
|
|
@Bindable var settings: SettingsManager
|
|
|
|
let originalName: String
|
|
let initialValue: String
|
|
|
|
@State private var optionValue = ""
|
|
@State private var showingDeleteConfirmation = false
|
|
|
|
private var trimmedValue: String {
|
|
optionValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
}
|
|
|
|
private var canSave: Bool { !trimmedValue.isEmpty }
|
|
|
|
private func save() {
|
|
if canSave {
|
|
var options = settings.customMPVOptions
|
|
options[originalName] = trimmedValue
|
|
settings.customMPVOptions = options
|
|
}
|
|
dismiss()
|
|
}
|
|
|
|
private func delete() {
|
|
var options = settings.customMPVOptions
|
|
options.removeValue(forKey: originalName)
|
|
settings.customMPVOptions = options
|
|
dismiss()
|
|
}
|
|
|
|
var body: some View {
|
|
#if os(macOS)
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
Text(String(localized: "settings.mpvOptions.editOption.title"))
|
|
.font(.headline)
|
|
|
|
Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 12, verticalSpacing: 10) {
|
|
GridRow {
|
|
Text(String(localized: "settings.mpvOptions.optionName"))
|
|
.gridColumnAlignment(.trailing)
|
|
Text(originalName)
|
|
.monospaced()
|
|
.foregroundStyle(.secondary)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
GridRow {
|
|
Text(String(localized: "settings.mpvOptions.optionValue"))
|
|
.gridColumnAlignment(.trailing)
|
|
TextField("", text: $optionValue)
|
|
.textFieldStyle(.roundedBorder)
|
|
.autocorrectionDisabled()
|
|
}
|
|
}
|
|
|
|
HStack {
|
|
Button(role: .destructive) {
|
|
showingDeleteConfirmation = true
|
|
} label: {
|
|
Label(String(localized: "settings.mpvOptions.deleteOption"), systemImage: "trash")
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Button(String(localized: "common.cancel")) {
|
|
dismiss()
|
|
}
|
|
.keyboardShortcut(.cancelAction)
|
|
|
|
Button(String(localized: "common.save")) {
|
|
save()
|
|
}
|
|
.keyboardShortcut(.defaultAction)
|
|
.disabled(!canSave)
|
|
}
|
|
}
|
|
.padding(20)
|
|
.frame(minWidth: 420)
|
|
.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 {
|
|
Section {
|
|
LabeledContent(String(localized: "settings.mpvOptions.optionName")) {
|
|
Text(originalName)
|
|
}
|
|
|
|
TextField(
|
|
String(localized: "settings.mpvOptions.optionValue"),
|
|
text: $optionValue
|
|
)
|
|
.textInputAutocapitalization(.never)
|
|
.autocorrectionDisabled()
|
|
}
|
|
|
|
Section {
|
|
Button(role: .destructive) {
|
|
showingDeleteConfirmation = true
|
|
} label: {
|
|
Label(String(localized: "settings.mpvOptions.deleteOption"), systemImage: "trash")
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle(String(localized: "settings.mpvOptions.editOption.title"))
|
|
#if os(iOS)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
#endif
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button(String(localized: "common.cancel")) {
|
|
dismiss()
|
|
}
|
|
}
|
|
ToolbarItem(placement: .confirmationAction) {
|
|
Button(String(localized: "common.save")) {
|
|
save()
|
|
}
|
|
.disabled(!canSave)
|
|
}
|
|
}
|
|
.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) {}
|
|
}
|
|
.presentationCompactAdaptation(.sheet)
|
|
}
|
|
.onAppear {
|
|
optionValue = initialValue
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview
|
|
|
|
#Preview {
|
|
NavigationStack {
|
|
MPVOptionsSettingsView()
|
|
}
|
|
.appEnvironment(.preview)
|
|
}
|