mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 01:39:46 +00:00
Yattee v2 rewrite
This commit is contained in:
@@ -0,0 +1,624 @@
|
||||
//
|
||||
// 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 {
|
||||
PreviewSection(viewModel: viewModel, layout: previewLayout)
|
||||
PresetSection(
|
||||
viewModel: viewModel,
|
||||
trackedPresetName: $trackedActivePresetName,
|
||||
showCreateSheet: $showCreatePresetSheet
|
||||
)
|
||||
AppearanceSection(
|
||||
viewModel: viewModel,
|
||||
style: $style,
|
||||
buttonSize: $buttonSize,
|
||||
fontStyle: $fontStyle
|
||||
)
|
||||
LayoutSectionsSection(viewModel: viewModel)
|
||||
CommentsPillSection(viewModel: viewModel)
|
||||
#if os(iOS)
|
||||
GesturesSectionsSection(viewModel: viewModel)
|
||||
#endif
|
||||
SystemControlsSection(viewModel: viewModel)
|
||||
VolumeSection(viewModel: viewModel)
|
||||
}
|
||||
.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 {
|
||||
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 }
|
||||
}
|
||||
|
||||
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: {
|
||||
Label(
|
||||
String(localized: "settings.playerControls.centerControls"),
|
||||
systemImage: "play.circle"
|
||||
)
|
||||
}
|
||||
.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 {
|
||||
Label(
|
||||
String(localized: "settings.playerControls.bottomButtons"),
|
||||
systemImage: "rectangle.bottomthird.inset.filled"
|
||||
)
|
||||
Spacer()
|
||||
Text("\(viewModel.currentLayout.bottomSection.buttons.count)")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.disabled(!viewModel.canEditActivePreset)
|
||||
|
||||
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)
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
Reference in New Issue
Block a user