remember recent searches on-device and show them under the search box
This commit is contained in:
@@ -81,6 +81,27 @@ function hevcCapable() {
|
|||||||
} catch { return false; }
|
} 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
|
// OPFS bridge wrappers — return the same shape as the Tauri cache_* commands
|
||||||
// so the rest of app.js works without changes.
|
// so the rest of app.js works without changes.
|
||||||
|
|
||||||
@@ -344,6 +365,7 @@ const els = {
|
|||||||
status: $('status'),
|
status: $('status'),
|
||||||
searchForm: $('searchForm'),
|
searchForm: $('searchForm'),
|
||||||
searchInput: $('searchInput'),
|
searchInput: $('searchInput'),
|
||||||
|
searchRecent: $('searchRecent'),
|
||||||
playlistList: $('playlistList'),
|
playlistList: $('playlistList'),
|
||||||
newPlaylistBtn: $('newPlaylistBtn'),
|
newPlaylistBtn: $('newPlaylistBtn'),
|
||||||
audioOnlyToggle: $('audioOnlyToggle'),
|
audioOnlyToggle: $('audioOnlyToggle'),
|
||||||
@@ -8503,6 +8525,8 @@ function wireUI() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const q = els.searchInput.value.trim();
|
const q = els.searchInput.value.trim();
|
||||||
if (!q) return;
|
if (!q) return;
|
||||||
|
RecentSearches.add(q);
|
||||||
|
hideRecentSearches();
|
||||||
view = { type: 'search' };
|
view = { type: 'search' };
|
||||||
render();
|
render();
|
||||||
showSearchSkeletons();
|
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) => {
|
document.querySelectorAll('.nav-item').forEach((b) => {
|
||||||
if (!b.dataset.view) return; // e.g. 📱 Remote opens a panel, not a view
|
if (!b.dataset.view) return; // e.g. 📱 Remote opens a panel, not a view
|
||||||
b.addEventListener('click', () => { view = { type: b.dataset.view }; render(); });
|
b.addEventListener('click', () => { view = { type: b.dataset.view }; render(); });
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
<button type="submit" class="search-btn">Search</button>
|
<button type="submit" class="search-btn">Search</button>
|
||||||
|
<div id="searchRecent" class="search-recent hidden" role="listbox" aria-label="Recent searches"></div>
|
||||||
</form>
|
</form>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -273,7 +273,70 @@ body::after {
|
|||||||
background: linear-gradient(180deg, rgba(16,16,19,0.7), transparent);
|
background: linear-gradient(180deg, rgba(16,16,19,0.7), transparent);
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
}
|
}
|
||||||
.search-form { flex: 1; display: flex; gap: 10px; max-width: 760px; }
|
.search-form { flex: 1; display: flex; gap: 10px; max-width: 760px; position: relative; }
|
||||||
|
|
||||||
|
/* Recent-searches dropdown — device-local, kept in localStorage (RecentSearches in app.js) */
|
||||||
|
.search-recent {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 8px);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 60;
|
||||||
|
background: var(--bg-2);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 18px 40px -12px rgba(0, 0, 0, 0.5);
|
||||||
|
padding: 6px;
|
||||||
|
max-height: 320px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.search-recent.hidden { display: none; }
|
||||||
|
.search-recent-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 9px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-2);
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: var(--ui);
|
||||||
|
}
|
||||||
|
.search-recent-row:hover, .search-recent-row:focus-visible { background: var(--bg-3); color: var(--text); }
|
||||||
|
.search-recent-row .sr-icon { flex-shrink: 0; opacity: 0.55; font-size: 12px; }
|
||||||
|
.search-recent-row .sr-text {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.search-recent-row .sr-remove {
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 3px 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.search-recent-row .sr-remove:hover { color: #fff; background: var(--accent); }
|
||||||
|
.search-recent-clear {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--line-soft);
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-family: var(--ui);
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 8px 6px 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.search-recent-clear:hover { color: var(--text); }
|
||||||
|
|
||||||
/* Hamburger toggle — hidden on desktop, revealed in the mobile breakpoint */
|
/* Hamburger toggle — hidden on desktop, revealed in the mobile breakpoint */
|
||||||
.sidebar-toggle {
|
.sidebar-toggle {
|
||||||
|
|||||||
Reference in New Issue
Block a user