mirror of
https://github.com/yattee/yattee.git
synced 2026-07-22 15:22:01 +00:00
Render the macOS control bar button row and top bar from the active preset's sections via a new MacOSControlsSectionRenderer, replacing the hardcoded QuickTime-style rows in MacOSControlBar. Add a keepOnTop button type and macOS-specific button availability lists, and adapt the player controls settings editors (preset editor, section editor, button configuration) for macOS. Also apply the preset font style to the control bar time labels, and update the built-in macOS Default preset order (queue before transport, fullscreen at the end), bumping builtInPresetsVersion to 7.
657 lines
24 KiB
Swift
657 lines
24 KiB
Swift
//
|
|
// PlayerControlsSettingsView.swift
|
|
// Yattee
|
|
//
|
|
// Main settings view for player controls customization.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
/// Settings view for customizing player controls layout and presets.
|
|
struct PlayerControlsSettingsView: View {
|
|
@Environment(\.appEnvironment) private var appEnvironment
|
|
@State private var viewModel: PlayerControlsSettingsViewModel?
|
|
|
|
var body: some View {
|
|
Group {
|
|
if let viewModel {
|
|
PlayerControlsSettingsContent(viewModel: viewModel)
|
|
} else {
|
|
ProgressView()
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
}
|
|
}
|
|
.navigationTitle(String(localized: "settings.playerControls.title"))
|
|
#if os(iOS)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
#endif
|
|
.task {
|
|
if viewModel == nil, let appEnv = appEnvironment {
|
|
let vm = PlayerControlsSettingsViewModel(
|
|
layoutService: appEnv.playerControlsLayoutService,
|
|
settingsManager: appEnv.settingsManager
|
|
)
|
|
viewModel = vm
|
|
await vm.loadPresets()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Content View
|
|
|
|
private struct PlayerControlsSettingsContent: View {
|
|
@Bindable var viewModel: PlayerControlsSettingsViewModel
|
|
|
|
// Local state for immediate UI updates
|
|
@State private var style: ControlsStyle = .glass
|
|
@State private var buttonSize: ButtonSize = .medium
|
|
@State private var fontStyle: ControlsFontStyle = .system
|
|
|
|
// Refresh trigger to force view update on navigation back
|
|
@State private var refreshID = UUID()
|
|
|
|
// Track active preset locally to ensure view updates
|
|
@State private var trackedActivePresetName: String = "Default"
|
|
|
|
// Create preset sheet state (kept at parent level for stability across refreshes)
|
|
@State private var showCreatePresetSheet = false
|
|
@State private var pendingPresetCreation: PendingPresetCreation?
|
|
|
|
private struct PendingPresetCreation: Equatable {
|
|
let name: String
|
|
let basePresetID: UUID?
|
|
}
|
|
|
|
/// Layout with local settings override for immediate preview updates
|
|
private var previewLayout: PlayerControlsLayout {
|
|
var layout = viewModel.currentLayout
|
|
layout.globalSettings.style = style
|
|
layout.globalSettings.buttonSize = buttonSize
|
|
layout.globalSettings.fontStyle = fontStyle
|
|
return layout
|
|
}
|
|
|
|
var body: some View {
|
|
Form {
|
|
// The phone-shaped preview and comments pill don't apply to the macOS player.
|
|
#if os(iOS)
|
|
PreviewSection(viewModel: viewModel, layout: previewLayout)
|
|
#endif
|
|
PresetSection(
|
|
viewModel: viewModel,
|
|
trackedPresetName: $trackedActivePresetName,
|
|
showCreateSheet: $showCreatePresetSheet
|
|
)
|
|
AppearanceSection(
|
|
viewModel: viewModel,
|
|
style: $style,
|
|
buttonSize: $buttonSize,
|
|
fontStyle: $fontStyle
|
|
)
|
|
LayoutSectionsSection(viewModel: viewModel)
|
|
#if os(iOS)
|
|
CommentsPillSection(viewModel: viewModel)
|
|
GesturesSectionsSection(viewModel: viewModel)
|
|
#endif
|
|
SystemControlsSection(viewModel: viewModel)
|
|
VolumeSection(viewModel: viewModel)
|
|
}
|
|
#if os(macOS)
|
|
.formStyle(.grouped)
|
|
#endif
|
|
.id(refreshID)
|
|
.alert(
|
|
String(localized: "settings.playerControls.error"),
|
|
isPresented: .constant(viewModel.error != nil)
|
|
) {
|
|
Button(String(localized: "settings.playerControls.ok")) {
|
|
viewModel.clearError()
|
|
}
|
|
} message: {
|
|
if let error = viewModel.error {
|
|
Text(error)
|
|
}
|
|
}
|
|
.sheet(isPresented: $showCreatePresetSheet) {
|
|
PresetEditorView(
|
|
mode: .create(
|
|
baseLayouts: viewModel.presets,
|
|
activePreset: viewModel.activePreset
|
|
),
|
|
onSave: { name, basePresetID in
|
|
pendingPresetCreation = PendingPresetCreation(name: name, basePresetID: basePresetID)
|
|
}
|
|
)
|
|
}
|
|
.task(id: pendingPresetCreation) {
|
|
guard let creation = pendingPresetCreation else { return }
|
|
let basePreset = creation.basePresetID.flatMap { id in
|
|
viewModel.presets.first { $0.id == id }
|
|
}
|
|
await viewModel.createPreset(name: creation.name, basedOn: basePreset)
|
|
pendingPresetCreation = nil
|
|
trackedActivePresetName = viewModel.activePreset?.name ?? "Default"
|
|
NotificationCenter.default.post(name: .presetSelectionDidChange, object: viewModel.activePreset?.name)
|
|
}
|
|
.onAppear {
|
|
style = viewModel.currentLayout.globalSettings.style
|
|
buttonSize = viewModel.currentLayout.globalSettings.buttonSize
|
|
fontStyle = viewModel.currentLayout.globalSettings.fontStyle
|
|
trackedActivePresetName = viewModel.activePreset?.name ?? "Default"
|
|
// Force refresh on every appear to pick up changes from sub-editors
|
|
refreshID = UUID()
|
|
}
|
|
.onChange(of: viewModel.activePreset?.id) { _, _ in
|
|
// Sync when preset changes
|
|
style = viewModel.currentLayout.globalSettings.style
|
|
buttonSize = viewModel.currentLayout.globalSettings.buttonSize
|
|
fontStyle = viewModel.currentLayout.globalSettings.fontStyle
|
|
trackedActivePresetName = viewModel.activePreset?.name ?? "Default"
|
|
}
|
|
.onChange(of: viewModel.activePreset?.name) { _, newName in
|
|
// Also track name changes (for initial load)
|
|
trackedActivePresetName = newName ?? "Default"
|
|
}
|
|
.onReceive(NotificationCenter.default.publisher(for: .presetSelectionDidChange)) { notification in
|
|
if let name = notification.object as? String {
|
|
trackedActivePresetName = name
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview Section
|
|
|
|
private struct PreviewSection: View {
|
|
@Bindable var viewModel: PlayerControlsSettingsViewModel
|
|
let layout: PlayerControlsLayout
|
|
|
|
var body: some View {
|
|
Section {
|
|
PlayerControlsPreviewView(
|
|
layout: layout,
|
|
isLandscape: viewModel.isPreviewingLandscape
|
|
)
|
|
.frame(height: 200)
|
|
.listRowInsets(EdgeInsets())
|
|
|
|
Picker(
|
|
String(localized: "settings.playerControls.previewOrientation"),
|
|
selection: $viewModel.isPreviewingLandscape
|
|
) {
|
|
Text(String(localized: "settings.playerControls.portrait"))
|
|
.tag(false)
|
|
Text(String(localized: "settings.playerControls.landscape"))
|
|
.tag(true)
|
|
}
|
|
.pickerStyle(.segmented)
|
|
.padding(.horizontal)
|
|
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Preset Section
|
|
|
|
private struct PresetSection: View {
|
|
@Bindable var viewModel: PlayerControlsSettingsViewModel
|
|
@Binding var trackedPresetName: String
|
|
@Binding var showCreateSheet: Bool
|
|
|
|
private var isBuiltInPreset: Bool {
|
|
viewModel.activePreset?.isBuiltIn == true
|
|
}
|
|
|
|
var body: some View {
|
|
Section {
|
|
NavigationLink {
|
|
PresetSelectorView(viewModel: viewModel, onPresetSelected: { name in
|
|
trackedPresetName = name
|
|
})
|
|
} label: {
|
|
HStack {
|
|
Text(String(localized: "settings.playerControls.activePreset"))
|
|
Spacer()
|
|
Text(trackedPresetName)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
|
|
if isBuiltInPreset {
|
|
Button {
|
|
showCreateSheet = true
|
|
} label: {
|
|
Text(String(localized: "settings.playerControls.newPreset"))
|
|
}
|
|
}
|
|
} footer: {
|
|
if isBuiltInPreset {
|
|
Text(String(localized: "settings.playerControls.builtInPresetHint"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Appearance Section
|
|
|
|
private struct AppearanceSection: View {
|
|
@Bindable var viewModel: PlayerControlsSettingsViewModel
|
|
@Binding var style: ControlsStyle
|
|
@Binding var buttonSize: ButtonSize
|
|
@Binding var fontStyle: ControlsFontStyle
|
|
|
|
var body: some View {
|
|
Section {
|
|
// The macOS bar draws its own glass capsule; the per-button style has no effect there.
|
|
#if os(iOS)
|
|
Picker(
|
|
String(localized: "settings.playerControls.style"),
|
|
selection: $style
|
|
) {
|
|
ForEach(ControlsStyle.allCases, id: \.self) { styleOption in
|
|
Text(styleOption.displayName).tag(styleOption)
|
|
}
|
|
}
|
|
.disabled(!viewModel.canEditActivePreset)
|
|
.onChange(of: style) { _, newStyle in
|
|
guard newStyle != viewModel.currentLayout.globalSettings.style else { return }
|
|
viewModel.updateGlobalSettingsSync { $0.style = newStyle }
|
|
}
|
|
#endif
|
|
|
|
Picker(
|
|
String(localized: "settings.playerControls.buttonSize"),
|
|
selection: $buttonSize
|
|
) {
|
|
ForEach(ButtonSize.allCases, id: \.self) { size in
|
|
Text(size.displayName).tag(size)
|
|
}
|
|
}
|
|
.disabled(!viewModel.canEditActivePreset)
|
|
.onChange(of: buttonSize) { _, newSize in
|
|
guard newSize != viewModel.currentLayout.globalSettings.buttonSize else { return }
|
|
viewModel.updateGlobalSettingsSync { $0.buttonSize = newSize }
|
|
}
|
|
|
|
Picker(
|
|
String(localized: "settings.playerControls.fontStyle"),
|
|
selection: $fontStyle
|
|
) {
|
|
ForEach(ControlsFontStyle.allCases, id: \.self) { fontStyleOption in
|
|
Text(fontStyleOption.displayName).tag(fontStyleOption)
|
|
}
|
|
}
|
|
.disabled(!viewModel.canEditActivePreset)
|
|
.onChange(of: fontStyle) { _, newFontStyle in
|
|
guard newFontStyle != viewModel.currentLayout.globalSettings.fontStyle else { return }
|
|
viewModel.updateGlobalSettingsSync { $0.fontStyle = newFontStyle }
|
|
}
|
|
} header: {
|
|
Text(String(localized: "settings.playerControls.appearance"))
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Layout Sections Section
|
|
|
|
private struct LayoutSectionsSection: View {
|
|
@Bindable var viewModel: PlayerControlsSettingsViewModel
|
|
|
|
var body: some View {
|
|
Section {
|
|
NavigationLink {
|
|
SectionEditorView(
|
|
sectionType: .top,
|
|
viewModel: viewModel
|
|
)
|
|
} label: {
|
|
HStack {
|
|
Label(
|
|
String(localized: "settings.playerControls.topButtons"),
|
|
systemImage: "rectangle.topthird.inset.filled"
|
|
)
|
|
Spacer()
|
|
Text("\(viewModel.currentLayout.topSection.buttons.count)")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.disabled(!viewModel.canEditActivePreset)
|
|
|
|
NavigationLink {
|
|
CenterControlsSettingsView(viewModel: viewModel)
|
|
} label: {
|
|
#if os(macOS)
|
|
Label(
|
|
String(localized: "settings.playerControls.seekDurations", defaultValue: "Seek Durations"),
|
|
systemImage: "arrow.trianglehead.clockwise"
|
|
)
|
|
#else
|
|
Label(
|
|
String(localized: "settings.playerControls.centerControls"),
|
|
systemImage: "play.circle"
|
|
)
|
|
#endif
|
|
}
|
|
.disabled(!viewModel.canEditActivePreset)
|
|
|
|
NavigationLink {
|
|
ProgressBarSettingsView(viewModel: viewModel)
|
|
} label: {
|
|
Label(
|
|
String(localized: "settings.playerControls.progressBar"),
|
|
systemImage: "slider.horizontal.below.rectangle"
|
|
)
|
|
}
|
|
.disabled(!viewModel.canEditActivePreset)
|
|
|
|
NavigationLink {
|
|
SectionEditorView(
|
|
sectionType: .bottom,
|
|
viewModel: viewModel
|
|
)
|
|
} label: {
|
|
HStack {
|
|
#if os(macOS)
|
|
Label(
|
|
String(localized: "settings.playerControls.controlBarButtons", defaultValue: "Control Bar Buttons"),
|
|
systemImage: "rectangle.bottomthird.inset.filled"
|
|
)
|
|
#else
|
|
Label(
|
|
String(localized: "settings.playerControls.bottomButtons"),
|
|
systemImage: "rectangle.bottomthird.inset.filled"
|
|
)
|
|
#endif
|
|
Spacer()
|
|
Text("\(viewModel.currentLayout.bottomSection.buttons.count)")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.disabled(!viewModel.canEditActivePreset)
|
|
|
|
// The player pill only exists in the iOS player.
|
|
#if os(iOS)
|
|
NavigationLink {
|
|
PlayerPillEditorView(viewModel: viewModel)
|
|
} label: {
|
|
HStack {
|
|
Label(
|
|
String(localized: "settings.playerControls.playerPill"),
|
|
systemImage: "capsule"
|
|
)
|
|
Spacer()
|
|
Text("\(viewModel.playerPillSettings.buttons.count)")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.disabled(!viewModel.canEditActivePreset)
|
|
#endif
|
|
|
|
NavigationLink {
|
|
MiniPlayerEditorView(viewModel: viewModel)
|
|
} label: {
|
|
Label(
|
|
String(localized: "settings.playerControls.miniPlayer"),
|
|
systemImage: "pip"
|
|
)
|
|
}
|
|
.disabled(!viewModel.canEditActivePreset)
|
|
} header: {
|
|
Text(String(localized: "settings.playerControls.layoutSections"))
|
|
} footer: {
|
|
if !viewModel.canEditActivePreset {
|
|
Text(String(localized: "settings.playerControls.duplicateToEdit"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Comments Pill Section
|
|
|
|
private struct CommentsPillSection: View {
|
|
@Bindable var viewModel: PlayerControlsSettingsViewModel
|
|
|
|
// Local state for immediate UI updates
|
|
@State private var commentsPillMode: CommentsPillMode = .pill
|
|
|
|
var body: some View {
|
|
Section {
|
|
Picker(
|
|
String(localized: "commentsPill.mode.title"),
|
|
selection: $commentsPillMode
|
|
) {
|
|
ForEach(CommentsPillMode.allCases, id: \.self) { mode in
|
|
Text(mode.displayName).tag(mode)
|
|
}
|
|
}
|
|
.disabled(!viewModel.canEditActivePreset)
|
|
.onChange(of: commentsPillMode) { _, newValue in
|
|
guard newValue != viewModel.commentsPillMode else { return }
|
|
viewModel.syncCommentsPillMode(newValue)
|
|
}
|
|
}
|
|
.onAppear {
|
|
commentsPillMode = viewModel.commentsPillMode
|
|
}
|
|
.onChange(of: viewModel.activePreset?.id) { _, _ in
|
|
commentsPillMode = viewModel.commentsPillMode
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Gestures Sections Section (iOS only)
|
|
|
|
#if os(iOS)
|
|
private struct GesturesSectionsSection: View {
|
|
@Bindable var viewModel: PlayerControlsSettingsViewModel
|
|
|
|
var body: some View {
|
|
Section {
|
|
NavigationLink {
|
|
TapGesturesSettingsView(viewModel: viewModel)
|
|
} label: {
|
|
HStack {
|
|
Label(
|
|
String(localized: "gestures.tap.title", defaultValue: "Tap Gestures"),
|
|
systemImage: "hand.tap"
|
|
)
|
|
Spacer()
|
|
if viewModel.gesturesSettings.tapGestures.isEnabled {
|
|
Text(viewModel.gesturesSettings.tapGestures.layout.layoutDescription)
|
|
.foregroundStyle(.secondary)
|
|
} else {
|
|
Text(String(localized: "common.disabled", defaultValue: "Disabled"))
|
|
.foregroundStyle(.tertiary)
|
|
}
|
|
}
|
|
}
|
|
.disabled(!viewModel.canEditActivePreset)
|
|
|
|
NavigationLink {
|
|
SeekGestureSettingsView(viewModel: viewModel)
|
|
} label: {
|
|
HStack {
|
|
Label(
|
|
String(localized: "gestures.seek.title", defaultValue: "Seek Gesture"),
|
|
systemImage: "hand.draw"
|
|
)
|
|
Spacer()
|
|
if viewModel.gesturesSettings.seekGesture.isEnabled {
|
|
Text(viewModel.gesturesSettings.seekGesture.sensitivity.displayName)
|
|
.foregroundStyle(.secondary)
|
|
} else {
|
|
Text(String(localized: "common.disabled", defaultValue: "Disabled"))
|
|
.foregroundStyle(.tertiary)
|
|
}
|
|
}
|
|
}
|
|
.disabled(!viewModel.canEditActivePreset)
|
|
|
|
NavigationLink {
|
|
PanscanGestureSettingsView(viewModel: viewModel)
|
|
} label: {
|
|
HStack {
|
|
Label(
|
|
String(localized: "gestures.panscan.title", defaultValue: "Panscan Gesture"),
|
|
systemImage: "hand.pinch"
|
|
)
|
|
Spacer()
|
|
if viewModel.gesturesSettings.panscanGesture.isEnabled {
|
|
if viewModel.gesturesSettings.panscanGesture.snapToEnds {
|
|
Text(String(localized: "gestures.panscan.snap", defaultValue: "Snap"))
|
|
.foregroundStyle(.secondary)
|
|
} else {
|
|
Text(String(localized: "gestures.panscan.freeZoom", defaultValue: "Free Zoom"))
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
} else {
|
|
Text(String(localized: "common.disabled", defaultValue: "Disabled"))
|
|
.foregroundStyle(.tertiary)
|
|
}
|
|
}
|
|
}
|
|
.disabled(!viewModel.canEditActivePreset)
|
|
} header: {
|
|
Text(String(localized: "gestures.section.title", defaultValue: "Gestures"))
|
|
} footer: {
|
|
Text(String(localized: "gestures.section.footer", defaultValue: "Control playback with gestures when player controls are hidden."))
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// MARK: - System Controls Section
|
|
|
|
private struct SystemControlsSection: View {
|
|
@Environment(\.appEnvironment) private var appEnvironment
|
|
@Bindable var viewModel: PlayerControlsSettingsViewModel
|
|
|
|
// Local state for immediate UI updates
|
|
@State private var systemControlsMode: SystemControlsMode = .seek
|
|
@State private var systemControlsSeekDuration: SystemControlsSeekDuration = .tenSeconds
|
|
|
|
var body: some View {
|
|
Section {
|
|
Picker(
|
|
String(localized: "settings.playback.systemControls.mode"),
|
|
selection: $systemControlsMode
|
|
) {
|
|
ForEach(SystemControlsMode.allCases, id: \.self) { mode in
|
|
Text(mode.displayName).tag(mode)
|
|
}
|
|
}
|
|
.disabled(!viewModel.canEditActivePreset)
|
|
.onChange(of: systemControlsMode) { _, newMode in
|
|
guard newMode != viewModel.systemControlsMode else { return }
|
|
viewModel.updateSystemControlsModeSync(newMode)
|
|
appEnvironment?.playerService.reconfigureSystemControls(
|
|
mode: newMode,
|
|
duration: viewModel.systemControlsSeekDuration
|
|
)
|
|
}
|
|
|
|
if systemControlsMode == .seek {
|
|
Picker(
|
|
String(localized: "settings.playback.systemControls.seekDuration"),
|
|
selection: $systemControlsSeekDuration
|
|
) {
|
|
ForEach(SystemControlsSeekDuration.allCases, id: \.self) { duration in
|
|
Text(duration.displayName).tag(duration)
|
|
}
|
|
}
|
|
.disabled(!viewModel.canEditActivePreset)
|
|
.onChange(of: systemControlsSeekDuration) { _, newDuration in
|
|
guard newDuration != viewModel.systemControlsSeekDuration else { return }
|
|
viewModel.updateSystemControlsSeekDurationSync(newDuration)
|
|
appEnvironment?.playerService.reconfigureSystemControls(
|
|
mode: viewModel.systemControlsMode,
|
|
duration: newDuration
|
|
)
|
|
}
|
|
}
|
|
} header: {
|
|
Text(String(localized: "settings.playback.systemControls.header"))
|
|
} footer: {
|
|
if systemControlsMode == .seek {
|
|
Text(String(localized: "settings.playback.systemControls.seek.footer"))
|
|
} else {
|
|
Text(String(localized: "settings.playback.systemControls.skipTrack.footer"))
|
|
}
|
|
}
|
|
.onAppear {
|
|
syncLocalState()
|
|
}
|
|
.onChange(of: viewModel.activePreset?.id) { _, _ in
|
|
syncLocalState()
|
|
}
|
|
}
|
|
|
|
private func syncLocalState() {
|
|
systemControlsMode = viewModel.systemControlsMode
|
|
systemControlsSeekDuration = viewModel.systemControlsSeekDuration
|
|
}
|
|
}
|
|
|
|
// MARK: - Volume Section
|
|
|
|
private struct VolumeSection: View {
|
|
@Environment(\.appEnvironment) private var appEnvironment
|
|
@Bindable var viewModel: PlayerControlsSettingsViewModel
|
|
|
|
// Local state for immediate UI updates
|
|
@State private var volumeMode: VolumeMode = .mpv
|
|
|
|
var body: some View {
|
|
Section {
|
|
Picker(
|
|
String(localized: "settings.playback.volume.mode"),
|
|
selection: $volumeMode
|
|
) {
|
|
ForEach(VolumeMode.allCases, id: \.self) { mode in
|
|
Text(mode.displayName).tag(mode)
|
|
}
|
|
}
|
|
.disabled(!viewModel.canEditActivePreset)
|
|
.onChange(of: volumeMode) { _, newMode in
|
|
guard newMode != viewModel.volumeMode else { return }
|
|
viewModel.updateVolumeModeSync(newMode)
|
|
if newMode == .system {
|
|
// Set MPV to 100% when switching to system mode
|
|
appEnvironment?.playerService.currentBackend?.volume = 1.0
|
|
appEnvironment?.playerService.state.volume = 1.0
|
|
}
|
|
// Broadcast the mode change to remote devices
|
|
appEnvironment?.remoteControlCoordinator.broadcastStateUpdate()
|
|
}
|
|
} header: {
|
|
Text(String(localized: "settings.playback.volume.header"))
|
|
} footer: {
|
|
if volumeMode == .mpv {
|
|
Text(String(localized: "settings.playback.volume.mpv.footer"))
|
|
} else {
|
|
Text(String(localized: "settings.playback.volume.system.footer"))
|
|
}
|
|
}
|
|
.onAppear {
|
|
syncLocalState()
|
|
}
|
|
.onChange(of: viewModel.activePreset?.id) { _, _ in
|
|
syncLocalState()
|
|
}
|
|
}
|
|
|
|
private func syncLocalState() {
|
|
volumeMode = viewModel.volumeMode
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview
|
|
|
|
#Preview {
|
|
NavigationStack {
|
|
PlayerControlsSettingsView()
|
|
}
|
|
.appEnvironment(.preview)
|
|
}
|