obs-r2-uploader/worker/admin.html

1189 lines
30 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Video Admin Panel</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #0f0f0f;
color: #fff;
padding: 20px;
}
header {
background: #1a1a1a;
padding: 20px;
border-radius: 12px;
margin-bottom: 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
h1 {
font-size: 1.8rem;
}
.subtitle {
color: #aaa;
font-size: 0.9rem;
}
.header-buttons {
display: flex;
gap: 10px;
}
.upload-btn {
background: #2ecc71;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
}
.upload-btn:hover {
background: #27ae60;
}
.logout-btn {
background: #e74c3c;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
}
.logout-btn:hover {
background: #c0392b;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: #1a1a1a;
padding: 20px;
border-radius: 12px;
text-align: center;
}
.stat-value {
font-size: 2.5rem;
font-weight: bold;
color: #3ea6ff;
margin-bottom: 5px;
}
.stat-label {
color: #aaa;
font-size: 0.9rem;
}
.video-list {
background: #1a1a1a;
border-radius: 12px;
overflow: hidden;
}
.video-list-header {
padding: 20px;
border-bottom: 1px solid #2a2a2a;
}
.search-bar {
width: 100%;
padding: 12px;
background: #2a2a2a;
border: 1px solid #3a3a3a;
border-radius: 6px;
color: white;
font-size: 1rem;
}
.video-item {
display: grid;
grid-template-columns: 120px 1fr auto;
gap: 20px;
padding: 20px;
border-bottom: 1px solid #2a2a2a;
align-items: center;
}
.video-item:hover {
background: #222;
}
.video-item:last-child {
border-bottom: none;
}
.video-thumbnail {
width: 120px;
height: 68px;
background: #000;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
cursor: pointer;
transition: transform 0.2s;
}
.video-thumbnail:hover {
transform: scale(1.05);
}
.video-thumbnail::after {
content: '▶';
position: absolute;
font-size: 2rem;
color: white;
text-shadow: 0 2px 8px rgba(0,0,0,0.8);
pointer-events: none;
}
.video-thumbnail video {
width: 100%;
height: 100%;
object-fit: cover;
}
.video-info {
flex: 1;
}
.video-name {
font-size: 1.1rem;
margin-bottom: 8px;
word-break: break-word;
}
.video-meta {
color: #aaa;
font-size: 0.85rem;
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.video-controls {
display: flex;
flex-direction: column;
gap: 10px;
min-width: 200px;
}
.visibility-select {
padding: 8px 12px;
background: #2a2a2a;
border: 1px solid #3a3a3a;
border-radius: 6px;
color: white;
font-size: 0.9rem;
cursor: pointer;
}
.visibility-select:focus {
outline: none;
border-color: #3ea6ff;
}
.visibility-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: bold;
text-transform: uppercase;
}
.visibility-private {
background: #e74c3c;
color: white;
}
.visibility-shareable {
background: #2ecc71;
color: white;
}
.visibility-clip {
background: #f39c12;
color: white;
}
.action-buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
transition: background 0.2s;
text-decoration: none;
display: inline-block;
}
.btn-primary {
background: #3ea6ff;
color: white;
}
.btn-primary:hover {
background: #2988d8;
}
.btn-secondary {
background: #555;
color: white;
}
.btn-secondary:hover {
background: #666;
}
.btn-danger {
background: #e74c3c;
color: white;
}
.btn-danger:hover {
background: #c0392b;
}
.clip-generator {
margin-top: 10px;
padding: 15px;
background: #2a2a2a;
border-radius: 6px;
display: none;
}
.clip-generator.active {
display: block;
}
.clip-inputs {
display: flex;
gap: 10px;
margin-bottom: 10px;
align-items: center;
}
.clip-inputs input {
padding: 6px 10px;
background: #1a1a1a;
border: 1px solid #3a3a3a;
border-radius: 4px;
color: white;
width: 80px;
font-size: 0.85rem;
}
.clip-inputs label {
font-size: 0.85rem;
color: #aaa;
}
.toast {
position: fixed;
bottom: 20px;
right: 20px;
background: #3ea6ff;
color: white;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
opacity: 0;
transition: opacity 0.3s;
z-index: 1000;
max-width: 400px;
}
.toast.show {
opacity: 1;
}
.toast.error {
background: #e74c3c;
}
.toast.success {
background: #2ecc71;
}
.loading {
text-align: center;
padding: 40px;
color: #aaa;
}
.spinner {
border: 3px solid #2a2a2a;
border-top: 3px solid #3ea6ff;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 2000;
justify-content: center;
align-items: center;
}
.modal.active {
display: flex;
}
.modal-content {
background: #1a1a1a;
padding: 30px;
border-radius: 12px;
max-width: 500px;
width: 90%;
}
.modal-header {
margin-bottom: 20px;
}
.modal-body {
margin-bottom: 20px;
}
.modal-footer {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.upload-area {
border: 2px dashed #3a3a3a;
border-radius: 8px;
padding: 40px;
text-align: center;
margin-bottom: 20px;
cursor: pointer;
transition: all 0.3s;
}
.upload-area:hover,
.upload-area.drag-over {
border-color: #3ea6ff;
background: #1a1a1a;
}
.upload-area.uploading {
pointer-events: none;
opacity: 0.6;
}
.upload-icon {
font-size: 3rem;
margin-bottom: 10px;
}
.file-input {
display: none;
}
.upload-progress {
display: none;
margin-top: 20px;
}
.upload-progress.active {
display: block;
}
.progress-bar {
width: 100%;
height: 8px;
background: #2a2a2a;
border-radius: 4px;
overflow: hidden;
margin-bottom: 10px;
}
.progress-fill {
height: 100%;
background: #3ea6ff;
width: 0%;
transition: width 0.3s;
}
.progress-text {
color: #aaa;
font-size: 0.9rem;
}
.empty-state {
text-align: center;
padding: 80px 40px;
color: #aaa;
background: linear-gradient(135deg, #1a1a1a 0%, #0f0f0f 100%);
border-radius: 12px;
margin: 20px;
}
.empty-state-icon {
font-size: 6rem;
margin-bottom: 30px;
opacity: 0.7;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
}
.empty-state h3 {
font-size: 2rem;
margin-bottom: 15px;
color: #fff;
font-weight: 600;
}
.empty-state p {
margin-bottom: 30px;
font-size: 1.1rem;
line-height: 1.6;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.empty-state-actions {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
margin-bottom: 40px;
}
.empty-state-actions .btn {
padding: 12px 24px;
font-size: 1rem;
min-width: 160px;
}
.upload-methods {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
max-width: 900px;
margin: 40px auto 0;
text-align: left;
}
.upload-method {
background: #2a2a2a;
padding: 25px;
border-radius: 10px;
transition: all 0.3s;
border: 2px solid transparent;
}
.upload-method:hover {
border-color: #3ea6ff;
transform: translateY(-5px);
box-shadow: 0 8px 20px rgba(62, 166, 255, 0.2);
}
.upload-method-icon {
font-size: 2.5rem;
margin-bottom: 15px;
}
.upload-method h4 {
color: #fff;
margin-bottom: 10px;
font-size: 1.1rem;
}
.upload-method p {
color: #aaa;
font-size: 0.9rem;
line-height: 1.5;
margin: 0;
}
.upload-method code {
display: block;
background: #1a1a1a;
padding: 8px 12px;
border-radius: 4px;
margin-top: 10px;
font-size: 0.85rem;
color: #3ea6ff;
overflow-x: auto;
}
.video-player-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.95);
z-index: 3000;
justify-content: center;
align-items: center;
padding: 20px;
}
.video-player-modal.active {
display: flex;
}
.video-player-container {
position: relative;
width: 100%;
max-width: 1200px;
background: #000;
border-radius: 8px;
overflow: hidden;
}
.video-player-container video {
width: 100%;
height: auto;
display: block;
}
.video-player-close {
position: absolute;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
color: white;
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
font-size: 1.5rem;
cursor: pointer;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
}
.video-player-close:hover {
background: rgba(255, 255, 255, 0.2);
}
.video-player-title {
position: absolute;
top: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 10px 15px;
border-radius: 6px;
font-size: 0.9rem;
max-width: calc(100% - 100px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 768px) {
.video-item {
grid-template-columns: 1fr;
}
.video-controls {
min-width: auto;
}
.action-buttons {
justify-content: flex-start;
}
.header-buttons {
flex-direction: column;
}
}
</style>
</head>
<body>
<header>
<div>
<h1>🎥 Video Admin Panel</h1>
<p class="subtitle">Manage video visibility and sharing</p>
</div>
<div class="header-buttons">
<button class="upload-btn" onclick="openUploadModal()">📤 Upload Video</button>
<button class="logout-btn" onclick="logout()">Logout</button>
</div>
</header>
<div class="stats">
<div class="stat-card">
<div class="stat-value" id="totalVideos">0</div>
<div class="stat-label">Total Videos</div>
</div>
<div class="stat-card">
<div class="stat-value" id="privateCount">0</div>
<div class="stat-label">Private</div>
</div>
<div class="stat-card">
<div class="stat-value" id="shareableCount">0</div>
<div class="stat-label">Shareable</div>
</div>
<div class="stat-card">
<div class="stat-value" id="clipCount">0</div>
<div class="stat-label">Clip Shareable</div>
</div>
</div>
<div class="video-list">
<div class="video-list-header">
<input type="text" class="search-bar" id="searchBar" placeholder="Search videos...">
</div>
<div id="videoContainer">
<div class="loading">
<div class="spinner"></div>
<p>Loading videos...</p>
</div>
</div>
</div>
<div id="toast" class="toast"></div>
<!-- Upload Modal -->
<div id="uploadModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Upload Video</h2>
</div>
<div class="modal-body">
<div class="upload-area" id="uploadArea" onclick="document.getElementById('fileInput').click()">
<div class="upload-icon">📁</div>
<h3>Drop video file here</h3>
<p>or click to browse</p>
<p style="color: #666; font-size: 0.85rem; margin-top: 10px;">Supports: MP4, MKV, MOV, AVI, WebM</p>
</div>
<input type="file" id="fileInput" class="file-input" accept="video/*" onchange="handleFileSelect(event)">
<div id="uploadProgress" class="upload-progress">
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="progress-text" id="progressText">Uploading...</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeUploadModal()">Close</button>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div id="deleteModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Delete Video?</h2>
</div>
<div class="modal-body">
<p>Are you sure you want to delete <strong id="deleteVideoName"></strong>?</p>
<p style="color: #e74c3c; margin-top: 10px;">This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeDeleteModal()">Cancel</button>
<button class="btn btn-danger" onclick="confirmDelete()">Delete</button>
</div>
</div>
</div>
<!-- Video Player Modal -->
<div id="videoPlayerModal" class="video-player-modal" onclick="closeVideoPlayer(event)">
<div class="video-player-container" onclick="event.stopPropagation()">
<button class="video-player-close" onclick="closeVideoPlayer()">×</button>
<div class="video-player-title" id="videoPlayerTitle">Video</div>
<video id="videoPlayer" controls autoplay>
<source id="videoPlayerSource" src="" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
</div>
<script>
let videos = [];
let videoToDelete = null;
// Load videos on page load
document.addEventListener('DOMContentLoaded', () => {
loadVideos();
// Search functionality
document.getElementById('searchBar').addEventListener('input', (e) => {
filterVideos(e.target.value);
});
});
async function loadVideos() {
try {
const response = await fetch('/admin/api/videos', {
credentials: 'include'
});
if (!response.ok) {
if (response.status === 401) {
window.location.href = '/admin/login';
return;
}
throw new Error('Failed to load videos');
}
const data = await response.json();
videos = data.videos;
renderVideos(videos);
updateStats(videos);
} catch (error) {
console.error('Error loading videos:', error);
showToast('Failed to load videos', 'error');
}
}
function renderVideos(videosToRender) {
const container = document.getElementById('videoContainer');
if (videosToRender.length === 0) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">🎬</div>
<h3>Welcome to Your Video Library!</h3>
<p>Your video collection is empty. Start by uploading your first video and share it with the world.</p>
<div class="empty-state-actions">
<button class="btn btn-primary" onclick="openUploadModal()" style="background: #2ecc71;">
📤 Upload Video
</button>
</div>
<div class="upload-methods">
<div class="upload-method">
<div class="upload-method-icon">🖱️</div>
<h4>Browser Upload</h4>
<p>Click the upload button above to drag & drop videos directly from your browser. Perfect for quick uploads!</p>
</div>
<div class="upload-method">
<div class="upload-method-icon">⌨️</div>
<h4>Command Line</h4>
<p>Upload videos using the terminal for larger files and automation.</p>
<code>./scripts/upload.sh video.mp4</code>
</div>
<div class="upload-method">
<div class="upload-method-icon">👁️</div>
<h4>Auto-Upload</h4>
<p>Watch a folder and automatically upload new OBS recordings as they're created.</p>
<code>./scripts/start-watcher.sh</code>
</div>
</div>
</div>
`;
return;
}
container.innerHTML = videosToRender.map(video => `
<div class="video-item">
<div class="video-thumbnail" onclick="playVideo('${video.name}')">
<video preload="metadata" muted>
<source src="/${video.name}#t=1" type="${getContentType(video.name)}">
</video>
</div>
<div class="video-info">
<div class="video-name">${video.name}</div>
<div class="video-meta">
<span>📦 ${formatSize(video.size)}</span>
<span>📅 ${formatDate(video.uploaded)}</span>
<span class="visibility-badge visibility-${video.visibility || 'shareable'}">
${video.visibility || 'shareable'}
</span>
</div>
</div>
<div class="video-controls">
<select class="visibility-select" onchange="updateVisibility('${video.name}', this.value)">
<option value="private" ${video.visibility === 'private' ? 'selected' : ''}>🔒 Private</option>
<option value="shareable" ${!video.visibility || video.visibility === 'shareable' ? 'selected' : ''}>🔗 Shareable</option>
<option value="clip_shareable" ${video.visibility === 'clip_shareable' ? 'selected' : ''}>✂️ Clip Shareable</option>
</select>
<div class="action-buttons">
<a class="btn btn-primary" href="/${video.name}" target="_blank">▶ Watch</a>
<button class="btn btn-primary" onclick="copyLink('${video.name}')">Copy Link</button>
<button class="btn btn-secondary" onclick="toggleClipGenerator('${video.name}')">Create Clip</button>
<button class="btn btn-danger" onclick="deleteVideo('${video.name}')">Delete</button>
</div>
<div id="clip-${video.name}" class="clip-generator">
<div class="clip-inputs">
<label>Start:</label>
<input type="text" id="start-${video.name}" placeholder="00:00" />
<label>End:</label>
<input type="text" id="end-${video.name}" placeholder="00:30" />
</div>
<button class="btn btn-primary" onclick="generateClipLink('${video.name}')">Generate Clip Link</button>
</div>
</div>
</div>
`).join('');
}
function filterVideos(query) {
const filtered = videos.filter(video =>
video.name.toLowerCase().includes(query.toLowerCase())
);
renderVideos(filtered);
}
function updateStats(videos) {
document.getElementById('totalVideos').textContent = videos.length;
const counts = videos.reduce((acc, video) => {
const vis = video.visibility || 'shareable';
acc[vis] = (acc[vis] || 0) + 1;
return acc;
}, {});
document.getElementById('privateCount').textContent = counts.private || 0;
document.getElementById('shareableCount').textContent = counts.shareable || 0;
document.getElementById('clipCount').textContent = counts.clip_shareable || 0;
}
async function updateVisibility(filename, visibility) {
try {
const response = await fetch('/admin/api/videos/visibility', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({ filename, visibility })
});
if (!response.ok) throw new Error('Failed to update visibility');
showToast(`Updated ${filename} to ${visibility}`, 'success');
// Reload videos to update stats
await loadVideos();
} catch (error) {
console.error('Error updating visibility:', error);
showToast('Failed to update visibility', 'error');
}
}
function copyLink(filename) {
const url = `${window.location.origin}/${filename}`;
navigator.clipboard.writeText(url).then(() => {
showToast('Link copied to clipboard!', 'success');
}).catch(() => {
showToast('Failed to copy link', 'error');
});
}
function toggleClipGenerator(filename) {
const clipGen = document.getElementById(`clip-${filename}`);
clipGen.classList.toggle('active');
}
function generateClipLink(filename) {
const start = document.getElementById(`start-${filename}`).value;
const end = document.getElementById(`end-${filename}`).value;
if (!start || !end) {
showToast('Please enter start and end times', 'error');
return;
}
const url = `${window.location.origin}/clip/${filename}?start=${start}&end=${end}`;
navigator.clipboard.writeText(url).then(() => {
showToast('Clip link copied to clipboard!', 'success');
}).catch(() => {
showToast('Failed to copy clip link', 'error');
});
}
function deleteVideo(filename) {
videoToDelete = filename;
document.getElementById('deleteVideoName').textContent = filename;
document.getElementById('deleteModal').classList.add('active');
}
function closeDeleteModal() {
document.getElementById('deleteModal').classList.remove('active');
videoToDelete = null;
}
async function confirmDelete() {
if (!videoToDelete) return;
try {
const response = await fetch(`/admin/api/videos/${encodeURIComponent(videoToDelete)}`, {
method: 'DELETE',
credentials: 'include'
});
if (!response.ok) throw new Error('Failed to delete video');
showToast(`Deleted ${videoToDelete}`, 'success');
closeDeleteModal();
await loadVideos();
} catch (error) {
console.error('Error deleting video:', error);
showToast('Failed to delete video', 'error');
}
}
function openUploadModal() {
document.getElementById('uploadModal').classList.add('active');
setupDragAndDrop();
}
function closeUploadModal() {
document.getElementById('uploadModal').classList.remove('active');
resetUploadUI();
}
function setupDragAndDrop() {
const uploadArea = document.getElementById('uploadArea');
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('drag-over');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('drag-over');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('drag-over');
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFileUpload(files[0]);
}
});
}
function handleFileSelect(event) {
const file = event.target.files[0];
if (file) {
handleFileUpload(file);
}
}
async function handleFileUpload(file) {
// Validate file type
const validTypes = ['video/mp4', 'video/x-matroska', 'video/quicktime', 'video/x-msvideo', 'video/webm'];
if (!validTypes.includes(file.type) && !file.name.match(/\.(mp4|mkv|mov|avi|webm)$/i)) {
showToast('Invalid file type. Please upload a video file.', 'error');
return;
}
// Show progress
const uploadArea = document.getElementById('uploadArea');
const uploadProgress = document.getElementById('uploadProgress');
const progressFill = document.getElementById('progressFill');
const progressText = document.getElementById('progressText');
uploadArea.classList.add('uploading');
uploadProgress.classList.add('active');
try {
// Create form data
const formData = new FormData();
formData.append('video', file);
// Upload with progress tracking
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
progressFill.style.width = percentComplete + '%';
progressText.textContent = `Uploading ${file.name}... ${Math.round(percentComplete)}%`;
}
});
xhr.addEventListener('load', async () => {
if (xhr.status === 200) {
showToast(`Successfully uploaded ${file.name}!`, 'success');
closeUploadModal();
await loadVideos(); // Reload video list
} else {
showToast('Upload failed. Please try again.', 'error');
resetUploadUI();
}
});
xhr.addEventListener('error', () => {
showToast('Upload failed. Please try again.', 'error');
resetUploadUI();
});
xhr.open('POST', '/admin/api/upload');
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.withCredentials = true;
xhr.send(formData);
} catch (error) {
console.error('Upload error:', error);
showToast('Upload failed. Please try again.', 'error');
resetUploadUI();
}
}
function resetUploadUI() {
const uploadArea = document.getElementById('uploadArea');
const uploadProgress = document.getElementById('uploadProgress');
const progressFill = document.getElementById('progressFill');
const progressText = document.getElementById('progressText');
const fileInput = document.getElementById('fileInput');
uploadArea.classList.remove('uploading');
uploadProgress.classList.remove('active');
progressFill.style.width = '0%';
progressText.textContent = 'Uploading...';
fileInput.value = '';
}
function logout() {
document.cookie = 'admin_auth=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
window.location.href = '/admin/login';
}
function showToast(message, type = 'info') {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = `toast show ${type}`;
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
function formatSize(bytes) {
if (bytes < 1024 * 1024) {
return (bytes / 1024).toFixed(1) + ' KB';
} else if (bytes < 1024 * 1024 * 1024) {
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
} else {
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
}
}
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
}
function getContentType(filename) {
const ext = filename.split('.').pop().toLowerCase();
const types = {
'mp4': 'video/mp4',
'mkv': 'video/x-matroska',
'mov': 'video/quicktime',
'avi': 'video/x-msvideo',
'webm': 'video/webm'
};
return types[ext] || 'video/mp4';
}
function playVideo(filename) {
const modal = document.getElementById('videoPlayerModal');
const player = document.getElementById('videoPlayer');
const source = document.getElementById('videoPlayerSource');
const title = document.getElementById('videoPlayerTitle');
// Set video source and title
source.src = `/${filename}`;
source.type = getContentType(filename);
title.textContent = filename;
// Reload video and show modal
player.load();
modal.classList.add('active');
// Pause video when clicking outside
modal.onclick = (e) => {
if (e.target === modal) {
closeVideoPlayer();
}
};
// Support ESC key to close
document.addEventListener('keydown', handleEscKey);
}
function closeVideoPlayer(event) {
if (event && event.target !== document.getElementById('videoPlayerModal')) {
return;
}
const modal = document.getElementById('videoPlayerModal');
const player = document.getElementById('videoPlayer');
// Pause and reset video
player.pause();
player.currentTime = 0;
// Hide modal
modal.classList.remove('active');
// Remove ESC key listener
document.removeEventListener('keydown', handleEscKey);
}
function handleEscKey(e) {
if (e.key === 'Escape') {
closeVideoPlayer();
}
}
</script>
</body>
</html>