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.
This commit is contained in:
Arkadiusz Fal
2026-04-13 20:59:25 +02:00
parent bcb0864fca
commit 831773a609
2 changed files with 119 additions and 0 deletions

View File

@@ -512,6 +512,36 @@ struct InstanceBrowseView: View {
private var searchFiltersStrip: some View { private var searchFiltersStrip: some View {
HStack(spacing: 12) { 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 // Filter button
Button { Button {
showFilterSheet = true showFilterSheet = true
@@ -521,6 +551,7 @@ struct InstanceBrowseView: View {
: "line.3.horizontal.decrease.circle.fill") : "line.3.horizontal.decrease.circle.fill")
.font(.title2) .font(.title2)
} }
#endif
// Content type segmented picker // Content type segmented picker
Picker("", selection: Binding( Picker("", selection: Binding(
@@ -542,6 +573,33 @@ struct InstanceBrowseView: View {
} }
} }
#if os(tvOS)
private func filterMenu<T: Hashable & Identifiable & CaseIterable>(
title: String,
selection: Binding<T>,
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 // MARK: - Feed Channel Filter Strip
private var feedChannelFilterStrip: some View { private var feedChannelFilterStrip: some View {

View File

@@ -384,6 +384,36 @@ struct SearchView: View {
private var searchFiltersStrip: some View { private var searchFiltersStrip: some View {
HStack(spacing: 12) { 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 // Filter button
Button { Button {
showFilterSheet = true showFilterSheet = true
@@ -393,6 +423,7 @@ struct SearchView: View {
: "line.3.horizontal.decrease.circle.fill") : "line.3.horizontal.decrease.circle.fill")
.font(.title2) .font(.title2)
} }
#endif
// Content type segmented picker // Content type segmented picker
Picker("", selection: Binding( Picker("", selection: Binding(
@@ -417,6 +448,36 @@ struct SearchView: View {
} }
} }
#if os(tvOS)
private func filterMenu<T: Hashable & Identifiable & CaseIterable>(
title: String,
selection: Binding<T>,
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 // MARK: - Search History Helpers
private var displayedSearchHistory: [SearchHistory] { private var displayedSearchHistory: [SearchHistory] {