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" : {
"comment" : "macOS-specific playback settings section header",
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -12064,6 +12065,7 @@
},
"settings.playback.queue.footer" : {
"comment" : "Queue section footer",
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -17730,16 +17732,6 @@
}
}
},
"viewOptions.showSidebar" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Show Sidebar"
}
}
}
},
"viewOptions.layout" : {
"localizations" : {
"en" : {
@@ -17830,6 +17822,16 @@
}
}
},
"viewOptions.showSidebar" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Show Sidebar"
}
}
}
},
"viewOptions.title" : {
"comment" : "View Options sheet title",
"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
var body: some View {
Form {
SettingsFormContainer {
if let settings = appEnvironment?.settingsManager {
QualitySection(settings: settings)
AudioSection(settings: settings)
@@ -21,9 +21,6 @@ struct PlaybackSettingsView: View {
#if os(iOS)
OrientationSection(settings: settings)
#endif
#if os(macOS)
MacOSSection(settings: settings)
#endif
}
}
#if !os(tvOS)
@@ -41,7 +38,7 @@ private struct QualitySection: View {
@Bindable var settings: SettingsManager
var body: some View {
Section {
SettingsFormSection("settings.playback.video.header") {
PlatformMenuPicker(
String(localized: "settings.playback.quality.preferred"),
selection: $settings.preferredQuality
@@ -61,8 +58,6 @@ private struct QualitySection: View {
}
}
#endif
} header: {
Text(String(localized: "settings.playback.video.header"))
}
}
}
@@ -161,7 +156,7 @@ private struct AudioSection: View {
}
var body: some View {
Section {
SettingsFormSection("settings.playback.audio.header") {
Picker(
String(localized: "settings.playback.audio.preferredLanguage"),
selection: Binding(
@@ -178,8 +173,6 @@ private struct AudioSection: View {
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 {
Section {
SettingsFormSection("settings.playback.subtitles.header") {
Picker(
String(localized: "settings.playback.subtitles.preferredLanguage"),
selection: Binding(
@@ -228,8 +221,6 @@ private struct SubtitlesSection: View {
} label: {
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
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(
String(localized: "settings.playback.resumeAction"),
selection: $settings.resumeAction
@@ -263,14 +276,6 @@ private struct BehaviorSection: View {
isOn: $settings.tvOSMenuButtonClosesVideo
)
#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
var body: some View {
Section {
SettingsFormSection("settings.playback.queue.header") {
Toggle(
String(localized: "settings.playback.queue.enabled"),
isOn: $settings.queueEnabled
@@ -319,12 +324,6 @@ private struct QueueSection: View {
}
.disabled(!settings.queueEnabled)
#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 {
Section {
SettingsFormSection("settings.playback.orientation.header") {
Toggle(
String(localized: "settings.playback.orientation.rotateToMatchAspectRatio"),
isOn: $settings.rotateToMatchAspectRatio
@@ -352,35 +351,6 @@ private struct OrientationSection: View {
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)
} detail: {
if appEnvironment != nil {
Group {
switch selectedSection {
case .sources:
SourcesListView()
case .icloud:
iCloudSettingsView()
case .appearance:
AppearanceSettingsView()
case .layoutNavigation:
LayoutNavigationSettingsView()
case .playback:
PlaybackSettingsView()
case .notifications:
NotificationSettingsView()
case .downloads:
DownloadSettingsView()
case .privacy:
PrivacySettingsView()
case .youtubeEnhancements:
YouTubeEnhancementsSettingsView()
case .advanced:
AdvancedSettingsView()
case .about:
AboutView()
case .none:
Text(String(localized: "settings.placeholder.selectSection"))
.frame(maxWidth: .infinity, maxHeight: .infinity)
NavigationStack {
Group {
switch selectedSection {
case .sources:
SourcesListView()
case .icloud:
iCloudSettingsView()
case .appearance:
AppearanceSettingsView()
case .layoutNavigation:
LayoutNavigationSettingsView()
case .playback:
PlaybackSettingsView()
case .notifications:
NotificationSettingsView()
case .downloads:
DownloadSettingsView()
case .privacy:
PrivacySettingsView()
case .youtubeEnhancements:
YouTubeEnhancementsSettingsView()
case .advanced:
AdvancedSettingsView()
case .about:
AboutView()
case .none:
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
var body: some View {
Form {
SettingsFormContainer {
fontSection
colorsSection
styleSection
@@ -36,7 +36,7 @@ struct SubtitlesSettingsView: View {
// MARK: - Font Section
private var fontSection: some View {
Section {
SettingsFormSection {
PlatformMenuPicker(
String(localized: "settings.subtitles.font"),
selection: $settings.font
@@ -84,7 +84,7 @@ struct SubtitlesSettingsView: View {
// MARK: - Colors Section
private var colorsSection: some View {
Section {
SettingsFormSection("settings.subtitles.colorsSection") {
#if os(tvOS)
HStack {
Text(String(localized: "settings.subtitles.textColor"))
@@ -175,15 +175,13 @@ struct SubtitlesSettingsView: View {
)
}
#endif
} header: {
Text(String(localized: "settings.subtitles.colorsSection"))
}
}
// MARK: - Style Section
private var styleSection: some View {
Section {
SettingsFormSection("settings.subtitles.styleSection") {
Toggle(
String(localized: "settings.subtitles.bold"),
isOn: $settings.isBold
@@ -195,15 +193,13 @@ struct SubtitlesSettingsView: View {
isOn: $settings.isItalic
)
.onChange(of: settings.isItalic) { _, _ in saveSettings() }
} header: {
Text(String(localized: "settings.subtitles.styleSection"))
}
}
// MARK: - Position Section
private var positionSection: some View {
Section {
SettingsFormSection("settings.subtitles.positionSection", footer: "settings.subtitles.positionFooter") {
#if os(tvOS)
LabeledContent(
String(localized: "settings.subtitles.positionSection"),
@@ -218,17 +214,13 @@ struct SubtitlesSettingsView: View {
)
.onChange(of: settings.bottomMargin) { _, _ in saveSettings() }
#endif
} header: {
Text(String(localized: "settings.subtitles.positionSection"))
} footer: {
Text(String(localized: "settings.subtitles.positionFooter"))
}
}
// MARK: - Reset Section
private var resetSection: some View {
Section {
SettingsFormSection {
Button(role: .destructive) {
settings = .default
saveSettings()