release(v2.3.2): fix media preview URLs and tighten hover card layout
This commit is contained in:
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,5 +1,18 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Changes 12/3/2025 (v2.3.2)
|
||||||
|
|
||||||
|
release(v2.3.2): fix media preview URLs and tighten hover card layout
|
||||||
|
|
||||||
|
- Reuse the working preview URL as a base when stepping between images/videos
|
||||||
|
so next/prev navigation keeps using the same inline/download endpoint
|
||||||
|
- Preserve video progress tracking and watched badges while fixing black-screen
|
||||||
|
playback issues across browsers
|
||||||
|
- Slightly shrink the file hover preview card (width/height, grid columns,
|
||||||
|
gaps, snippet/props heights) for a more compact, less intrusive peek
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Changes 12/3/2025 (v2.3.1)
|
## Changes 12/3/2025 (v2.3.1)
|
||||||
|
|
||||||
release(v2.3.1): polish file list actions & hover preview peak
|
release(v2.3.1): polish file list actions & hover preview peak
|
||||||
|
|||||||
@@ -393,17 +393,17 @@ function ensureHoverPreviewEl() {
|
|||||||
const propsEl = el.querySelector(".hover-preview-props");
|
const propsEl = el.querySelector(".hover-preview-props");
|
||||||
|
|
||||||
if (card) {
|
if (card) {
|
||||||
card.style.minWidth = "420px";
|
card.style.minWidth = "380px"; // was 420
|
||||||
card.style.maxWidth = "640px";
|
card.style.maxWidth = "600px"; // was 640
|
||||||
card.style.minHeight = "220px";
|
card.style.minHeight = "200px"; // was 220
|
||||||
card.style.padding = "10px 12px";
|
card.style.padding = "8px 10px"; // slightly tighter padding
|
||||||
card.style.overflow = "hidden";
|
card.style.overflow = "hidden";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (grid) {
|
if (grid) {
|
||||||
grid.style.display = "grid";
|
grid.style.display = "grid";
|
||||||
grid.style.gridTemplateColumns = "220px minmax(260px, 1fr)";
|
grid.style.gridTemplateColumns = "200px minmax(240px, 1fr)"; // both columns ~9% smaller
|
||||||
grid.style.gap = "12px";
|
grid.style.gap = "10px";
|
||||||
grid.style.alignItems = "center";
|
grid.style.alignItems = "center";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,13 +426,14 @@ function ensureHoverPreviewEl() {
|
|||||||
thumb.style.display = "flex";
|
thumb.style.display = "flex";
|
||||||
thumb.style.alignItems = "center";
|
thumb.style.alignItems = "center";
|
||||||
thumb.style.justifyContent = "center";
|
thumb.style.justifyContent = "center";
|
||||||
thumb.style.minHeight = "140px";
|
thumb.style.minHeight = "120px"; // was 140
|
||||||
thumb.style.marginBottom = "6px";
|
thumb.style.marginBottom = "4px"; // slightly tighter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (snippet) {
|
if (snippet) {
|
||||||
snippet.style.marginTop = "4px";
|
snippet.style.marginTop = "4px";
|
||||||
snippet.style.maxHeight = "140px";
|
snippet.style.maxHeight = "120px";
|
||||||
snippet.style.overflow = "auto";
|
snippet.style.overflow = "auto";
|
||||||
snippet.style.fontSize = "0.78rem";
|
snippet.style.fontSize = "0.78rem";
|
||||||
snippet.style.whiteSpace = "pre-wrap";
|
snippet.style.whiteSpace = "pre-wrap";
|
||||||
@@ -464,9 +465,9 @@ function ensureHoverPreviewEl() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (propsEl) {
|
if (propsEl) {
|
||||||
propsEl.style.fontSize = "0.78rem";
|
propsEl.style.fontSize = "0.76rem";
|
||||||
propsEl.style.lineHeight = "1.3";
|
propsEl.style.lineHeight = "1.3";
|
||||||
propsEl.style.maxHeight = "160px";
|
propsEl.style.maxHeight = "140px";
|
||||||
propsEl.style.overflow = "auto";
|
propsEl.style.overflow = "auto";
|
||||||
propsEl.style.paddingRight = "4px";
|
propsEl.style.paddingRight = "4px";
|
||||||
propsEl.style.wordBreak = "break-word";
|
propsEl.style.wordBreak = "break-word";
|
||||||
|
|||||||
@@ -503,6 +503,23 @@ export function previewFile(fileUrl, fileName) {
|
|||||||
const isVideo = VID_RE.test(lower);
|
const isVideo = VID_RE.test(lower);
|
||||||
const isAudio = AUD_RE.test(lower);
|
const isAudio = AUD_RE.test(lower);
|
||||||
|
|
||||||
|
// Base preview URL from the link we clicked
|
||||||
|
const baseUrl = fileUrl;
|
||||||
|
|
||||||
|
// Use the same preview endpoint, just swap the "file" param.
|
||||||
|
function siblingPreviewUrl(newName) {
|
||||||
|
try {
|
||||||
|
const u = new URL(baseUrl, window.location.origin);
|
||||||
|
u.searchParams.set('file', newName);
|
||||||
|
// cache-bust so we don’t get stale frames
|
||||||
|
u.searchParams.set('t', String(Date.now()));
|
||||||
|
return u.toString();
|
||||||
|
} catch {
|
||||||
|
// Fallback: go through generic download/inline endpoint
|
||||||
|
return buildPreviewUrl(folder, newName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setTitle(overlay, name);
|
setTitle(overlay, name);
|
||||||
if (isSvg) {
|
if (isSvg) {
|
||||||
const downloadBtn = makeDownloadButton(folder, () => name);
|
const downloadBtn = makeDownloadButton(folder, () => name);
|
||||||
@@ -582,7 +599,7 @@ const navigate = (dir) => {
|
|||||||
img.dataset.scale = 1;
|
img.dataset.scale = 1;
|
||||||
img.dataset.rotate = 0;
|
img.dataset.rotate = 0;
|
||||||
img.style.transform = 'scale(1) rotate(0deg)';
|
img.style.transform = 'scale(1) rotate(0deg)';
|
||||||
img.src = buildPreviewUrl(folder, newFile);
|
img.src = siblingPreviewUrl(newFile); // <-- changed
|
||||||
};
|
};
|
||||||
|
|
||||||
if (images.length > 1) {
|
if (images.length > 1) {
|
||||||
@@ -610,212 +627,226 @@ const navigate = (dir) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------- VIDEOS -------------------- */
|
/* -------------------- VIDEOS -------------------- */
|
||||||
if (isVideo) {
|
if (isVideo) {
|
||||||
let video = document.createElement("video");
|
let video = document.createElement("video");
|
||||||
video.controls = true;
|
video.controls = true;
|
||||||
video.preload = 'auto'; // hint browser to start fetching quickly
|
video.preload = 'auto'; // hint browser to start fetching quickly
|
||||||
video.style.maxWidth = "88vw";
|
video.style.maxWidth = "88vw";
|
||||||
video.style.maxHeight = "88vh";
|
video.style.maxHeight = "88vh";
|
||||||
video.style.objectFit = "contain";
|
video.style.objectFit = "contain";
|
||||||
container.appendChild(video);
|
container.appendChild(video);
|
||||||
|
|
||||||
// Apply last-used volume/mute, and persist future changes
|
|
||||||
loadSavedMediaVolume(video);
|
|
||||||
attachVolumePersistence(video);
|
|
||||||
|
|
||||||
const markBtnIcon = makeTopIcon('check_circle', t("mark_as_viewed") || "Mark as viewed");
|
|
||||||
const clearBtnIcon = makeTopIcon('restart_alt', t("clear_progress") || "Clear progress");
|
|
||||||
|
|
||||||
// Track which file is currently active
|
// Apply last-used volume/mute, and persist future changes
|
||||||
let currentName = name;
|
loadSavedMediaVolume(video);
|
||||||
|
attachVolumePersistence(video);
|
||||||
|
|
||||||
const downloadBtn = makeDownloadButton(folder, () => currentName);
|
// Top-right action icons (Material icons, theme-aware)
|
||||||
|
const markBtnIcon = makeTopIcon('check_circle', t("mark_as_viewed") || "Mark as viewed");
|
||||||
|
const clearBtnIcon = makeTopIcon('restart_alt', t("clear_progress") || "Clear progress");
|
||||||
|
|
||||||
// Order: Download | Mark | Reset
|
// Track which file is currently active
|
||||||
actionWrap.appendChild(downloadBtn);
|
let currentName = name;
|
||||||
actionWrap.appendChild(markBtnIcon);
|
|
||||||
actionWrap.appendChild(clearBtnIcon);
|
|
||||||
|
|
||||||
const videos = (Array.isArray(fileData) ? fileData : []).filter(f => VID_RE.test(f.name));
|
// Use the URL we were passed in (old behavior) for the *first* video,
|
||||||
overlay.mediaType = 'video';
|
// fall back to API URL if for some reason it's empty.
|
||||||
overlay.mediaList = videos;
|
const initialUrl = fileUrl && fileUrl.trim()
|
||||||
overlay.mediaIndex = Math.max(0, videos.findIndex(f => f.name === name));
|
? fileUrl
|
||||||
setNavVisibility(overlay, videos.length > 1, videos.length > 1);
|
: buildPreviewUrl(folder, name);
|
||||||
|
|
||||||
const setVideoSrc = (nm) => {
|
const downloadBtn = makeDownloadButton(folder, () => currentName);
|
||||||
currentName = nm;
|
|
||||||
video.src = buildPreviewUrl(folder, nm);
|
// Order: Download | Mark | Reset
|
||||||
setTitle(overlay, nm);
|
actionWrap.appendChild(downloadBtn);
|
||||||
};
|
actionWrap.appendChild(markBtnIcon);
|
||||||
|
actionWrap.appendChild(clearBtnIcon);
|
||||||
const SAVE_INTERVAL_MS = 5000;
|
|
||||||
let lastSaveAt = 0;
|
const videos = (Array.isArray(fileData) ? fileData : []).filter(f => VID_RE.test(f.name));
|
||||||
let pending = false;
|
overlay.mediaType = 'video';
|
||||||
|
overlay.mediaList = videos;
|
||||||
async function getProgress(nm) {
|
overlay.mediaIndex = Math.max(0, videos.findIndex(f => f.name === name));
|
||||||
try {
|
setNavVisibility(overlay, videos.length > 1, videos.length > 1);
|
||||||
const res = await fetch(`/api/media/getProgress.php?folder=${encodeURIComponent(folder)}&file=${encodeURIComponent(nm)}&t=${Date.now()}`, { credentials: "include" });
|
|
||||||
const data = await res.json();
|
// Helper: set src for a given video name
|
||||||
return data && data.state ? data.state : null;
|
const setVideoSrc = (nm) => {
|
||||||
} catch { return null; }
|
currentName = nm;
|
||||||
}
|
|
||||||
|
// For the current file, reuse the original working URL.
|
||||||
async function sendProgress({nm, seconds, duration, completed, clear}) {
|
// For other files (next/prev), go through the API.
|
||||||
try {
|
const url = (nm === name) ? initialUrl : buildPreviewUrl(folder, nm);
|
||||||
pending = true;
|
|
||||||
const res = await fetch("/api/media/updateProgress.php", {
|
video.src = url;
|
||||||
method: "POST",
|
video.src = siblingPreviewUrl(nm);
|
||||||
credentials: "include",
|
setTitle(overlay, nm);
|
||||||
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
};
|
||||||
body: JSON.stringify({ folder, file: nm, seconds, duration, completed, clear })
|
|
||||||
});
|
const SAVE_INTERVAL_MS = 5000;
|
||||||
const data = await res.json();
|
let lastSaveAt = 0;
|
||||||
pending = false;
|
let pending = false;
|
||||||
return data;
|
|
||||||
} catch (e) {
|
async function getProgress(nm) {
|
||||||
pending = false;
|
try {
|
||||||
console.error(e);
|
const res = await fetch(`/api/media/getProgress.php?folder=${encodeURIComponent(folder)}&file=${encodeURIComponent(nm)}&t=${Date.now()}`, { credentials: "include" });
|
||||||
return null;
|
const data = await res.json();
|
||||||
}
|
return data && data.state ? data.state : null;
|
||||||
}
|
} catch { return null; }
|
||||||
|
}
|
||||||
const lsKey = (nm) => `videoProgress-${folder}/${nm}`;
|
|
||||||
|
async function sendProgress({nm, seconds, duration, completed, clear}) {
|
||||||
function renderStatus(state) {
|
try {
|
||||||
if (!statusChip) return;
|
pending = true;
|
||||||
|
const res = await fetch("/api/media/updateProgress.php", {
|
||||||
// Completed
|
method: "POST",
|
||||||
if (state && state.completed) {
|
credentials: "include",
|
||||||
statusChip.textContent = (t('viewed') || 'Viewed') + ' ✓';
|
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
|
||||||
statusChip.style.display = 'inline-block';
|
body: JSON.stringify({ folder, file: nm, seconds, duration, completed, clear })
|
||||||
statusChip.style.borderColor = 'rgba(34,197,94,.45)';
|
|
||||||
statusChip.style.background = 'rgba(34,197,94,.15)';
|
|
||||||
statusChip.style.color = '#22c55e';
|
|
||||||
markBtnIcon.style.display = 'none';
|
|
||||||
clearBtnIcon.style.display = '';
|
|
||||||
clearBtnIcon.title = t('reset_progress') || t('clear_progress') || 'Reset';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In progress
|
|
||||||
if (state && Number.isFinite(state.seconds) && Number.isFinite(state.duration) && state.duration > 0) {
|
|
||||||
const pct = Math.max(1, Math.min(99, Math.round((state.seconds / state.duration) * 100)));
|
|
||||||
statusChip.textContent = `${pct}%`;
|
|
||||||
statusChip.style.display = 'inline-block';
|
|
||||||
|
|
||||||
const dark = document.documentElement.classList.contains('dark-mode');
|
|
||||||
const ORANGE_HEX = '#ea580c';
|
|
||||||
statusChip.style.color = ORANGE_HEX;
|
|
||||||
statusChip.style.borderColor = dark ? 'rgba(234,88,12,.55)' : 'rgba(234,88,12,.45)';
|
|
||||||
statusChip.style.background = dark ? 'rgba(234,88,12,.18)' : 'rgba(234,88,12,.12)';
|
|
||||||
|
|
||||||
markBtnIcon.style.display = '';
|
|
||||||
clearBtnIcon.style.display = '';
|
|
||||||
clearBtnIcon.title = t('reset_progress') || t('clear_progress') || 'Reset';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No progress
|
|
||||||
statusChip.style.display = 'none';
|
|
||||||
markBtnIcon.style.display = '';
|
|
||||||
clearBtnIcon.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Event handlers (use currentName instead of rebinding per file) ----
|
|
||||||
video.addEventListener("loadedmetadata", async () => {
|
|
||||||
const nm = currentName;
|
|
||||||
try {
|
|
||||||
const state = await getProgress(nm);
|
|
||||||
if (state && Number.isFinite(state.seconds) && state.seconds > 0 && state.seconds < (video.duration || Infinity)) {
|
|
||||||
video.currentTime = state.seconds;
|
|
||||||
const seconds = Math.floor(video.currentTime || 0);
|
|
||||||
const duration = Math.floor(video.duration || 0);
|
|
||||||
setFileProgressBadge(nm, seconds, duration);
|
|
||||||
showToast((t("resumed_from") || "Resumed from") + " " + Math.floor(state.seconds) + "s");
|
|
||||||
} else {
|
|
||||||
const ls = localStorage.getItem(lsKey(nm));
|
|
||||||
if (ls) video.currentTime = parseFloat(ls);
|
|
||||||
}
|
|
||||||
renderStatus(state || null);
|
|
||||||
} catch {
|
|
||||||
renderStatus(null);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
const data = await res.json();
|
||||||
video.addEventListener("timeupdate", async () => {
|
pending = false;
|
||||||
const now = Date.now();
|
return data;
|
||||||
if ((now - lastSaveAt) < SAVE_INTERVAL_MS || pending) return;
|
} catch (e) {
|
||||||
lastSaveAt = now;
|
pending = false;
|
||||||
|
console.error(e);
|
||||||
const nm = currentName;
|
return null;
|
||||||
const seconds = Math.floor(video.currentTime || 0);
|
}
|
||||||
const duration = Math.floor(video.duration || 0);
|
}
|
||||||
|
|
||||||
sendProgress({ nm, seconds, duration });
|
const lsKey = (nm) => `videoProgress-${folder}/${nm}`;
|
||||||
setFileProgressBadge(nm, seconds, duration);
|
|
||||||
try { localStorage.setItem(lsKey(nm), String(seconds)); } catch {}
|
function renderStatus(state) {
|
||||||
renderStatus({ seconds, duration, completed: false });
|
if (!statusChip) return;
|
||||||
});
|
|
||||||
|
// Completed
|
||||||
video.addEventListener("ended", async () => {
|
if (state && state.completed) {
|
||||||
const nm = currentName;
|
statusChip.textContent = (t('viewed') || 'Viewed') + ' ✓';
|
||||||
const duration = Math.floor(video.duration || 0);
|
statusChip.style.display = 'inline-block';
|
||||||
await sendProgress({ nm, seconds: duration, duration, completed: true });
|
statusChip.style.borderColor = 'rgba(34,197,94,.45)';
|
||||||
try { localStorage.removeItem(lsKey(nm)); } catch {}
|
statusChip.style.background = 'rgba(34,197,94,.15)';
|
||||||
showToast(t("marked_viewed") || "Marked as viewed");
|
statusChip.style.color = '#22c55e';
|
||||||
setFileWatchedBadge(nm, true);
|
markBtnIcon.style.display = 'none';
|
||||||
renderStatus({ seconds: duration, duration, completed: true });
|
clearBtnIcon.style.display = '';
|
||||||
});
|
clearBtnIcon.title = t('reset_progress') || t('clear_progress') || 'Reset';
|
||||||
|
|
||||||
markBtnIcon.onclick = async () => {
|
|
||||||
const nm = currentName;
|
|
||||||
const duration = Math.floor(video.duration || 0);
|
|
||||||
await sendProgress({ nm, seconds: duration, duration, completed: true });
|
|
||||||
showToast(t("marked_viewed") || "Marked as viewed");
|
|
||||||
setFileWatchedBadge(nm, true);
|
|
||||||
renderStatus({ seconds: duration, duration, completed: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
clearBtnIcon.onclick = async () => {
|
|
||||||
const nm = currentName;
|
|
||||||
await sendProgress({ nm, seconds: 0, duration: null, completed: false, clear: true });
|
|
||||||
try { localStorage.removeItem(lsKey(nm)); } catch {}
|
|
||||||
showToast(t("progress_cleared") || "Progress cleared");
|
|
||||||
setFileWatchedBadge(nm, false);
|
|
||||||
renderStatus(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const navigate = (dir) => {
|
|
||||||
if (!overlay.mediaList || overlay.mediaList.length < 2) return;
|
|
||||||
overlay.mediaIndex = (overlay.mediaIndex + dir + overlay.mediaList.length) % overlay.mediaList.length;
|
|
||||||
const nm = overlay.mediaList[overlay.mediaIndex].name;
|
|
||||||
currentName = nm; // keep download button in sync
|
|
||||||
setVideoSrc(nm);
|
|
||||||
renderStatus(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (videos.length > 1) {
|
|
||||||
prevBtn.addEventListener('click', (e) => { e.stopPropagation(); navigate(-1); });
|
|
||||||
nextBtn.addEventListener('click', (e) => { e.stopPropagation(); navigate(+1); });
|
|
||||||
const onKey = (e) => {
|
|
||||||
if (!document.body.contains(overlay)) {
|
|
||||||
window.removeEventListener("keydown", onKey);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (e.key === "ArrowLeft") navigate(-1);
|
|
||||||
if (e.key === "ArrowRight") navigate(+1);
|
|
||||||
};
|
|
||||||
window.addEventListener("keydown", onKey);
|
|
||||||
overlay._onKey = onKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
setVideoSrc(name);
|
|
||||||
renderStatus(null);
|
|
||||||
overlay.style.display = "flex";
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In progress
|
||||||
|
if (state && Number.isFinite(state.seconds) && Number.isFinite(state.duration) && state.duration > 0) {
|
||||||
|
const pct = Math.max(1, Math.min(99, Math.round((state.seconds / state.duration) * 100)));
|
||||||
|
statusChip.textContent = `${pct}%`;
|
||||||
|
statusChip.style.display = 'inline-block';
|
||||||
|
|
||||||
|
const dark = document.documentElement.classList.contains('dark-mode');
|
||||||
|
const ORANGE_HEX = '#ea580c';
|
||||||
|
statusChip.style.color = ORANGE_HEX;
|
||||||
|
statusChip.style.borderColor = dark ? 'rgba(234,88,12,.55)' : 'rgba(234,88,12,.45)';
|
||||||
|
statusChip.style.background = dark ? 'rgba(234,88,12,.18)' : 'rgba(234,88,12,.12)';
|
||||||
|
|
||||||
|
markBtnIcon.style.display = '';
|
||||||
|
clearBtnIcon.style.display = '';
|
||||||
|
clearBtnIcon.title = t('reset_progress') || t('clear_progress') || 'Reset';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No progress
|
||||||
|
statusChip.style.display = 'none';
|
||||||
|
markBtnIcon.style.display = '';
|
||||||
|
clearBtnIcon.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Event handlers (use currentName instead of rebinding per file) ----
|
||||||
|
video.addEventListener("loadedmetadata", async () => {
|
||||||
|
const nm = currentName;
|
||||||
|
try {
|
||||||
|
const state = await getProgress(nm);
|
||||||
|
if (state && Number.isFinite(state.seconds) && state.seconds > 0 && state.seconds < (video.duration || Infinity)) {
|
||||||
|
video.currentTime = state.seconds;
|
||||||
|
const seconds = Math.floor(video.currentTime || 0);
|
||||||
|
const duration = Math.floor(video.duration || 0);
|
||||||
|
setFileProgressBadge(nm, seconds, duration);
|
||||||
|
showToast((t("resumed_from") || "Resumed from") + " " + Math.floor(state.seconds) + "s");
|
||||||
|
} else {
|
||||||
|
const ls = localStorage.getItem(lsKey(nm));
|
||||||
|
if (ls) video.currentTime = parseFloat(ls);
|
||||||
|
}
|
||||||
|
renderStatus(state || null);
|
||||||
|
} catch {
|
||||||
|
renderStatus(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
video.addEventListener("timeupdate", async () => {
|
||||||
|
const now = Date.now();
|
||||||
|
if ((now - lastSaveAt) < SAVE_INTERVAL_MS || pending) return;
|
||||||
|
lastSaveAt = now;
|
||||||
|
|
||||||
|
const nm = currentName;
|
||||||
|
const seconds = Math.floor(video.currentTime || 0);
|
||||||
|
const duration = Math.floor(video.duration || 0);
|
||||||
|
|
||||||
|
sendProgress({ nm, seconds, duration });
|
||||||
|
setFileProgressBadge(nm, seconds, duration);
|
||||||
|
try { localStorage.setItem(lsKey(nm), String(seconds)); } catch {}
|
||||||
|
renderStatus({ seconds, duration, completed: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
video.addEventListener("ended", async () => {
|
||||||
|
const nm = currentName;
|
||||||
|
const duration = Math.floor(video.duration || 0);
|
||||||
|
await sendProgress({ nm, seconds: duration, duration, completed: true });
|
||||||
|
try { localStorage.removeItem(lsKey(nm)); } catch {}
|
||||||
|
showToast(t("marked_viewed") || "Marked as viewed");
|
||||||
|
setFileWatchedBadge(nm, true);
|
||||||
|
renderStatus({ seconds: duration, duration, completed: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
markBtnIcon.onclick = async () => {
|
||||||
|
const nm = currentName;
|
||||||
|
const duration = Math.floor(video.duration || 0);
|
||||||
|
await sendProgress({ nm, seconds: duration, duration, completed: true });
|
||||||
|
showToast(t("marked_viewed") || "Marked as viewed");
|
||||||
|
setFileWatchedBadge(nm, true);
|
||||||
|
renderStatus({ seconds: duration, duration, completed: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
clearBtnIcon.onclick = async () => {
|
||||||
|
const nm = currentName;
|
||||||
|
await sendProgress({ nm, seconds: 0, duration: null, completed: false, clear: true });
|
||||||
|
try { localStorage.removeItem(lsKey(nm)); } catch {}
|
||||||
|
showToast(t("progress_cleared") || "Progress cleared");
|
||||||
|
setFileWatchedBadge(nm, false);
|
||||||
|
renderStatus(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigate = (dir) => {
|
||||||
|
if (!overlay.mediaList || overlay.mediaList.length < 2) return;
|
||||||
|
overlay.mediaIndex = (overlay.mediaIndex + dir + overlay.mediaList.length) % overlay.mediaList.length;
|
||||||
|
const nm = overlay.mediaList[overlay.mediaIndex].name;
|
||||||
|
setVideoSrc(nm);
|
||||||
|
renderStatus(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (videos.length > 1) {
|
||||||
|
prevBtn.addEventListener('click', (e) => { e.stopPropagation(); navigate(-1); });
|
||||||
|
nextBtn.addEventListener('click', (e) => { e.stopPropagation(); navigate(+1); });
|
||||||
|
const onKey = (e) => {
|
||||||
|
if (!document.body.contains(overlay)) {
|
||||||
|
window.removeEventListener("keydown", onKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === "ArrowLeft") navigate(-1);
|
||||||
|
if (e.key === "ArrowRight") navigate(+1);
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", onKey);
|
||||||
|
overlay._onKey = onKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kick off first video using the original working URL
|
||||||
|
setVideoSrc(name);
|
||||||
|
renderStatus(null);
|
||||||
|
overlay.style.display = "flex";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------- AUDIO / OTHER -------------------- */
|
/* -------------------- AUDIO / OTHER -------------------- */
|
||||||
if (isAudio) {
|
if (isAudio) {
|
||||||
const audio = document.createElement("audio");
|
const audio = document.createElement("audio");
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# === Update FileRise to v2.1.0 (safe rsync, no composer on demo) ===
|
# === Update FileRise to v2.3.2 (safe rsync, no composer on demo) ===
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
VER="v2.1.0"
|
VER="v2.3.2"
|
||||||
ASSET="FileRise-${VER}.zip" # matches GitHub release asset name
|
ASSET="FileRise-${VER}.zip" # matches GitHub release asset name
|
||||||
|
|
||||||
WEBROOT="/var/www"
|
WEBROOT="/var/www"
|
||||||
|
|||||||
Reference in New Issue
Block a user