diff --git a/BACKLOG.md b/BACKLOG.md index 0887d95..85a9737 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -16,7 +16,7 @@ bottom as they come up. ## UX / polish - [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. - [ ] Download/preload progress indicator on cards (percent or a bar, not just a pulse). - [ ] Remember and restore the last playback position per video. diff --git a/frontend/app.js b/frontend/app.js index ec23613..ccc0c50 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -561,6 +561,23 @@ function currentList() { 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 = ` +
+
+
+
+
+
`; + els.cards.appendChild(s); + } +} + function renderList() { if (view.type === 'settings') { renderSettings(); return; } @@ -956,15 +973,15 @@ function wireUI() { if (!q) return; view = { type: 'search' }; render(); - els.status.classList.remove('hidden'); - els.status.textContent = 'Searching…'; - els.cards.innerHTML = ''; + showSearchSkeletons(); try { const res = await API.search(q); if (!res || !res.ok) throw new Error(res?.error || 'Search failed'); searchResults = res.results || []; renderList(); } catch (err) { + els.cards.innerHTML = ''; + els.status.classList.remove('hidden'); els.status.textContent = '⚠ ' + err.message; } }); diff --git a/frontend/styles.css b/frontend/styles.css index 2288350..64f468a 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -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; } +/* ===================== 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 { display: flex; diff --git a/releases/YT Player_1.0.0_x64-setup.exe b/releases/YT Player_1.0.0_x64-setup.exe index d00d091..0dd335f 100755 Binary files a/releases/YT Player_1.0.0_x64-setup.exe and b/releases/YT Player_1.0.0_x64-setup.exe differ diff --git a/releases/YT Player_1.0.0_x64_en-US.msi b/releases/YT Player_1.0.0_x64_en-US.msi index f787171..064598c 100755 Binary files a/releases/YT Player_1.0.0_x64_en-US.msi and b/releases/YT Player_1.0.0_x64_en-US.msi differ diff --git a/releases/ytplayer.exe b/releases/ytplayer.exe index 2a09f10..af70ee4 100644 Binary files a/releases/ytplayer.exe and b/releases/ytplayer.exe differ