Move instance browse pickers into macOS toolbar

The Popular/Trending tab picker now lives in the toolbar (principal
placement) instead of inline above the content. Once a search is
submitted it swaps to the search content type picker, with the search
filters button alongside it as a popover, matching the global Search
view. Items stay mounted invisible on macOS 26 so the toolbar layout
stays stable while typing.
This commit is contained in:
Arkadiusz Fal
2026-06-12 21:04:30 +02:00
parent 7ecd408f89
commit 4cda0bbc7a

View File

@@ -170,14 +170,8 @@ struct InstanceBrowseView: View {
// Tab picker (hidden during search)
if !isInSearchMode {
Picker("", selection: $selectedTab) {
ForEach(availableTabs) { tab in
Label(tab.title, systemImage: tab.systemImage)
.tag(tab)
}
}
.pickerStyle(.segmented)
.padding()
tabPicker
.padding()
}
// Content
@@ -248,27 +242,25 @@ struct InstanceBrowseView: View {
.overlay(
ScrollView {
VStack(spacing: 0) {
// Tab picker (hidden during search)
// Tab picker (hidden during search; lives in the toolbar on macOS)
#if !os(macOS)
if !isInSearchMode {
Picker("", selection: $selectedTab) {
ForEach(availableTabs) { tab in
Label(tab.title, systemImage: tab.systemImage)
.tag(tab)
}
}
.pickerStyle(.segmented)
.padding()
tabPicker
.padding()
}
#endif
// Feed channel filter strip (hidden during search)
if selectedTab == .feed && !feedSubscriptions.isEmpty && !isInSearchMode {
feedChannelFilterStrip
}
// Search filter strip (shown persistently after search submitted)
// Search filter strip (shown persistently after search submitted; lives in the toolbar on macOS)
#if !os(macOS)
if isInSearchMode && (searchViewModel?.hasSearched ?? false) && instance.supportsSearchFilters {
searchFiltersStrip
}
#endif
// Content
Group {
@@ -341,6 +333,45 @@ struct InstanceBrowseView: View {
.toolbarTitleDisplayMode(.inlineLarge)
.toolbar {
#if os(macOS)
if #available(macOS 26, *) {
// Search filters button appears only while showing search results.
// Items stay mounted (invisible/inert) otherwise so the toolbar layout is stable.
ToolbarItem(placement: .navigation) {
searchFiltersToolbarButton
.opacity(showsSearchFiltersInToolbar ? 1 : 0)
.disabled(!showsSearchFiltersInToolbar)
.accessibilityHidden(!showsSearchFiltersInToolbar)
}
.sharedBackgroundVisibility(showsSearchFiltersInToolbar ? .automatic : .hidden)
// Search content type picker while showing results, browse tab picker otherwise
ToolbarItem(placement: .principal) {
Group {
if showsSearchFiltersInToolbar {
searchContentTypePicker
} else {
tabPicker
}
}
.fixedSize()
.opacity(isToolbarPickerVisible ? 1 : 0)
.disabled(!isToolbarPickerVisible)
.accessibilityHidden(!isToolbarPickerVisible)
}
.sharedBackgroundVisibility(isToolbarPickerVisible ? .automatic : .hidden)
} else if showsSearchFiltersInToolbar {
ToolbarItem(placement: .navigation) {
searchFiltersToolbarButton
}
ToolbarItem(placement: .principal) {
searchContentTypePicker
.fixedSize()
}
} else if !isInSearchMode {
ToolbarItem(placement: .principal) {
tabPicker
.fixedSize()
}
}
// Pin the trailing group (search field + toolbar buttons) to the right edge,
// matching the global Search view.
if #available(macOS 26, *) {
@@ -440,23 +471,91 @@ struct InstanceBrowseView: View {
searchViewModel?.fetchSuggestions(for: newValue)
}
}
#if !os(macOS)
.sheet(isPresented: $showFilterSheet) {
SearchFiltersSheet(onApply: {
Task {
await searchViewModel?.search(query: searchText)
}
}, filters: Binding(
get: { searchViewModel?.filters ?? .defaults },
set: { searchViewModel?.filters = $0 }
))
searchFiltersSheetContent
#if !os(tvOS)
.presentationDetents([.medium, .large])
.presentationDetents([.medium, .large])
#endif
}
#endif
}
// MARK: - Computed Properties
private var searchFiltersSheetContent: some View {
SearchFiltersSheet(onApply: {
Task {
await searchViewModel?.search(query: searchText)
}
}, filters: Binding(
get: { searchViewModel?.filters ?? .defaults },
set: { searchViewModel?.filters = $0 }
))
}
#if os(macOS)
private var isShowingSearchResults: Bool {
isInSearchMode && (searchViewModel?.hasSearched ?? false)
}
private var showsSearchFiltersInToolbar: Bool {
isShowingSearchResults && instance.supportsSearchFilters
}
private var isToolbarPickerVisible: Bool {
// Tab picker stays visible while typing; it swaps to the search content
// type picker only once a search has been submitted
!isShowingSearchResults || showsSearchFiltersInToolbar
}
private var searchContentTypePicker: some View {
Picker("", selection: Binding(
get: { searchViewModel?.filters.type ?? .video },
set: { searchViewModel?.filters.type = $0 }
)) {
ForEach(SearchContentType.allCases) { type in
Text(type.title).tag(type)
}
}
.pickerStyle(.segmented)
.onChange(of: searchViewModel?.filters.type) { _, _ in
Task {
await searchViewModel?.search(query: searchText)
}
}
}
private var searchFiltersToolbarButton: some View {
Button {
showFilterSheet = true
} label: {
Label(String(localized: "search.filters"), systemImage: (searchViewModel?.filters.isDefault ?? true)
? "line.3.horizontal.decrease.circle"
: "line.3.horizontal.decrease.circle.fill")
}
.popover(isPresented: $showFilterSheet, arrowEdge: .bottom) {
searchFiltersSheetContent
}
}
#endif
private var tabPicker: some View {
Picker("", selection: $selectedTab) {
ForEach(availableTabs) { tab in
#if os(macOS)
// Labels render icon-only in a macOS toolbar segmented control
Text(tab.title)
.tag(tab)
#else
Label(tab.title, systemImage: tab.systemImage)
.tag(tab)
#endif
}
}
.pickerStyle(.segmented)
}
private var availableTabs: [BrowseTab] {
if instance.supportsFeed && isLoggedIn {
// Playlists tab only available for Invidious (Piped playlists to be added in future)