mirror of
https://github.com/yattee/yattee.git
synced 2026-07-20 22:32:01 +00:00
Drive macOS player control bars from presets
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.
This commit is contained in:
@@ -36,7 +36,9 @@ struct ButtonConfigurationView: View {
|
||||
var body: some View {
|
||||
if let config = configuration {
|
||||
Form {
|
||||
// Visibility mode (all buttons)
|
||||
// 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"),
|
||||
@@ -54,15 +56,26 @@ struct ButtonConfigurationView: View {
|
||||
} 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)
|
||||
|
||||
@@ -20,8 +20,28 @@ struct CenterControlsSettingsView: View {
|
||||
@State private var leftSlider: SideSliderType = .disabled
|
||||
@State private var rightSlider: SideSliderType = .disabled
|
||||
|
||||
/// On macOS the center overlay doesn't exist; the seek durations always
|
||||
/// apply (keyboard arrows and default seek buttons), so the controls are
|
||||
/// always visible there.
|
||||
private var seekBackwardControlsVisible: Bool {
|
||||
#if os(macOS)
|
||||
return true
|
||||
#else
|
||||
return showSeekBackward
|
||||
#endif
|
||||
}
|
||||
|
||||
private var seekForwardControlsVisible: Bool {
|
||||
#if os(macOS)
|
||||
return true
|
||||
#else
|
||||
return showSeekForward
|
||||
#endif
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
#if os(iOS)
|
||||
// Preview
|
||||
Section {
|
||||
CenterPreviewView(
|
||||
@@ -47,9 +67,11 @@ struct CenterControlsSettingsView: View {
|
||||
} header: {
|
||||
Text(String(localized: "settings.playerControls.center.playback"))
|
||||
}
|
||||
#endif
|
||||
|
||||
// Seek backward settings
|
||||
Section {
|
||||
#if os(iOS)
|
||||
Toggle(
|
||||
String(localized: "settings.playerControls.center.showSeekBackward"),
|
||||
isOn: $showSeekBackward
|
||||
@@ -57,8 +79,9 @@ struct CenterControlsSettingsView: View {
|
||||
.onChange(of: showSeekBackward) { _, newValue in
|
||||
viewModel.updateCenterSettingsSync { $0.showSeekBackward = newValue }
|
||||
}
|
||||
#endif
|
||||
|
||||
if showSeekBackward {
|
||||
if seekBackwardControlsVisible {
|
||||
#if !os(tvOS)
|
||||
HStack {
|
||||
Text(String(localized: "settings.playerControls.center.seekBackwardTime"))
|
||||
@@ -97,6 +120,7 @@ struct CenterControlsSettingsView: View {
|
||||
|
||||
// Seek forward settings
|
||||
Section {
|
||||
#if os(iOS)
|
||||
Toggle(
|
||||
String(localized: "settings.playerControls.center.showSeekForward"),
|
||||
isOn: $showSeekForward
|
||||
@@ -104,8 +128,9 @@ struct CenterControlsSettingsView: View {
|
||||
.onChange(of: showSeekForward) { _, newValue in
|
||||
viewModel.updateCenterSettingsSync { $0.showSeekForward = newValue }
|
||||
}
|
||||
#endif
|
||||
|
||||
if showSeekForward {
|
||||
if seekForwardControlsVisible {
|
||||
#if !os(tvOS)
|
||||
HStack {
|
||||
Text(String(localized: "settings.playerControls.center.seekForwardTime"))
|
||||
@@ -140,6 +165,13 @@ struct CenterControlsSettingsView: View {
|
||||
}
|
||||
} header: {
|
||||
Text(String(localized: "settings.playerControls.center.seekForward"))
|
||||
} footer: {
|
||||
#if os(macOS)
|
||||
Text(String(
|
||||
localized: "settings.playerControls.center.seekDurationsFooter",
|
||||
defaultValue: "Seek durations are used by the ← and → keyboard shortcuts and by seek buttons added to the player."
|
||||
))
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
@@ -175,7 +207,12 @@ struct CenterControlsSettingsView: View {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if os(macOS)
|
||||
.formStyle(.grouped)
|
||||
.navigationTitle(String(localized: "settings.playerControls.seekDurations", defaultValue: "Seek Durations"))
|
||||
#else
|
||||
.navigationTitle(String(localized: "settings.playerControls.centerControls"))
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
|
||||
@@ -30,6 +30,8 @@ struct MiniPlayerEditorView: View {
|
||||
.navigationTitle(String(localized: "settings.playerControls.miniPlayer"))
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#elseif os(macOS)
|
||||
.formStyle(.grouped)
|
||||
#endif
|
||||
.onAppear {
|
||||
syncLocalState()
|
||||
@@ -274,6 +276,8 @@ struct MiniPlayerButtonConfigurationView: View {
|
||||
.navigationTitle(config.buttonType.displayName)
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#elseif os(macOS)
|
||||
.formStyle(.grouped)
|
||||
#endif
|
||||
.onAppear {
|
||||
syncFromConfiguration(config)
|
||||
|
||||
@@ -74,7 +74,10 @@ private struct PlayerControlsSettingsContent: View {
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
// The phone-shaped preview and comments pill don't apply to the macOS player.
|
||||
#if os(iOS)
|
||||
PreviewSection(viewModel: viewModel, layout: previewLayout)
|
||||
#endif
|
||||
PresetSection(
|
||||
viewModel: viewModel,
|
||||
trackedPresetName: $trackedActivePresetName,
|
||||
@@ -87,13 +90,16 @@ private struct PlayerControlsSettingsContent: View {
|
||||
fontStyle: $fontStyle
|
||||
)
|
||||
LayoutSectionsSection(viewModel: viewModel)
|
||||
CommentsPillSection(viewModel: viewModel)
|
||||
#if os(iOS)
|
||||
CommentsPillSection(viewModel: viewModel)
|
||||
GesturesSectionsSection(viewModel: viewModel)
|
||||
#endif
|
||||
SystemControlsSection(viewModel: viewModel)
|
||||
VolumeSection(viewModel: viewModel)
|
||||
}
|
||||
#if os(macOS)
|
||||
.formStyle(.grouped)
|
||||
#endif
|
||||
.id(refreshID)
|
||||
.alert(
|
||||
String(localized: "settings.playerControls.error"),
|
||||
@@ -237,6 +243,8 @@ private struct AppearanceSection: View {
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
// The macOS bar draws its own glass capsule; the per-button style has no effect there.
|
||||
#if os(iOS)
|
||||
Picker(
|
||||
String(localized: "settings.playerControls.style"),
|
||||
selection: $style
|
||||
@@ -250,6 +258,7 @@ private struct AppearanceSection: View {
|
||||
guard newStyle != viewModel.currentLayout.globalSettings.style else { return }
|
||||
viewModel.updateGlobalSettingsSync { $0.style = newStyle }
|
||||
}
|
||||
#endif
|
||||
|
||||
Picker(
|
||||
String(localized: "settings.playerControls.buttonSize"),
|
||||
@@ -312,10 +321,17 @@ private struct LayoutSectionsSection: View {
|
||||
NavigationLink {
|
||||
CenterControlsSettingsView(viewModel: viewModel)
|
||||
} label: {
|
||||
#if os(macOS)
|
||||
Label(
|
||||
String(localized: "settings.playerControls.seekDurations", defaultValue: "Seek Durations"),
|
||||
systemImage: "arrow.trianglehead.clockwise"
|
||||
)
|
||||
#else
|
||||
Label(
|
||||
String(localized: "settings.playerControls.centerControls"),
|
||||
systemImage: "play.circle"
|
||||
)
|
||||
#endif
|
||||
}
|
||||
.disabled(!viewModel.canEditActivePreset)
|
||||
|
||||
@@ -336,10 +352,17 @@ private struct LayoutSectionsSection: View {
|
||||
)
|
||||
} label: {
|
||||
HStack {
|
||||
#if os(macOS)
|
||||
Label(
|
||||
String(localized: "settings.playerControls.controlBarButtons", defaultValue: "Control Bar Buttons"),
|
||||
systemImage: "rectangle.bottomthird.inset.filled"
|
||||
)
|
||||
#else
|
||||
Label(
|
||||
String(localized: "settings.playerControls.bottomButtons"),
|
||||
systemImage: "rectangle.bottomthird.inset.filled"
|
||||
)
|
||||
#endif
|
||||
Spacer()
|
||||
Text("\(viewModel.currentLayout.bottomSection.buttons.count)")
|
||||
.foregroundStyle(.secondary)
|
||||
@@ -347,6 +370,8 @@ private struct LayoutSectionsSection: View {
|
||||
}
|
||||
.disabled(!viewModel.canEditActivePreset)
|
||||
|
||||
// The player pill only exists in the iOS player.
|
||||
#if os(iOS)
|
||||
NavigationLink {
|
||||
PlayerPillEditorView(viewModel: viewModel)
|
||||
} label: {
|
||||
@@ -361,6 +386,7 @@ private struct LayoutSectionsSection: View {
|
||||
}
|
||||
}
|
||||
.disabled(!viewModel.canEditActivePreset)
|
||||
#endif
|
||||
|
||||
NavigationLink {
|
||||
MiniPlayerEditorView(viewModel: viewModel)
|
||||
|
||||
@@ -85,39 +85,22 @@ struct PresetEditorView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
#if os(macOS)
|
||||
macOSDialog
|
||||
#else
|
||||
NavigationStack {
|
||||
Form {
|
||||
// Base Layout Picker (only for create mode)
|
||||
if case .create(_, _) = mode {
|
||||
Section {
|
||||
Picker(
|
||||
String(localized: "settings.playerControls.baseLayout"),
|
||||
selection: $selectedBaseLayoutID
|
||||
) {
|
||||
ForEach(baseLayouts) { preset in
|
||||
Text(preset.name).tag(preset.id as UUID?)
|
||||
}
|
||||
}
|
||||
basePicker
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
TextField(mode.placeholder, text: $name)
|
||||
.focused($isNameFocused)
|
||||
.submitLabel(.done)
|
||||
.onSubmit(saveIfValid)
|
||||
nameField
|
||||
} footer: {
|
||||
HStack {
|
||||
if trimmedName.count > LayoutPreset.maxNameLength {
|
||||
Text(String(localized: "settings.playerControls.nameTooLong"))
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
Spacer()
|
||||
Text("\(trimmedName.count)/\(LayoutPreset.maxNameLength)")
|
||||
.foregroundStyle(
|
||||
trimmedName.count > LayoutPreset.maxNameLength ? .red : .secondary
|
||||
)
|
||||
}
|
||||
nameFooter
|
||||
}
|
||||
}
|
||||
.navigationTitle(mode.title)
|
||||
@@ -145,11 +128,87 @@ struct PresetEditorView: View {
|
||||
#if os(iOS)
|
||||
.presentationDetents([.medium])
|
||||
#endif
|
||||
#if os(macOS)
|
||||
.frame(minWidth: 500, minHeight: 350)
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Shared Controls
|
||||
|
||||
private var basePicker: some View {
|
||||
Picker(
|
||||
String(localized: "settings.playerControls.baseLayout"),
|
||||
selection: $selectedBaseLayoutID
|
||||
) {
|
||||
ForEach(baseLayouts) { preset in
|
||||
Text(preset.name).tag(preset.id as UUID?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var nameField: some View {
|
||||
TextField(mode.placeholder, text: $name)
|
||||
.focused($isNameFocused)
|
||||
.submitLabel(.done)
|
||||
.onSubmit(saveIfValid)
|
||||
}
|
||||
|
||||
private var nameFooter: some View {
|
||||
HStack {
|
||||
if trimmedName.count > LayoutPreset.maxNameLength {
|
||||
Text(String(localized: "settings.playerControls.nameTooLong"))
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
Spacer()
|
||||
Text("\(trimmedName.count)/\(LayoutPreset.maxNameLength)")
|
||||
.foregroundStyle(
|
||||
trimmedName.count > LayoutPreset.maxNameLength ? .red : .secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - macOS Dialog
|
||||
|
||||
#if os(macOS)
|
||||
/// Compact dialog layout matching native macOS sheets (title, fields,
|
||||
/// trailing action buttons) instead of the iOS navigation-bar form.
|
||||
private var macOSDialog: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text(mode.title)
|
||||
.font(.headline)
|
||||
|
||||
if case .create(_, _) = mode {
|
||||
basePicker
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
nameField
|
||||
.textFieldStyle(.roundedBorder)
|
||||
|
||||
nameFooter
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(String(localized: "settings.playerControls.cancel")) {
|
||||
dismiss()
|
||||
}
|
||||
.keyboardShortcut(.cancelAction)
|
||||
|
||||
Button(mode.saveButtonTitle) {
|
||||
saveIfValid()
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.disabled(!isValid)
|
||||
}
|
||||
}
|
||||
.padding(20)
|
||||
.frame(width: 400)
|
||||
.onAppear {
|
||||
isNameFocused = true
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private func saveIfValid() {
|
||||
guard isValid else { return }
|
||||
onSave(trimmedName, selectedBaseLayoutID)
|
||||
|
||||
@@ -29,6 +29,8 @@ struct ProgressBarSettingsView: View {
|
||||
.navigationTitle(String(localized: "settings.playerControls.progressBar"))
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#elseif os(macOS)
|
||||
.formStyle(.grouped)
|
||||
#endif
|
||||
.onAppear {
|
||||
loadSettings()
|
||||
|
||||
@@ -30,7 +30,8 @@ struct SectionEditorView: View {
|
||||
.listRowInsets(EdgeInsets())
|
||||
.listRowBackground(Color.clear)
|
||||
|
||||
// Orientation toggle
|
||||
// Orientation toggle; macOS has no portrait/landscape distinction
|
||||
#if os(iOS)
|
||||
Picker(
|
||||
String(localized: "settings.playerControls.previewOrientation"),
|
||||
selection: $viewModel.isPreviewingLandscape
|
||||
@@ -42,6 +43,7 @@ struct SectionEditorView: View {
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.listRowBackground(Color.clear)
|
||||
#endif
|
||||
|
||||
// Added buttons
|
||||
Section {
|
||||
@@ -182,11 +184,13 @@ private struct ButtonRow: View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(configuration.buttonType.displayName)
|
||||
|
||||
#if os(iOS)
|
||||
if configuration.visibilityMode != .both {
|
||||
Text(configuration.visibilityMode.displayName)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -59,6 +59,8 @@ struct SettingsView: View {
|
||||
LayoutNavigationSettingsView()
|
||||
case .playback:
|
||||
PlaybackSettingsView()
|
||||
case .playerControls:
|
||||
PlayerControlsSettingsView()
|
||||
case .notifications:
|
||||
NotificationSettingsView()
|
||||
case .downloads:
|
||||
@@ -355,6 +357,7 @@ enum SettingsSection: String, CaseIterable, Identifiable {
|
||||
case appearance
|
||||
case layoutNavigation
|
||||
case playback
|
||||
case playerControls
|
||||
case notifications
|
||||
case downloads
|
||||
case privacy
|
||||
@@ -371,6 +374,7 @@ enum SettingsSection: String, CaseIterable, Identifiable {
|
||||
case .appearance: return String(localized: "settings.appearance.sectionTitle")
|
||||
case .layoutNavigation: return String(localized: "settings.layoutNavigation.title")
|
||||
case .playback: return String(localized: "settings.playback.sectionTitle")
|
||||
case .playerControls: return String(localized: "settings.playerControls.title")
|
||||
case .notifications: return String(localized: "settings.notifications.title")
|
||||
case .downloads: return String(localized: "settings.downloads.title")
|
||||
case .privacy: return String(localized: "settings.privacy.title")
|
||||
@@ -387,6 +391,7 @@ enum SettingsSection: String, CaseIterable, Identifiable {
|
||||
case .appearance: return "paintbrush"
|
||||
case .layoutNavigation: return "hand.tap"
|
||||
case .playback: return "play.circle"
|
||||
case .playerControls: return "slider.horizontal.below.rectangle"
|
||||
case .notifications: return "bell.badge"
|
||||
case .downloads: return "arrow.down.circle"
|
||||
case .privacy: return "hand.raised"
|
||||
|
||||
Reference in New Issue
Block a user