first commit
This commit is contained in:
257
frontend/app.js
Normal file
257
frontend/app.js
Normal file
@@ -0,0 +1,257 @@
|
||||
// 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 = `<div class="video-title">${this.escapeHtml(video.title)}</div>`;
|
||||
|
||||
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 = '<div class="spinner"></div>';
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user