Files
yattee/Yattee/Views/Settings/PlayerControls/ButtonConfigurationView.swift
2026-02-08 18:33:56 +01:00

383 lines
13 KiB
Swift

//
// ButtonConfigurationView.swift
// Yattee
//
// View for configuring individual button settings.
//
import SwiftUI
/// View for configuring a single button's settings.
struct ButtonConfigurationView: View {
let buttonID: UUID
let sectionType: LayoutSectionType
@Bindable var viewModel: PlayerControlsSettingsViewModel
// Local state for immediate UI updates
@State private var visibilityMode: VisibilityMode = .both
@State private var spacerIsFlexible: Bool = true
@State private var spacerWidth: Double = 20
@State private var sliderBehavior: SliderBehavior = .expandOnTap
@State private var seekSeconds: Double = 10
@State private var seekDirection: SeekDirection = .forward
@State private var timeDisplayFormat: TimeDisplayFormat = .currentAndTotal
@State private var titleAuthorShowSourceImage: Bool = true
@State private var titleAuthorShowTitle: Bool = true
@State private var titleAuthorShowSourceName: Bool = true
/// Look up the current configuration from the view model's layout.
private var configuration: ControlButtonConfiguration? {
let section = sectionType == .top
? viewModel.currentLayout.topSection
: viewModel.currentLayout.bottomSection
return section.buttons.first { $0.id == buttonID }
}
var body: some View {
if let config = configuration {
Form {
// Visibility mode (all buttons)
Section {
Picker(
String(localized: "settings.playerControls.visibility"),
selection: $visibilityMode
) {
ForEach(VisibilityMode.allCases, id: \.self) { mode in
Text(mode.displayName).tag(mode)
}
}
.onChange(of: visibilityMode) { _, newValue in
updateVisibility(newValue)
}
} header: {
Text(String(localized: "settings.playerControls.visibilityHeader"))
} footer: {
Text(String(localized: "settings.playerControls.visibilityFooter"))
}
// Type-specific settings
if config.buttonType.hasSettings {
typeSpecificSettings(for: config)
}
}
.navigationTitle(config.buttonType.displayName)
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
.onAppear {
syncFromConfiguration(config)
}
} else {
ContentUnavailableView(
String(localized: "settings.playerControls.buttonNotFound"),
systemImage: "exclamationmark.triangle"
)
}
}
// MARK: - Sync from Configuration
private func syncFromConfiguration(_ config: ControlButtonConfiguration) {
visibilityMode = config.visibilityMode
switch config.settings {
case .spacer(let settings):
spacerIsFlexible = settings.isFlexible
spacerWidth = Double(settings.fixedWidth)
case .slider(let settings):
sliderBehavior = settings.sliderBehavior
case .seek(let settings):
seekSeconds = Double(settings.seconds)
seekDirection = settings.direction
case .timeDisplay(let settings):
timeDisplayFormat = settings.format
case .titleAuthor(let settings):
titleAuthorShowSourceImage = settings.showSourceImage
titleAuthorShowTitle = settings.showTitle
titleAuthorShowSourceName = settings.showSourceName
case .none:
break
}
}
// MARK: - Type-Specific Settings
@ViewBuilder
private func typeSpecificSettings(for config: ControlButtonConfiguration) -> some View {
switch config.buttonType {
case .spacer:
spacerSettings
case .brightness, .volume:
sliderSettings
case .seekBackward, .seekForward:
seekSettings
case .seek:
seekSettingsForHorizontal
case .timeDisplay:
timeDisplaySettings
case .titleAuthor:
titleAuthorSettings
default:
EmptyView()
}
}
// MARK: - Spacer Settings
@ViewBuilder
private var spacerSettings: some View {
Section {
Toggle(
String(localized: "settings.playerControls.spacer.flexible"),
isOn: $spacerIsFlexible
)
.onChange(of: spacerIsFlexible) { _, newValue in
updateSpacerSettings(isFlexible: newValue, width: Int(spacerWidth))
}
#if !os(tvOS)
if !spacerIsFlexible {
HStack {
Text(String(localized: "settings.playerControls.spacer.width"))
Spacer()
Text("\(Int(spacerWidth))pt")
.foregroundStyle(.secondary)
}
Slider(
value: $spacerWidth,
in: 4...100,
step: 2
)
.onChange(of: spacerWidth) { _, newValue in
updateSpacerSettings(isFlexible: spacerIsFlexible, width: Int(newValue))
}
}
#endif
} header: {
Text(String(localized: "settings.playerControls.spacer.header"))
} footer: {
Text(String(localized: "settings.playerControls.spacer.footer"))
}
}
// MARK: - Slider Settings (Brightness/Volume)
@ViewBuilder
private var sliderSettings: some View {
Section {
Picker(
String(localized: "settings.playerControls.slider.behavior"),
selection: $sliderBehavior
) {
ForEach(SliderBehavior.allCases, id: \.self) { behavior in
Text(behavior.displayName).tag(behavior)
}
}
.onChange(of: sliderBehavior) { _, newValue in
updateSliderSettings(behavior: newValue)
}
} header: {
Text(String(localized: "settings.playerControls.slider.header"))
} footer: {
Text(String(localized: "settings.playerControls.slider.footer"))
}
}
private func updateSliderSettings(behavior: SliderBehavior) {
let settings = SliderSettings(sliderBehavior: behavior)
updateSettings(.slider(settings))
}
// MARK: - Seek Settings
@ViewBuilder
private var seekSettings: some View {
Section {
#if !os(tvOS)
HStack {
Text(String(localized: "settings.playerControls.seek.seconds"))
Spacer()
Text("\(Int(seekSeconds))s")
.foregroundStyle(.secondary)
}
Slider(
value: $seekSeconds,
in: 1...60,
step: 1
)
.onChange(of: seekSeconds) { _, newValue in
updateSettings(.seek(SeekSettings(seconds: Int(newValue), direction: seekDirection)))
}
#endif
// Quick presets
HStack {
ForEach([5, 10, 15, 30], id: \.self) { preset in
Button("\(preset)s") {
seekSeconds = Double(preset)
}
.buttonStyle(.bordered)
.tint(Int(seekSeconds) == preset ? .accentColor : .secondary)
}
}
} header: {
Text(String(localized: "settings.playerControls.seek.header"))
}
}
// MARK: - Seek Settings for Horizontal Sections
@ViewBuilder
private var seekSettingsForHorizontal: some View {
Section {
// Direction picker
Picker(
String(localized: "settings.playerControls.seek.direction"),
selection: $seekDirection
) {
ForEach(SeekDirection.allCases, id: \.self) { direction in
Text(direction.displayName).tag(direction)
}
}
.onChange(of: seekDirection) { _, newValue in
updateSettings(.seek(SeekSettings(seconds: Int(seekSeconds), direction: newValue)))
}
#if !os(tvOS)
HStack {
Text(String(localized: "settings.playerControls.seek.seconds"))
Spacer()
Text("\(Int(seekSeconds))s")
.foregroundStyle(.secondary)
}
Slider(
value: $seekSeconds,
in: 1...60,
step: 1
)
.onChange(of: seekSeconds) { _, newValue in
updateSettings(.seek(SeekSettings(seconds: Int(newValue), direction: seekDirection)))
}
#endif
// Quick presets
HStack {
ForEach([5, 10, 15, 30], id: \.self) { preset in
Button("\(preset)s") {
seekSeconds = Double(preset)
updateSettings(.seek(SeekSettings(seconds: preset, direction: seekDirection)))
}
.buttonStyle(.bordered)
.tint(Int(seekSeconds) == preset ? .accentColor : .secondary)
}
}
} header: {
Text(String(localized: "settings.playerControls.seek.header"))
}
}
// MARK: - Time Display Settings
@ViewBuilder
private var timeDisplaySettings: some View {
Section {
Picker(
String(localized: "settings.playerControls.timeDisplay.format"),
selection: $timeDisplayFormat
) {
ForEach(TimeDisplayFormat.allCases, id: \.self) { format in
Text(format.displayName).tag(format)
}
}
.onChange(of: timeDisplayFormat) { _, newValue in
updateSettings(.timeDisplay(TimeDisplaySettings(format: newValue)))
}
} header: {
Text(String(localized: "settings.playerControls.timeDisplay.header"))
} footer: {
Text(String(localized: "settings.playerControls.timeDisplay.footer"))
}
}
// MARK: - Title/Author Settings
@ViewBuilder
private var titleAuthorSettings: some View {
Section {
Toggle(
String(localized: "settings.playerControls.titleAuthor.showSourceImage"),
isOn: $titleAuthorShowSourceImage
)
.onChange(of: titleAuthorShowSourceImage) { _, _ in
updateTitleAuthorSettings()
}
Toggle(
String(localized: "settings.playerControls.titleAuthor.showTitle"),
isOn: $titleAuthorShowTitle
)
.onChange(of: titleAuthorShowTitle) { _, _ in
updateTitleAuthorSettings()
}
Toggle(
String(localized: "settings.playerControls.titleAuthor.showSourceName"),
isOn: $titleAuthorShowSourceName
)
.onChange(of: titleAuthorShowSourceName) { _, _ in
updateTitleAuthorSettings()
}
} header: {
Text(String(localized: "settings.playerControls.titleAuthor.header"))
} footer: {
Text(String(localized: "settings.playerControls.titleAuthor.footer"))
}
}
private func updateTitleAuthorSettings() {
let settings = TitleAuthorSettings(
showSourceImage: titleAuthorShowSourceImage,
showTitle: titleAuthorShowTitle,
showSourceName: titleAuthorShowSourceName
)
updateSettings(.titleAuthor(settings))
}
// MARK: - Update Helpers
private func updateVisibility(_ mode: VisibilityMode) {
guard var updated = configuration else { return }
updated.visibilityMode = mode
viewModel.updateButtonConfigurationSync(updated, in: sectionType)
}
private func updateSpacerSettings(isFlexible: Bool, width: Int) {
let settings = SpacerSettings(isFlexible: isFlexible, fixedWidth: width)
updateSettings(.spacer(settings))
}
private func updateSettings(_ settings: ButtonSettings) {
guard var updated = configuration else { return }
updated.settings = settings
viewModel.updateButtonConfigurationSync(updated, in: sectionType)
}
}
// MARK: - Preview
#Preview {
NavigationStack {
ButtonConfigurationView(
buttonID: UUID(),
sectionType: .top,
viewModel: PlayerControlsSettingsViewModel(
layoutService: PlayerControlsLayoutService(),
settingsManager: SettingsManager()
)
)
}
}