feat: env-driven upload limits + request timeout, inline video playback, range requests
- Add UPLOAD_FILE_LIMIT_MB/UPLOAD_TOTAL_LIMIT_MB env (defaults 50/200), compute request timeout from total limit or UPLOAD_REQUEST_TIMEOUT_MS - Expose upload limits via /api/public-settings and sync in frontend (remove hardcoded 50MB assumption) - Add byte-range support in storage (getRange/streamRangeTo) and serve Content-Range/Accept-Ranges for S3/local - Implement inline playable video delivery for browser formats (mp4/m4v/webm/ogv) with ?play=1, range requests, proper 206/416 - Add video modal in journal UI with player and download fallback - Update docs (AGENTS.md/PRD.md/README.md), styles for video modal, add instructions/TODO.md and screenshots - Extend MIME types for media
This commit is contained in:
+60
-3
@@ -34,6 +34,14 @@ function isImageFile(name) {
|
||||
return /\.(jpe?g|jfif|png|gif|webp|bmp|avif|svg|ico)$/i.test(name || '');
|
||||
}
|
||||
|
||||
function isPlayableVideoFile(name) {
|
||||
return /\.(mp4|m4v|webm|ogv)$/i.test(name || '');
|
||||
}
|
||||
|
||||
function isVideoFile(name) {
|
||||
return /\.(mp4|m4v|webm|ogv|mov|mkv|avi|mpeg|mpg|3gp|ts)$/i.test(name || '');
|
||||
}
|
||||
|
||||
function mainPhotoPath(e) {
|
||||
if (e.photo_path) return e.photo_path;
|
||||
if (e.photos && e.photos.length) return e.photos[0].photo_path;
|
||||
@@ -43,9 +51,16 @@ function mainPhotoPath(e) {
|
||||
function filesHTML(files) {
|
||||
if (!files || !files.length) return '';
|
||||
return `<div class="entry-files">${files.map(f => {
|
||||
const link = isImageFile(f.name)
|
||||
? `<a class="f-link" href="#" data-img="${API}/api/files/${f.token}" title="Открыть">${esc(f.name)}</a>`
|
||||
: `<a class="f-link" href="${API}/api/files/${f.token}" download title="Скачать">${esc(f.name)}</a>`;
|
||||
const url = `${API}/api/files/${f.token}`;
|
||||
let link;
|
||||
if (isImageFile(f.name)) {
|
||||
link = `<a class="f-link" href="#" data-img="${url}" title="Открыть">${esc(f.name)}</a>`;
|
||||
} else if (isPlayableVideoFile(f.name)) {
|
||||
link = `<a class="f-link" href="${url}?play=1" data-video="${url}?play=1" data-video-name="${esc(f.name)}" data-video-token="${f.token}" title="Смотреть">${esc(f.name)}</a>`;
|
||||
} else {
|
||||
const note = isVideoFile(f.name) ? 'Скачать (браузер не умеет играть этот формат)' : 'Скачать';
|
||||
link = `<a class="f-link" href="${url}" download title="${note}">${esc(f.name)}</a>`;
|
||||
}
|
||||
return `<span class="f-item">${link}<button class="f-detach" data-detach="${f.id}" title="Открепить файл">✕</button></span>`;
|
||||
}).join('')}</div>`;
|
||||
}
|
||||
@@ -1589,6 +1604,48 @@ document.getElementById('aiCorrectModal').addEventListener('click', e => {
|
||||
if (e.target === e.currentTarget) closeAiCorrect();
|
||||
});
|
||||
|
||||
function openVideo(url, name, token) {
|
||||
const modal = document.getElementById('videoModal');
|
||||
const player = document.getElementById('videoPlayer');
|
||||
const title = document.getElementById('videoTitle');
|
||||
const dl = document.getElementById('videoDownload');
|
||||
if (!modal || !player) return;
|
||||
player.src = url;
|
||||
player.currentTime = 0;
|
||||
title.textContent = name || 'Видео';
|
||||
dl.href = `${API}/api/files/${token}`;
|
||||
dl.setAttribute('download', '');
|
||||
modal.classList.add('open');
|
||||
const play = player.play();
|
||||
if (play && typeof play.catch === 'function') play.catch(() => {});
|
||||
}
|
||||
|
||||
function closeVideo() {
|
||||
const modal = document.getElementById('videoModal');
|
||||
const player = document.getElementById('videoPlayer');
|
||||
if (!modal || !player) return;
|
||||
try { player.pause(); } catch {}
|
||||
player.removeAttribute('src');
|
||||
player.load();
|
||||
modal.classList.remove('open');
|
||||
}
|
||||
|
||||
document.getElementById('videoModal').addEventListener('click', e => {
|
||||
if (e.target === e.currentTarget) closeVideo();
|
||||
});
|
||||
document.getElementById('videoClose').addEventListener('click', closeVideo);
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key !== 'Escape') return;
|
||||
if (document.getElementById('videoModal').classList.contains('open')) closeVideo();
|
||||
});
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
const v = e.target.closest('[data-video]');
|
||||
if (!v) return;
|
||||
e.preventDefault();
|
||||
openVideo(v.dataset.video, v.dataset.videoName, v.dataset.videoToken);
|
||||
});
|
||||
|
||||
document.getElementById('exportCsvBtn').addEventListener('click', exportCSV);
|
||||
document.getElementById('createLinkBtn').addEventListener('click', openLinkCreate);
|
||||
document.querySelectorAll('#viewToggle button').forEach(b => b.addEventListener('click', () => setView(b.dataset.view)));
|
||||
|
||||
Reference in New Issue
Block a user