mirror of
https://github.com/yattee/yattee.git
synced 2026-07-20 14:22:02 +00:00
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:
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user