mirror of
https://github.com/yattee/yattee.git
synced 2026-05-12 10:25:02 +00:00
Improve tvOS settings layout: use navigation instead of sheets, fix focus clipping
- Replace sheets with navigationDestination for Add/Edit Source on tvOS (tvOS sheets have fixed size that doesn't fit the content) - Fix focused cell clipping by replacing TVSettingsContainer's frame-based layout with safeAreaInset, matching the main settings view pattern - Use standard List with .listStyle(.grouped) for Sources on tvOS - Add sidebar icons and titles to TVSettingsContainer for all settings subviews, utilizing the left column space - Remove redundant large navigation titles on tvOS (shown in sidebar) - Move Edit Source Save button from toolbar into form above Delete button for better tvOS focus navigation
This commit is contained in:
@@ -51,7 +51,9 @@ struct AboutView: View {
|
||||
versionInfoSection
|
||||
mpvInfoSection
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.navigationTitle(String(localized: "settings.about.title"))
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
|
||||
@@ -25,43 +25,28 @@ struct AddSourceView: View {
|
||||
@State private var discoveredAllowInvalidCerts = false
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
#if os(tvOS)
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Button(String(localized: "common.cancel")) {
|
||||
dismiss()
|
||||
}
|
||||
.buttonStyle(TVToolbarButtonStyle())
|
||||
Spacer()
|
||||
Text(String(localized: "sources.newSource"))
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
Spacer()
|
||||
Text(String(localized: "common.cancel"))
|
||||
.opacity(0)
|
||||
}
|
||||
.padding(.horizontal, 48)
|
||||
.padding(.vertical, 24)
|
||||
|
||||
listContent
|
||||
}
|
||||
#if os(tvOS)
|
||||
listContent
|
||||
.navigationDestination(isPresented: $navigateToWebDAV) {
|
||||
AddWebDAVView(
|
||||
prefillURL: discoveredWebDAVURL,
|
||||
prefillName: discoveredName,
|
||||
prefillAllowInvalidCertificates: discoveredAllowInvalidCerts,
|
||||
dismissSheet: dismiss
|
||||
prefillAllowInvalidCertificates: discoveredAllowInvalidCerts
|
||||
)
|
||||
}
|
||||
.navigationDestination(isPresented: $navigateToSMB) {
|
||||
AddSMBView(
|
||||
prefillServer: discoveredSMBServer,
|
||||
prefillName: discoveredName,
|
||||
dismissSheet: dismiss
|
||||
prefillName: discoveredName
|
||||
)
|
||||
}
|
||||
#else
|
||||
.sheet(isPresented: $showingNetworkDiscovery) {
|
||||
NetworkShareDiscoverySheet(filterType: selectedShareType) { share in
|
||||
handleSelectedShare(share)
|
||||
}
|
||||
}
|
||||
#else
|
||||
NavigationStack {
|
||||
listContent
|
||||
.navigationTitle(String(localized: "sources.newSource"))
|
||||
#if os(iOS)
|
||||
@@ -92,13 +77,13 @@ struct AddSourceView: View {
|
||||
dismissSheet: dismiss
|
||||
)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.sheet(isPresented: $showingNetworkDiscovery) {
|
||||
NetworkShareDiscoverySheet(filterType: selectedShareType) { share in
|
||||
handleSelectedShare(share)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private var listContent: some View {
|
||||
@@ -113,19 +98,31 @@ struct AddSourceView: View {
|
||||
#endif
|
||||
|
||||
NavigationLink {
|
||||
#if os(tvOS)
|
||||
AddWebDAVView()
|
||||
#else
|
||||
AddWebDAVView(dismissSheet: dismiss)
|
||||
#endif
|
||||
} label: {
|
||||
Label(String(localized: "sources.addWebDAV"), systemImage: "externaldrive.connected.to.line.below")
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
#if os(tvOS)
|
||||
AddSMBView()
|
||||
#else
|
||||
AddSMBView(dismissSheet: dismiss)
|
||||
#endif
|
||||
} label: {
|
||||
Label(String(localized: "sources.addSMB"), systemImage: "server.rack")
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
#if os(tvOS)
|
||||
AddRemoteServerView()
|
||||
#else
|
||||
AddRemoteServerView(dismissSheet: dismiss)
|
||||
#endif
|
||||
} label: {
|
||||
Label(String(localized: "sources.addRemoteServer"), systemImage: "globe")
|
||||
}
|
||||
|
||||
@@ -35,7 +35,9 @@ struct AdvancedSettingsView: View {
|
||||
#endif
|
||||
developerSection
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.navigationTitle(String(localized: "settings.advanced.title"))
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
|
||||
@@ -35,7 +35,9 @@ struct AppearanceSettingsView: View {
|
||||
ThumbnailSection(settings: settings)
|
||||
}
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.navigationTitle(String(localized: "settings.appearance.title"))
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
|
||||
@@ -77,31 +77,11 @@ private struct EditRemoteServerContent: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
#if os(tvOS)
|
||||
formContent
|
||||
.accessibilityIdentifier("editSource.view")
|
||||
#else
|
||||
NavigationStack {
|
||||
#if os(tvOS)
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Button(String(localized: "common.cancel")) {
|
||||
dismiss()
|
||||
}
|
||||
.buttonStyle(TVToolbarButtonStyle())
|
||||
Spacer()
|
||||
Text(String(localized: "sources.editSource"))
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
Spacer()
|
||||
Button(String(localized: "common.save")) {
|
||||
saveChanges()
|
||||
}
|
||||
.buttonStyle(TVToolbarButtonStyle())
|
||||
}
|
||||
.padding(.horizontal, 48)
|
||||
.padding(.vertical, 24)
|
||||
|
||||
formContent
|
||||
.accessibilityIdentifier("editSource.view")
|
||||
}
|
||||
#else
|
||||
formContent
|
||||
.navigationTitle(String(localized: "sources.editSource"))
|
||||
#if os(iOS)
|
||||
@@ -120,8 +100,8 @@ private struct EditRemoteServerContent: View {
|
||||
}
|
||||
}
|
||||
.accessibilityIdentifier("editSource.view")
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private var formContent: some View {
|
||||
@@ -313,6 +293,17 @@ private struct EditRemoteServerContent: View {
|
||||
testResultSection(result)
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
Section {
|
||||
Button {
|
||||
saveChanges()
|
||||
} label: {
|
||||
Label(String(localized: "common.save"), systemImage: "checkmark.circle")
|
||||
}
|
||||
.buttonStyle(TVSettingsButtonStyle())
|
||||
}
|
||||
#endif
|
||||
|
||||
Section {
|
||||
Button(role: .destructive) {
|
||||
showingDeleteConfirmation = true
|
||||
@@ -529,35 +520,14 @@ private struct EditFileSourceContent: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
#if os(tvOS)
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Button(String(localized: "common.cancel")) {
|
||||
dismiss()
|
||||
}
|
||||
.buttonStyle(TVToolbarButtonStyle())
|
||||
Spacer()
|
||||
Text(String(localized: "sources.editSource"))
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
Spacer()
|
||||
Button(String(localized: "common.save")) {
|
||||
saveChanges()
|
||||
}
|
||||
.disabled(name.isEmpty)
|
||||
.buttonStyle(TVToolbarButtonStyle())
|
||||
}
|
||||
.padding(.horizontal, 48)
|
||||
.padding(.vertical, 24)
|
||||
|
||||
formContent
|
||||
}
|
||||
#if os(tvOS)
|
||||
formContent
|
||||
.onAppear {
|
||||
hasExistingPassword = appEnvironment?.mediaSourcesManager.password(for: source) != nil
|
||||
smbProtocolVersion = source.smbProtocolVersion ?? .auto
|
||||
}
|
||||
#else
|
||||
#else
|
||||
NavigationStack {
|
||||
formContent
|
||||
.navigationTitle(String(localized: "sources.editSource"))
|
||||
#if os(iOS)
|
||||
@@ -581,8 +551,8 @@ private struct EditFileSourceContent: View {
|
||||
hasExistingPassword = appEnvironment?.mediaSourcesManager.password(for: source) != nil
|
||||
smbProtocolVersion = source.smbProtocolVersion ?? .auto
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private var formContent: some View {
|
||||
@@ -718,6 +688,18 @@ private struct EditFileSourceContent: View {
|
||||
}
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
Section {
|
||||
Button {
|
||||
saveChanges()
|
||||
} label: {
|
||||
Label(String(localized: "common.save"), systemImage: "checkmark.circle")
|
||||
}
|
||||
.disabled(name.isEmpty)
|
||||
.buttonStyle(TVSettingsButtonStyle())
|
||||
}
|
||||
#endif
|
||||
|
||||
Section {
|
||||
Button(role: .destructive) {
|
||||
showingDeleteConfirmation = true
|
||||
|
||||
@@ -30,7 +30,9 @@ struct LayoutNavigationSettingsView: View {
|
||||
HandoffSection(settings: settings)
|
||||
}
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.navigationTitle(String(localized: "settings.layoutNavigation.title"))
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
|
||||
@@ -26,7 +26,9 @@ struct PlaybackSettingsView: View {
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.navigationTitle(String(localized: "settings.playback.title"))
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
|
||||
@@ -19,7 +19,9 @@ struct PrivacySettingsView: View {
|
||||
historySection
|
||||
searchSection
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.navigationTitle(String(localized: "settings.privacy.title"))
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
|
||||
@@ -21,6 +21,8 @@ struct SettingsView: View {
|
||||
#if os(macOS)
|
||||
macOSSettings
|
||||
.frame(minWidth: 600, minHeight: 400)
|
||||
#elseif os(tvOS)
|
||||
tvOSSettings
|
||||
#else
|
||||
iOSSettings
|
||||
#endif
|
||||
@@ -83,9 +85,107 @@ struct SettingsView: View {
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - iOS/tvOS Settings
|
||||
// MARK: - tvOS Settings
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
#if os(tvOS)
|
||||
private var tvOSSettings: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
if let appEnvironment {
|
||||
NavigationLink {
|
||||
TVSettingsContainer(systemImage: "server.rack", title: String(localized: "sources.title")) { SourcesListView() }
|
||||
} label: {
|
||||
HStack {
|
||||
Label(String(localized: "sources.title"), systemImage: "server.rack")
|
||||
Spacer()
|
||||
if appEnvironment.mediaSourcesManager.hasSourcesNeedingPassword {
|
||||
Image(systemName: "exclamationmark.circle.fill")
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
}
|
||||
}
|
||||
.accessibilityIdentifier("settings.row.sources")
|
||||
|
||||
NavigationLink {
|
||||
TVSettingsContainer(systemImage: "icloud", title: String(localized: "settings.icloud.title")) { iCloudSettingsView() }
|
||||
} label: {
|
||||
HStack {
|
||||
Label(String(localized: "settings.icloud.title"), systemImage: "icloud")
|
||||
#if DEBUG
|
||||
Spacer()
|
||||
Text(String(localized: "settings.icloud.dev.badge"))
|
||||
.font(.caption2.bold())
|
||||
.foregroundStyle(.white)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(.orange, in: Capsule())
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink { TVSettingsContainer(systemImage: "paintbrush", title: String(localized: "settings.appearance.sectionTitle")) { AppearanceSettingsView() } } label: {
|
||||
Label(String(localized: "settings.appearance.sectionTitle"), systemImage: "paintbrush")
|
||||
}
|
||||
|
||||
NavigationLink { TVSettingsContainer(systemImage: "hand.tap", title: String(localized: "settings.layoutNavigation.title")) { LayoutNavigationSettingsView() } } label: {
|
||||
Label(String(localized: "settings.layoutNavigation.title"), systemImage: "hand.tap")
|
||||
}
|
||||
|
||||
NavigationLink { TVSettingsContainer(systemImage: "play.circle", title: String(localized: "settings.playback.sectionTitle")) { PlaybackSettingsView() } } label: {
|
||||
Label(String(localized: "settings.playback.sectionTitle"), systemImage: "play.circle")
|
||||
}
|
||||
|
||||
NavigationLink { TVSettingsContainer(systemImage: "hand.raised", title: String(localized: "settings.privacy.title")) { PrivacySettingsView() } } label: {
|
||||
Label(String(localized: "settings.privacy.title"), systemImage: "hand.raised")
|
||||
}
|
||||
|
||||
NavigationLink { TVSettingsContainer(systemImage: "gearshape.2", title: String(localized: "settings.advanced.title")) { AdvancedSettingsView() } } label: {
|
||||
Label(String(localized: "settings.advanced.title"), systemImage: "gearshape.2")
|
||||
}
|
||||
|
||||
if appEnvironment.instancesManager.enabledInstances.contains(where: \.isYouTubeInstance) {
|
||||
NavigationLink { TVSettingsContainer(systemImage: "play.rectangle", title: String(localized: "settings.youtubeEnhancements.title")) { YouTubeEnhancementsSettingsView() } } label: {
|
||||
Label(String(localized: "settings.youtubeEnhancements.title"), systemImage: "play.rectangle")
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink { TVSettingsContainer(systemImage: "info.circle", title: String(localized: "settings.about.title")) { AboutView() } } label: {
|
||||
Label(String(localized: "settings.about.title"), systemImage: "info.circle")
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.grouped)
|
||||
.safeAreaInset(edge: .leading) {
|
||||
VStack(spacing: 20) {
|
||||
Spacer()
|
||||
|
||||
Image("AppIconPreview")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 200, height: 200)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 46))
|
||||
|
||||
Text(verbatim: "Yattee")
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
Text("\(appVersion) (\(buildNumber))")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: 400)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
.accessibilityIdentifier("settings.view")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - iOS Settings
|
||||
|
||||
#if os(iOS)
|
||||
private var iOSSettings: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
@@ -206,12 +306,8 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.navigationTitle(String(localized: "settings.title"))
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
.toolbar {
|
||||
if showCloseButton {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
@@ -288,6 +384,50 @@ enum SettingsSection: String, CaseIterable, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - tvOS Settings Container
|
||||
|
||||
#if os(tvOS)
|
||||
struct TVSettingsContainer<Content: View>: View {
|
||||
let content: Content
|
||||
var systemImage: String?
|
||||
var title: String?
|
||||
|
||||
init(systemImage: String? = nil, title: String? = nil, @ViewBuilder content: () -> Content) {
|
||||
self.content = content()
|
||||
self.systemImage = systemImage
|
||||
self.title = title
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
content
|
||||
.safeAreaInset(edge: .leading) {
|
||||
if let systemImage {
|
||||
VStack(spacing: 16) {
|
||||
Spacer()
|
||||
Image(systemName: systemImage)
|
||||
.font(.system(size: 80))
|
||||
.foregroundStyle(.secondary)
|
||||
if let title {
|
||||
Text(title)
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: 400)
|
||||
.allowsHitTesting(false)
|
||||
} else {
|
||||
Spacer()
|
||||
.frame(width: 400)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#Preview {
|
||||
SettingsView()
|
||||
.appEnvironment(.preview)
|
||||
|
||||
@@ -42,7 +42,9 @@ struct SourcesListView: View {
|
||||
sourcesList
|
||||
}
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.navigationTitle(String(localized: "sources.title"))
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
@@ -56,12 +58,24 @@ struct SourcesListView: View {
|
||||
.accessibilityIdentifier("sources.addButton")
|
||||
}
|
||||
}
|
||||
#if os(tvOS)
|
||||
.navigationDestination(isPresented: $showingAddSheet) {
|
||||
TVSettingsContainer(systemImage: "plus.circle", title: String(localized: "sources.newSource")) { AddSourceView() }
|
||||
}
|
||||
#else
|
||||
.sheet(isPresented: $showingAddSheet) {
|
||||
AddSourceView()
|
||||
}
|
||||
#endif
|
||||
#if os(tvOS)
|
||||
.navigationDestination(item: $sourceToEdit) { source in
|
||||
TVSettingsContainer(systemImage: "pencil.circle", title: String(localized: "sources.editSource")) { EditSourceView(source: source) }
|
||||
}
|
||||
#else
|
||||
.sheet(item: $sourceToEdit) { source in
|
||||
EditSourceView(source: source)
|
||||
}
|
||||
#endif
|
||||
.confirmationDialog(
|
||||
deleteConfirmationMessage,
|
||||
isPresented: $showingDeleteConfirmation,
|
||||
@@ -98,6 +112,37 @@ struct SourcesListView: View {
|
||||
// MARK: - Sources List
|
||||
|
||||
private var sourcesList: some View {
|
||||
#if os(tvOS)
|
||||
List {
|
||||
if let manager = instancesManager, !manager.instances.isEmpty {
|
||||
Section(String(localized: "sources.section.remoteServers")) {
|
||||
let instances = manager.instances.sorted { $0.dateAdded < $1.dateAdded }
|
||||
ForEach(instances) { instance in
|
||||
Button {
|
||||
sourceToEdit = .remoteServer(instance)
|
||||
} label: {
|
||||
instanceRow(instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let allFileSources = allMediaSources
|
||||
if !allFileSources.isEmpty {
|
||||
Section(String(localized: "sources.section.fileSources")) {
|
||||
ForEach(allFileSources) { source in
|
||||
let needsPassword = mediaSourcesManager?.needsPassword(for: source) ?? false
|
||||
Button {
|
||||
sourceToEdit = .fileSource(source)
|
||||
} label: {
|
||||
mediaSourceRow(source, needsPassword: needsPassword)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.grouped)
|
||||
#else
|
||||
(listStyle == .inset ? ListBackgroundStyle.grouped.color : ListBackgroundStyle.plain.color)
|
||||
.ignoresSafeArea()
|
||||
.overlay(
|
||||
@@ -108,6 +153,7 @@ struct SourcesListView: View {
|
||||
}
|
||||
}
|
||||
)
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Section Header
|
||||
@@ -131,9 +177,13 @@ struct SourcesListView: View {
|
||||
LazyVStack(spacing: 0) {
|
||||
content()
|
||||
}
|
||||
#if os(tvOS)
|
||||
.padding(.horizontal, 16)
|
||||
#else
|
||||
.background(ListBackgroundStyle.card.color)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.padding(.horizontal, 16)
|
||||
#endif
|
||||
.padding(.bottom, 16)
|
||||
} else {
|
||||
LazyVStack(spacing: 0) {
|
||||
|
||||
@@ -18,7 +18,9 @@ struct YouTubeEnhancementsSettingsView: View {
|
||||
DeArrowSection(settings: settings)
|
||||
}
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.navigationTitle(String(localized: "settings.youtubeEnhancements.title"))
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
|
||||
@@ -82,7 +82,9 @@ struct iCloudSettingsView: View {
|
||||
syncStatusSection
|
||||
}
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.navigationTitle(String(localized: "settings.icloud.title"))
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user