mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 17:59:45 +00:00
Yattee v2 rewrite
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
//
|
||||
// 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()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user