Make Playback and Subtitles settings feel native on macOS

Add shared SettingsFormContainer/SettingsFormSection helpers that mirror
the Sources screen styling (uppercase subheadline headers, divider-
bracketed cards, ScrollView + LazyVStack) on macOS while keeping the
standard Form/Section layout on iOS and tvOS.

Convert PlaybackSettingsView and SubtitlesSettingsView to the new
helpers, wrap the macOS Settings detail pane in a NavigationStack so
NavigationLink pushes (Subtitles Appearance) render in the detail
column, fold the macOS-only Player Mode + Auto-resize player controls
into the Behavior section, and drop the unused queue footer.
This commit is contained in:
Arkadiusz Fal
2026-04-20 23:01:46 +02:00
parent fef9a07aa9
commit 14b874022b
5 changed files with 211 additions and 110 deletions

View File

@@ -11924,6 +11924,7 @@
}, },
"settings.playback.macOS.header" : { "settings.playback.macOS.header" : {
"comment" : "macOS-specific playback settings section header", "comment" : "macOS-specific playback settings section header",
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -12064,6 +12065,7 @@
}, },
"settings.playback.queue.footer" : { "settings.playback.queue.footer" : {
"comment" : "Queue section footer", "comment" : "Queue section footer",
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -17730,16 +17732,6 @@
} }
} }
}, },
"viewOptions.showSidebar" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Show Sidebar"
}
}
}
},
"viewOptions.layout" : { "viewOptions.layout" : {
"localizations" : { "localizations" : {
"en" : { "en" : {
@@ -17830,6 +17822,16 @@
} }
} }
}, },
"viewOptions.showSidebar" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Show Sidebar"
}
}
}
},
"viewOptions.title" : { "viewOptions.title" : {
"comment" : "View Options sheet title", "comment" : "View Options sheet title",
"localizations" : { "localizations" : {

View File

@@ -0,0 +1,135 @@
//
// MacOSSettings.swift
// Yattee
//
// Shared helpers that make Settings screens feel native on macOS while
// keeping the iOS/tvOS Form-based layout unchanged.
//
// The reference implementation these helpers mirror is SourcesListView.swift:
// uppercase subheadline section headers, divider-bracketed cards (no rounded
// background), and a ScrollView + LazyVStack container instead of Form.
//
import SwiftUI
/// Root container for a macOS-native settings screen.
///
/// - On macOS: renders a `ScrollView` + `LazyVStack` so sections can use
/// custom dividers and typography instead of Form's grouped cards.
/// - On iOS/tvOS: renders a standard `Form` (unchanged from the iOS layout).
struct SettingsFormContainer<Content: View>: View {
@ViewBuilder let content: () -> Content
var body: some View {
#if os(macOS)
ScrollView {
LazyVStack(alignment: .leading, spacing: 0) {
content()
}
.padding(.vertical, 8)
.frame(maxWidth: .infinity, alignment: .leading)
}
#else
Form {
content()
}
#endif
}
}
/// A settings section with header and optional footer.
///
/// - On macOS: renders an uppercase `.subheadline` header, a top divider,
/// content with consistent padding, a bottom divider, and an optional
/// caption-sized footer.
/// - On iOS/tvOS: renders a standard `Section { } header: { } footer: { }`.
struct SettingsFormSection<Content: View>: View {
let header: LocalizedStringKey?
let footer: LocalizedStringKey?
@ViewBuilder let content: () -> Content
init(
_ header: LocalizedStringKey? = nil,
footer: LocalizedStringKey? = nil,
@ViewBuilder content: @escaping () -> Content
) {
self.header = header
self.footer = footer
self.content = content
}
var body: some View {
#if os(macOS)
macOSSection
#else
platformSection
#endif
}
#if os(macOS)
private var macOSSection: some View {
VStack(alignment: .leading, spacing: 0) {
if let header {
Text(header)
.font(.subheadline)
.textCase(.uppercase)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 16)
.padding(.top, 12)
.padding(.bottom, 4)
}
Divider()
VStack(alignment: .leading, spacing: 10) {
content()
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 16)
.padding(.vertical, 10)
Divider()
if let footer {
Text(footer)
.font(.caption)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 16)
.padding(.top, 6)
}
}
.padding(.bottom, 12)
}
#else
@ViewBuilder
private var platformSection: some View {
if let header, let footer {
Section {
content()
} header: {
Text(header)
} footer: {
Text(footer)
}
} else if let header {
Section {
content()
} header: {
Text(header)
}
} else if let footer {
Section {
content()
} footer: {
Text(footer)
}
} else {
Section {
content()
}
}
}
#endif
}

View File

@@ -11,7 +11,7 @@ struct PlaybackSettingsView: View {
@Environment(\.appEnvironment) private var appEnvironment @Environment(\.appEnvironment) private var appEnvironment
var body: some View { var body: some View {
Form { SettingsFormContainer {
if let settings = appEnvironment?.settingsManager { if let settings = appEnvironment?.settingsManager {
QualitySection(settings: settings) QualitySection(settings: settings)
AudioSection(settings: settings) AudioSection(settings: settings)
@@ -21,9 +21,6 @@ struct PlaybackSettingsView: View {
#if os(iOS) #if os(iOS)
OrientationSection(settings: settings) OrientationSection(settings: settings)
#endif #endif
#if os(macOS)
MacOSSection(settings: settings)
#endif
} }
} }
#if !os(tvOS) #if !os(tvOS)
@@ -41,7 +38,7 @@ private struct QualitySection: View {
@Bindable var settings: SettingsManager @Bindable var settings: SettingsManager
var body: some View { var body: some View {
Section { SettingsFormSection("settings.playback.video.header") {
PlatformMenuPicker( PlatformMenuPicker(
String(localized: "settings.playback.quality.preferred"), String(localized: "settings.playback.quality.preferred"),
selection: $settings.preferredQuality selection: $settings.preferredQuality
@@ -61,8 +58,6 @@ private struct QualitySection: View {
} }
} }
#endif #endif
} header: {
Text(String(localized: "settings.playback.video.header"))
} }
} }
} }
@@ -161,7 +156,7 @@ private struct AudioSection: View {
} }
var body: some View { var body: some View {
Section { SettingsFormSection("settings.playback.audio.header") {
Picker( Picker(
String(localized: "settings.playback.audio.preferredLanguage"), String(localized: "settings.playback.audio.preferredLanguage"),
selection: Binding( selection: Binding(
@@ -178,8 +173,6 @@ private struct AudioSection: View {
Text(language.name).tag(language.code) Text(language.name).tag(language.code)
} }
} }
} header: {
Text(String(localized: "settings.playback.audio.header"))
} }
} }
} }
@@ -205,7 +198,7 @@ private struct SubtitlesSection: View {
} }
var body: some View { var body: some View {
Section { SettingsFormSection("settings.playback.subtitles.header") {
Picker( Picker(
String(localized: "settings.playback.subtitles.preferredLanguage"), String(localized: "settings.playback.subtitles.preferredLanguage"),
selection: Binding( selection: Binding(
@@ -228,8 +221,6 @@ private struct SubtitlesSection: View {
} label: { } label: {
Label(String(localized: "settings.playback.subtitles.appearance"), systemImage: "textformat.size") Label(String(localized: "settings.playback.subtitles.appearance"), systemImage: "textformat.size")
} }
} header: {
Text(String(localized: "settings.playback.subtitles.header"))
} }
} }
} }
@@ -240,7 +231,29 @@ private struct BehaviorSection: View {
@Bindable var settings: SettingsManager @Bindable var settings: SettingsManager
var body: some View { var body: some View {
Section { #if os(tvOS)
let footer: LocalizedStringKey? = "settings.playback.tvOSMenuButtonClosesVideo.footer"
#else
let footer: LocalizedStringKey? = nil
#endif
SettingsFormSection("settings.playback.behavior.header", footer: footer) {
#if os(macOS)
PlatformMenuPicker(
String(localized: "settings.playback.macOS.playerMode"),
selection: $settings.macPlayerMode
) {
ForEach(MacPlayerMode.allCases, id: \.self) { mode in
Text(mode.displayName).tag(mode)
}
}
Toggle(
String(localized: "settings.playback.macOS.autoResizePlayer"),
isOn: $settings.playerSheetAutoResize
)
#endif
PlatformMenuPicker( PlatformMenuPicker(
String(localized: "settings.playback.resumeAction"), String(localized: "settings.playback.resumeAction"),
selection: $settings.resumeAction selection: $settings.resumeAction
@@ -263,14 +276,6 @@ private struct BehaviorSection: View {
isOn: $settings.tvOSMenuButtonClosesVideo isOn: $settings.tvOSMenuButtonClosesVideo
) )
#endif #endif
} header: {
Text(String(localized: "settings.playback.behavior.header"))
} footer: {
#if os(tvOS)
Text(String(localized: "settings.playback.tvOSMenuButtonClosesVideo.footer"))
#else
EmptyView()
#endif
} }
} }
} }
@@ -281,7 +286,7 @@ private struct QueueSection: View {
@Bindable var settings: SettingsManager @Bindable var settings: SettingsManager
var body: some View { var body: some View {
Section { SettingsFormSection("settings.playback.queue.header") {
Toggle( Toggle(
String(localized: "settings.playback.queue.enabled"), String(localized: "settings.playback.queue.enabled"),
isOn: $settings.queueEnabled isOn: $settings.queueEnabled
@@ -319,12 +324,6 @@ private struct QueueSection: View {
} }
.disabled(!settings.queueEnabled) .disabled(!settings.queueEnabled)
#endif #endif
} header: {
Text(String(localized: "settings.playback.queue.header"))
} footer: {
Text(String(localized: "settings.playback.queue.footer"))
} }
} }
} }
@@ -340,7 +339,7 @@ private struct OrientationSection: View {
} }
var body: some View { var body: some View {
Section { SettingsFormSection("settings.playback.orientation.header") {
Toggle( Toggle(
String(localized: "settings.playback.orientation.rotateToMatchAspectRatio"), String(localized: "settings.playback.orientation.rotateToMatchAspectRatio"),
isOn: $settings.rotateToMatchAspectRatio isOn: $settings.rotateToMatchAspectRatio
@@ -352,35 +351,6 @@ private struct OrientationSection: View {
isOn: $settings.preferPortraitBrowsing isOn: $settings.preferPortraitBrowsing
) )
} }
} header: {
Text(String(localized: "settings.playback.orientation.header"))
}
}
}
#endif
// MARK: - macOS Section
#if os(macOS)
private struct MacOSSection: View {
@Bindable var settings: SettingsManager
var body: some View {
Section(String(localized: "settings.playback.macOS.header")) {
Picker(
String(localized: "settings.playback.macOS.playerMode"),
selection: $settings.macPlayerMode
) {
ForEach(MacPlayerMode.allCases, id: \.self) { mode in
Text(mode.displayName).tag(mode)
}
}
.pickerStyle(.segmented)
Toggle(
String(localized: "settings.playback.macOS.autoResizePlayer"),
isOn: $settings.playerSheetAutoResize
)
} }
} }
} }

View File

@@ -41,33 +41,35 @@ struct SettingsView: View {
.navigationSplitViewColumnWidth(min: 180, ideal: 200) .navigationSplitViewColumnWidth(min: 180, ideal: 200)
} detail: { } detail: {
if appEnvironment != nil { if appEnvironment != nil {
Group { NavigationStack {
switch selectedSection { Group {
case .sources: switch selectedSection {
SourcesListView() case .sources:
case .icloud: SourcesListView()
iCloudSettingsView() case .icloud:
case .appearance: iCloudSettingsView()
AppearanceSettingsView() case .appearance:
case .layoutNavigation: AppearanceSettingsView()
LayoutNavigationSettingsView() case .layoutNavigation:
case .playback: LayoutNavigationSettingsView()
PlaybackSettingsView() case .playback:
case .notifications: PlaybackSettingsView()
NotificationSettingsView() case .notifications:
case .downloads: NotificationSettingsView()
DownloadSettingsView() case .downloads:
case .privacy: DownloadSettingsView()
PrivacySettingsView() case .privacy:
case .youtubeEnhancements: PrivacySettingsView()
YouTubeEnhancementsSettingsView() case .youtubeEnhancements:
case .advanced: YouTubeEnhancementsSettingsView()
AdvancedSettingsView() case .advanced:
case .about: AdvancedSettingsView()
AboutView() case .about:
case .none: AboutView()
Text(String(localized: "settings.placeholder.selectSection")) case .none:
.frame(maxWidth: .infinity, maxHeight: .infinity) Text(String(localized: "settings.placeholder.selectSection"))
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
} }
} }
} }

View File

@@ -12,7 +12,7 @@ struct SubtitlesSettingsView: View {
@State private var settings: SubtitleSettings = .default @State private var settings: SubtitleSettings = .default
var body: some View { var body: some View {
Form { SettingsFormContainer {
fontSection fontSection
colorsSection colorsSection
styleSection styleSection
@@ -36,7 +36,7 @@ struct SubtitlesSettingsView: View {
// MARK: - Font Section // MARK: - Font Section
private var fontSection: some View { private var fontSection: some View {
Section { SettingsFormSection {
PlatformMenuPicker( PlatformMenuPicker(
String(localized: "settings.subtitles.font"), String(localized: "settings.subtitles.font"),
selection: $settings.font selection: $settings.font
@@ -84,7 +84,7 @@ struct SubtitlesSettingsView: View {
// MARK: - Colors Section // MARK: - Colors Section
private var colorsSection: some View { private var colorsSection: some View {
Section { SettingsFormSection("settings.subtitles.colorsSection") {
#if os(tvOS) #if os(tvOS)
HStack { HStack {
Text(String(localized: "settings.subtitles.textColor")) Text(String(localized: "settings.subtitles.textColor"))
@@ -175,15 +175,13 @@ struct SubtitlesSettingsView: View {
) )
} }
#endif #endif
} header: {
Text(String(localized: "settings.subtitles.colorsSection"))
} }
} }
// MARK: - Style Section // MARK: - Style Section
private var styleSection: some View { private var styleSection: some View {
Section { SettingsFormSection("settings.subtitles.styleSection") {
Toggle( Toggle(
String(localized: "settings.subtitles.bold"), String(localized: "settings.subtitles.bold"),
isOn: $settings.isBold isOn: $settings.isBold
@@ -195,15 +193,13 @@ struct SubtitlesSettingsView: View {
isOn: $settings.isItalic isOn: $settings.isItalic
) )
.onChange(of: settings.isItalic) { _, _ in saveSettings() } .onChange(of: settings.isItalic) { _, _ in saveSettings() }
} header: {
Text(String(localized: "settings.subtitles.styleSection"))
} }
} }
// MARK: - Position Section // MARK: - Position Section
private var positionSection: some View { private var positionSection: some View {
Section { SettingsFormSection("settings.subtitles.positionSection", footer: "settings.subtitles.positionFooter") {
#if os(tvOS) #if os(tvOS)
LabeledContent( LabeledContent(
String(localized: "settings.subtitles.positionSection"), String(localized: "settings.subtitles.positionSection"),
@@ -218,17 +214,13 @@ struct SubtitlesSettingsView: View {
) )
.onChange(of: settings.bottomMargin) { _, _ in saveSettings() } .onChange(of: settings.bottomMargin) { _, _ in saveSettings() }
#endif #endif
} header: {
Text(String(localized: "settings.subtitles.positionSection"))
} footer: {
Text(String(localized: "settings.subtitles.positionFooter"))
} }
} }
// MARK: - Reset Section // MARK: - Reset Section
private var resetSection: some View { private var resetSection: some View {
Section { SettingsFormSection {
Button(role: .destructive) { Button(role: .destructive) {
settings = .default settings = .default
saveSettings() saveSettings()