1189 lines
30 KiB
HTML
1189 lines
30 KiB
HTML
<!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>
|