mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
feat(search): scoped live search on browse pages (#938)
* feat(artists): scoped live search badge replaces page filter Move Artists browse text search into the header Live Search with a page scope badge (Users icon), field-local undo, and double-click/backspace to clear scope. Block the live-search dropdown while scoped so results only filter the Artists grid; mobile overlay follows the same rules. * fix(artists): plain grid for scoped search fixes broken card layout Route the browse grid through VirtualCardGrid, switch to non-virtual CSS grid when live search filters the catalog, reset scroll on filter changes, and skip content-visibility on plain tiles to avoid blank/black cards. * fix(search): scope badge double-Backspace and single clear control Require two Backspaces on an empty scoped field after prior text input; one Backspace still clears the badge when the field was never filled. Move live-search clear/advanced controls inside the field pill, drop the extra outer clear button, and use type=text to avoid native search clears. * fix(search): drop duplicate outer live-search clear button Keep the native in-field clear on type=search and the original pill layout; remove only the extra × control outside the search border. Reset dropdown state when the query is cleared via the native control. * refactor(search): generic scoped browse query helper, drop dead code Rename artistsBrowseSearchQuery to scopedBrowseSearchQuery with an expectedScope argument; wire Artists via useScopedBrowseSearchQuery. Remove unused liveSearchScoped dropdown helper (scoped mode blocks it). * feat(search): ghost scope badge and single-click badge remove After clearing the artists scope on /artists, show a faded ghost chip to restore page-only search while keeping the global search placeholder. Active badge removes on one click; tooltips and styles updated. * feat(search): scoped live search for All Albums and New Releases Wire albums and newReleases scope badges with debounced album title search (local index title-only FTS + filtered search3). Plain grid, scroll reset, and session query restore on album grid browse pages. * fix(browse): preserve scroll restore after album/artist detail back Only reset in-page scroll when filter resetKey changes, not when isScrollRestorePending clears after session restore. * feat(search): scoped live search for Tracks browse Wire /tracks to header live search with wide title/artist/album FTS, hide hero and discovery rails while search is active, and remove the inline search field from the browse list. * fix(search): clear header query when leaving scoped browse pages Prevent global live search from firing on album/detail routes after a scoped browse query; browse session stashes still restore on back. * fix(tracks): restore scroll after back from detail during scoped search Hold stashed song results across fetchSongPage churn, defer leave-stash teardown past AppShell scroll reset, restore tracks scroll after the list is ready, and save scroll snapshot when opening artist from song context menu. * fix(tracks): hide discovery headings during scoped search Hide the page subtitle and "Browse all tracks" section title when tracks search is active, matching hero/rails chrome behavior. * feat(search): scoped live search for Composers browse Wire /composers to header live search with composers scope badge, session stash, scroll restore, and plain grid/list during text filter. Remove the in-page filter input; add i18n and navigation helpers. * docs: CHANGELOG and credits for scoped browse live search (PR #938)
This commit is contained in:
+38
-67
@@ -9,8 +9,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { APP_MAIN_SCROLL_VIEWPORT_ID, ARTISTS_INPAGE_SCROLL_VIEWPORT_ID } from '../constants/appScroll';
|
||||
import { useElementClientHeightById, useElementClientHeightForElement } from '../hooks/useResizeClientHeight';
|
||||
import { useCardGridMetrics } from '../hooks/useCardGridMetrics';
|
||||
import { useRemeasureGridVirtualizer } from '../hooks/useRemeasureGridVirtualizer';
|
||||
import { useVirtualizerScrollMargin } from '../hooks/useVirtualizerScrollMargin';
|
||||
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
|
||||
import {
|
||||
@@ -32,10 +30,12 @@ import { ArtistsListView } from '../components/artists/ArtistsListView';
|
||||
import InpageScrollSentinel from '../components/InpageScrollSentinel';
|
||||
import { useArtistsBrowseFilters, type ArtistBrowseScrollSnapshot } from '../hooks/useArtistsBrowseFilters';
|
||||
import { useArtistsBrowseScrollRestore } from '../hooks/useArtistsBrowseScrollRestore';
|
||||
import { useArtistsBrowseScrollReset } from '../hooks/useArtistsBrowseScrollReset';
|
||||
import { useNavigateToArtist } from '../hooks/useNavigateToArtist';
|
||||
import { peekArtistBrowseScrollRestore } from '../store/artistBrowseSessionStore';
|
||||
import { readArtistBrowseRestore } from '../utils/navigation/albumDetailNavigation';
|
||||
|
||||
import { useScopedBrowseSearchQuery } from '../store/liveSearchScopeStore';
|
||||
import { useLibraryIndexStore } from '../store/libraryIndexStore';
|
||||
|
||||
export default function Artists() {
|
||||
@@ -51,8 +51,6 @@ export default function Artists() {
|
||||
);
|
||||
|
||||
const {
|
||||
filter,
|
||||
setFilter,
|
||||
letterFilter,
|
||||
setLetterFilter,
|
||||
starredOnly,
|
||||
@@ -61,6 +59,8 @@ export default function Artists() {
|
||||
setViewMode,
|
||||
} = useArtistsBrowseFilters(serverId, scrollSnapshotRef);
|
||||
|
||||
const artistsSearchQuery = useScopedBrowseSearchQuery('artists');
|
||||
|
||||
const {
|
||||
scrollBodyEl: artistsScrollBodyEl,
|
||||
bindScrollBody: bindArtistsScrollBody,
|
||||
@@ -91,13 +91,18 @@ export default function Artists() {
|
||||
});
|
||||
|
||||
const { textSearchArtists, textSearchLoading, effectiveFilter } = useBrowseArtistTextSearch(
|
||||
filter,
|
||||
artistsSearchQuery,
|
||||
indexEnabled,
|
||||
serverId,
|
||||
);
|
||||
const artists = textSearchArtists ?? catalogArtists;
|
||||
const loading = catalogLoading || textSearchLoading;
|
||||
const textSearchActive = textSearchArtists != null;
|
||||
/** Scoped/plain text filter — canonical CSS grid, not row virtualization (small result sets). */
|
||||
const artistBrowsePlainLayout =
|
||||
perfFlags.disableMainstageVirtualLists
|
||||
|| textSearchActive
|
||||
|| artistsSearchQuery.trim().length > 0;
|
||||
|
||||
const {
|
||||
visibleCount,
|
||||
@@ -105,7 +110,7 @@ export default function Artists() {
|
||||
loadMore: sliceLoadMore,
|
||||
} = useClientSliceInfiniteScroll({
|
||||
pageSize: PAGE_SIZE,
|
||||
resetDeps: [filter, letterFilter, starredOnly, viewMode, musicLibraryFilterVersion, serverId],
|
||||
resetDeps: [artistsSearchQuery, letterFilter, starredOnly, viewMode, musicLibraryFilterVersion, serverId],
|
||||
getScrollRoot: getArtistsScrollRoot,
|
||||
scrollRootEl: artistsScrollBodyEl,
|
||||
restoreDisplayCount: restoreVisibleCountRef.current,
|
||||
@@ -224,7 +229,7 @@ export default function Artists() {
|
||||
});
|
||||
|
||||
const mainstageHeaderTight = useMainstageInpageHeaderTight(artistsScrollBodyEl, [
|
||||
filter,
|
||||
artistsSearchQuery,
|
||||
letterFilter,
|
||||
starredOnly,
|
||||
viewMode,
|
||||
@@ -243,48 +248,6 @@ export default function Artists() {
|
||||
[getArtistsScrollRoot],
|
||||
);
|
||||
|
||||
const artistGridMeasureRef = useRef<HTMLDivElement>(null);
|
||||
const { gridCols: artistGridCols, rowHeightEst: artistGridRowHeightEst } = useCardGridMetrics(
|
||||
artistGridMeasureRef,
|
||||
viewMode === 'grid',
|
||||
'artist',
|
||||
visible.length,
|
||||
);
|
||||
|
||||
const artistVirtualRowCount = Math.max(0, Math.ceil(visible.length / Math.max(1, artistGridCols)));
|
||||
|
||||
const artistGridOverscan = Math.max(
|
||||
2,
|
||||
Math.ceil(artistsInpageScrollHeight / Math.max(1, artistGridRowHeightEst)),
|
||||
);
|
||||
|
||||
const artistGridScrollMargin = useVirtualizerScrollMargin(
|
||||
artistGridMeasureRef,
|
||||
getInpageScrollElement,
|
||||
{
|
||||
active: !perfFlags.disableMainstageVirtualLists && viewMode === 'grid',
|
||||
deps: [artistVirtualRowCount, artistGridCols],
|
||||
},
|
||||
);
|
||||
|
||||
const artistGridVirtualizer = useVirtualizer({
|
||||
count:
|
||||
perfFlags.disableMainstageVirtualLists || viewMode !== 'grid'
|
||||
? 0
|
||||
: artistVirtualRowCount,
|
||||
getScrollElement: getInpageScrollElement,
|
||||
estimateSize: () => artistGridRowHeightEst,
|
||||
overscan: artistGridOverscan,
|
||||
scrollMargin: artistGridScrollMargin,
|
||||
});
|
||||
|
||||
useRemeasureGridVirtualizer(artistGridVirtualizer, {
|
||||
active: !perfFlags.disableMainstageVirtualLists && viewMode === 'grid' && artistVirtualRowCount > 0,
|
||||
gridCols: artistGridCols,
|
||||
rowHeightEst: artistGridRowHeightEst,
|
||||
virtualRowCount: artistVirtualRowCount,
|
||||
});
|
||||
|
||||
const artistListOverscan = Math.max(
|
||||
12,
|
||||
Math.ceil(artistsInpageScrollHeight / ARTIST_LIST_ROW_EST),
|
||||
@@ -295,14 +258,14 @@ export default function Artists() {
|
||||
artistListWrapRef,
|
||||
getInpageScrollElement,
|
||||
{
|
||||
active: !perfFlags.disableMainstageVirtualLists && viewMode === 'list',
|
||||
active: !artistBrowsePlainLayout && viewMode === 'list',
|
||||
deps: [artistListFlatRows.length],
|
||||
},
|
||||
);
|
||||
|
||||
const artistListVirtualizer = useVirtualizer({
|
||||
count:
|
||||
perfFlags.disableMainstageVirtualLists || viewMode !== 'list' ? 0 : artistListFlatRows.length,
|
||||
artistBrowsePlainLayout || viewMode !== 'list' ? 0 : artistListFlatRows.length,
|
||||
getScrollElement: getInpageScrollElement,
|
||||
estimateSize: index => {
|
||||
const row = artistListFlatRows[index];
|
||||
@@ -320,6 +283,27 @@ export default function Artists() {
|
||||
scrollMargin: artistListScrollMargin,
|
||||
});
|
||||
|
||||
const browseScrollResetKey = [
|
||||
artistsSearchQuery,
|
||||
letterFilter,
|
||||
starredOnly,
|
||||
viewMode,
|
||||
serverId,
|
||||
musicLibraryFilterVersion,
|
||||
textSearchArtists?.length ?? '',
|
||||
textSearchArtists?.[0]?.id ?? '',
|
||||
].join('\0');
|
||||
|
||||
useArtistsBrowseScrollReset({
|
||||
scrollSnapshotRef,
|
||||
getScrollRoot: getArtistsScrollRoot,
|
||||
isScrollRestorePending,
|
||||
resetKey: browseScrollResetKey,
|
||||
viewMode,
|
||||
listVirtualize: !artistBrowsePlainLayout,
|
||||
listVirtualizer: artistListVirtualizer,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`content-body animate-fade-in mainstage-inpage-split${mainstageHeaderTight ? ' mainstage-inpage--header-tight' : ''}`}
|
||||
@@ -333,14 +317,6 @@ export default function Artists() {
|
||||
? t('artists.selectionCount', { count: selectedIds.size })
|
||||
: t('artists.title')}
|
||||
</h1>
|
||||
<input
|
||||
className="input"
|
||||
style={{ maxWidth: 220 }}
|
||||
placeholder={t('artists.search')}
|
||||
value={filter}
|
||||
onChange={e => setFilter(e.target.value)}
|
||||
id="artist-filter-input"
|
||||
/>
|
||||
{textSearchLoading && (
|
||||
<div className="spinner" style={{ width: 16, height: 16, flexShrink: 0 }} />
|
||||
)}
|
||||
@@ -432,13 +408,8 @@ export default function Artists() {
|
||||
{!loading && !pendingLetterMatch && viewMode === 'grid' && (
|
||||
<ArtistsGridView
|
||||
visible={visible}
|
||||
gridCols={artistGridCols}
|
||||
measureRef={artistGridMeasureRef}
|
||||
virtualization={
|
||||
perfFlags.disableMainstageVirtualLists
|
||||
? null
|
||||
: { virtualizer: artistGridVirtualizer, scrollMargin: artistGridScrollMargin }
|
||||
}
|
||||
disableVirtualization={artistBrowsePlainLayout}
|
||||
layoutKey={browseScrollResetKey}
|
||||
selectionMode={selectionMode}
|
||||
selectedIds={selectedIds}
|
||||
selectedArtists={selectedArtists}
|
||||
@@ -452,7 +423,7 @@ export default function Artists() {
|
||||
|
||||
{!loading && !pendingLetterMatch && viewMode === 'list' && (
|
||||
<ArtistsListView
|
||||
virtualized={!perfFlags.disableMainstageVirtualLists}
|
||||
virtualized={!artistBrowsePlainLayout}
|
||||
groups={groups}
|
||||
letters={letters}
|
||||
artistListFlatRows={artistListFlatRows}
|
||||
|
||||
Reference in New Issue
Block a user