mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 09:49:46 +00:00
374 lines
13 KiB
Swift
374 lines
13 KiB
Swift
//
|
|
// CenterControlsSettingsView.swift
|
|
// Yattee
|
|
//
|
|
// View for configuring center section player controls.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
/// View for configuring center section controls (play/pause, seek buttons).
|
|
struct CenterControlsSettingsView: View {
|
|
@Bindable var viewModel: PlayerControlsSettingsViewModel
|
|
|
|
// Local state that mirrors ViewModel - ensures immediate UI updates
|
|
@State private var showPlayPause: Bool = true
|
|
@State private var showSeekBackward: Bool = true
|
|
@State private var showSeekForward: Bool = true
|
|
@State private var seekBackwardSeconds: Double = 10
|
|
@State private var seekForwardSeconds: Double = 10
|
|
@State private var leftSlider: SideSliderType = .disabled
|
|
@State private var rightSlider: SideSliderType = .disabled
|
|
|
|
var body: some View {
|
|
Form {
|
|
// Preview
|
|
Section {
|
|
CenterPreviewView(
|
|
settings: previewSettings,
|
|
buttonBackground: viewModel.currentLayout.globalSettings.buttonBackground,
|
|
theme: viewModel.currentLayout.globalSettings.theme
|
|
)
|
|
.listRowInsets(EdgeInsets())
|
|
.listRowBackground(Color.clear)
|
|
} header: {
|
|
Text(String(localized: "settings.playerControls.preview"))
|
|
}
|
|
|
|
// Play/Pause toggle
|
|
Section {
|
|
Toggle(
|
|
String(localized: "settings.playerControls.center.showPlayPause"),
|
|
isOn: $showPlayPause
|
|
)
|
|
.onChange(of: showPlayPause) { _, newValue in
|
|
viewModel.updateCenterSettingsSync { $0.showPlayPause = newValue }
|
|
}
|
|
} header: {
|
|
Text(String(localized: "settings.playerControls.center.playback"))
|
|
}
|
|
|
|
// Seek backward settings
|
|
Section {
|
|
Toggle(
|
|
String(localized: "settings.playerControls.center.showSeekBackward"),
|
|
isOn: $showSeekBackward
|
|
)
|
|
.onChange(of: showSeekBackward) { _, newValue in
|
|
viewModel.updateCenterSettingsSync { $0.showSeekBackward = newValue }
|
|
}
|
|
|
|
if showSeekBackward {
|
|
#if !os(tvOS)
|
|
HStack {
|
|
Text(String(localized: "settings.playerControls.center.seekBackwardTime"))
|
|
Spacer()
|
|
Text("\(Int(seekBackwardSeconds))s")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Slider(
|
|
value: $seekBackwardSeconds,
|
|
in: 1...90,
|
|
step: 1
|
|
)
|
|
.onChange(of: seekBackwardSeconds) { _, newValue in
|
|
viewModel.updateCenterSettingsSync { $0.seekBackwardSeconds = Int(newValue) }
|
|
}
|
|
#endif
|
|
|
|
// Quick presets
|
|
HStack(spacing: 8) {
|
|
ForEach([5, 10, 15, 30, 45, 60], id: \.self) { seconds in
|
|
Button("\(seconds)s") {
|
|
seekBackwardSeconds = Double(seconds)
|
|
}
|
|
.buttonStyle(.bordered)
|
|
#if !os(tvOS)
|
|
.controlSize(.small)
|
|
#endif
|
|
.tint(Int(seekBackwardSeconds) == seconds ? .accentColor : .secondary)
|
|
}
|
|
}
|
|
}
|
|
} header: {
|
|
Text(String(localized: "settings.playerControls.center.seekBackward"))
|
|
}
|
|
|
|
// Seek forward settings
|
|
Section {
|
|
Toggle(
|
|
String(localized: "settings.playerControls.center.showSeekForward"),
|
|
isOn: $showSeekForward
|
|
)
|
|
.onChange(of: showSeekForward) { _, newValue in
|
|
viewModel.updateCenterSettingsSync { $0.showSeekForward = newValue }
|
|
}
|
|
|
|
if showSeekForward {
|
|
#if !os(tvOS)
|
|
HStack {
|
|
Text(String(localized: "settings.playerControls.center.seekForwardTime"))
|
|
Spacer()
|
|
Text("\(Int(seekForwardSeconds))s")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Slider(
|
|
value: $seekForwardSeconds,
|
|
in: 1...90,
|
|
step: 1
|
|
)
|
|
.onChange(of: seekForwardSeconds) { _, newValue in
|
|
viewModel.updateCenterSettingsSync { $0.seekForwardSeconds = Int(newValue) }
|
|
}
|
|
#endif
|
|
|
|
// Quick presets
|
|
HStack(spacing: 8) {
|
|
ForEach([5, 10, 15, 30, 45, 60], id: \.self) { seconds in
|
|
Button("\(seconds)s") {
|
|
seekForwardSeconds = Double(seconds)
|
|
}
|
|
.buttonStyle(.bordered)
|
|
#if !os(tvOS)
|
|
.controlSize(.small)
|
|
#endif
|
|
.tint(Int(seekForwardSeconds) == seconds ? .accentColor : .secondary)
|
|
}
|
|
}
|
|
}
|
|
} header: {
|
|
Text(String(localized: "settings.playerControls.center.seekForward"))
|
|
}
|
|
|
|
#if os(iOS)
|
|
// Side sliders section (iOS only)
|
|
Section {
|
|
Picker(
|
|
String(localized: "settings.playerControls.center.leftSlider"),
|
|
selection: $leftSlider
|
|
) {
|
|
ForEach(SideSliderType.allCases, id: \.self) { type in
|
|
Text(type.displayName).tag(type)
|
|
}
|
|
}
|
|
.onChange(of: leftSlider) { _, newValue in
|
|
viewModel.updateCenterSettingsSync { $0.leftSlider = newValue }
|
|
}
|
|
|
|
Picker(
|
|
String(localized: "settings.playerControls.center.rightSlider"),
|
|
selection: $rightSlider
|
|
) {
|
|
ForEach(SideSliderType.allCases, id: \.self) { type in
|
|
Text(type.displayName).tag(type)
|
|
}
|
|
}
|
|
.onChange(of: rightSlider) { _, newValue in
|
|
viewModel.updateCenterSettingsSync { $0.rightSlider = newValue }
|
|
}
|
|
} header: {
|
|
Text(String(localized: "settings.playerControls.center.sliders"))
|
|
} footer: {
|
|
Text(String(localized: "settings.playerControls.center.slidersFooter"))
|
|
}
|
|
#endif
|
|
}
|
|
.navigationTitle(String(localized: "settings.playerControls.centerControls"))
|
|
#if os(iOS)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
#endif
|
|
.onAppear {
|
|
syncFromViewModel()
|
|
}
|
|
}
|
|
|
|
// MARK: - Computed Properties
|
|
|
|
/// Settings for preview - uses local state for immediate updates
|
|
private var previewSettings: CenterSectionSettings {
|
|
CenterSectionSettings(
|
|
showPlayPause: showPlayPause,
|
|
showSeekBackward: showSeekBackward,
|
|
showSeekForward: showSeekForward,
|
|
seekBackwardSeconds: Int(seekBackwardSeconds),
|
|
seekForwardSeconds: Int(seekForwardSeconds),
|
|
leftSlider: leftSlider,
|
|
rightSlider: rightSlider
|
|
)
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
/// Syncs local state from ViewModel
|
|
private func syncFromViewModel() {
|
|
let settings = viewModel.centerSettings
|
|
showPlayPause = settings.showPlayPause
|
|
showSeekBackward = settings.showSeekBackward
|
|
showSeekForward = settings.showSeekForward
|
|
seekBackwardSeconds = Double(settings.seekBackwardSeconds)
|
|
seekForwardSeconds = Double(settings.seekForwardSeconds)
|
|
leftSlider = settings.leftSlider
|
|
rightSlider = settings.rightSlider
|
|
}
|
|
}
|
|
|
|
// MARK: - Center Preview
|
|
|
|
#if os(iOS)
|
|
/// Preview representation of a vertical side slider.
|
|
private struct SliderPreview: View {
|
|
let type: SideSliderType
|
|
let buttonBackground: ButtonBackgroundStyle
|
|
|
|
var body: some View {
|
|
VStack(spacing: 4) {
|
|
if let icon = type.systemImage {
|
|
Image(systemName: icon)
|
|
.font(.system(size: 10, weight: .medium))
|
|
.foregroundStyle(.white)
|
|
}
|
|
|
|
// Slider track preview
|
|
ZStack(alignment: .bottom) {
|
|
Capsule()
|
|
.fill(.white.opacity(0.3))
|
|
Capsule()
|
|
.fill(.white)
|
|
.frame(height: 30) // ~50% fill for preview
|
|
}
|
|
.frame(width: 3, height: 60)
|
|
}
|
|
.padding(.vertical, 8)
|
|
.padding(.horizontal, 4)
|
|
.frame(width: 24)
|
|
.modifier(SliderPreviewBackgroundModifier(buttonBackground: buttonBackground))
|
|
}
|
|
}
|
|
|
|
/// Applies background to slider preview based on button background style.
|
|
private struct SliderPreviewBackgroundModifier: ViewModifier {
|
|
let buttonBackground: ButtonBackgroundStyle
|
|
|
|
func body(content: Content) -> some View {
|
|
if let glassStyle = buttonBackground.glassStyle {
|
|
content.glassBackground(glassStyle, in: .capsule, fallback: .ultraThinMaterial)
|
|
} else {
|
|
content.background(.ultraThinMaterial.opacity(0.5), in: Capsule())
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
private struct CenterPreviewView: View {
|
|
let settings: CenterSectionSettings
|
|
let buttonBackground: ButtonBackgroundStyle
|
|
let theme: ControlsTheme
|
|
|
|
private let buttonSize: CGFloat = 32
|
|
private let playButtonSize: CGFloat = 44
|
|
private let spacing: CGFloat = 24
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
Image("PlayerControlsPreviewBackground")
|
|
.resizable()
|
|
.scaledToFill()
|
|
|
|
// Gradient shade like actual player
|
|
LinearGradient(
|
|
colors: [.black.opacity(0.7), .clear, .black.opacity(0.7)],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
|
|
// Center controls
|
|
HStack(spacing: spacing) {
|
|
if settings.showSeekBackward {
|
|
CenterButtonPreview(
|
|
systemImage: settings.seekBackwardSystemImage,
|
|
size: buttonSize,
|
|
buttonBackground: buttonBackground
|
|
)
|
|
}
|
|
|
|
if settings.showPlayPause {
|
|
CenterButtonPreview(
|
|
systemImage: "play.fill",
|
|
size: playButtonSize,
|
|
buttonBackground: buttonBackground
|
|
)
|
|
}
|
|
|
|
if settings.showSeekForward {
|
|
CenterButtonPreview(
|
|
systemImage: settings.seekForwardSystemImage,
|
|
size: buttonSize,
|
|
buttonBackground: buttonBackground
|
|
)
|
|
}
|
|
}
|
|
|
|
#if os(iOS)
|
|
// Side sliders preview
|
|
HStack {
|
|
if settings.leftSlider != .disabled {
|
|
SliderPreview(type: settings.leftSlider, buttonBackground: buttonBackground)
|
|
.padding(.leading, 8)
|
|
}
|
|
Spacer()
|
|
if settings.rightSlider != .disabled {
|
|
SliderPreview(type: settings.rightSlider, buttonBackground: buttonBackground)
|
|
.padding(.trailing, 8)
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
.frame(height: 100)
|
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
|
.padding(.horizontal)
|
|
.padding(.vertical, 8)
|
|
.modifier(PreviewThemeModifier(theme: theme))
|
|
}
|
|
}
|
|
|
|
private struct CenterButtonPreview: View {
|
|
let systemImage: String
|
|
let size: CGFloat
|
|
let buttonBackground: ButtonBackgroundStyle
|
|
|
|
/// Frame size - slightly larger when backgrounds are enabled.
|
|
private var frameSize: CGFloat {
|
|
buttonBackground.glassStyle != nil ? size * 1.4 : size * 1.2
|
|
}
|
|
|
|
var body: some View {
|
|
if let glassStyle = buttonBackground.glassStyle {
|
|
Image(systemName: systemImage)
|
|
.font(.system(size: size * 0.7))
|
|
.foregroundStyle(.white)
|
|
.frame(width: frameSize, height: frameSize)
|
|
.glassBackground(glassStyle, in: .circle, fallback: .ultraThinMaterial)
|
|
} else {
|
|
Image(systemName: systemImage)
|
|
.font(.system(size: size * 0.7))
|
|
.foregroundStyle(.white)
|
|
.frame(width: frameSize, height: frameSize)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview
|
|
|
|
#Preview {
|
|
NavigationStack {
|
|
CenterControlsSettingsView(
|
|
viewModel: PlayerControlsSettingsViewModel(
|
|
layoutService: PlayerControlsLayoutService(),
|
|
settingsManager: SettingsManager()
|
|
)
|
|
)
|
|
}
|
|
}
|