Add loading skeletons for search results
Replace bare 'Searching…' text with 8 animated skeleton cards that mirror the real card layout (thumbnail, title, channel, menu dot). Shimmer animation sweeps across placeholder blocks until results arrive. Files: - frontend/app.js: showSearchSkeletons() function, wired into search submit - frontend/styles.css: .skeleton-card, .skeleton-shimmer, .skeleton-line, .skeleton-dot styles with shimmer animation
This commit is contained in:
@@ -16,7 +16,7 @@ bottom as they come up.
|
|||||||
|
|
||||||
## UX / polish
|
## UX / polish
|
||||||
- [x] Empty-state designs for empty playlists and empty history (match the new hero).
|
- [x] Empty-state designs for empty playlists and empty history (match the new hero).
|
||||||
- [ ] Loading skeletons for search results instead of a bare "Searching…".
|
- [x] Loading skeletons for search results instead of a bare "Searching…".
|
||||||
- [ ] Keyboard-shortcut help overlay (press `?`), listing space / arrows / f / m.
|
- [ ] Keyboard-shortcut help overlay (press `?`), listing space / arrows / f / m.
|
||||||
- [ ] Download/preload progress indicator on cards (percent or a bar, not just a pulse).
|
- [ ] Download/preload progress indicator on cards (percent or a bar, not just a pulse).
|
||||||
- [ ] Remember and restore the last playback position per video.
|
- [ ] Remember and restore the last playback position per video.
|
||||||
|
|||||||
@@ -561,6 +561,23 @@ function currentList() {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showSearchSkeletons() {
|
||||||
|
els.status.classList.add('hidden');
|
||||||
|
els.cards.innerHTML = '';
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const s = document.createElement('div');
|
||||||
|
s.className = 'card skeleton-card';
|
||||||
|
s.innerHTML = `
|
||||||
|
<div class="thumb skeleton-shimmer"></div>
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="skeleton-line skeleton-shimmer" style="width:85%"></div>
|
||||||
|
<div class="skeleton-line skeleton-shimmer" style="width:55%;margin-top:8px"></div>
|
||||||
|
</div>
|
||||||
|
<div class="skeleton-dot skeleton-shimmer"></div>`;
|
||||||
|
els.cards.appendChild(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderList() {
|
function renderList() {
|
||||||
if (view.type === 'settings') { renderSettings(); return; }
|
if (view.type === 'settings') { renderSettings(); return; }
|
||||||
|
|
||||||
@@ -956,15 +973,15 @@ function wireUI() {
|
|||||||
if (!q) return;
|
if (!q) return;
|
||||||
view = { type: 'search' };
|
view = { type: 'search' };
|
||||||
render();
|
render();
|
||||||
els.status.classList.remove('hidden');
|
showSearchSkeletons();
|
||||||
els.status.textContent = 'Searching…';
|
|
||||||
els.cards.innerHTML = '';
|
|
||||||
try {
|
try {
|
||||||
const res = await API.search(q);
|
const res = await API.search(q);
|
||||||
if (!res || !res.ok) throw new Error(res?.error || 'Search failed');
|
if (!res || !res.ok) throw new Error(res?.error || 'Search failed');
|
||||||
searchResults = res.results || [];
|
searchResults = res.results || [];
|
||||||
renderList();
|
renderList();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
els.cards.innerHTML = '';
|
||||||
|
els.status.classList.remove('hidden');
|
||||||
els.status.textContent = '⚠ ' + err.message;
|
els.status.textContent = '⚠ ' + err.message;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -558,6 +558,53 @@ input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.25); }
|
|||||||
|
|
||||||
.status { padding: 16px 20px; color: var(--text-dim); font-size: 13px; }
|
.status { padding: 16px 20px; color: var(--text-dim); font-size: 13px; }
|
||||||
|
|
||||||
|
/* ===================== Skeleton loading ===================== */
|
||||||
|
.skeleton-card {
|
||||||
|
cursor: default !important;
|
||||||
|
pointer-events: none;
|
||||||
|
border-color: transparent !important;
|
||||||
|
animation: cardIn 0.45s var(--ease) backwards !important;
|
||||||
|
}
|
||||||
|
.skeleton-shimmer {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg-3);
|
||||||
|
border-radius: 9px;
|
||||||
|
}
|
||||||
|
.skeleton-shimmer::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
rgba(255,255,255,0.04) 40%,
|
||||||
|
rgba(255,255,255,0.07) 50%,
|
||||||
|
rgba(255,255,255,0.04) 60%,
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
animation: shimmer 1.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { transform: translateX(-100%); }
|
||||||
|
100% { transform: translateX(100%); }
|
||||||
|
}
|
||||||
|
.skeleton-card .thumb {
|
||||||
|
background: var(--bg-3);
|
||||||
|
}
|
||||||
|
.skeleton-line {
|
||||||
|
height: 14px;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.skeleton-dot {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
align-self: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===================== Empty state ===================== */
|
/* ===================== Empty state ===================== */
|
||||||
.empty-state {
|
.empty-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user