From 4cda0bbc7a2d73c7e57af4e03c9defab09a34afd Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Fri, 12 Jun 2026 21:04:30 +0200 Subject: [PATCH] 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. --- .../Views/Instances/InstanceBrowseView.swift | 153 ++++++++++++++---- 1 file changed, 126 insertions(+), 27 deletions(-) diff --git a/Yattee/Views/Instances/InstanceBrowseView.swift b/Yattee/Views/Instances/InstanceBrowseView.swift index 7480ebda..3c133e4c 100644 --- a/Yattee/Views/Instances/InstanceBrowseView.swift +++ b/Yattee/Views/Instances/InstanceBrowseView.swift @@ -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)