mirror of
https://github.com/yattee/yattee.git
synced 2026-07-20 14:22:02 +00:00
The NavigationStack-level background wasn't enough: macOS draws its own translucent material behind pushed navigation pages, and Form/List containers draw a translucent scroll background on top of anything placed behind them, so several pages kept the wallpaper-tinted look. Bake the opaque background into SettingsFormContainer (covers all pages built on it, pushed or not) and add opaqueSettingsFormBackground(), which hides the container's scroll background before applying the opaque one, to every macOS-reachable Form/List settings page: player controls and its sub-editors, sidebar settings, edit source, legacy data import, subscription/playlist import, customize home and its shortcut style page, and the log viewer. Contributors, translators and the remote control device page get the plain opaque background. Claude-Session: https://claude.ai/code/session_0154KH8RAVAvm6iVanhmoW8o
658 lines
24 KiB
Swift
658 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
|
|
.opaqueSettingsFormBackground()
|
|
.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.controlBar", defaultValue: "Control Bar"),
|
|
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)
|
|
}
|