You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

385 lines
13 KiB
HTML

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ video.username }} - Video</title>
<style>
body {
font-family: Arial, sans-serif;
background: #111;
color: #eee;
text-align: center;
margin: 0;
padding: 0;
}
video {
margin-top: 20px;
width: 80%;
max-width: 1000px;
border: 2px solid #444;
border-radius: 8px;
background: #000;
}
.meta {
margin: 20px auto;
text-align: left;
max-width: 800px;
background: #222;
padding: 15px;
border-radius: 6px;
}
.meta p {
margin: 8px 0;
}
a {
color: #0af;
text-decoration: none;
}
.fav-btn {
font-size: 1.1rem;
line-height: 1;
border: none;
background: transparent;
cursor: pointer;
padding: .2rem .35rem;
}
.fav-btn[aria-pressed="true"] {
color: gold;
}
.fav-btn:hover {
transform: scale(1.05);
}
</style>
<style>
.player {
margin: 20px auto;
width: 80%;
max-width: 1000px;
background: #000;
border: 2px solid #444;
border-radius: 8px;
overflow: hidden;
}
.player video {
width: 100%;
display: block;
}
.controls {
display: flex;
align-items: center;
gap: 10px;
background: #1b1b1b;
padding: 10px;
border-top: 1px solid #333;
}
.controls button,
.controls select,
.controls input[type="range"] {
background: #222;
color: #eee;
border: 1px solid #444;
border-radius: 4px;
padding: 6px 8px;
cursor: pointer;
}
.controls button:hover {
background: #2a2a2a;
}
.time {
font-variant-numeric: tabular-nums;
min-width: 90px;
text-align: right;
}
.range {
flex: 1;
}
.moments {
margin: 20px auto;
text-align: left;
max-width: 800px;
background: #222;
padding: 12px;
border-radius: 6px;
}
.moments h3 {
margin: 0 0 8px 0;
}
.moment-pill {
display: inline-block;
margin: 6px 6px 0 0;
padding: 6px 10px;
background: #0a2740;
color: #cfeaff;
border: 1px solid #0d3b63;
border-radius: 999px;
cursor: pointer;
font-size: 0.95rem;
}
.moment-pill:hover {
background: #103553;
}
</style>
<script>
function openFolder(filePath) {
fetch('/open-folder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file_path: filePath })
}).then(res => res.json()).then(data => {
if (data.success) {
console.log("Opened folder successfully");
} else {
alert("Failed to open folder");
}
});
}
</script>
<script>
function deleteFile(filePath, videoId) {
if (!confirm("Are you sure you want to permanently delete this file?")) return;
fetch('/delete-file', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file_path: filePath, video_id: videoId })
})
.then(res => res.json())
.then(data => {
if (data.success) {
alert("File deleted successfully");
// redirect back to the user page
window.location.href = "/user/{{ video.username }}";
} else {
alert("Failed to delete: " + (data.error || 'Unknown error'));
}
})
.catch(err => alert("Error: " + err));
}
</script>
<script>
document.addEventListener('click', async (e) => {
const btn = e.target.closest('#favBtn');
if (!btn) return;
const vid = btn.dataset.videoId;
try {
const res = await fetch(`/api/fav/toggle/${encodeURIComponent(vid)}`, { method: 'POST' });
const data = await res.json();
if (!res.ok || !data.ok) throw new Error(data.error || 'Failed');
const isFav = data.is_favorite;
btn.textContent = isFav ? '★' : '☆';
btn.setAttribute('aria-pressed', isFav ? 'true' : 'false');
btn.title = isFav ? 'Unfavorite' : 'Favorite';
} catch (err) {
alert('Could not toggle favorite. ' + (err?.message || ''));
}
});
</script>
</head>
<body>
<h1>{{ video.username }} - {{ video.site }}</h1>
<div>
<a href="/user/{{ video.username }}">⬅ Back to {{ video.username }}'s Videos</a>
</div>
<div class="player">
<video id="player" muted poster="{{ video.thumbnail }}">
<source src="/video/stream/{{ video.video_id }}" type="video/mp4">
Your browser does not support the video tag.
</video>
<div class="controls">
<button id="playPause">▶️</button>
<span class="time" id="currentTime">00:00</span>
<input id="seek" class="range" type="range" min="0" max="0" value="0" step="1">
<span class="time" id="duration">00:00</span>
<input id="volume" type="range" min="0" max="1" step="0.01" value="1" title="Volume">
<select id="speed" title="Speed">
<option value="0.5">0.5×</option>
<option value="0.75">0.75×</option>
<option value="1" selected>1×</option>
<option value="1.25">1.25×</option>
<option value="1.5">1.5×</option>
<option value="2">2×</option>
</select>
<button id="addMoment" title="Add moment"> Add Moment</button>
</div>
</div>
<div class="moments">
<h3>Moments</h3>
<div id="momentsList"></div>
</div>
<div class="meta">
<button onclick="openFolder('{{ video.filepath }}')">Open Folder</button>
<button onclick="deleteFile('{{ video.filepath }}','{{ video.video_id }}')" style="margin-left:8px">Delete
File</button>
<!-- Favorite star -->
<button id="favBtn" class="fav-btn" data-video-id="{{ video.video_id }}"
aria-pressed="{{ 'true' if video.is_favorite else 'false' }}"
title="{{ 'Unfavorite' if video.is_favorite else 'Favorite' }}" style="margin-left:8px">
{{ '★' if video.is_favorite else '☆' }}
</button>
</div>
<div class="meta">
<a href="{{ url_for('web.user_page', username=video.username) }}" class="username-link">
{{ video.username }}
</a>
<p><strong>Size:</strong> {{ "%.2f"|format(video.size/1024) }} GB</p>
<p><strong>Duration:</strong> {{ video.duration if video.duration else "Unknown" }}</p>
<p><strong>Path:</strong> {{ video.filepath }}</p>
<p><strong>Date:</strong> {{ video.created_at }}</p>
</div>
<script>
(function () {
const video = document.getElementById('player');
const playPause = document.getElementById('playPause');
const seek = document.getElementById('seek');
const vol = document.getElementById('volume');
const speed = document.getElementById('speed');
const cur = document.getElementById('currentTime');
const dur = document.getElementById('duration');
const addBtn = document.getElementById('addMoment');
const list = document.getElementById('momentsList');
const videoId = "{{ video.video_id }}";
// format seconds → hh:mm:ss
function fmt(t) {
t = Math.max(0, Math.floor(t || 0));
const h = Math.floor(t / 3600);
const m = Math.floor((t % 3600) / 60);
const s = t % 60;
return (h > 0 ? String(h).padStart(2, '0') + ':' : '') +
String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
}
// load existing moments
async function loadMoments() {
list.innerHTML = '';
try {
const res = await fetch(`/api/moments/${encodeURIComponent(videoId)}`);
const data = await res.json();
if (!data.ok) throw new Error(data.error || 'Failed');
for (const m of data.moments) {
const pill = document.createElement('button');
pill.className = 'moment-pill';
pill.textContent = fmt(m.timestamp);
pill.dataset.ts = m.timestamp;
pill.addEventListener('click', () => {
video.currentTime = Number(pill.dataset.ts);
video.play();
playPause.textContent = '⏸';
});
list.appendChild(pill);
}
if (list.children.length === 0) {
list.textContent = 'No moments yet.';
}
} catch (e) {
list.textContent = 'Could not load moments.';
console.error(e);
}
}
// add new moment at current time
async function addMoment() {
const ts = Math.floor(video.currentTime || 0);
try {
const res = await fetch(`/api/moments/${encodeURIComponent(videoId)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ timestamp: ts })
});
const data = await res.json();
if (!data.ok) throw new Error(data.error || 'Failed');
// append immediately
const pill = document.createElement('button');
pill.className = 'moment-pill';
pill.textContent = fmt(ts);
pill.dataset.ts = ts;
pill.addEventListener('click', () => {
video.currentTime = ts;
video.play();
playPause.textContent = '⏸';
});
if (list.textContent === 'No moments yet.') list.textContent = '';
list.appendChild(pill);
} catch (e) {
alert('Could not add moment: ' + (e?.message || ''));
}
}
// player controls
playPause.addEventListener('click', () => {
if (video.paused) { video.play(); playPause.textContent = '⏸'; }
else { video.pause(); playPause.textContent = '▶️'; }
});
video.addEventListener('play', () => playPause.textContent = '⏸');
video.addEventListener('pause', () => playPause.textContent = '▶️');
video.addEventListener('loadedmetadata', () => {
seek.max = Math.floor(video.duration || 0);
dur.textContent = fmt(video.duration);
});
video.addEventListener('timeupdate', () => {
cur.textContent = fmt(video.currentTime);
if (!seek.matches(':active')) {
seek.value = Math.floor(video.currentTime || 0);
}
});
seek.addEventListener('input', () => {
video.currentTime = Number(seek.value || 0);
});
vol.addEventListener('input', () => video.volume = Number(vol.value));
speed.addEventListener('change', () => video.playbackRate = Number(speed.value));
addBtn.addEventListener('click', addMoment);
// kick off
loadMoments();
})();
</script>
</body>
</html>