mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 09:49:46 +00:00
383 lines
13 KiB
Swift
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()
|
|
)
|
|
)
|
|
}
|
|
}
|