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