diff --git a/frontend/app.js b/frontend/app.js index c47775a..8e99ed7 100755 --- a/frontend/app.js +++ b/frontend/app.js @@ -8014,15 +8014,24 @@ function wireUI() { scrollListIntoViewPortrait(); const metaBtn = $('searchUpdateBtn'); if (metaBtn) { metaBtn.disabled = true; metaBtn.textContent = 'Updating…'; } - let networkDone = false, paintedLocal = false; + let networkDone = false, paintedLocal = false, localHits = []; + // The server's own matches stay at the top; YouTube's answer is appended + // below them (no duplicates), so the list grows instead of being swapped. + const merged = (yt) => { + if (!localHits.length) return yt; + const seen = new Set(localHits.map((v) => v.id)); + return [...localHits, ...yt.filter((v) => !seen.has(v.id))]; + }; if (!instant && !refresh) { - // Known videos from the server's metadata store, painted until YouTube answers. + // Known videos from the server's metadata store, shown while YouTube searches. API.searchLocal(q).then((lr) => { if (networkDone || mySeq !== searchSeq || !lr || !lr.ok || !(lr.results || []).length) return; paintedLocal = true; + localHits = lr.results; searchResults = lr.results; searchAttempted = true; renderList(); + if (els.status) { els.status.classList.remove('hidden'); els.status.textContent = 'Searching YouTube for more…'; } }).catch(() => {}); } try { @@ -8030,7 +8039,8 @@ function wireUI() { networkDone = true; if (mySeq !== searchSeq) return; // superseded by a newer search if (!res || !res.ok) throw new Error(res?.error || 'Search failed'); - const results = res.results || []; + const results = merged(res.results || []); + if (els.status && paintedLocal) els.status.classList.add('hidden'); searchResults = results; searchAttempted = true; renderList(); @@ -8047,17 +8057,18 @@ function wireUI() { if (mySeq !== searchSeq || view.type !== 'search') return; try { const more = await API.search(q); - if (mySeq !== searchSeq || !more?.ok || (more.results || []).length <= results.length) return; - searchResults = more.results; + const full = merged(more?.results || []); + if (mySeq !== searchSeq || !more?.ok || full.length <= results.length) return; + searchResults = full; renderList(); - RecentSearches.cacheResults(q, more.results, Date.now()); + RecentSearches.cacheResults(q, full, Date.now()); showSearchMeta(q, Date.now()); } catch { /* keep the first page */ } }, 10000); } catch (err) { networkDone = true; if (mySeq !== searchSeq) return; - if (paintedLocal) return; // keep the server's known videos + if (paintedLocal) { if (els.status) els.status.classList.add('hidden'); toast('YouTube search failed — showing videos this server knows'); return; } // keep the server's known videos if (instant) { showSearchMeta(q, instant.ts); if (refresh) toast('Could not update: ' + err.message); return; } // keep the saved copy els.cards.innerHTML = ''; els.status.classList.remove('hidden'); diff --git a/frontend/styles.css b/frontend/styles.css index 3f864a0..f772af1 100755 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -1039,6 +1039,13 @@ input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.25); } backdrop-filter: blur(2px); } +/* Search/list cards: the server already holds a copy (fast start, no YouTube). */ +.thumb .server-badge { + position: absolute; left: 5px; top: 5px; display: inline-flex; align-items: center; gap: 3px; + padding: 2px 6px 2px 4px; border-radius: 5px; background: rgba(0,0,0,0.82); color: #fff; + font: 600 10px/1.2 var(--ui, system-ui), sans-serif; letter-spacing: 0; pointer-events: none; +} +.thumb .server-badge svg { width: 11px; height: 11px; fill: none; stroke: var(--ok, #4ade80); stroke-width: 2.2; stroke-linecap: round; } .card-info { min-width: 0; flex: 1; display: flex; flex-direction: column; justify-content: center; } .card-title { font-size: 13.5px; font-weight: 600; line-height: 1.32; diff --git a/frontend/views-core.js b/frontend/views-core.js index 56b705a..6051d94 100644 --- a/frontend/views-core.js +++ b/frontend/views-core.js @@ -1564,6 +1564,8 @@ function renderCard(v, index, list) { const card = document.createElement('div'); const isCached = cachedIds.has(v.id); const isDownloading = downloading.has(v.id); + if (v.onServer) knownServerCachedIds.add(v.id); + const onServer = !isCached && (v.onServer || knownServerCachedIds.has(v.id)); card.className = 'card' + (isCached ? ' cached' : '') + (isDownloading ? ' downloading' : '') + (selectedIds.has(v.id) ? ' selected' : ''); card.dataset.id = v.id; card._video = v; // read by f7-layout.js swipe actions @@ -1572,6 +1574,7 @@ function renderCard(v, index, list) {