mirror of
https://github.com/yattee/yattee.git
synced 2026-05-12 10:25:02 +00:00
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:
@@ -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" : {
|
||||||
|
|||||||
135
Yattee/Views/Settings/MacOSSettings.swift
Normal file
135
Yattee/Views/Settings/MacOSSettings.swift
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ struct SettingsView: View {
|
|||||||
.navigationSplitViewColumnWidth(min: 180, ideal: 200)
|
.navigationSplitViewColumnWidth(min: 180, ideal: 200)
|
||||||
} detail: {
|
} detail: {
|
||||||
if appEnvironment != nil {
|
if appEnvironment != nil {
|
||||||
|
NavigationStack {
|
||||||
Group {
|
Group {
|
||||||
switch selectedSection {
|
switch selectedSection {
|
||||||
case .sources:
|
case .sources:
|
||||||
@@ -73,6 +74,7 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// MARK: - tvOS Settings
|
// MARK: - tvOS Settings
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user