docker.videokonverter/video-konverter/app/static/js/filebrowser.js
data 37dff4de69 feat: VideoKonverter v2.9 - Projekt-Reset aus Docker-Image
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>
2026-02-27 11:41:48 +01:00

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">&#8617;</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}')">&#128193;</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">&#127909;</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})">&times;</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);
}