Projekt aus Docker-Image videoconverter:2.9 extrahiert. Enthält zweiphasigen Import-Workflow mit Serien-Zuordnung. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
301 lines
9.1 KiB
JavaScript
301 lines
9.1 KiB
JavaScript
/**
|
|
* Filebrowser + Upload fuer VideoKonverter
|
|
*/
|
|
|
|
// === Filebrowser ===
|
|
|
|
let fbCurrentPath = "/mnt";
|
|
let fbSelectedFiles = new Set();
|
|
let fbSelectedDirs = new Set();
|
|
|
|
function openFileBrowser() {
|
|
document.getElementById("filebrowser-overlay").style.display = "flex";
|
|
fbSelectedFiles.clear();
|
|
fbSelectedDirs.clear();
|
|
fbNavigate("/mnt");
|
|
}
|
|
|
|
function closeFileBrowser() {
|
|
document.getElementById("filebrowser-overlay").style.display = "none";
|
|
}
|
|
|
|
function closeBrowserOnOverlay(e) {
|
|
if (e.target === e.currentTarget) closeFileBrowser();
|
|
}
|
|
|
|
async function fbNavigate(path) {
|
|
fbCurrentPath = path;
|
|
fbSelectedFiles.clear();
|
|
fbSelectedDirs.clear();
|
|
updateFbSelection();
|
|
|
|
const content = document.getElementById("fb-content");
|
|
content.innerHTML = '<div class="fb-loading">Lade...</div>';
|
|
|
|
try {
|
|
const resp = await fetch("/api/browse?path=" + encodeURIComponent(path));
|
|
const data = await resp.json();
|
|
|
|
if (!resp.ok) {
|
|
content.innerHTML = `<div class="fb-error">${data.error}</div>`;
|
|
return;
|
|
}
|
|
|
|
renderBreadcrumb(data.path);
|
|
renderBrowser(data);
|
|
} catch (e) {
|
|
content.innerHTML = '<div class="fb-error">Verbindungsfehler</div>';
|
|
}
|
|
}
|
|
|
|
function renderBreadcrumb(path) {
|
|
const bc = document.getElementById("fb-breadcrumb");
|
|
const parts = path.split("/").filter(Boolean);
|
|
let html = '<span class="bc-item" onclick="fbNavigate(\'/mnt\')">/mnt</span>';
|
|
|
|
let current = "";
|
|
for (const part of parts) {
|
|
current += "/" + part;
|
|
if (current === "/mnt") continue;
|
|
html += ` <span class="bc-sep">/</span> `;
|
|
html += `<span class="bc-item" onclick="fbNavigate('${current}')">${part}</span>`;
|
|
}
|
|
bc.innerHTML = html;
|
|
}
|
|
|
|
function renderBrowser(data) {
|
|
const content = document.getElementById("fb-content");
|
|
let html = "";
|
|
|
|
// "Nach oben" Link
|
|
if (data.parent) {
|
|
html += `<div class="fb-item fb-dir fb-parent" onclick="fbNavigate('${data.parent}')">
|
|
<span class="fb-icon">↩</span>
|
|
<span class="fb-name">..</span>
|
|
</div>`;
|
|
}
|
|
|
|
// Ordner
|
|
for (const dir of data.dirs) {
|
|
const badge = dir.video_count > 0 ? `<span class="fb-badge">${dir.video_count} Videos</span>` : "";
|
|
html += `<div class="fb-item fb-dir" ondblclick="fbNavigate('${dir.path}')">
|
|
<label class="fb-check" onclick="event.stopPropagation()">
|
|
<input type="checkbox" onchange="fbToggleDir('${dir.path}', this.checked)">
|
|
</label>
|
|
<span class="fb-icon" onclick="fbNavigate('${dir.path}')">📁</span>
|
|
<span class="fb-name" onclick="fbNavigate('${dir.path}')">${dir.name}</span>
|
|
${badge}
|
|
</div>`;
|
|
}
|
|
|
|
// Dateien
|
|
for (const file of data.files) {
|
|
html += `<div class="fb-item fb-file">
|
|
<label class="fb-check">
|
|
<input type="checkbox" onchange="fbToggleFile('${file.path}', this.checked)">
|
|
</label>
|
|
<span class="fb-icon">🎥</span>
|
|
<span class="fb-name">${file.name}</span>
|
|
<span class="fb-size">${file.size_human}</span>
|
|
</div>`;
|
|
}
|
|
|
|
if (data.dirs.length === 0 && data.files.length === 0) {
|
|
html = '<div class="fb-empty">Keine Videodateien in diesem Ordner</div>';
|
|
}
|
|
|
|
content.innerHTML = html;
|
|
}
|
|
|
|
function fbToggleFile(path, checked) {
|
|
if (checked) fbSelectedFiles.add(path);
|
|
else fbSelectedFiles.delete(path);
|
|
updateFbSelection();
|
|
}
|
|
|
|
function fbToggleDir(path, checked) {
|
|
if (checked) fbSelectedDirs.add(path);
|
|
else fbSelectedDirs.delete(path);
|
|
updateFbSelection();
|
|
}
|
|
|
|
function fbSelectAll() {
|
|
const checks = document.querySelectorAll("#fb-content input[type=checkbox]");
|
|
const allChecked = Array.from(checks).every(c => c.checked);
|
|
|
|
checks.forEach(c => {
|
|
c.checked = !allChecked;
|
|
c.dispatchEvent(new Event("change"));
|
|
});
|
|
}
|
|
|
|
function updateFbSelection() {
|
|
const count = fbSelectedFiles.size + fbSelectedDirs.size;
|
|
document.getElementById("fb-selected-count").textContent = `${count} ausgewaehlt`;
|
|
document.getElementById("fb-convert").disabled = count === 0;
|
|
}
|
|
|
|
async function fbConvertSelected() {
|
|
const paths = [...fbSelectedFiles, ...fbSelectedDirs];
|
|
if (paths.length === 0) return;
|
|
|
|
document.getElementById("fb-convert").disabled = true;
|
|
document.getElementById("fb-convert").textContent = "Wird gesendet...";
|
|
|
|
try {
|
|
const resp = await fetch("/api/convert", {
|
|
method: "POST",
|
|
headers: {"Content-Type": "application/json"},
|
|
body: JSON.stringify({files: paths}),
|
|
});
|
|
const data = await resp.json();
|
|
|
|
showToast(data.message, "success");
|
|
closeFileBrowser();
|
|
} catch (e) {
|
|
showToast("Fehler beim Senden", "error");
|
|
} finally {
|
|
document.getElementById("fb-convert").disabled = false;
|
|
document.getElementById("fb-convert").textContent = "Konvertieren";
|
|
}
|
|
}
|
|
|
|
// === Upload ===
|
|
|
|
let uploadFiles = [];
|
|
|
|
function openUpload() {
|
|
document.getElementById("upload-overlay").style.display = "flex";
|
|
uploadFiles = [];
|
|
document.getElementById("upload-list").innerHTML = "";
|
|
document.getElementById("upload-progress").style.display = "none";
|
|
document.getElementById("upload-start").disabled = true;
|
|
document.getElementById("upload-input").value = "";
|
|
}
|
|
|
|
function closeUpload() {
|
|
document.getElementById("upload-overlay").style.display = "none";
|
|
}
|
|
|
|
function closeUploadOnOverlay(e) {
|
|
if (e.target === e.currentTarget) closeUpload();
|
|
}
|
|
|
|
function handleDragOver(e) {
|
|
e.preventDefault();
|
|
e.currentTarget.classList.add("drag-over");
|
|
}
|
|
|
|
function handleDragLeave(e) {
|
|
e.currentTarget.classList.remove("drag-over");
|
|
}
|
|
|
|
function handleDrop(e) {
|
|
e.preventDefault();
|
|
e.currentTarget.classList.remove("drag-over");
|
|
addFiles(e.dataTransfer.files);
|
|
}
|
|
|
|
function handleFileSelect(e) {
|
|
addFiles(e.target.files);
|
|
}
|
|
|
|
function addFiles(fileList) {
|
|
for (const file of fileList) {
|
|
if (!uploadFiles.some(f => f.name === file.name && f.size === file.size)) {
|
|
uploadFiles.push(file);
|
|
}
|
|
}
|
|
renderUploadList();
|
|
}
|
|
|
|
function removeUploadFile(index) {
|
|
uploadFiles.splice(index, 1);
|
|
renderUploadList();
|
|
}
|
|
|
|
function renderUploadList() {
|
|
const list = document.getElementById("upload-list");
|
|
if (uploadFiles.length === 0) {
|
|
list.innerHTML = "";
|
|
document.getElementById("upload-start").disabled = true;
|
|
return;
|
|
}
|
|
|
|
let html = "";
|
|
uploadFiles.forEach((file, i) => {
|
|
const size = file.size < 1024 * 1024
|
|
? (file.size / 1024).toFixed(0) + " KiB"
|
|
: file.size < 1024 * 1024 * 1024
|
|
? (file.size / (1024 * 1024)).toFixed(1) + " MiB"
|
|
: (file.size / (1024 * 1024 * 1024)).toFixed(2) + " GiB";
|
|
html += `<div class="upload-item">
|
|
<span class="upload-item-name">${file.name}</span>
|
|
<span class="upload-item-size">${size}</span>
|
|
<button class="btn-danger btn-small" onclick="removeUploadFile(${i})">×</button>
|
|
</div>`;
|
|
});
|
|
list.innerHTML = html;
|
|
document.getElementById("upload-start").disabled = false;
|
|
}
|
|
|
|
async function startUpload() {
|
|
if (uploadFiles.length === 0) return;
|
|
|
|
const btn = document.getElementById("upload-start");
|
|
btn.disabled = true;
|
|
btn.textContent = "Wird hochgeladen...";
|
|
|
|
const progress = document.getElementById("upload-progress");
|
|
const bar = document.getElementById("upload-bar");
|
|
const status = document.getElementById("upload-status");
|
|
progress.style.display = "block";
|
|
|
|
const formData = new FormData();
|
|
uploadFiles.forEach(f => formData.append("files", f));
|
|
|
|
try {
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open("POST", "/api/upload");
|
|
|
|
xhr.upload.onprogress = (e) => {
|
|
if (e.lengthComputable) {
|
|
const pct = (e.loaded / e.total) * 100;
|
|
bar.style.width = pct + "%";
|
|
const loaded = (e.loaded / (1024 * 1024)).toFixed(1);
|
|
const total = (e.total / (1024 * 1024)).toFixed(1);
|
|
status.textContent = `${loaded} / ${total} MiB (${pct.toFixed(0)}%)`;
|
|
}
|
|
};
|
|
|
|
const result = await new Promise((resolve, reject) => {
|
|
xhr.onload = () => {
|
|
if (xhr.status === 200) resolve(JSON.parse(xhr.responseText));
|
|
else reject(JSON.parse(xhr.responseText));
|
|
};
|
|
xhr.onerror = () => reject({error: "Netzwerkfehler"});
|
|
xhr.send(formData);
|
|
});
|
|
|
|
showToast(result.message, "success");
|
|
closeUpload();
|
|
} catch (e) {
|
|
showToast(e.error || "Upload fehlgeschlagen", "error");
|
|
btn.disabled = false;
|
|
btn.textContent = "Hochladen & Konvertieren";
|
|
}
|
|
}
|
|
|
|
// === Toast ===
|
|
|
|
function showToast(message, type) {
|
|
const container = document.getElementById("toast-container");
|
|
if (!container) return;
|
|
|
|
const toast = document.createElement("div");
|
|
toast.className = `toast ${type}`;
|
|
toast.textContent = message;
|
|
container.appendChild(toast);
|
|
|
|
setTimeout(() => toast.remove(), 3000);
|
|
}
|