Offer the searches from before while you type

Searches are kept in local storage and the five that best fit what is in
the box are offered under it: the ones it starts, then the ones it starts
a word of, then the ones it appears in anywhere, most recently searched
first. Walk them with the arrows, take one with enter or a tap, dismiss
with escape. The part that matched is picked out, and rows are built from
text nodes -- these strings are the reader's own and they are going back
on screen.

The box searches as you type, so there is no submit to record and storing
every pause would fill the list with the way to a query rather than the
query: a new entry swallows the ones it grew out of ("hot", "hote",
"hotel" leaves "hotel"), and going back to something shorter adds
nothing. Enter records straight away, since that is a reader saying they
meant it.

That turned up a bug the suggestions would have run into constantly: a
search started while the box's own last search was still out was dropped
-- loadVideos saw a load running and returned -- leaving the grid on the
previous query. Changing the result set now stops the load that is
running before starting the next one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPZFnNdHbPGDTqQUNiE4ZN
This commit is contained in:
Simon
2026-09-22 14:52:50 +00:00
parent 553d610e5b
commit f977852253
7 changed files with 569 additions and 2 deletions

View File

@@ -48,6 +48,73 @@ App.session = App.session || {};
localStorage.setItem(FEED_END_BEHAVIOR_KEY, nextBehavior === 'scroll' ? 'scroll' : 'loop');
};
// ---------------------------------------------------------------------
// Searches
//
// What was searched for before, newest first. The grid searches as you
// type, so the box never sees a "submit" to record: every pause in the
// typing is a search of its own and storing each one would fill the list
// with the way to a query rather than the query. So a new entry swallows
// the ones it grew out of -- typing "hot", "hote", "hotel" leaves "hotel"
// -- and going back to something shorter than what is already stored adds
// nothing, since that is a step back through the same typing.
// ---------------------------------------------------------------------
const SEARCH_HISTORY_MAX = 50;
App.storage.getSearches = function() {
try {
const stored = JSON.parse(localStorage.getItem(App.constants.SEARCH_HISTORY_KEY));
return Array.isArray(stored) ? stored.filter((q) => typeof q === 'string' && q.trim()) : [];
} catch (err) {
return [];
}
};
App.storage.rememberSearch = function(query) {
const next = (query || '').trim();
if (!next) return App.storage.getSearches();
const lower = next.toLowerCase();
const stored = App.storage.getSearches();
// Already on the way to something longer: nothing new to record.
if (stored.some((q) => q.toLowerCase() !== lower && q.toLowerCase().startsWith(lower))) {
return stored;
}
const kept = stored.filter((q) => !lower.startsWith(q.toLowerCase()));
kept.unshift(next);
const capped = kept.slice(0, SEARCH_HISTORY_MAX);
try {
localStorage.setItem(App.constants.SEARCH_HISTORY_KEY, JSON.stringify(capped));
} catch (err) { /* a full or blocked store is not worth failing a search over */ }
return capped;
};
App.storage.forgetSearches = function() {
localStorage.removeItem(App.constants.SEARCH_HISTORY_KEY);
};
// The past searches worth offering for what is being typed, best first.
// Best means where the typing lands in them -- at the front, at the start
// of a word, anywhere at all -- and then which was searched for most
// recently. What is already typed in full is not a suggestion.
App.storage.suggestSearches = function(query, limit) {
const want = Math.max(1, limit || 5);
const typed = (query || '').trim().toLowerCase();
const scored = [];
App.storage.getSearches().forEach((entry, age) => {
const candidate = entry.toLowerCase();
if (candidate === typed) return;
let rank = 0;
if (typed) {
const at = candidate.indexOf(typed);
if (at < 0) return;
rank = at === 0 ? 3 : (candidate[at - 1] === ' ' ? 2 : 1);
}
scored.push({ entry: entry, rank: rank, age: age });
});
scored.sort((a, b) => (b.rank - a.rank) || (a.age - b.age));
return scored.slice(0, want).map((s) => s.entry);
};
// Grid density: 'comfortable' (default) or 'compact' (more, smaller columns).
App.storage.getDensity = function() {
return localStorage.getItem('density') === 'compact' ? 'compact' : 'comfortable';