diff --git a/frontend/app.js b/frontend/app.js index 028beb1..d7bf2e6 100755 --- a/frontend/app.js +++ b/frontend/app.js @@ -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) => ` +
+ 🕐${escapeHtml(q)} + +
`).join('') + ''; + } + 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(); }); diff --git a/frontend/index.html b/frontend/index.html index 679caa9..90eef71 100755 --- a/frontend/index.html +++ b/frontend/index.html @@ -100,6 +100,7 @@ autocomplete="off" /> + diff --git a/frontend/styles.css b/frontend/styles.css index 054f556..863574c 100755 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -273,7 +273,70 @@ body::after { background: linear-gradient(180deg, rgba(16,16,19,0.7), transparent); 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 */ .sidebar-toggle {