Rework tvOS sources list with sidebar and full-screen add flow

Give TVSidebarDetailContainer an optional bottom action slot and use it to
show the Add Source button beside the sources list on tvOS. Switch the
Settings > Sources list from a focus-capturing List to the same
ScrollView+LazyVStack layout MediaSourcesView already uses, drop
.buttonStyle(.card) so row icons no longer clip, and bump the row
icon-to-title spacing to 24pt. Replace the sheet-based Add/Edit flow in
MediaSourcesView with navigationDestinations wrapped in the sidebar
container, and decorate each Add Source form (WebDAV, SMB, remote server,
PeerTube browse) with its own sidebar icon and title.
This commit is contained in:
Arkadiusz Fal
2026-04-17 00:27:24 +02:00
parent 10a27a8105
commit 6d3bea7678
5 changed files with 182 additions and 78 deletions

View File

@@ -9,37 +9,54 @@
#if os(tvOS)
import SwiftUI
struct TVSidebarDetailContainer<Content: View>: View {
struct TVSidebarDetailContainer<Content: View, BottomAction: View>: View {
let content: Content
let bottomAction: BottomAction
var systemImage: String?
var title: String?
init(systemImage: String? = nil, title: String? = nil, @ViewBuilder content: () -> Content) {
init(
systemImage: String? = nil,
title: String? = nil,
@ViewBuilder bottomAction: () -> BottomAction = { EmptyView() },
@ViewBuilder content: () -> Content
) {
self.content = content()
self.bottomAction = bottomAction()
self.systemImage = systemImage
self.title = title
}
var body: some View {
content
.focusSection()
.safeAreaInset(edge: .leading) {
if let systemImage {
VStack(spacing: 16) {
VStack(spacing: 0) {
Spacer()
Image(systemName: systemImage)
.font(.system(size: 80))
.foregroundStyle(.secondary)
if let title {
Text(title)
.font(.title3)
.fontWeight(.semibold)
VStack(spacing: 16) {
Image(systemName: systemImage)
.font(.system(size: 80))
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
if let title {
Text(title)
.font(.title3)
.fontWeight(.semibold)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
}
.allowsHitTesting(false)
if BottomAction.self != EmptyView.self {
bottomAction
.padding(.top, 40)
.focusSection()
}
Spacer()
}
.frame(width: 400)
.allowsHitTesting(false)
} else {
Spacer()
.frame(width: 400)