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 */