mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 01:39:46 +00:00
Yattee v2 rewrite
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
//
|
||||
// TapGesturesSettingsView.swift
|
||||
// Yattee
|
||||
//
|
||||
// Settings view for configuring tap gestures.
|
||||
//
|
||||
|
||||
#if os(iOS)
|
||||
import SwiftUI
|
||||
|
||||
/// Settings view for configuring tap gestures on the player.
|
||||
struct TapGesturesSettingsView: View {
|
||||
@Bindable var viewModel: PlayerControlsSettingsViewModel
|
||||
|
||||
// Local state for immediate UI updates
|
||||
@State private var isEnabled: Bool = false
|
||||
@State private var layout: TapZoneLayout = .horizontalSplit
|
||||
@State private var doubleTapInterval: Int = 300
|
||||
@State private var zoneConfigurations: [TapZoneConfiguration] = []
|
||||
|
||||
// Navigation state
|
||||
@State private var selectedZonePosition: TapZonePosition?
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
enableSection
|
||||
if isEnabled {
|
||||
layoutSection
|
||||
previewSection
|
||||
zonesSection
|
||||
timingSection
|
||||
}
|
||||
}
|
||||
.navigationTitle(String(localized: "gestures.tap.title", defaultValue: "Tap Gestures"))
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
.onAppear {
|
||||
syncFromViewModel()
|
||||
}
|
||||
.onChange(of: viewModel.activePreset?.id) { _, _ in
|
||||
syncFromViewModel()
|
||||
}
|
||||
.sheet(item: $selectedZonePosition) { position in
|
||||
NavigationStack {
|
||||
zoneActionPicker(for: position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sections
|
||||
|
||||
private var enableSection: some View {
|
||||
Section {
|
||||
Toggle(
|
||||
String(localized: "gestures.tap.enable", defaultValue: "Enable Tap Gestures"),
|
||||
isOn: $isEnabled
|
||||
)
|
||||
.onChange(of: isEnabled) { _, newValue in
|
||||
viewModel.updateTapGesturesSettingsSync { $0.isEnabled = newValue }
|
||||
}
|
||||
.disabled(!viewModel.canEditActivePreset)
|
||||
} footer: {
|
||||
Text(String(localized: "gestures.tap.enableFooter", defaultValue: "Double-tap zones on the player to trigger actions when controls are hidden."))
|
||||
}
|
||||
}
|
||||
|
||||
private var layoutSection: some View {
|
||||
Section {
|
||||
TapZoneLayoutPicker(selectedLayout: $layout)
|
||||
.onChange(of: layout) { _, newLayout in
|
||||
// Update configurations for new layout
|
||||
zoneConfigurations = TapGesturesSettings.defaultConfigurations(for: newLayout)
|
||||
viewModel.updateTapGesturesSettingsSync { settings in
|
||||
settings = settings.withLayout(newLayout)
|
||||
}
|
||||
}
|
||||
.disabled(!viewModel.canEditActivePreset)
|
||||
} header: {
|
||||
Text(String(localized: "gestures.tap.layout", defaultValue: "Zone Layout"))
|
||||
} footer: {
|
||||
Text(String(localized: "gestures.tap.layoutFooter", defaultValue: "Choose how the screen is divided into tap zones."))
|
||||
}
|
||||
}
|
||||
|
||||
private var previewSection: some View {
|
||||
Section {
|
||||
TapZonePreview(
|
||||
layout: layout,
|
||||
configurations: zoneConfigurations,
|
||||
onZoneTapped: { position in
|
||||
if viewModel.canEditActivePreset {
|
||||
selectedZonePosition = position
|
||||
}
|
||||
}
|
||||
)
|
||||
.listRowInsets(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16))
|
||||
} header: {
|
||||
Text(String(localized: "gestures.tap.preview", defaultValue: "Preview"))
|
||||
} footer: {
|
||||
Text(String(localized: "gestures.tap.previewFooter", defaultValue: "Tap a zone to configure its action."))
|
||||
}
|
||||
}
|
||||
|
||||
private var zonesSection: some View {
|
||||
Section {
|
||||
ForEach(layout.positions, id: \.self) { position in
|
||||
Button {
|
||||
if viewModel.canEditActivePreset {
|
||||
selectedZonePosition = position
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Text(position.displayName)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Spacer()
|
||||
|
||||
if let config = zoneConfigurations.first(where: { $0.position == position }) {
|
||||
Label {
|
||||
Text(actionSummary(for: config.action))
|
||||
} icon: {
|
||||
Image(systemName: config.action.systemImage)
|
||||
}
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.subheadline)
|
||||
}
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
}
|
||||
.disabled(!viewModel.canEditActivePreset)
|
||||
}
|
||||
} header: {
|
||||
Text(String(localized: "gestures.tap.zones", defaultValue: "Zone Actions"))
|
||||
}
|
||||
}
|
||||
|
||||
private var timingSection: some View {
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Text(String(localized: "gestures.tap.doubleTapWindow", defaultValue: "Double-Tap Window"))
|
||||
Spacer()
|
||||
Text("\(doubleTapInterval)ms")
|
||||
.foregroundStyle(.secondary)
|
||||
.monospacedDigit()
|
||||
}
|
||||
|
||||
Slider(
|
||||
value: Binding(
|
||||
get: { Double(doubleTapInterval) },
|
||||
set: {
|
||||
doubleTapInterval = Int($0)
|
||||
viewModel.updateTapGesturesSettingsSync { $0.doubleTapInterval = doubleTapInterval }
|
||||
}
|
||||
),
|
||||
in: Double(TapGesturesSettings.doubleTapIntervalRange.lowerBound)...Double(TapGesturesSettings.doubleTapIntervalRange.upperBound),
|
||||
step: 25
|
||||
)
|
||||
.disabled(!viewModel.canEditActivePreset)
|
||||
}
|
||||
} header: {
|
||||
Text(String(localized: "gestures.tap.timing", defaultValue: "Timing"))
|
||||
} footer: {
|
||||
Text(String(localized: "gestures.tap.timingFooter", defaultValue: "Time window to detect a double-tap. Lower values are faster but may conflict with single-tap."))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
@ViewBuilder
|
||||
private func zoneActionPicker(for position: TapZonePosition) -> some View {
|
||||
let binding = Binding<TapGestureAction>(
|
||||
get: {
|
||||
zoneConfigurations.first { $0.position == position }?.action ?? .togglePlayPause
|
||||
},
|
||||
set: { newAction in
|
||||
if let index = zoneConfigurations.firstIndex(where: { $0.position == position }) {
|
||||
zoneConfigurations[index] = zoneConfigurations[index].withAction(newAction)
|
||||
viewModel.updateTapZoneConfigurationSync(zoneConfigurations[index])
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
TapZoneActionPicker(position: position, action: binding)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button(role: .cancel) {
|
||||
selectedZonePosition = nil
|
||||
} label: {
|
||||
Label("Close", systemImage: "xmark")
|
||||
.labelStyle(.iconOnly)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func syncFromViewModel() {
|
||||
let settings = viewModel.gesturesSettings.tapGestures
|
||||
isEnabled = settings.isEnabled
|
||||
layout = settings.layout
|
||||
doubleTapInterval = settings.doubleTapInterval
|
||||
zoneConfigurations = settings.zoneConfigurations
|
||||
}
|
||||
|
||||
private func actionSummary(for action: TapGestureAction) -> String {
|
||||
switch action {
|
||||
case .seekForward(let seconds):
|
||||
"+\(seconds)s"
|
||||
case .seekBackward(let seconds):
|
||||
"-\(seconds)s"
|
||||
case .togglePlayPause:
|
||||
String(localized: "gestures.action.playPause.short", defaultValue: "Play/Pause")
|
||||
case .toggleFullscreen:
|
||||
String(localized: "gestures.action.fullscreen.short", defaultValue: "Fullscreen")
|
||||
case .togglePiP:
|
||||
String(localized: "gestures.action.pip.short", defaultValue: "PiP")
|
||||
case .playNext:
|
||||
String(localized: "gestures.action.next.short", defaultValue: "Next")
|
||||
case .playPrevious:
|
||||
String(localized: "gestures.action.previous.short", defaultValue: "Previous")
|
||||
case .cyclePlaybackSpeed:
|
||||
String(localized: "gestures.action.speed.short", defaultValue: "Speed")
|
||||
case .toggleMute:
|
||||
String(localized: "gestures.action.mute.short", defaultValue: "Mute")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
TapGesturesSettingsView(
|
||||
viewModel: PlayerControlsSettingsViewModel(
|
||||
layoutService: PlayerControlsLayoutService(),
|
||||
settingsManager: SettingsManager()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user