window.App = window.App || {};
App.videos = App.videos || {};
(function() {
const state = App.state;
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) App.videos.loadVideos();
}, {
threshold: 1.0
});
App.videos.observeSentinel = function() {
const sentinel = document.getElementById('sentinel');
if (sentinel) {
observer.observe(sentinel);
}
};
App.videos.formatDuration = function(seconds) {
if (!seconds || seconds <= 0) return '';
const totalSeconds = Math.floor(seconds);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
if (hours > 0) {
return `${hours}h ${String(minutes).padStart(2, '0')}m`;
}
return `${minutes}m`;
};
// Fetches the next page of videos and renders them into the grid.
App.videos.loadVideos = async function() {
const session = App.storage.getSession();
if (!session) return;
if (state.isLoading || !state.hasNextPage) return;
const searchInput = document.getElementById('search-input');
const query = searchInput ? searchInput.value : "";
let body = {
channel: session.channel.id,
query: query || "",
page: state.currentPage,
perPage: state.perPage,
server: session.server
};
Object.entries(session.options).forEach(([key, value]) => {
if (Array.isArray(value)) {
body[key] = value.map((entry) => entry.id).join(", ");
} else if (value && value.id) {
body[key] = value.id;
}
});
try {
state.isLoading = true;
App.videos.updateLoadMoreState();
state.currentLoadController = new AbortController();
const response = await fetch('/api/videos', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body),
signal: state.currentLoadController.signal
});
const videos = await response.json();
App.videos.renderVideos(videos);
state.hasNextPage = videos && videos.pageInfo ? videos.pageInfo.hasNextPage !== false : true;
state.currentPage++;
App.videos.ensureViewportFilled();
} catch (err) {
if (err.name !== 'AbortError') {
console.error("Failed to load videos:", err);
}
} finally {
state.isLoading = false;
state.currentLoadController = null;
App.videos.updateLoadMoreState();
}
};
// Renders new cards for videos, wiring favorites + playback behavior.
App.videos.renderVideos = function(videos) {
const grid = document.getElementById('video-grid');
if (!grid) return;
const items = videos && Array.isArray(videos.items) ? videos.items : [];
const favoritesSet = App.favorites.getSet();
items.forEach(v => {
if (state.renderedVideoIds.has(v.id)) return;
const card = document.createElement('div');
card.className = 'video-card';
const durationText = App.videos.formatDuration(v.duration);
const favoriteKey = App.favorites.getKey(v);
card.innerHTML = `
${durationText}
` : ''} `; const favoriteBtn = card.querySelector('.favorite-btn'); if (favoriteBtn && favoriteKey) { App.favorites.setButtonState(favoriteBtn, favoritesSet.has(favoriteKey)); favoriteBtn.onclick = (event) => { event.stopPropagation(); App.favorites.toggle(v); }; } card.onclick = () => App.player.open(v.url); grid.appendChild(card); state.renderedVideoIds.add(v.id); }); App.videos.ensureViewportFilled(); }; App.videos.handleSearch = function(value) { state.currentPage = 1; state.hasNextPage = true; state.renderedVideoIds.clear(); const grid = document.getElementById('video-grid'); if (grid) grid.innerHTML = ""; App.videos.updateLoadMoreState(); App.videos.loadVideos(); }; App.videos.resetAndReload = function() { if (state.currentLoadController) { state.currentLoadController.abort(); state.currentLoadController = null; state.isLoading = false; } state.currentPage = 1; state.hasNextPage = true; state.renderedVideoIds.clear(); const grid = document.getElementById('video-grid'); if (grid) grid.innerHTML = ""; App.videos.updateLoadMoreState(); App.videos.loadVideos(); }; App.videos.ensureViewportFilled = function() { if (!state.hasNextPage || state.isLoading) return; const grid = document.getElementById('video-grid'); if (!grid) return; const docHeight = document.documentElement.scrollHeight; if (docHeight <= window.innerHeight + 120) { window.setTimeout(() => App.videos.loadVideos(), 0); } }; App.videos.updateLoadMoreState = function() { const loadMoreBtn = document.getElementById('load-more-btn'); if (!loadMoreBtn) return; loadMoreBtn.disabled = state.isLoading || !state.hasNextPage; loadMoreBtn.style.display = state.hasNextPage ? 'flex' : 'none'; }; })();