diff --git a/Yattee/Localizable.xcstrings b/Yattee/Localizable.xcstrings index 6f882446..3d19040f 100644 --- a/Yattee/Localizable.xcstrings +++ b/Yattee/Localizable.xcstrings @@ -3624,6 +3624,39 @@ } } }, + "home.tvos.empty.title" : { + "comment" : "tvOS Home empty-state title shown on fresh install", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Welcome to Yattee" + } + } + } + }, + "home.tvos.empty.message" : { + "comment" : "tvOS Home empty-state message shown on fresh install", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add a media source to start watching." + } + } + } + }, + "home.tvos.empty.openSources" : { + "comment" : "tvOS Home empty-state button that opens the Sources screen", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open Sources" + } + } + } + }, "home.history.clear" : { "comment" : "Clear history button", "localizations" : { diff --git a/Yattee/Views/Home/HomeView.swift b/Yattee/Views/Home/HomeView.swift index 649bc2bb..84c2aed9 100644 --- a/Yattee/Views/Home/HomeView.swift +++ b/Yattee/Views/Home/HomeView.swift @@ -166,12 +166,63 @@ struct HomeView: View { // MARK: - Main Content + #if os(tvOS) + /// True on tvOS when no Home section would render content — prevents a focus trap + /// on fresh installs where the detail pane would otherwise be completely empty. + private var isHomeEmpty: Bool { + let sections = settingsManager?.visibleSections() ?? HomeSectionItem.defaultOrder.filter { HomeSectionItem.defaultVisibility[$0] == true } + for section in sections { + switch section { + case .continueWatching: + if !recentContinueWatching.isEmpty { return false } + case .feed: + if !feedCache.videos.isEmpty { return false } + case .bookmarks: + if !recentBookmarks.isEmpty { return false } + case .history: + if !recentHistory.isEmpty { return false } + case .downloads: + break + case .instanceContent, .mediaSource: + return false + } + } + return true + } + + private var emptyHomeView: some View { + VStack(spacing: 32) { + VStack(spacing: 12) { + Text(String(localized: "home.tvos.empty.title")) + .font(.title2) + .fontWeight(.semibold) + Text(String(localized: "home.tvos.empty.message")) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + } + Button { + appEnvironment?.navigationCoordinator.selectedSidebarItem = .sources + } label: { + Label(String(localized: "home.tvos.empty.openSources"), systemImage: "externaldrive.connected.to.line.below") + .padding(.horizontal, 16) + .padding(.vertical, 8) + } + } + .padding(60) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + #endif + @ViewBuilder private var mainContent: some View { #if os(tvOS) - ScrollView { - LazyVStack(spacing: 0) { - homeContent + if isHomeEmpty { + emptyHomeView + } else { + ScrollView { + LazyVStack(spacing: 0) { + homeContent + } } } #else diff --git a/Yattee/Views/Navigation/UnifiedTabView.swift b/Yattee/Views/Navigation/UnifiedTabView.swift index ac0543be..d1fe0ed8 100644 --- a/Yattee/Views/Navigation/UnifiedTabView.swift +++ b/Yattee/Views/Navigation/UnifiedTabView.swift @@ -664,6 +664,11 @@ struct UnifiedTabView: View { .onChange(of: navigationCoordinator?.pendingNavigation) { _, newValue in handlePendingNavigation(newValue) } + .onChange(of: navigationCoordinator?.selectedSidebarItem) { _, newItem in + guard let item = newItem else { return } + selection = item + navigationCoordinator?.selectedSidebarItem = nil + } } /// Applies the configured startup tab on first appearance.