diff --git a/frontend/app.js b/frontend/app.js index 2df5749..62ec21c 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -75,6 +75,7 @@ class EncryptedVideoScroller { async init() { this.setupEventListeners(); + this.setupIntersectionObserver(); await this.loadVideos(); this.renderVideos(); } @@ -88,9 +89,12 @@ class EncryptedVideoScroller { // Keyboard document.addEventListener('keydown', (e) => this.handleKeydown(e)); - // Mouse wheel + // Mouse wheel - now allows smooth scroll instead of snapping document.addEventListener('wheel', (e) => this.handleWheel(e), { passive: false }); + // Scroll event to update currentIndex and manage auto-play + this.container.addEventListener('scroll', () => this.handleScroll()); + // Toggle header on tap (but not on buttons) this.container.addEventListener('click', (e) => { if (!e.target.closest('.download-btn') && !e.target.closest('.video-title')) { @@ -104,6 +108,51 @@ class EncryptedVideoScroller { } } + setupIntersectionObserver() { + // Observer for loading/decryption - starts early + const loadOptions = { + root: this.container, + threshold: 0.05, // Trigger when 5% visible + rootMargin: '50%' // Start loading 50% before video enters viewport + }; + + this.loadObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + // Video is approaching or becoming visible - start decryption + const video = entry.target.querySelector('video'); + const dataId = entry.target.dataset.index; + + if (video && this.videos[dataId] && !this.decryptedBlobs[this.videos[dataId].id]) { + // Start decryption early so it's ready when scrolled into view + this.decryptAndPlayVideo(this.videos[dataId], video); + } + } + }); + }, loadOptions); + + // Observer for playback - ensures video is playing when visible + const playOptions = { + root: this.container, + threshold: 0.5 // Ensure playback when 50%+ visible + }; + + this.playObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + const video = entry.target.querySelector('video'); + if (!video) return; + + if (entry.isIntersecting && entry.intersectionRatio > 0.5) { + // Video is 50%+ visible - ensure it's playing + video.play().catch(e => console.log('Play error:', e)); + } else { + // Not visible enough - pause it + video.pause(); + } + }); + }, playOptions); + } + toggleHeader() { const header = document.querySelector('.app-header'); header.classList.toggle('hidden'); @@ -136,13 +185,33 @@ class EncryptedVideoScroller { } handleWheel(e) { - e.preventDefault(); - - if (e.deltaY > 0) { - this.nextVideo(); - } else { - this.prevVideo(); - } + // Allow smooth scrolling - don't prevent default + // Just let native scroll happen + // Keyboard and swipe will still snap + } + + handleScroll() { + // Update currentIndex based on which video is most visible + const containerTop = this.container.getBoundingClientRect().top; + const containerHeight = this.container.clientHeight; + const viewportCenter = containerHeight / 2; + + let closestIndex = 0; + let closestDistance = Math.abs(containerHeight); + + const items = this.container.querySelectorAll('.video-item'); + items.forEach((item, index) => { + const rect = item.getBoundingClientRect(); + const itemCenter = rect.top + rect.height / 2 - containerTop; + const distance = Math.abs(itemCenter - viewportCenter); + + if (distance < closestDistance) { + closestDistance = distance; + closestIndex = index; + } + }); + + this.currentIndex = closestIndex; } handleKeydown(e) { @@ -182,22 +251,27 @@ class EncryptedVideoScroller { } 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); - + // Render all videos in DOM for smooth scrolling // Clear existing videos const existingItems = this.container.querySelectorAll('.video-item'); existingItems.forEach(item => item.remove()); - // Render videos - for (let i = startIndex; i < endIndex; i++) { + // Render all videos + for (let i = 0; i < this.videos.length; i++) { if (this.videos[i]) { this.renderVideo(this.videos[i], i); } } - this.updateActiveVideo(); + // Scroll to current video + this.scrollToVideo(this.currentIndex); + } + + scrollToVideo(index) { + const item = this.container.querySelector(`[data-index="${index}"]`); + if (item) { + item.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } } renderVideo(video, index) { @@ -210,7 +284,7 @@ class EncryptedVideoScroller { videoEl.preload = 'auto'; videoEl.controls = false; videoEl.loop = true; - videoEl.autoplay = index === this.currentIndex; + videoEl.autoplay = false; // Intersection Observer will handle autoplay videoEl.muted = true; videoEl.playsInline = true; videoEl.setAttribute('webkit-playsinline', 'webkit-playsinline'); @@ -224,7 +298,7 @@ class EncryptedVideoScroller { info.innerHTML = `