// Add global error handler window.addEventListener('error', (event) => { console.error('[Ticky Error]', event.message, 'at', event.filename + ':' + event.lineno); }); class VideoScroller { constructor() { this.currentIndex = 0; this.videos = []; this.isLoading = false; this.container = document.getElementById('videoContainer'); this.loadingEl = document.getElementById('loading'); this.touchStartY = 0; this.touchEndY = 0; this.isDragging = false; this.lastLoadTime = 0; this.minLoadInterval = 500; // Get API URL from current location const protocol = window.location.protocol; const hostname = window.location.hostname; this.apiUrl = `${protocol}//${hostname}:3222`; this.init(); } async init() { this.setupEventListeners(); await this.loadVideos(); this.renderVideos(); } setupEventListeners() { // Touch events for swipe - prevent default to avoid page scrolling document.addEventListener('touchstart', (e) => this.handleTouchStart(e), { passive: false }); document.addEventListener('touchmove', (e) => e.preventDefault(), { passive: false }); document.addEventListener('touchend', (e) => this.handleTouchEnd(e), { passive: false }); // Keyboard navigation document.addEventListener('keydown', (e) => this.handleKeydown(e)); // Infinite scroll - load more when nearing the end this.container.addEventListener('scroll', () => this.handleScroll()); // Mouse wheel document.addEventListener('wheel', (e) => this.handleWheel(e), { passive: false }); } handleTouchStart(e) { this.touchStartY = e.changedTouches[0].clientY; this.isDragging = true; } handleTouchEnd(e) { this.touchEndY = e.changedTouches[0].clientY; this.isDragging = false; this.handleSwipe(); } handleSwipe() { const diff = this.touchStartY - this.touchEndY; const minSwipeDistance = 50; if (Math.abs(diff) < minSwipeDistance) return; if (diff > 0) { // Swiped up - next video this.nextVideo(); } else { // Swiped down - previous video this.prevVideo(); } } handleWheel(e) { e.preventDefault(); if (e.deltaY > 0) { this.nextVideo(); } else { this.prevVideo(); } } handleKeydown(e) { if (e.key === 'ArrowDown' || e.key === ' ') { e.preventDefault(); this.nextVideo(); } else if (e.key === 'ArrowUp') { e.preventDefault(); this.prevVideo(); } } async loadVideos() { try { this.showLoading(); const response = await fetch(`${this.apiUrl}/api/videos?limit=10`); const newVideos = await response.json(); if (Array.isArray(newVideos)) { this.videos.push(...newVideos); } this.lastLoadTime = Date.now(); } catch (error) { console.error('Error loading videos:', error); this.showLoadingError(); } finally { this.hideLoading(); } } renderVideos() { // Keep only current and adjacent videos in DOM for performance const startIndex = Math.max(0, this.currentIndex - 1); const endIndex = Math.min(this.videos.length, this.currentIndex + 2); // Clear existing videos const existingItems = this.container.querySelectorAll('.video-item'); existingItems.forEach(item => item.remove()); // Render videos for (let i = startIndex; i < endIndex; i++) { if (this.videos[i]) { this.renderVideo(this.videos[i], i); } } this.updateActiveVideo(); } renderVideo(video, index) { const item = document.createElement('div'); item.className = 'video-item'; item.dataset.index = index; item.style.minHeight = '100vh'; const videoEl = document.createElement('video'); videoEl.src = `${this.apiUrl}${video.url}`; videoEl.preload = 'auto'; videoEl.controls = false; videoEl.loop = true; videoEl.autoplay = index === this.currentIndex; videoEl.muted = true; videoEl.playsInline = true; videoEl.setAttribute('webkit-playsinline', 'webkit-playsinline'); videoEl.setAttribute('x5-playsinline', 'x5-playsinline'); videoEl.style.width = '100%'; videoEl.style.height = '100%'; videoEl.style.objectFit = 'contain'; const info = document.createElement('div'); info.className = 'video-info'; info.innerHTML = `
${this.escapeHtml(video.title)}
`; item.appendChild(videoEl); item.appendChild(info); // Lazy load: only play current video if (index === this.currentIndex) { videoEl.play().catch(e => console.log('Play error:', e)); } this.container.appendChild(item); } updateActiveVideo() { const items = this.container.querySelectorAll('.video-item'); items.forEach(item => { const index = parseInt(item.dataset.index); const video = item.querySelector('video'); item.classList.remove('active', 'prev', 'next'); if (index === this.currentIndex) { item.classList.add('active'); video.play().catch(e => console.log('Play error:', e)); } else if (index < this.currentIndex) { item.classList.add('prev'); video.pause(); } else { item.classList.add('next'); video.pause(); } }); // Preload next video this.preloadNextVideo(); } preloadNextVideo() { const nextIndex = this.currentIndex + 1; if (nextIndex < this.videos.length && nextIndex > this.currentIndex + 1) { // Preload video metadata const nextVideo = this.videos[nextIndex]; if (nextVideo) { const img = new Image(); img.src = `${this.apiUrl}${nextVideo.url}`; } } } async nextVideo() { if (this.currentIndex < this.videos.length - 1) { this.currentIndex++; this.renderVideos(); } // Load more videos when near the end const now = Date.now(); if (this.currentIndex > this.videos.length - 3 && now - this.lastLoadTime > this.minLoadInterval && !this.isLoading) { await this.loadVideos(); } } prevVideo() { if (this.currentIndex > 0) { this.currentIndex--; this.renderVideos(); } } handleScroll() { // Could be used for horizontal scroll on desktop } showLoading() { this.isLoading = true; this.loadingEl.classList.add('show'); this.loadingEl.innerHTML = '
'; } hideLoading() { this.isLoading = false; this.loadingEl.classList.remove('show'); } showLoadingError() { this.loadingEl.innerHTML = 'Failed to load videos'; this.loadingEl.classList.add('show'); setTimeout(() => this.loadingEl.classList.remove('show'), 3000); } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } } // Initialize on page load document.addEventListener('DOMContentLoaded', () => { new VideoScroller(); });