Use menu-style pickers in tvOS settings

Introduce PlatformMenuPicker that wraps short-option pickers in
LabeledContent + .pickerStyle(.menu) on tvOS so they render as a
compact dropdown instead of pushing a full-screen option list. On
iOS/macOS it falls through to a plain Picker, leaving rendering
unchanged.

Applied across Playback, Subtitles, Sidebar, Privacy, and Advanced
settings. Long language lists in PlaybackSettingsView are left as
push-style.
This commit is contained in:
Arkadiusz Fal
2026-04-16 18:40:19 +02:00
parent df232ad69a
commit e2f3107833
6 changed files with 58 additions and 16 deletions

View File

@@ -99,7 +99,7 @@ struct AdvancedSettingsView: View {
@ViewBuilder
private func feedSection(settingsManager: SettingsManager) -> some View {
Section {
Picker(selection: Binding(
PlatformMenuPicker(selection: Binding(
get: { settingsManager.feedCacheValidityMinutes },
set: { settingsManager.feedCacheValidityMinutes = $0 }
)) {
@@ -192,7 +192,7 @@ struct AdvancedSettingsView: View {
private var mpvSection: some View {
if let settingsManager = appEnvironment?.settingsManager {
Section {
Picker(selection: Binding(
PlatformMenuPicker(selection: Binding(
get: { settingsManager.mpvBufferSeconds },
set: { settingsManager.mpvBufferSeconds = $0 }
)) {

View File

@@ -0,0 +1,42 @@
//
// PlatformMenuPicker.swift
// Yattee
//
// A Picker wrapper that renders as a compact menu inside LabeledContent on tvOS,
// and as a standard inline Picker on iOS/macOS. Use this for short option lists
// in settings forms so tvOS shows a dropdown menu rather than pushing a sub-view.
//
import SwiftUI
struct PlatformMenuPicker<Selection: Hashable, Label: View, Content: View>: View {
@Binding var selection: Selection
@ViewBuilder var content: () -> Content
@ViewBuilder var label: () -> Label
var body: some View {
#if os(tvOS)
LabeledContent {
Picker(selection: $selection, content: content) { EmptyView() }
.pickerStyle(.menu)
.labelsHidden()
} label: {
label()
}
#else
Picker(selection: $selection, content: content, label: label)
#endif
}
}
extension PlatformMenuPicker where Label == Text {
init(
_ titleKey: String,
selection: Binding<Selection>,
@ViewBuilder content: @escaping () -> Content
) {
self._selection = selection
self.content = content
self.label = { Text(titleKey) }
}
}

View File

@@ -42,7 +42,7 @@ private struct QualitySection: View {
var body: some View {
Section {
Picker(
PlatformMenuPicker(
String(localized: "settings.playback.quality.preferred"),
selection: $settings.preferredQuality
) {
@@ -241,7 +241,7 @@ private struct BehaviorSection: View {
var body: some View {
Section(String(localized: "settings.playback.behavior.header")) {
Picker(
PlatformMenuPicker(
String(localized: "settings.playback.resumeAction"),
selection: $settings.resumeAction
) {
@@ -273,7 +273,7 @@ private struct QueueSection: View {
)
#if os(tvOS)
Picker(
PlatformMenuPicker(
String(localized: "settings.playback.queue.autoPlayCountdown"),
selection: $settings.queueAutoPlayCountdown
) {

View File

@@ -54,7 +54,7 @@ struct PrivacySettingsView: View {
isOn: Bindable(settingsManager).saveWatchHistory
)
Picker(
PlatformMenuPicker(
String(localized: "settings.behavior.historyRetention"),
selection: Binding(
get: { settingsManager.historyRetentionDays },
@@ -104,7 +104,7 @@ struct PrivacySettingsView: View {
isOn: Bindable(settingsManager).saveRecentPlaylists
)
Picker(
PlatformMenuPicker(
String(localized: "settings.behavior.searchHistoryLimit"),
selection: Binding(
get: { settingsManager.searchHistoryLimit },

View File

@@ -153,7 +153,7 @@ struct SidebarSettingsView: View {
private var startupSection: some View {
Section {
Picker(String(localized: "settings.sidebar.startup.tab"), selection: startupTabBinding) {
PlatformMenuPicker(String(localized: "settings.sidebar.startup.tab"), selection: startupTabBinding) {
ForEach(validStartupTabs) { item in
Text(item.localizedTitle).tag(item)
}
@@ -288,7 +288,7 @@ struct SidebarSettingsView: View {
}
// Source sort order
Picker(String(localized: "settings.sidebar.sourceSort"), selection: sourceSortBinding) {
PlatformMenuPicker(String(localized: "settings.sidebar.sourceSort"), selection: sourceSortBinding) {
ForEach(SidebarSourceSort.allCases) { sort in
Text(sort.localizedTitle).tag(sort)
}
@@ -309,7 +309,7 @@ struct SidebarSettingsView: View {
if sourcesLimitEnabledBinding.wrappedValue {
#if os(tvOS)
// tvOS uses Picker instead of Slider (Slider/Stepper unavailable)
Picker(String(localized: "settings.sidebar.maxSources"), selection: maxSourcesBinding) {
PlatformMenuPicker(String(localized: "settings.sidebar.maxSources"), selection: maxSourcesBinding) {
ForEach([5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 60, 70, 80, 90, 100], id: \.self) { value in
Text("\(value)").tag(value)
}
@@ -360,7 +360,7 @@ struct SidebarSettingsView: View {
}
// Channel sort order
Picker(String(localized: "settings.sidebar.channelSort"), selection: channelSortBinding) {
PlatformMenuPicker(String(localized: "settings.sidebar.channelSort"), selection: channelSortBinding) {
ForEach(SidebarChannelSort.allCases.filter { $0 != .custom }) { sort in
Text(sort.localizedTitle).tag(sort)
}
@@ -381,7 +381,7 @@ struct SidebarSettingsView: View {
if channelsLimitEnabledBinding.wrappedValue {
#if os(tvOS)
// tvOS uses Picker instead of Slider (Slider/Stepper unavailable)
Picker(String(localized: "settings.sidebar.maxChannels"), selection: maxChannelsBinding) {
PlatformMenuPicker(String(localized: "settings.sidebar.maxChannels"), selection: maxChannelsBinding) {
ForEach([5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 60, 70, 80, 90, 100], id: \.self) { value in
Text("\(value)").tag(value)
}
@@ -432,7 +432,7 @@ struct SidebarSettingsView: View {
}
// Playlist sort order
Picker(String(localized: "settings.sidebar.playlistSort"), selection: playlistSortBinding) {
PlatformMenuPicker(String(localized: "settings.sidebar.playlistSort"), selection: playlistSortBinding) {
ForEach(SidebarPlaylistSort.allCases) { sort in
Text(sort.localizedTitle).tag(sort)
}
@@ -453,7 +453,7 @@ struct SidebarSettingsView: View {
if playlistsLimitEnabledBinding.wrappedValue {
#if os(tvOS)
// tvOS uses Picker instead of Slider (Slider/Stepper unavailable)
Picker(String(localized: "settings.sidebar.maxPlaylists"), selection: maxPlaylistsBinding) {
PlatformMenuPicker(String(localized: "settings.sidebar.maxPlaylists"), selection: maxPlaylistsBinding) {
ForEach([5, 10, 15, 20, 25, 30], id: \.self) { value in
Text("\(value)").tag(value)
}

View File

@@ -37,7 +37,7 @@ struct SubtitlesSettingsView: View {
private var fontSection: some View {
Section {
Picker(
PlatformMenuPicker(
String(localized: "settings.subtitles.font"),
selection: $settings.font
) {
@@ -49,7 +49,7 @@ struct SubtitlesSettingsView: View {
#if os(tvOS)
// tvOS uses Picker instead of Slider (Slider unavailable)
Picker(String(localized: "settings.subtitles.fontSize"), selection: $settings.fontSize) {
PlatformMenuPicker(String(localized: "settings.subtitles.fontSize"), selection: $settings.fontSize) {
ForEach([20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100], id: \.self) { size in
Text("settings.subtitles.fontSize \(size)").tag(size)
}