mirror of
https://github.com/yattee/yattee.git
synced 2026-05-12 18:35:05 +00:00
Make tvOS detail dismiss button opt-in and unstick more views
TVSidebarDetailContainer now exposes a showsDismissButton flag instead of always attaching a Done toolbar item. The button is only enabled where a view can end up with no focusable element on its own — Device Capabilities (informational rows) and the Import Playlists/Subscriptions flows. Wrap Contributors, Translators, Acknowledgements, and Device Capabilities destinations in TVSidebarDetailContainer for the consistent sidebar look, and make the Translators/Acknowledgements rows focusable on tvOS by wrapping them in Buttons so the Menu remote button can pop the stack.
This commit is contained in:
@@ -9,22 +9,24 @@
|
|||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct TVSidebarDetailContainer<Content: View, BottomAction: View>: View {
|
struct TVSidebarDetailContainer<Content: View>: View {
|
||||||
let content: Content
|
let content: Content
|
||||||
let bottomAction: BottomAction
|
|
||||||
var systemImage: String?
|
var systemImage: String?
|
||||||
var title: String?
|
var title: String?
|
||||||
|
var showsDismissButton: Bool
|
||||||
|
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
init(
|
init(
|
||||||
systemImage: String? = nil,
|
systemImage: String? = nil,
|
||||||
title: String? = nil,
|
title: String? = nil,
|
||||||
@ViewBuilder bottomAction: () -> BottomAction = { EmptyView() },
|
showsDismissButton: Bool = false,
|
||||||
@ViewBuilder content: () -> Content
|
@ViewBuilder content: () -> Content
|
||||||
) {
|
) {
|
||||||
self.content = content()
|
self.content = content()
|
||||||
self.bottomAction = bottomAction()
|
|
||||||
self.systemImage = systemImage
|
self.systemImage = systemImage
|
||||||
self.title = title
|
self.title = title
|
||||||
|
self.showsDismissButton = showsDismissButton
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -32,30 +34,19 @@ struct TVSidebarDetailContainer<Content: View, BottomAction: View>: View {
|
|||||||
.focusSection()
|
.focusSection()
|
||||||
.safeAreaInset(edge: .leading) {
|
.safeAreaInset(edge: .leading) {
|
||||||
if let systemImage {
|
if let systemImage {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 16) {
|
||||||
Spacer()
|
Image(systemName: systemImage)
|
||||||
VStack(spacing: 16) {
|
.font(.system(size: 80))
|
||||||
Image(systemName: systemImage)
|
.foregroundStyle(.secondary)
|
||||||
.font(.system(size: 80))
|
if let title {
|
||||||
|
Text(title)
|
||||||
|
.font(.title3)
|
||||||
|
.fontWeight(.semibold)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
if let title {
|
.multilineTextAlignment(.center)
|
||||||
Text(title)
|
|
||||||
.font(.title3)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.allowsHitTesting(false)
|
|
||||||
|
|
||||||
if BottomAction.self != EmptyView.self {
|
|
||||||
bottomAction
|
|
||||||
.padding(.top, 40)
|
|
||||||
.focusSection()
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
|
.allowsHitTesting(false)
|
||||||
.frame(width: 400)
|
.frame(width: 400)
|
||||||
} else {
|
} else {
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -63,22 +54,17 @@ struct TVSidebarDetailContainer<Content: View, BottomAction: View>: View {
|
|||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.toolbar {
|
||||||
}
|
if showsDismissButton {
|
||||||
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
struct TVDismissBottomButton: View {
|
Button {
|
||||||
var title: String = String(localized: "common.done")
|
dismiss()
|
||||||
var systemImage: String = "chevron.backward"
|
} label: {
|
||||||
|
Label(String(localized: "common.done"), systemImage: "chevron.backward")
|
||||||
@Environment(\.dismiss) private var dismiss
|
}
|
||||||
|
}
|
||||||
var body: some View {
|
}
|
||||||
Button {
|
}
|
||||||
dismiss()
|
|
||||||
} label: {
|
|
||||||
Label(title, systemImage: systemImage)
|
|
||||||
}
|
|
||||||
.buttonStyle(TVSettingsButtonStyle())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -23,19 +23,34 @@ struct AboutView: View {
|
|||||||
SettingsFormSection {
|
SettingsFormSection {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
ContributorsView()
|
TVSidebarDetailContainer(
|
||||||
|
systemImage: "person.3",
|
||||||
|
title: String(localized: "settings.contributors.title")
|
||||||
|
) {
|
||||||
|
ContributorsView()
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label(String(localized: "settings.contributors.title"), systemImage: "person.3")
|
Label(String(localized: "settings.contributors.title"), systemImage: "person.3")
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
TranslationContributorsView()
|
TVSidebarDetailContainer(
|
||||||
|
systemImage: "globe",
|
||||||
|
title: String(localized: "settings.translators.title")
|
||||||
|
) {
|
||||||
|
TranslationContributorsView()
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label(String(localized: "settings.translators.title"), systemImage: "globe")
|
Label(String(localized: "settings.translators.title"), systemImage: "globe")
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
AcknowledgementsView()
|
TVSidebarDetailContainer(
|
||||||
|
systemImage: "heart.text.square",
|
||||||
|
title: String(localized: "settings.acknowledgements.title")
|
||||||
|
) {
|
||||||
|
AcknowledgementsView()
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label(String(localized: "settings.acknowledgements.title"), systemImage: "heart.text.square")
|
Label(String(localized: "settings.acknowledgements.title"), systemImage: "heart.text.square")
|
||||||
}
|
}
|
||||||
@@ -58,7 +73,13 @@ struct AboutView: View {
|
|||||||
SettingsFormSection {
|
SettingsFormSection {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
DeviceCapabilitiesView()
|
TVSidebarDetailContainer(
|
||||||
|
systemImage: "cpu",
|
||||||
|
title: String(localized: "settings.advanced.deviceCapabilities"),
|
||||||
|
showsDismissButton: true
|
||||||
|
) {
|
||||||
|
DeviceCapabilitiesView()
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label(String(localized: "settings.advanced.deviceCapabilities"), systemImage: "cpu")
|
Label(String(localized: "settings.advanced.deviceCapabilities"), systemImage: "cpu")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,9 +26,6 @@ struct AcknowledgementsView: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func dependencyLink(_ name: String, url: String) -> some View {
|
private func dependencyLink(_ name: String, url: String) -> some View {
|
||||||
#if os(tvOS)
|
|
||||||
Text(name)
|
|
||||||
#else
|
|
||||||
Button {
|
Button {
|
||||||
if let url = URL(string: url) {
|
if let url = URL(string: url) {
|
||||||
openURL(url)
|
openURL(url)
|
||||||
@@ -37,15 +34,16 @@ struct AcknowledgementsView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Text(name)
|
Text(name)
|
||||||
Spacer()
|
Spacer()
|
||||||
|
#if !os(tvOS)
|
||||||
Image(systemName: "arrow.up.right")
|
Image(systemName: "arrow.up.right")
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
#endif
|
#endif
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -242,7 +242,8 @@ private struct EditRemoteServerContent: View {
|
|||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
TVSidebarDetailContainer(
|
TVSidebarDetailContainer(
|
||||||
systemImage: "person.2",
|
systemImage: "person.2",
|
||||||
title: String(localized: "sources.import.subscriptions")
|
title: String(localized: "sources.import.subscriptions"),
|
||||||
|
showsDismissButton: true
|
||||||
) {
|
) {
|
||||||
ImportSubscriptionsView(instance: instance)
|
ImportSubscriptionsView(instance: instance)
|
||||||
}
|
}
|
||||||
@@ -258,7 +259,8 @@ private struct EditRemoteServerContent: View {
|
|||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
TVSidebarDetailContainer(
|
TVSidebarDetailContainer(
|
||||||
systemImage: "list.bullet.rectangle",
|
systemImage: "list.bullet.rectangle",
|
||||||
title: String(localized: "sources.import.playlists")
|
title: String(localized: "sources.import.playlists"),
|
||||||
|
showsDismissButton: true
|
||||||
) {
|
) {
|
||||||
ImportPlaylistsView(instance: instance)
|
ImportPlaylistsView(instance: instance)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ struct ImportPlaylistsView: View {
|
|||||||
let instance: Instance
|
let instance: Instance
|
||||||
|
|
||||||
@Environment(\.appEnvironment) private var appEnvironment
|
@Environment(\.appEnvironment) private var appEnvironment
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
|
|
||||||
@State private var playlists: [Playlist] = []
|
@State private var playlists: [Playlist] = []
|
||||||
@State private var importedPlaylistIDs: Set<String> = []
|
@State private var importedPlaylistIDs: Set<String> = []
|
||||||
@@ -60,15 +59,6 @@ struct ImportPlaylistsView: View {
|
|||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
#endif
|
#endif
|
||||||
.toolbar {
|
.toolbar {
|
||||||
#if os(tvOS)
|
|
||||||
ToolbarItem(placement: .cancellationAction) {
|
|
||||||
Button {
|
|
||||||
dismiss()
|
|
||||||
} label: {
|
|
||||||
Label(String(localized: "common.done"), systemImage: "chevron.backward")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
if !unimportedPlaylists.isEmpty && importingPlaylistID == nil {
|
if !unimportedPlaylists.isEmpty && importingPlaylistID == nil {
|
||||||
ToolbarItem(placement: .primaryAction) {
|
ToolbarItem(placement: .primaryAction) {
|
||||||
Button {
|
Button {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ struct ImportSubscriptionsView: View {
|
|||||||
let instance: Instance
|
let instance: Instance
|
||||||
|
|
||||||
@Environment(\.appEnvironment) private var appEnvironment
|
@Environment(\.appEnvironment) private var appEnvironment
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
|
|
||||||
@State private var channels: [Channel] = []
|
@State private var channels: [Channel] = []
|
||||||
@State private var subscribedChannelIDs: Set<String> = []
|
@State private var subscribedChannelIDs: Set<String> = []
|
||||||
@@ -51,15 +50,6 @@ struct ImportSubscriptionsView: View {
|
|||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
#endif
|
#endif
|
||||||
.toolbar {
|
.toolbar {
|
||||||
#if os(tvOS)
|
|
||||||
ToolbarItem(placement: .cancellationAction) {
|
|
||||||
Button {
|
|
||||||
dismiss()
|
|
||||||
} label: {
|
|
||||||
Label(String(localized: "common.done"), systemImage: "chevron.backward")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
if !unsubscribedChannels.isEmpty {
|
if !unsubscribedChannels.isEmpty {
|
||||||
ToolbarItem(placement: .primaryAction) {
|
ToolbarItem(placement: .primaryAction) {
|
||||||
Button {
|
Button {
|
||||||
|
|||||||
@@ -47,6 +47,16 @@ struct TranslationContributorsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func contributorRow(_ contributor: TranslationContributor) -> some View {
|
private func contributorRow(_ contributor: TranslationContributor) -> some View {
|
||||||
|
#if os(tvOS)
|
||||||
|
Button {} label: {
|
||||||
|
contributorRowContent(contributor)
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
contributorRowContent(contributor)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private func contributorRowContent(_ contributor: TranslationContributor) -> some View {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
// Avatar
|
// Avatar
|
||||||
LazyImage(url: contributor.gravatarURL) { state in
|
LazyImage(url: contributor.gravatarURL) { state in
|
||||||
|
|||||||
Reference in New Issue
Block a user