diff --git a/Yattee/Localizable.xcstrings b/Yattee/Localizable.xcstrings index ccf8d67d..7ef6e12d 100644 --- a/Yattee/Localizable.xcstrings +++ b/Yattee/Localizable.xcstrings @@ -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" : { diff --git a/Yattee/Views/Settings/MacOSSettings.swift b/Yattee/Views/Settings/MacOSSettings.swift new file mode 100644 index 00000000..d8955c01 --- /dev/null +++ b/Yattee/Views/Settings/MacOSSettings.swift @@ -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: 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: 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 +} diff --git a/Yattee/Views/Settings/PlaybackSettingsView.swift b/Yattee/Views/Settings/PlaybackSettingsView.swift index 676dfbd6..64937512 100644 --- a/Yattee/Views/Settings/PlaybackSettingsView.swift +++ b/Yattee/Views/Settings/PlaybackSettingsView.swift @@ -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 - ) } } } diff --git a/Yattee/Views/Settings/SettingsView.swift b/Yattee/Views/Settings/SettingsView.swift index 752f3510..3dc14434 100644 --- a/Yattee/Views/Settings/SettingsView.swift +++ b/Yattee/Views/Settings/SettingsView.swift @@ -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) + } } } } diff --git a/Yattee/Views/Settings/SubtitlesSettingsView.swift b/Yattee/Views/Settings/SubtitlesSettingsView.swift index 19c60d2b..e5584fba 100644 --- a/Yattee/Views/Settings/SubtitlesSettingsView.swift +++ b/Yattee/Views/Settings/SubtitlesSettingsView.swift @@ -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()