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 {