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) // Tab picker (hidden during search)
if !isInSearchMode { if !isInSearchMode {
Picker("", selection: $selectedTab) { tabPicker
ForEach(availableTabs) { tab in .padding()
Label(tab.title, systemImage: tab.systemImage)
.tag(tab)
}
}
.pickerStyle(.segmented)
.padding()
} }
// Content // Content
@@ -248,27 +242,25 @@ struct InstanceBrowseView: View {
.overlay( .overlay(
ScrollView { ScrollView {
VStack(spacing: 0) { VStack(spacing: 0) {
// Tab picker (hidden during search) // Tab picker (hidden during search; lives in the toolbar on macOS)
#if !os(macOS)
if !isInSearchMode { if !isInSearchMode {
Picker("", selection: $selectedTab) { tabPicker
ForEach(availableTabs) { tab in .padding()
Label(tab.title, systemImage: tab.systemImage)
.tag(tab)
}
}
.pickerStyle(.segmented)
.padding()
} }
#endif
// Feed channel filter strip (hidden during search) // Feed channel filter strip (hidden during search)
if selectedTab == .feed && !feedSubscriptions.isEmpty && !isInSearchMode { if selectedTab == .feed && !feedSubscriptions.isEmpty && !isInSearchMode {
feedChannelFilterStrip 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 { if isInSearchMode && (searchViewModel?.hasSearched ?? false) && instance.supportsSearchFilters {
searchFiltersStrip searchFiltersStrip
} }
#endif
// Content // Content
Group { Group {
@@ -341,6 +333,45 @@ struct InstanceBrowseView: View {
.toolbarTitleDisplayMode(.inlineLarge) .toolbarTitleDisplayMode(.inlineLarge)
.toolbar { .toolbar {
#if os(macOS) #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, // Pin the trailing group (search field + toolbar buttons) to the right edge,
// matching the global Search view. // matching the global Search view.
if #available(macOS 26, *) { if #available(macOS 26, *) {
@@ -440,23 +471,91 @@ struct InstanceBrowseView: View {
searchViewModel?.fetchSuggestions(for: newValue) searchViewModel?.fetchSuggestions(for: newValue)
} }
} }
#if !os(macOS)
.sheet(isPresented: $showFilterSheet) { .sheet(isPresented: $showFilterSheet) {
SearchFiltersSheet(onApply: { searchFiltersSheetContent
Task {
await searchViewModel?.search(query: searchText)
}
}, filters: Binding(
get: { searchViewModel?.filters ?? .defaults },
set: { searchViewModel?.filters = $0 }
))
#if !os(tvOS) #if !os(tvOS)
.presentationDetents([.medium, .large]) .presentationDetents([.medium, .large])
#endif #endif
} }
#endif
} }
// MARK: - Computed Properties // 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] { private var availableTabs: [BrowseTab] {
if instance.supportsFeed && isLoggedIn { if instance.supportsFeed && isLoggedIn {
// Playlists tab only available for Invidious (Piped playlists to be added in future) // Playlists tab only available for Invidious (Piped playlists to be added in future)