Spaces:
Running
Running
File size: 6,091 Bytes
74cf6bd |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
// Common functionality
// Initialize on page load
document.addEventListener('DOMContentLoaded', () => {
// Display recent videos in the footer on page load
loadFooterRecentVideos();
// Handle theme switching
const themeItems = document.querySelectorAll('.theme-item');
themeItems.forEach(item => {
item.addEventListener('click', () => {
const theme = item.dataset.theme;
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
});
});
// Apply saved theme from localStorage if available
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
document.documentElement.setAttribute('data-theme', savedTheme);
}
});
// Format seconds to MM:SS format
function formatTime(seconds) {
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
// Error handling function
function handleError(error) {
console.error('Error:', error);
return `<div role="alert" class="alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Error: ${error.message || 'Something went wrong'}</span>
<div>
<button class="btn btn-sm btn-ghost" onclick="window.location.reload()">Retry</button>
</div>
</div>`;
}
// Toast notification function
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `alert alert-${type} fixed bottom-4 right-4 max-w-xs z-50 shadow-lg`;
// Different icon based on type
let icon = '';
switch(type) {
case 'success':
icon = `<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>`;
break;
case 'warning':
icon = `<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>`;
break;
case 'error':
icon = `<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>`;
break;
default: // info
icon = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>`;
}
toast.innerHTML = `
${icon}
<span>${message}</span>
<div>
<button class="btn btn-sm btn-ghost" onclick="this.parentElement.parentElement.remove()">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
`;
document.body.appendChild(toast);
// Auto-dismiss after 3 seconds
setTimeout(() => {
toast.classList.add('opacity-0', 'transition-opacity', 'duration-500');
setTimeout(() => toast.remove(), 500);
}, 3000);
}
// Extract video ID from YouTube URL
function extractVideoId(url) {
const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
const match = url.match(regExp);
return (match && match[7].length === 11) ? match[7] : null;
}
// Load recent videos into the footer from the API
function loadFooterRecentVideos() {
const footerRecentVideos = document.getElementById('footer-recent-videos');
if (!footerRecentVideos) return;
// Show loading state
footerRecentVideos.innerHTML = '<p class="text-sm opacity-70">Loading recent videos...</p>';
// Fetch recent videos from server API
fetch('/api/video/recent?limit=3')
.then(response => {
if (!response.ok) {
throw new Error('Failed to fetch recent videos');
}
return response.json();
})
.then(videos => {
if (videos && videos.length > 0) {
// Generate HTML for recent videos
const videoLinks = videos.map(video => {
return `
<a href="/video/${video.video_id}" class="link link-hover block py-1 truncate">
<span class="text-xs text-primary">▶</span> ${video.title || `Video ${video.video_id}`}
</a>
`;
}).join('');
// Add videos to the footer
footerRecentVideos.innerHTML = videoLinks;
} else {
footerRecentVideos.innerHTML = '<p class="text-sm opacity-70">No recent videos</p>';
}
})
.catch(error => {
console.error('Error loading footer videos:', error);
footerRecentVideos.innerHTML = '<p class="text-sm opacity-70">Failed to load recent videos</p>';
});
}
|