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" : { "search.type.all" : {
"localizations" : { "localizations" : {
"en" : { "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" : { "sidebar.mainItem.downloads" : {
"comment" : "Sidebar main navigation item for downloads", "comment" : "Sidebar main navigation item for downloads",
"localizations" : { "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" : { "sidebar.mainItem.remoteControl" : {
"comment" : "Sidebar main navigation item for remote control", "comment" : "Sidebar main navigation item for remote control",
"localizations" : { "localizations" : {

View File

@@ -84,7 +84,9 @@ struct ChannelView: View {
#if os(tvOS) #if os(tvOS)
// tvOS-specific state // tvOS-specific state
@State private var isDescriptionScrollLocked = false @State private var isDescriptionScrollLocked = false
@State private var tvOSShowSearchTab = false
@FocusState private var isSubscribeFocused: Bool @FocusState private var isSubscribeFocused: Bool
@FocusState private var isTVSearchFieldFocused: Bool
#endif #endif
// Header configuration // Header configuration
@@ -630,13 +632,43 @@ struct ChannelView: View {
ForEach([ChannelTab.videos, .shorts, .streams, .playlists]) { tab in ForEach([ChannelTab.videos, .shorts, .streams, .playlists]) { tab in
tvOSTabButton(for: tab) 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 @ViewBuilder
private func tvOSTabButton(for tab: ChannelTab) -> some View { private func tvOSTabButton(for tab: ChannelTab) -> some View {
let isSelected = selectedTab == tab let isSelected = !tvOSShowSearchTab && selectedTab == tab
let action = { let action = {
if tvOSShowSearchTab {
tvOSShowSearchTab = false
isSearchActive = false
}
if selectedTab != tab { if selectedTab != tab {
selectedTab = tab selectedTab = tab
Task { Task {
@@ -646,6 +678,7 @@ struct ChannelView: View {
} }
let label = Label(tab.title, systemImage: tab.systemImage) let label = Label(tab.title, systemImage: tab.systemImage)
.fontWeight(isSelected ? .bold : .regular) .fontWeight(isSelected ? .bold : .regular)
.lineLimit(1)
if isSelected { if isSelected {
Button(action: action) { label } Button(action: action) { label }
@@ -663,28 +696,57 @@ struct ChannelView: View {
tvOSTabButtons tvOSTabButtons
} }
ScrollView { if tvOSShowSearchTab {
VStack(alignment: .leading, spacing: 0) { tvOSChannelSearchContent
Group { } else {
switch selectedTab { ScrollView {
case .videos: VStack(alignment: .leading, spacing: 0) {
videosGrid Group {
case .shorts: switch selectedTab {
shortsGrid case .videos:
case .streams: videosGrid
streamsGrid case .shorts:
case .playlists: shortsGrid
playlistsGrid case .streams:
case .about: streamsGrid
videosGrid case .playlists:
playlistsGrid
case .about:
videosGrid
}
} }
.id(selectedTab)
} }
.id(selectedTab) .padding(.vertical, 20)
}
.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)
} }
.padding(.vertical, 20)
} }
.scrollClipDisabled() .scrollClipDisabled()
} }
.onAppear {
isTVSearchFieldFocused = true
}
} }
@ViewBuilder @ViewBuilder