mirror of
https://github.com/yattee/yattee.git
synced 2026-07-20 22:32:01 +00:00
Render the macOS control bar button row and top bar from the active preset's sections via a new MacOSControlsSectionRenderer, replacing the hardcoded QuickTime-style rows in MacOSControlBar. Add a keepOnTop button type and macOS-specific button availability lists, and adapt the player controls settings editors (preset editor, section editor, button configuration) for macOS. Also apply the preset font style to the control bar time labels, and update the built-in macOS Default preset order (queue before transport, fullscreen at the end), bumping builtInPresetsVersion to 7.
397 lines
13 KiB
Swift
397 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); 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
|
|
.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()
|
|
)
|
|
)
|
|
}
|
|
}
|