diff --git a/frontend/app.js b/frontend/app.js index 542275b..b2a0960 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -112,6 +112,7 @@ async function loadVideos() { try { isLoading = true; + updateLoadMoreState(); const response = await fetch('/api/videos', { method: 'POST', headers: { @@ -128,6 +129,7 @@ async function loadVideos() { console.error("Failed to load videos:", err); } finally { isLoading = false; + updateLoadMoreState(); } } @@ -179,6 +181,13 @@ async function initApp() { observer.observe(sentinel); } + const loadMoreBtn = document.getElementById('load-more-btn'); + if (loadMoreBtn) { + loadMoreBtn.onclick = () => { + loadVideos(); + }; + } + await loadVideos(); } @@ -263,6 +272,7 @@ function handleSearch(value) { renderedVideoIds.clear(); const grid = document.getElementById('video-grid'); if (grid) grid.innerHTML = ""; + updateLoadMoreState(); loadVideos(); } @@ -371,6 +381,7 @@ function resetAndReload() { renderedVideoIds.clear(); const grid = document.getElementById('video-grid'); if (grid) grid.innerHTML = ""; + updateLoadMoreState(); loadVideos(); } @@ -384,6 +395,13 @@ function ensureViewportFilled() { } } +function updateLoadMoreState() { + const loadMoreBtn = document.getElementById('load-more-btn'); + if (!loadMoreBtn) return; + loadMoreBtn.disabled = isLoading || !hasNextPage; + loadMoreBtn.style.display = hasNextPage ? 'flex' : 'none'; +} + function renderMenu() { const session = getSession(); const serverEntries = getServerEntries(); diff --git a/frontend/index.html b/frontend/index.html index 0fe109b..bf4ff2e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -81,11 +81,14 @@
+ diff --git a/frontend/style.css b/frontend/style.css index a07c732..6dfb194 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -122,6 +122,11 @@ body.theme-light { color: var(--accent); } +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + /* CDN-served icon images (Heroicons) */ .icon-svg { width: 24px; @@ -450,6 +455,34 @@ body.theme-light .setting-item select option { display: block; } +.load-more-btn { + position: sticky; + left: 50%; + transform: translateX(-50%); + margin: 16px auto 32px auto; + width: 52px; + height: 52px; + border-radius: 999px; + border: 1px solid var(--border); + background: var(--bg-secondary); + color: var(--text-primary); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + z-index: 10; +} + +.load-more-btn:hover { + background: var(--bg-tertiary); +} + +.load-more-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + /* Grid Container */ .grid-container { display: grid; @@ -465,6 +498,88 @@ body.theme-light .setting-item select option { gap: 12px; padding: 16px; } + + .top-bar { + flex-wrap: wrap; + height: auto; + padding: 12px 16px; + } + + .search-container { + order: 3; + width: 100%; + max-width: 100%; + } + + .actions { + order: 2; + width: 100%; + justify-content: flex-end; + } +} + +@media (max-width: 480px) { + .logo { + font-size: 18px; + } + + .icon-btn { + width: 44px; + height: 44px; + } +} + +@media (min-width: 1600px) { + body { + font-size: 16px; + } + + .top-bar { + height: 72px; + padding: 0 36px; + } + + .grid-container { + gap: 24px; + padding: 32px 48px; + } + + .video-card h4 { + font-size: 16px; + } + + .video-card p { + font-size: 13px; + } +} + +@media (min-width: 1920px) { + .grid-container { + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + } +} + +@media (pointer: coarse) { + .icon-btn { + width: 52px; + height: 52px; + } + + .btn-secondary { + padding: 10px 14px; + } + + .setting-item select, + .input-row input { + padding: 10px 14px; + } +} + +@media (prefers-reduced-motion: reduce) { + * { + transition: none !important; + animation: none !important; + } } /* Video Card */