From 831773a609e7e6eb4508b1033b3066280831ac04 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Mon, 13 Apr 2026 20:59:25 +0200 Subject: [PATCH] Replace search filters sheet with inline menus on tvOS The filters sheet is too small and awkward on tvOS. Replace the filter button with inline Menu pickers for Sort By, Upload Date, and Duration directly in the filter strip. Applied to both SearchView and InstanceBrowseView. --- .../Views/Instances/InstanceBrowseView.swift | 58 ++++++++++++++++++ Yattee/Views/Search/SearchView.swift | 61 +++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/Yattee/Views/Instances/InstanceBrowseView.swift b/Yattee/Views/Instances/InstanceBrowseView.swift index dff333a9..d08484e7 100644 --- a/Yattee/Views/Instances/InstanceBrowseView.swift +++ b/Yattee/Views/Instances/InstanceBrowseView.swift @@ -512,6 +512,36 @@ struct InstanceBrowseView: View { private var searchFiltersStrip: some View { HStack(spacing: 12) { + #if os(tvOS) + // tvOS: Inline filter menus instead of sheet + filterMenu( + title: String(localized: "search.sort"), + selection: Binding( + get: { searchViewModel?.filters.sort ?? .relevance }, + set: { searchViewModel?.filters.sort = $0 } + ), + options: SearchSortOption.allCases, + labelForOption: { $0.title } + ) + filterMenu( + title: String(localized: "search.uploadDate"), + selection: Binding( + get: { searchViewModel?.filters.date ?? .any }, + set: { searchViewModel?.filters.date = $0 } + ), + options: SearchDateFilter.allCases, + labelForOption: { $0.title } + ) + filterMenu( + title: String(localized: "search.duration"), + selection: Binding( + get: { searchViewModel?.filters.duration ?? .any }, + set: { searchViewModel?.filters.duration = $0 } + ), + options: SearchDurationFilter.allCases, + labelForOption: { $0.title } + ) + #else // Filter button Button { showFilterSheet = true @@ -521,6 +551,7 @@ struct InstanceBrowseView: View { : "line.3.horizontal.decrease.circle.fill") .font(.title2) } + #endif // Content type segmented picker Picker("", selection: Binding( @@ -542,6 +573,33 @@ struct InstanceBrowseView: View { } } + #if os(tvOS) + private func filterMenu( + title: String, + selection: Binding, + options: [T], + labelForOption: @escaping (T) -> String + ) -> some View { + Menu { + ForEach(options) { option in + Button { + selection.wrappedValue = option + Task { await searchViewModel?.search(query: searchText) } + } label: { + if option == selection.wrappedValue { + Label(labelForOption(option), systemImage: "checkmark") + } else { + Text(labelForOption(option)) + } + } + } + } label: { + Text(title) + .font(.caption) + } + } + #endif + // MARK: - Feed Channel Filter Strip private var feedChannelFilterStrip: some View { diff --git a/Yattee/Views/Search/SearchView.swift b/Yattee/Views/Search/SearchView.swift index 9fe95168..e2c0667d 100644 --- a/Yattee/Views/Search/SearchView.swift +++ b/Yattee/Views/Search/SearchView.swift @@ -384,6 +384,36 @@ struct SearchView: View { private var searchFiltersStrip: some View { HStack(spacing: 12) { + #if os(tvOS) + // tvOS: Inline filter menus instead of sheet + filterMenu( + title: String(localized: "search.sort"), + selection: Binding( + get: { searchViewModel?.filters.sort ?? .relevance }, + set: { searchViewModel?.filters.sort = $0 } + ), + options: SearchSortOption.allCases, + labelForOption: { $0.title } + ) + filterMenu( + title: String(localized: "search.uploadDate"), + selection: Binding( + get: { searchViewModel?.filters.date ?? .any }, + set: { searchViewModel?.filters.date = $0 } + ), + options: SearchDateFilter.allCases, + labelForOption: { $0.title } + ) + filterMenu( + title: String(localized: "search.duration"), + selection: Binding( + get: { searchViewModel?.filters.duration ?? .any }, + set: { searchViewModel?.filters.duration = $0 } + ), + options: SearchDurationFilter.allCases, + labelForOption: { $0.title } + ) + #else // Filter button Button { showFilterSheet = true @@ -393,6 +423,7 @@ struct SearchView: View { : "line.3.horizontal.decrease.circle.fill") .font(.title2) } + #endif // Content type segmented picker Picker("", selection: Binding( @@ -417,6 +448,36 @@ struct SearchView: View { } } + #if os(tvOS) + private func filterMenu( + title: String, + selection: Binding, + options: [T], + labelForOption: @escaping (T) -> String + ) -> some View { + Menu { + ForEach(options) { option in + Button { + selection.wrappedValue = option + if let filters = searchViewModel?.filters { + saveFilters(filters) + } + Task { await searchViewModel?.search(query: searchTextBinding.wrappedValue) } + } label: { + if option == selection.wrappedValue { + Label(labelForOption(option), systemImage: "checkmark") + } else { + Text(labelForOption(option)) + } + } + } + } label: { + Text(title) + .font(.caption) + } + } + #endif + // MARK: - Search History Helpers private var displayedSearchHistory: [SearchHistory] {