diff --git a/frontend/app.js b/frontend/app.js index ece54f9..b97451a 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -2,7 +2,8 @@ let currentPage = 1; const perPage = 12; const renderedVideoIds = new Set(); -let currentQuery = ""; +let hasNextPage = true; +let isLoading = false; // 2. Observer Definition (Must be defined before initApp uses it) const observer = new IntersectionObserver((entries) => { @@ -74,11 +75,15 @@ async function InitializeServerStatus() { async function loadVideos() { const session = JSON.parse(localStorage.getItem('session')); if (!session) return; + if (isLoading || !hasNextPage) return; + + const searchInput = document.getElementById('search-input'); + const query = searchInput ? searchInput.value : ""; // Build the request body let body = { channel: session.channel.id, - query: currentQuery, + query: query || "", page: currentPage, perPage: perPage, server: session.server @@ -94,6 +99,7 @@ async function loadVideos() { }); try { + isLoading = true; const response = await fetch('/api/videos', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -101,17 +107,22 @@ async function loadVideos() { }); const videos = await response.json(); renderVideos(videos); + hasNextPage = videos && videos.pageInfo ? videos.pageInfo.hasNextPage !== false : true; currentPage++; + ensureViewportFilled(); } catch (err) { console.error("Failed to load videos:", err); + } finally { + isLoading = false; } } function renderVideos(videos) { const grid = document.getElementById('video-grid'); if (!grid) return; - - videos.items.forEach(v => { + + const items = videos && Array.isArray(videos.items) ? videos.items : []; + items.forEach(v => { if (renderedVideoIds.has(v.id)) return; const card = document.createElement('div'); @@ -170,8 +181,8 @@ function closePlayer() { } function handleSearch(value) { - currentQuery = value || ""; currentPage = 1; + hasNextPage = true; renderedVideoIds.clear(); const grid = document.getElementById('video-grid'); if (grid) grid.innerHTML = ""; @@ -224,12 +235,23 @@ function buildDefaultOptions(channel) { function resetAndReload() { currentPage = 1; + hasNextPage = true; renderedVideoIds.clear(); const grid = document.getElementById('video-grid'); if (grid) grid.innerHTML = ""; loadVideos(); } +function ensureViewportFilled() { + if (!hasNextPage || isLoading) return; + const grid = document.getElementById('video-grid'); + if (!grid) return; + const contentHeight = grid.getBoundingClientRect().bottom; + if (contentHeight < window.innerHeight + 120) { + window.setTimeout(() => loadVideos(), 0); + } +} + function renderMenu() { const session = getSession(); const serverEntries = getServerEntries(); @@ -239,6 +261,7 @@ function renderMenu() { const sourcesList = document.getElementById('sources-list'); const addSourceBtn = document.getElementById('add-source-btn'); const sourceInput = document.getElementById('source-input'); + const reloadChannelBtn = document.getElementById('reload-channel-btn'); if (!sourceSelect || !channelSelect || !filtersContainer) return; @@ -389,6 +412,12 @@ function renderMenu() { } }; } + + if (reloadChannelBtn) { + reloadChannelBtn.onclick = () => { + resetAndReload(); + }; + } } function renderFilters(container, session) { diff --git a/frontend/favicon.ico b/frontend/favicon.ico new file mode 100644 index 0000000..4f48701 Binary files /dev/null and b/frontend/favicon.ico differ diff --git a/frontend/index.html b/frontend/index.html index eb43f06..0fe109b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -13,6 +13,9 @@