mirror of
https://github.com/yattee/yattee.git
synced 2026-05-12 10:25:02 +00:00
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:
@@ -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" : {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user