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
398 lines
14 KiB
Swift
398 lines
14 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); portrait/wide modes have no
|
|
// meaning on macOS, where every button is always shown.
|
|
#if os(iOS)
|
|
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"))
|
|
}
|
|
#endif
|
|
|
|
// Type-specific settings
|
|
if config.buttonType.hasSettings {
|
|
typeSpecificSettings(for: config)
|
|
}
|
|
#if os(macOS)
|
|
if !config.buttonType.hasSettings {
|
|
Section {
|
|
Text(String(localized: "settings.playerControls.noButtonSettings"))
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
.navigationTitle(config.buttonType.displayName)
|
|
#if os(iOS)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
#elseif os(macOS)
|
|
.formStyle(.grouped)
|
|
#endif
|
|
.opaqueSettingsFormBackground()
|
|
.onAppear {
|
|
syncFromConfiguration(config)
|
|
}
|
|
} else {
|
|
ContentUnavailableView(
|
|
String(localized: "settings.playerControls.buttonNotFound"),
|
|
systemImage: "exclamationmark.triangle"
|
|
)
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
)
|
|
)
|
|
}
|
|
}
|