// KV-Tube Main JavaScript - YouTube Clone // Re-usable init function for SPA window.initApp = function () { const searchInput = document.getElementById('searchInput'); const resultsArea = document.getElementById('resultsArea'); // cleanup previous observers if any if (window.currentObserver) { window.currentObserver.disconnect(); } // Check APP_CONFIG if available (set in index.html) const socketConfig = window.APP_CONFIG || {}; const pageType = socketConfig.page || 'home'; if (searchInput) { // Clear previous event listeners to avoid duplicates (optional, but safer to just re-attach if we are careful) // Actually, searchInput is in the header, which is NOT replaced. // So we should NOT re-attach listener to searchInput every time. // We need to check if we already attached it. if (!searchInput.dataset.listenerAttached) { searchInput.addEventListener('keypress', async (e) => { if (e.key === 'Enter') { e.preventDefault(); const query = searchInput.value.trim(); if (query) { // Use navigation manager if available if (window.navigationManager) { window.navigationManager.navigateTo(`/results?search_query=${encodeURIComponent(query)}`); } else { window.location.href = `/results?search_query=${encodeURIComponent(query)}`; } } } }); searchInput.dataset.listenerAttached = 'true'; } // Handle Page Initialization - only if resultsArea exists (not on channel.html) if (resultsArea) { if (pageType === 'channel' && socketConfig.channelId) { console.log("Loading Channel:", socketConfig.channelId); loadChannelVideos(socketConfig.channelId); } else if (pageType === 'results' || socketConfig.query) { const q = socketConfig.query || new URLSearchParams(window.location.search).get('search_query'); if (q) { if (searchInput) searchInput.value = q; searchYouTube(q); } } else { // Default Home // Check if we are actually on home page based on URL or Config if (pageType === 'home') { loadTrending(); } } // Init Infinite Scroll initInfiniteScroll(); } } // Init Theme (check if already init) initTheme(); // Restore sidebar state from localStorage to prevent layout shift initSidebarState(); // Check for category in URL if we are on home and need to switch const urlParams = new URLSearchParams(window.location.search); const category = urlParams.get('category'); if (category && typeof switchCategory === 'function' && pageType === 'home') { // We might have already loaded trending above, but switchCategory handles UI state // It also triggers a load, so maybe we want to avoid double loading. // But switchCategory also sets the active pill. // Let's just set the active pill visually for now if we already loaded trending. const pill = document.querySelector(`.yt-chip[onclick*="'${category}'"]`); if (pill) { document.querySelectorAll('.yt-category-pill, .yt-chip').forEach(b => b.classList.remove('active')); pill.classList.add('active'); } // If switchCategory is called it will re-fetch. } }; document.addEventListener('DOMContentLoaded', window.initApp); // Note: Global variables like currentCategory are defined below let currentCategory = 'all'; let currentPage = 1; let isLoading = false; let hasMore = true; // Track if there are more videos to load // --- Lazy Loading --- const imageObserver = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; const src = img.getAttribute('data-src'); if (src) { img.src = src; img.onload = () => img.classList.add('loaded'); img.removeAttribute('data-src'); } observer.unobserve(img); } }); }, { rootMargin: '50px 0px', threshold: 0.1 }); window.observeImages = function () { document.querySelectorAll('img[data-src]').forEach(img => { imageObserver.observe(img); }); }; // --- Infinite Scroll --- function initInfiniteScroll() { const observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && !isLoading && hasMore) { loadMore(); } }, { rootMargin: '200px' }); // Create sentinel logic or observe existing footer/element // We'll observe a sentinel element at the bottom of the grid // Create sentinel logic or observe existing footer/element // We'll observe a sentinel element at the bottom of the grid const resultsArea = document.getElementById('resultsArea'); if (!resultsArea) return; // Exit if not on home page const sentinel = document.createElement('div'); sentinel.id = 'scroll-sentinel'; sentinel.style.width = '100%'; sentinel.style.height = '20px'; resultsArea.parentNode.appendChild(sentinel); observer.observe(sentinel); } // --- UI Helpers --- function renderSkeleton() { // Generate 8 skeleton cards return Array(8).fill(0).map(() => `
`).join(''); } function renderNoContent(message = 'Try searching for something else', title = 'No videos found') { return `
${title}
${message}
`; } // Render homepage with personalized sections function renderHomepageSections(sections, container, localHistory = []) { // Check if container exists if (!container) { console.warn('renderHomepageSections: container is null'); return; } // Create a map for quick history lookup const historyMap = {}; localHistory.forEach(v => { if (v && v.id) historyMap[v.id] = v; }); sections.forEach(section => { if (!section.videos || section.videos.length === 0) return; // Create section wrapper const sectionEl = document.createElement('div'); sectionEl.className = 'yt-homepage-section'; sectionEl.id = `section-${section.id}`; // Section header const header = document.createElement('div'); header.className = 'yt-section-header'; header.innerHTML = `

${escapeHtml(section.title)}

`; sectionEl.appendChild(header); // Video grid for this section const grid = document.createElement('div'); grid.className = 'yt-video-grid'; // LIMIT VISIBLE VIDEOS TO 8 (2 rows of 4 on desktop) const INITIAL_LIMIT = 8; const hasMore = section.videos.length > INITIAL_LIMIT; section.videos.forEach((video, index) => { // For continue watching if (video._from_history && historyMap[video.id]) { const hist = historyMap[video.id]; video.title = hist.title || video.title; video.uploader = hist.uploader || video.uploader; video.thumbnail = hist.thumbnail || video.thumbnail; } const card = document.createElement('div'); card.className = 'yt-video-card'; // Hide videos beyond limit initially if (index >= INITIAL_LIMIT) { card.classList.add('yt-hidden-video'); card.style.display = 'none'; } card.innerHTML = `
${escapeHtml(video.title || 'Video')} ${video.duration ? `${video.duration}` : ''}
${video.uploader ? video.uploader.charAt(0).toUpperCase() : 'Y'}

${escapeHtml(video.title || 'Unknown')}

${escapeHtml(video.uploader || 'Unknown')}

${formatViews(video.view_count)} views${video.upload_date ? ' • ' + formatDate(video.upload_date) : ''}

`; card.addEventListener('click', () => { const params = new URLSearchParams({ v: video.id, title: video.title || '', uploader: video.uploader || '', thumbnail: video.thumbnail || '' }); const dest = `/watch?${params.toString()}`; if (window.navigationManager) { window.navigationManager.navigateTo(dest); } else { window.location.href = dest; } }); grid.appendChild(card); }); sectionEl.appendChild(grid); // ADD LOAD MORE BUTTON IF NEEDED if (hasMore) { const btnContainer = document.createElement('div'); btnContainer.className = 'yt-section-footer'; btnContainer.style.textAlign = 'center'; btnContainer.style.padding = '10px'; const btn = document.createElement('button'); btn.className = 'yt-action-btn'; // Re-use existing or generic class btn.style.padding = '8px 24px'; btn.style.borderRadius = '18px'; btn.style.border = '1px solid var(--yt-border)'; btn.style.background = 'var(--yt-bg-secondary)'; btn.style.color = 'var(--yt-text-primary)'; btn.style.cursor = 'pointer'; btn.style.fontWeight = '500'; btn.innerText = 'Show more'; btn.onmouseover = () => btn.style.background = 'var(--yt-bg-hover)'; btn.onmouseout = () => btn.style.background = 'var(--yt-bg-secondary)'; btn.onclick = function () { // Reveal hidden videos const hidden = grid.querySelectorAll('.yt-hidden-video'); hidden.forEach(el => el.style.display = 'flex'); // Restore display btnContainer.remove(); // Remove button }; btnContainer.appendChild(btn); sectionEl.appendChild(btnContainer); } container.appendChild(sectionEl); }); if (window.observeImages) window.observeImages(); } async function searchYouTube(query) { if (isLoading) return; const resultsArea = document.getElementById('resultsArea'); const loadMoreArea = document.getElementById('loadMoreArea'); isLoading = true; resultsArea.innerHTML = renderSkeleton(); try { const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`); const data = await response.json(); if (data.error) { resultsArea.innerHTML = `

Error: ${data.error}

`; return; } displayResults(data, false); if (loadMoreArea) loadMoreArea.style.display = 'none'; } catch (error) { console.error('Search error:', error); resultsArea.innerHTML = `

Failed to fetch results

`; } finally { isLoading = false; } } // Switch category async function switchCategory(category, btn) { if (isLoading) return; // Update UI (Pills) document.querySelectorAll('.yt-category-pill').forEach(b => b.classList.remove('active')); if (btn && btn.classList) btn.classList.add('active'); // Update UI (Sidebar) document.querySelectorAll('.yt-sidebar-item').forEach(item => { item.classList.remove('active'); if (item.getAttribute('data-category') === category) { item.classList.add('active'); } }); // Reset state currentCategory = category; currentPage = 1; window.currentPage = 1; hasMore = true; // Reset infinite scroll const resultsArea = document.getElementById('resultsArea'); const videosSection = document.getElementById('videosSection'); // Show resultsArea (may have been hidden by homepage sections) resultsArea.style.display = ''; // Remove any homepage sections if (videosSection) { videosSection.querySelectorAll('.yt-homepage-section').forEach(el => el.remove()); } resultsArea.innerHTML = renderSkeleton(); // Hide pagination while loading const paginationArea = document.getElementById('paginationArea'); if (paginationArea) paginationArea.style.display = 'none'; // Handle Shorts Layout const shortsSection = document.getElementById('shortsSection'); // videosSection already declared above if (shortsSection) { if (category === 'shorts') { shortsSection.style.display = 'none'; // Hide carousel, show grid in results if (videosSection) videosSection.querySelector('h2').style.display = 'none'; // Optional: hide "Videos" header } else { shortsSection.style.display = 'block'; if (videosSection) videosSection.querySelector('h2').style.display = 'flex'; } } // Handle Special Categories if (category === 'history') { const response = await fetch('/api/history'); const data = await response.json(); displayResults(data, false); isLoading = false; return; } if (category === 'suggested') { // Build query params from localStorage history const history = JSON.parse(localStorage.getItem('kv_history') || '[]'); const titles = history.slice(0, 5).map(v => v.title).filter(Boolean).join(','); const channels = history.slice(0, 3).map(v => v.uploader).filter(Boolean).join(','); let url = '/api/suggested'; const params = new URLSearchParams(); if (titles) params.append('titles', titles); if (channels) params.append('channels', channels); if (params.toString()) url += '?' + params.toString(); const response = await fetch(url); const data = await response.json(); displayResults(data, false); isLoading = false; return; } // Load both videos and shorts with current category, sort, and region await loadTrending(true); // Also reload shorts to match category if (typeof loadShorts === 'function') { loadShorts(); } // Render pagination if (typeof renderPagination === 'function') { renderPagination(); } } // Load more videos async function loadMore() { currentPage++; await loadTrending(false); } // Load trending videos async function loadTrending(reset = true) { if (isLoading && reset) isLoading = false; const resultsArea = document.getElementById('resultsArea'); const loadMoreArea = document.getElementById('loadMoreArea'); const loadMoreBtn = document.getElementById('loadMoreBtn'); if (!resultsArea) return; // Exit if not on home page isLoading = true; if (!reset && loadMoreBtn) { loadMoreBtn.innerHTML = ' Loading...'; } else if (reset) { resultsArea.innerHTML = renderSkeleton(); } try { const regionValue = window.currentRegion || 'vietnam'; // For 'all' category, use new homepage API with personalization if (currentCategory === 'all') { // Build personalization params from localStorage const history = JSON.parse(localStorage.getItem('kv_history') || '[]'); const subscriptions = JSON.parse(localStorage.getItem('kv_subscriptions') || '[]'); const params = new URLSearchParams(); params.append('region', regionValue); params.append('page', currentPage); // Add Pagination params.append('_', Date.now()); // Cache buster if (history.length > 0 && reset) { // Only send history on first page for relevance const historyIds = history.slice(0, 10).map(v => v.id).filter(Boolean); const historyTitles = history.slice(0, 5).map(v => v.title).filter(Boolean); const historyChannels = history.slice(0, 5).map(v => v.uploader).filter(Boolean); if (historyIds.length) params.append('history', historyIds.join(',')); if (historyTitles.length) params.append('titles', historyTitles.join(',')); if (historyChannels.length) params.append('channels', historyChannels.join(',')); } if (subscriptions.length > 0 && reset) { const subIds = subscriptions.slice(0, 10).map(s => s.id).filter(Boolean); if (subIds.length) params.append('subs', subIds.join(',')); } // Show skeleton for infinite scroll if (!reset) { const videosSection = document.getElementById('videosSection'); // Avoid duplicates if (!document.getElementById('infinite-scroll-skeleton')) { const skelDiv = document.createElement('div'); skelDiv.id = 'infinite-scroll-skeleton'; skelDiv.className = 'yt-video-grid'; skelDiv.style.marginTop = '20px'; skelDiv.innerHTML = renderSkeleton(); // Reuse existing skeleton generator videosSection.appendChild(skelDiv); } } const response = await fetch(`/api/homepage?${params.toString()}`); const data = await response.json(); if (data.mode === 'sections' && data.data) { // Hide the grid-based resultsArea and render sections to parent resultsArea.style.display = 'none'; const videosSection = document.getElementById('videosSection'); if (reset) { // Remove previous sections if reset videosSection.querySelectorAll('.yt-homepage-section').forEach(el => el.remove()); } // Remove infinite scroll skeleton if it exists const existingSkeleton = document.getElementById('infinite-scroll-skeleton'); if (existingSkeleton) existingSkeleton.remove(); // Append new sections (for Infinite Scroll) renderHomepageSections(data.data, videosSection, history); isLoading = false; hasMore = data.data.length > 0; // Continue if we got sections return; } } // Fallback: Original trending logic for category pages const sortValue = window.currentSort || 'month'; const cb = reset ? `&_=${Date.now()}` : ''; const response = await fetch(`/api/trending?category=${currentCategory}&page=${currentPage}&sort=${sortValue}®ion=${regionValue}${cb}`); const data = await response.json(); if (data.error) { console.error('Trending error:', data.error); if (reset) { resultsArea.innerHTML = renderNoContent(`Error: ${data.error}`, 'Something went wrong'); } return; } if (data.mode === 'sections') { if (reset) resultsArea.innerHTML = ''; // Flatten all section videos into a single unified grid // User requested single vertical scroll instead of per-section carousels let allVideos = []; data.data.forEach(section => { const videos = section.videos || []; // Add section info to each video for potential category display videos.forEach(video => { video._sectionId = section.id; video._sectionTitle = section.title; }); allVideos = allVideos.concat(videos); }); // Render all videos in a unified grid allVideos.forEach(video => { const card = document.createElement('div'); card.className = 'yt-video-card'; card.innerHTML = `
${escapeHtml(video.title)} ${video.duration ? `${video.duration}` : ''}
${video.uploader ? video.uploader.charAt(0).toUpperCase() : 'Y'}

${escapeHtml(video.title)}

${escapeHtml(video.uploader || 'Unknown')}

${formatViews(video.view_count)} views

`; card.onclick = () => { const params = new URLSearchParams({ v: video.id, title: video.title || '', uploader: video.uploader || '', thumbnail: video.thumbnail || '' }); const dest = `/watch?${params.toString()}`; if (window.navigationManager) { window.navigationManager.navigateTo(dest); } else { window.location.href = dest; } }; resultsArea.appendChild(card); }); if (window.observeImages) window.observeImages(); return; } if (reset) resultsArea.innerHTML = ''; if (data.length === 0) { if (reset) { resultsArea.innerHTML = renderNoContent(); } hasMore = false; // Only stop if we get no results at all } else { displayResults(data, !reset); // Keep loading unless we got 0 videos // Multi-category API returns variable amounts, so don't limit by 20 hasMore = data.length > 0; } } catch (e) { console.error('Failed to load trending:', e); if (reset) { resultsArea.innerHTML = `

Connection error

`; } } finally { isLoading = false; } } // Display results with YouTube-style cards function displayResults(videos, append = false) { const resultsArea = document.getElementById('resultsArea'); if (!append) resultsArea.innerHTML = ''; if (videos.length === 0 && !append) { resultsArea.innerHTML = renderNoContent(); return; } videos.forEach(video => { const card = document.createElement('div'); if (currentCategory === 'shorts') { // Render as Short Card (Vertical) card.className = 'yt-short-card'; // Adjust styling for grid view if needed card.style.width = '100%'; card.style.maxWidth = '200px'; card.innerHTML = `

${escapeHtml(video.title)}

${formatViews(video.view_count)} views

`; } else { // Render as Standard Video Card card.className = 'yt-video-card'; card.innerHTML = `
${escapeHtml(video.title)} ${video.duration ? `${video.duration}` : ''}
${video.uploader ? video.uploader.charAt(0).toUpperCase() : 'Y'}

${escapeHtml(video.title)}

${escapeHtml(video.uploader || 'Unknown')}

${formatViews(video.view_count)} views • ${formatDate(video.upload_date)}

`; } card.addEventListener('click', (e) => { // Prevent navigation if clicking on channel link if (e.target.closest('.yt-channel-link')) return; const params = new URLSearchParams({ v: video.id, title: video.title || '', uploader: video.uploader || '', thumbnail: video.thumbnail || '' }); const dest = `/watch?${params.toString()}`; if (window.navigationManager) { window.navigationManager.navigateTo(dest); } else { window.location.href = dest; } }); resultsArea.appendChild(card); }); if (window.observeImages) window.observeImages(); } // Format view count (YouTube style) function formatViews(views) { if (!views) return '0'; const num = parseInt(views); if (num >= 1000000000) return (num / 1000000000).toFixed(1) + 'B'; if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'; if (num >= 1000) return (num / 1000).toFixed(1) + 'K'; return num.toLocaleString(); } // Format date (YouTube style: "2 hours ago", "3 days ago", etc.) function formatDate(dateStr) { if (!dateStr) return 'Recently'; // Ensure string dateStr = String(dateStr); console.log('[Debug] formatDate input:', dateStr); // Handle YYYYMMDD format if (/^\d{8}$/.test(dateStr)) { const year = dateStr.substring(0, 4); const month = dateStr.substring(4, 6); const day = dateStr.substring(6, 8); dateStr = `${year}-${month}-${day}`; } const date = new Date(dateStr); console.log('[Debug] Date Logic:', { input: dateStr, parsed: date, valid: !isNaN(date.getTime()) }); if (isNaN(date.getTime())) return 'Invalid Date'; const now = new Date(); const diffMs = now - date; const diffSec = Math.floor(diffMs / 1000); const diffMin = Math.floor(diffSec / 60); const diffHour = Math.floor(diffMin / 60); const diffDay = Math.floor(diffHour / 24); const diffWeek = Math.floor(diffDay / 7); const diffMonth = Math.floor(diffDay / 30); const diffYear = Math.floor(diffDay / 365); if (diffYear > 0) return `${diffYear} year${diffYear > 1 ? 's' : ''} ago`; if (diffMonth > 0) return `${diffMonth} month${diffMonth > 1 ? 's' : ''} ago`; if (diffWeek > 0) return `${diffWeek} week${diffWeek > 1 ? 's' : ''} ago`; if (diffDay > 0) return `${diffDay} day${diffDay > 1 ? 's' : ''} ago`; if (diffHour > 0) return `${diffHour} hour${diffHour > 1 ? 's' : ''} ago`; if (diffMin > 0) return `${diffMin} minute${diffMin > 1 ? 's' : ''} ago`; return 'Just now'; } // Escape HTML to prevent XSS function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Sidebar toggle (for mobile and desktop) function toggleSidebar() { const sidebar = document.getElementById('sidebar'); const main = document.getElementById('mainContent'); const overlay = document.querySelector('.yt-sidebar-overlay'); if (window.innerWidth <= 1024) { // Mobile: slide-in sidebar with overlay sidebar.classList.toggle('open'); if (overlay) { overlay.classList.toggle('active', sidebar.classList.contains('open')); } } else { // Desktop: collapse/expand sidebar sidebar.classList.toggle('collapsed'); main.classList.toggle('sidebar-collapsed'); localStorage.setItem('sidebarCollapsed', sidebar.classList.contains('collapsed')); } } // Close sidebar when clicking outside (mobile) document.addEventListener('click', (e) => { const sidebar = document.getElementById('sidebar'); const menuBtn = document.querySelector('.yt-menu-btn'); const overlay = document.querySelector('.yt-sidebar-overlay'); if (window.innerWidth <= 1024 && sidebar && sidebar.classList.contains('open') && !sidebar.contains(e.target) && menuBtn && !menuBtn.contains(e.target)) { sidebar.classList.remove('open'); if (overlay) { overlay.classList.remove('active'); } } }); // Initialize sidebar state from localStorage to prevent layout shift function initSidebarState() { const sidebar = document.getElementById('sidebar'); const main = document.getElementById('mainContent'); const overlay = document.querySelector('.yt-sidebar-overlay'); if (!sidebar || !main) return; // Mobile: always hide sidebar (it will slide in when toggled) if (window.innerWidth <= 1024) { sidebar.classList.remove('open', 'collapsed'); main.classList.remove('sidebar-collapsed'); if (overlay) overlay.classList.remove('active'); return; } // Desktop: Check if we're on watch page const isWatchPage = document.querySelector('.yt-watch-layout') !== null; // On watch page, always use mini sidebar for more video space if (isWatchPage) { sidebar.classList.add('collapsed'); main.classList.add('sidebar-collapsed'); return; } // On other pages, restore from localStorage for consistent experience const savedState = localStorage.getItem('sidebarCollapsed'); if (savedState === 'true') { sidebar.classList.add('collapsed'); main.classList.add('sidebar-collapsed'); } else { sidebar.classList.remove('collapsed'); main.classList.remove('sidebar-collapsed'); } } // Re-initialize sidebar state on window resize let resizeTimer; window.addEventListener('resize', () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { initSidebarState(); }, 150); }); // --- Theme Logic --- function initTheme() { // Check for saved preference let savedTheme = localStorage.getItem('theme'); // If no saved preference, default to dark theme if (!savedTheme) { savedTheme = 'dark'; } setTheme(savedTheme, false); // Initial set without saving (already saved or computed) } function setTheme(theme, save = true) { document.documentElement.setAttribute('data-theme', theme); if (save) { localStorage.setItem('theme', theme); } // Update UI Buttons (if on settings page) const btnLight = document.getElementById('themeBtnLight'); const btnDark = document.getElementById('themeBtnDark'); if (btnLight && btnDark) { btnLight.classList.remove('active'); btnDark.classList.remove('active'); if (theme === 'light') btnLight.classList.add('active'); else btnDark.classList.add('active'); } } // Ensure theme persists on back navigation (BFCache) window.addEventListener('pageshow', (event) => { // Re-apply theme from storage to ensure it matches user preference // even if page was restored from cache with old state const savedTheme = localStorage.getItem('theme'); if (savedTheme) { setTheme(savedTheme, false); } else { initTheme(); } }); // Sync across tabs window.addEventListener('storage', (event) => { if (event.key === 'theme') { setTheme(event.newValue, false); } }); // --- Profile Logic --- async function updateProfile(e) { if (e) e.preventDefault(); const displayName = document.getElementById('displayName').value; const btn = e.target.querySelector('button'); const originalText = btn.innerHTML; btn.disabled = true; btn.innerHTML = ' Saving...'; try { const response = await fetch('/api/update_profile', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ username: displayName }) }); const data = await response.json(); if (data.success) { showToast('Profile updated successfully!', 'success'); // Update UI immediately const avatarName = document.querySelector('.yt-avatar'); if (avatarName) avatarName.title = displayName; } else { showToast(data.message || 'Update failed', 'error'); } } catch (err) { showToast('Network error', 'error'); } finally { btn.disabled = false; btn.innerHTML = originalText; } } // --- Local Storage Helpers --- function getLibrary(type) { return JSON.parse(localStorage.getItem(`kv_${type}`) || '[]'); } function saveToLibrary(type, item) { let lib = getLibrary(type); // Filter out nulls/invalid items to self-heal storage lib = lib.filter(i => i && i.id); // Avoid duplicates if (!lib.some(i => i.id === item.id)) { lib.unshift(item); // Add to top localStorage.setItem(`kv_${type}`, JSON.stringify(lib)); if (type !== 'history') { showToast(`Saved to ${type}`, 'success'); } } } function removeFromLibrary(type, id) { let lib = getLibrary(type); lib = lib.filter(i => i && i.id !== id); localStorage.setItem(`kv_${type}`, JSON.stringify(lib)); showToast(`Removed from ${type}`, 'info'); // Refresh if on library page if (window.location.pathname === '/my-videos') { location.reload(); } } function isInLibrary(type, id) { const lib = getLibrary(type); return lib.some(i => i && i.id === id); } // --- Subscription Logic --- function toggleSubscribe(channelId, channelName, avatarUrl, btnElement) { event.stopPropagation(); // Prevent card clicks if (isInLibrary('subscriptions', channelId)) { removeFromLibrary('subscriptions', channelId); if (btnElement) { btnElement.classList.remove('subscribed'); btnElement.innerHTML = 'Subscribe'; } } else { saveToLibrary('subscriptions', { id: channelId, title: channelName, thumbnail: avatarUrl, timestamp: new Date().toISOString() }); if (btnElement) { btnElement.classList.add('subscribed'); btnElement.innerHTML = 'Subscribed'; } } } function checkSubscriptionStatus(channelId, btnElement) { if (isInLibrary('subscriptions', channelId)) { btnElement.classList.add('subscribed'); btnElement.innerHTML = 'Subscribed'; } } // Load Channel Videos async function loadChannelVideos(channelId) { const resultsArea = document.getElementById('resultsArea'); if (!resultsArea) return; // Guard: only works on pages with resultsArea isLoading = true; resultsArea.innerHTML = renderSkeleton(); try { const response = await fetch(`/api/channel?id=${encodeURIComponent(channelId)}`); const data = await response.json(); if (data.error) { resultsArea.innerHTML = renderNoContent(`Error: ${data.error}`, "Could not load channel."); return; } // Render header const headerHtml = `
${channelId.startsWith('UC') ? channelId[0] : (data[0]?.uploader?.[0] || 'C')}

${data[0]?.uploader || 'Channel Content'}

${data.length} Videos

`; // Videos const videosHtml = data.map(video => `
${escapeHtml(video.title)} ${video.duration ? `${video.duration}` : ''}

${escapeHtml(video.title)}

${formatViews(video.view_count)} views • ${formatDate(video.upload_date)}
`).join(''); resultsArea.innerHTML = headerHtml + videosHtml + '
'; if (window.observeImages) window.observeImages(); } catch (e) { console.error("Channel Load Error:", e); resultsArea.innerHTML = renderNoContent("Failed to load channel", "Please try again later."); } finally { isLoading = false; } }