feat(search): keep scrape-sourced YouTube results across a reload

A live YouTube search was always dropped on reload so it could never
re-spend quota. But the default scrape source costs no quota, so there's
no reason to bounce the user back to the feed — they can stay on their
results.

Feed now stamps history.state._ytScrape=true once a search resolves via
the scrape source; App restores _yt on reload only when that flag is set
(and re-runs the search, which is free). An api-source search is left
unmarked and still drops to the feed on reload, since re-fetching it would
cost ~100 units. The flag is cleared when a new search starts (Feed
re-stamps it once the new results' source is known).
This commit is contained in:
npeter83 2026-07-01 01:55:05 +02:00
parent 703ae41f2d
commit b498755067
2 changed files with 33 additions and 8 deletions

View file

@ -130,10 +130,18 @@ export default function App() {
// popstate handler below derives ytSearch from it, so Back steps player → search → feed in
// that order (a player opened over the results pops first, then the search, then the feed) —
// instead of the search vanishing because it had no history entry of its own.
const [ytSearch, setYtSearch] = useState<string | null>(null);
const [ytSearch, setYtSearch] = useState<string | null>(() => {
// Restore a live search across a reload ONLY when it was served by the zero-quota scrape
// source (re-fetching is free) — an api-source search would re-spend ~100 units, so it's
// dropped to the normal feed as before. The scrape flag rides in history.state (survives F5).
const st = window.history.state;
return st?._yt && st?._ytScrape ? (st._yt as string) : null;
});
const enterYtSearch = useCallback((q: string) => {
setYtSearch(q);
const st = window.history.state || {};
// Drop any prior _ytScrape marker — the new search's source isn't known yet; Feed re-stamps
// it once the results resolve, so a reload only restores a confirmed scrape-sourced search.
const { _ytScrape: _drop, ...st } = window.history.state || {};
if (st._yt) window.history.replaceState({ ...st, _yt: q }, ""); // refine current search
else window.history.pushState({ ...st, sfPage: "feed", _yt: q }, ""); // new sub-view entry
}, []);
@ -271,12 +279,17 @@ export default function App() {
// from history.state on popstate. (stripUrlParams preserves history.state, so this stamp
// survives a later query-string strip.)
useEffect(() => {
// Drop any stale _yt from a prior session (a reload starts on the normal feed; ytSearch
// begins null), so the first Back doesn't resurrect a search we're no longer showing.
// Drop a stale _yt (a reload starts on the normal feed; a search would re-spend quota), but
// KEEP _chan/_chanName so the open channel page survives F5 (channelView restores from it above).
const { _yt: _staleYt, ...rest } = window.history.state || {};
window.history.replaceState({ ...rest, sfPage: page }, "");
// A reload returns to the normal feed by dropping a stale _yt — EXCEPT when the search was
// scrape-sourced (zero quota): then we keep _yt + _ytScrape so it restores and re-fetches for
// free (ytSearch above already initialised from it). An api-source search is still dropped (it
// would re-spend ~100 units). _chan/_chanName are always kept so a channel page survives F5.
const st = window.history.state || {};
if (st._yt && st._ytScrape) {
window.history.replaceState({ ...st, sfPage: page }, "");
} else {
const { _yt: _staleYt, _ytScrape: _staleScrape, ...rest } = st;
window.history.replaceState({ ...rest, sfPage: page }, "");
}
function onPop(e: PopStateEvent) {
const p = (e.state?.sfPage as Page) ?? "feed";
// Guard a Back step that leaves Settings with unsaved changes: re-assert the Settings