diff --git a/CHANGELOG.PATCHED.md b/CHANGELOG.PATCHED.md index f87e281..925a91d 100644 --- a/CHANGELOG.PATCHED.md +++ b/CHANGELOG.PATCHED.md @@ -2,6 +2,7 @@ ## Fork's Nightly Changes *Changes that's already on the fork and waiting to be reviewed for merge into original Atlas* +- Update debounce logic for Browse and Library: Search in Catalog Browse and Library now debounces the text input and waits for a pause before filtering. Previously every keystroke updated `activeFilters.text` and ran `filterGamesWithState` (Library) or scheduled a catalog fetch, causing input lag on large libraries and a wasted local-filter pass even while browsing the server-side catalog. The input still echoes instantly from local state; clear bypasses the delay.[PR#398](https://github.com/towerwatchman/Atlas/pull/398) ## Independent Changes *Any fork-only changes that is not accepted for merged but valid, or independent changes to make the fork repo releasable (e.g. custom version or preventing updates)* diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b4fa63..f8d5da7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ - The version readout in the topnav and sidebar is now a button that opens that version's GitHub release page. The tag it builds matches what the release workflows publish -- `v` for stable and `v-nightly.` for nightly -- so it lands on the real release rather than a 404. (#143) - Added a "Download Version" entry to the split-button caret on the game detail page, beside "Manual Install". It opens the same downloads modal the UPDATE button does, listing every build and mirror the thread offers, so a different version can be fetched over one already installed. Previously an installed title with no pending update had no route to that modal at all: the primary button becomes PLAY once a version is installed, and the UPDATE button only renders when an update is flagged. The entry goes straight to the downloads modal rather than through the source picker, and is shown disabled with a reason for titles with no F95zone thread linked. +### Changed + + ### Fixed - Catalog tag filtering now matches Library. Library already did exact-token filtering via `splitListText`/`normalizeTagText`/`includesTag` `src/hooks/useFilters.js`; Catalog used substring `LOWER(col) LIKE '%tag%'`. Fix copies those Library helpers into shared `src/utils/tagTokens.js` ↔ `electron/db/tagTokens.js` and adds a stored `tags_filter` column plus an indexed `catalog_index_tags` table (`catalog_key, tag` + `idx_catalog_index_tags_tag`). Browse now filters via `EXISTS (SELECT 1 FROM catalog_index_tags WHERE tag=?)` hitting the index instead of `LIKE '%,token,%'` with leading wildcard on `tags_filter` (full scan, a few seconds per tag click). `COALESCE`/`ESCAPE`/`REPLACE` remain only for the union fallback path when the index is not ready. - Fixed MEGA v1 test timeout — legacy key derivation is intentionally slow and needed a longer test timeout. diff --git a/src/App.jsx b/src/App.jsx index aba0343..282f5ce 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -149,6 +149,7 @@ const App = () => { const [wishlistIdentityKeys, setWishlistIdentityKeys] = useState(new Set()) const [activeSavedFilterId, setActiveSavedFilterId] = useState('') const [savedFilterDeleteStateById, setSavedFilterDeleteStateById] = useState({}) + const [resetInputSignal, setResetInputSignal] = useState(0) // Banner card dimensions for Grid sizing — derived from the same // resolved template BannerTemplateProvider already computed once for // (see src/theme/BannerTemplateProvider.jsx), rather than @@ -1214,6 +1215,7 @@ const App = () => { setActiveSavedFilterId('') pendingLibraryScrollTopRestoreRef.current = 0 libraryScrollTopRef.current = 0 + setResetInputSignal((n) => n + 1) if (libraryMode === 'catalog') { // "Reset" in Browse mode should mean the whole catalog, not the // local library's installed-only default — otherwise resetting @@ -1889,7 +1891,11 @@ const App = () => { } }, [filterSidebarMode, selectedGame]) - const catalogResetDebounceRef = useRef(null) + // Search input already debounces via useDebouncedSearch (SearchBox/ + // SearchSidebar) before activeFilters updates, so debouncing again here + // would stack an extra delay before the spinner shows. Fetch immediately + // once the debounced filters arrive; the paramsKey guard still prevents + // the catalogTotal/enter-mode re-runs from wiping correct data. useEffect(() => { if (libraryMode !== 'catalog' || !browseAvailable) return const paramsKey = catalogParamsKey(catalogSearch, catalogQueryFilters) @@ -1902,18 +1908,8 @@ const App = () => { // the "banners flash, spinner, banners reload" sequence this fixes. return } - if (catalogResetDebounceRef.current) clearTimeout(catalogResetDebounceRef.current) - catalogResetDebounceRef.current = setTimeout(() => { - catalogResetDebounceRef.current = null - lastFetchedCatalogParamsKeyRef.current = paramsKey - fetchCatalogGames({ reset: true, search: catalogSearch, filters: catalogQueryFilters }) - }, 300) - return () => { - if (catalogResetDebounceRef.current) { - clearTimeout(catalogResetDebounceRef.current) - catalogResetDebounceRef.current = null - } - } + lastFetchedCatalogParamsKeyRef.current = paramsKey + fetchCatalogGames({ reset: true, search: catalogSearch, filters: catalogQueryFilters }) }, [browseAvailable, catalogQueryFilters, catalogSearch, catalogTotal, fetchCatalogGames, libraryMode]) // When the catalog index finishes building, anything already on screen in @@ -2106,7 +2102,7 @@ const App = () => { // layout there is no search box here and Collections is a nav // button instead (see TopNav's LEFT_ORDER).
- +
)} @@ -2244,6 +2240,7 @@ const App = () => { savedFilterDeleteStateById={savedFilterDeleteStateById} onApplySavedFilter={applySavedFilter} onDeleteSavedFilter={deleteSavedFilter} + resetInputSignal={resetInputSignal} onClose={() => setShowSearchSidebar(false)} /> @@ -2579,6 +2576,7 @@ const App = () => { savedFilterDeleteStateById={savedFilterDeleteStateById} onApplySavedFilter={applySavedFilter} onDeleteSavedFilter={deleteSavedFilter} + resetInputSignal={resetInputSignal} onClose={() => setShowSearchSidebar(false)} /> )} @@ -2601,6 +2599,7 @@ const App = () => { savedFilterDeleteStateById={savedFilterDeleteStateById} onApplySavedFilter={applySavedFilter} onDeleteSavedFilter={deleteSavedFilter} + resetInputSignal={resetInputSignal} onClose={() => setShowSearchSidebar(false)} /> )} diff --git a/src/components/search/SearchBox.jsx b/src/components/search/SearchBox.jsx index ad17d12..a1c1f30 100644 --- a/src/components/search/SearchBox.jsx +++ b/src/components/search/SearchBox.jsx @@ -1,4 +1,8 @@ -export default function SearchBox({ value = "", onSearchChange, onToggleSidebar }) { +import { useDebouncedSearch } from '../../hooks/useDebouncedSearch.js' + +export default function SearchBox({ value = "", onSearchChange, onToggleSidebar, resetInputSignal }) { + const { localValue, handleChange, handleClear } = useDebouncedSearch({ value, onSearchChange, resetInputSignal }) + const handleInputKeyDown = (event) => { event.stopPropagation() } @@ -10,15 +14,15 @@ export default function SearchBox({ value = "", onSearchChange, onToggleSidebar onSearchChange?.(e.target.value)} + value={localValue} + onChange={(e) => handleChange(e.target.value)} onKeyDown={handleInputKeyDown} className="bg-transparent outline-none text-text flex-1 px-2 focus:outline-none -webkit-app-region-no-drag" /> - {value && ( + {localValue && (