Add channel video search on tvOS

Surface the existing in-channel search feature on tvOS as a final tab
after Playlists. Selecting it reveals a search field and results area
below the tab row, reusing the same API and result views as iOS/macOS.
This commit is contained in:
Arkadiusz Fal
2026-04-17 05:05:49 +02:00
parent b479d63295
commit 025dc73e59
2 changed files with 100 additions and 28 deletions

View File

@@ -8359,6 +8359,16 @@
}
}
},
"search.title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Search"
}
}
}
},
"search.type.all" : {
"localizations" : {
"en" : {
@@ -14212,17 +14222,6 @@
}
}
},
"sidebar.mainItem.playlists" : {
"comment" : "Sidebar main navigation item for playlists",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Playlists"
}
}
}
},
"sidebar.mainItem.downloads" : {
"comment" : "Sidebar main navigation item for downloads",
"localizations" : {
@@ -14267,6 +14266,17 @@
}
}
},
"sidebar.mainItem.playlists" : {
"comment" : "Sidebar main navigation item for playlists",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Playlists"
}
}
}
},
"sidebar.mainItem.remoteControl" : {
"comment" : "Sidebar main navigation item for remote control",
"localizations" : {

View File

@@ -84,7 +84,9 @@ struct ChannelView: View {
#if os(tvOS)
// tvOS-specific state
@State private var isDescriptionScrollLocked = false
@State private var tvOSShowSearchTab = false
@FocusState private var isSubscribeFocused: Bool
@FocusState private var isTVSearchFieldFocused: Bool
#endif
// Header configuration
@@ -630,13 +632,43 @@ struct ChannelView: View {
ForEach([ChannelTab.videos, .shorts, .streams, .playlists]) { tab in
tvOSTabButton(for: tab)
}
if supportsChannelSearch {
tvOSSearchTabButton
}
}
}
@ViewBuilder
private var tvOSSearchTabButton: some View {
let isSelected = tvOSShowSearchTab
let action = {
if !tvOSShowSearchTab {
tvOSShowSearchTab = true
isSearchActive = true
}
}
let label = Label(String(localized: "search.title"), systemImage: "magnifyingglass")
.fontWeight(isSelected ? .bold : .regular)
.lineLimit(1)
if isSelected {
Button(action: action) { label }
.buttonStyle(.borderedProminent)
.tint(accentColor)
} else {
Button(action: action) { label }
.buttonStyle(.bordered)
}
}
@ViewBuilder
private func tvOSTabButton(for tab: ChannelTab) -> some View {
let isSelected = selectedTab == tab
let isSelected = !tvOSShowSearchTab && selectedTab == tab
let action = {
if tvOSShowSearchTab {
tvOSShowSearchTab = false
isSearchActive = false
}
if selectedTab != tab {
selectedTab = tab
Task {
@@ -646,6 +678,7 @@ struct ChannelView: View {
}
let label = Label(tab.title, systemImage: tab.systemImage)
.fontWeight(isSelected ? .bold : .regular)
.lineLimit(1)
if isSelected {
Button(action: action) { label }
@@ -663,6 +696,9 @@ struct ChannelView: View {
tvOSTabButtons
}
if tvOSShowSearchTab {
tvOSChannelSearchContent
} else {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
Group {
@@ -686,6 +722,32 @@ struct ChannelView: View {
.scrollClipDisabled()
}
}
}
private var tvOSChannelSearchContent: some View {
VStack(alignment: .leading, spacing: 20) {
HStack(spacing: 24) {
TextField(String(localized: "search.placeholder"), text: $searchText)
.textFieldStyle(.plain)
.focused($isTVSearchFieldFocused)
.onSubmit {
Task { await performSearch() }
}
}
.focusSection()
ScrollView {
if hasSearched || isSearchLoading {
searchResultsContent
.padding(.vertical, 20)
}
}
.scrollClipDisabled()
}
.onAppear {
isTVSearchFieldFocused = true
}
}
@ViewBuilder
private func tvOSLoadingContent(_ cached: CachedChannelData) -> some View {