remember recent searches on-device and show them under the search box

This commit is contained in:
Jonathan Sykes
2026-09-29 22:08:16 +08:00
parent b6fdff2438
commit 6480b314d5
3 changed files with 137 additions and 1 deletions

View File

@@ -81,6 +81,27 @@ function hevcCapable() {
} catch { return false; }
}
// Recent searches — device-local only (localStorage), never synced to the
// server or another device. Most-recent-first, case-insensitive de-dupe.
const RecentSearches = (() => {
const KEY = 'ytpRecentSearches';
const MAX = 10;
const load = () => { try { const l = JSON.parse(localStorage.getItem(KEY) || '[]'); return Array.isArray(l) ? l : []; } catch { return []; } };
const save = (list) => { try { localStorage.setItem(KEY, JSON.stringify(list)); } catch { /* storage full / blocked */ } };
return {
list: load,
add(q) {
q = (q || '').trim();
if (!q) return;
const list = load().filter((x) => x.toLowerCase() !== q.toLowerCase());
list.unshift(q);
save(list.slice(0, MAX));
},
remove(q) { save(load().filter((x) => x.toLowerCase() !== (q || '').toLowerCase())); },
clear() { save([]); },
};
})();
// OPFS bridge wrappers — return the same shape as the Tauri cache_* commands
// so the rest of app.js works without changes.
@@ -344,6 +365,7 @@ const els = {
status: $('status'),
searchForm: $('searchForm'),
searchInput: $('searchInput'),
searchRecent: $('searchRecent'),
playlistList: $('playlistList'),
newPlaylistBtn: $('newPlaylistBtn'),
audioOnlyToggle: $('audioOnlyToggle'),
@@ -8503,6 +8525,8 @@ function wireUI() {
e.preventDefault();
const q = els.searchInput.value.trim();
if (!q) return;
RecentSearches.add(q);
hideRecentSearches();
view = { type: 'search' };
render();
showSearchSkeletons();
@@ -8522,6 +8546,54 @@ function wireUI() {
}
});
// ---- Recent searches dropdown (device-local; see RecentSearches above) ----
function renderRecentSearches() {
const list = RecentSearches.list();
if (!list.length) { els.searchRecent.innerHTML = ''; return; }
els.searchRecent.innerHTML = list.map((q) => `
<div class="search-recent-row" data-q="${escapeHtml(q)}" role="option">
<span class="sr-icon">🕐</span><span class="sr-text">${escapeHtml(q)}</span>
<button type="button" class="sr-remove" data-remove="${escapeHtml(q)}" aria-label="Remove ‘${escapeHtml(q)}’ from recent searches">✕</button>
</div>`).join('') + '<button type="button" class="search-recent-clear" data-clear-all>Clear recent searches</button>';
}
function showRecentSearches() {
renderRecentSearches();
if (!RecentSearches.list().length) return;
els.searchRecent.classList.remove('hidden');
}
function hideRecentSearches() { els.searchRecent.classList.add('hidden'); }
els.searchInput.addEventListener('focus', showRecentSearches);
els.searchInput.addEventListener('input', () => {
if (els.searchInput.value.trim()) hideRecentSearches(); else showRecentSearches();
});
els.searchInput.addEventListener('keydown', (e) => { if (e.key === 'Escape') hideRecentSearches(); });
// A click on a row/remove/clear fires before the input's blur-driven hide
// would run, so use mousedown (fires first) rather than losing the target.
els.searchRecent.addEventListener('mousedown', (e) => {
const removeBtn = e.target.closest('[data-remove]');
if (removeBtn) {
e.preventDefault();
RecentSearches.remove(removeBtn.dataset.remove);
showRecentSearches();
return;
}
if (e.target.closest('[data-clear-all]')) {
e.preventDefault();
RecentSearches.clear();
hideRecentSearches();
return;
}
const row = e.target.closest('.search-recent-row');
if (row) {
e.preventDefault();
els.searchInput.value = row.dataset.q;
els.searchForm.requestSubmit();
}
});
document.addEventListener('click', (e) => {
if (!els.searchForm.contains(e.target)) hideRecentSearches();
});
document.querySelectorAll('.nav-item').forEach((b) => {
if (!b.dataset.view) return; // e.g. 📱 Remote opens a panel, not a view
b.addEventListener('click', () => { view = { type: b.dataset.view }; render(); });