diff --git a/build-plugin.sh b/build-plugin.sh index 54ab595..7315f46 100755 --- a/build-plugin.sh +++ b/build-plugin.sh @@ -2,7 +2,7 @@ set -eu PLUGIN="u-navigator" -VERSION="2026.06.23.r049" +VERSION="2026.06.24.r001" PKG_DIR="packages" WORK_DIR=".plugin-build" PKG_NAME="${PLUGIN}-${VERSION}.tgz" diff --git a/packages/u-navigator-2026.06.23.r049.tgz b/packages/u-navigator-2026.06.23.r049.tgz deleted file mode 100644 index 5fdc6dc..0000000 Binary files a/packages/u-navigator-2026.06.23.r049.tgz and /dev/null differ diff --git a/packages/u-navigator-2026.06.24.r001.tgz b/packages/u-navigator-2026.06.24.r001.tgz new file mode 100644 index 0000000..41e0f36 Binary files /dev/null and b/packages/u-navigator-2026.06.24.r001.tgz differ diff --git a/server/public/app.js b/server/public/app.js index 07a95a5..4071a0a 100644 --- a/server/public/app.js +++ b/server/public/app.js @@ -19,6 +19,8 @@ const state = { debug: [], nextDebugId: 1, jobs: new Map(), + uploadTransfers: new Map(), + nextUploadTransferId: 1, jobRefresh: new Map(), sizeJobs: new Map(), sizeResults: new Map(), @@ -634,13 +636,18 @@ function renderQueue(body) { function queueMarkup() { const jobs = [...state.jobs.values()].reverse(); - const jobMarkup = jobs.length ? jobs.map((job) => ` + const uploads = [...state.uploadTransfers.values()].reverse(); + const transferItems = [ + ...uploads.map(uploadTransferMarkup), + ...jobs.map((job) => `
${escapeHtml(job.type)} #${escapeHtml(job.id)}
${escapeHtml(job.status)} · ${escapeHtml(job.message || '')}
- `).join('') : '

Keine Transfers.

'; + `) + ]; + const jobMarkup = transferItems.length ? transferItems.join('') : '

Keine Transfers.

'; const debugSection = state.config?.debug ? debugMarkup() : ''; return ` @@ -649,6 +656,22 @@ function queueMarkup() { `; } +function uploadTransferMarkup(upload) { + const progress = Math.max(0, Math.min(100, Number(upload.progress) || 0)); + const fileText = `${upload.completedFiles + upload.skippedFiles}/${upload.totalFiles} Dateien`; + const byteText = `${formatBytes(upload.uploadedBytes)} / ${formatBytes(upload.totalBytes)}`; + const detail = upload.currentFile ? ` · ${upload.currentFile}` : ''; + const skipped = upload.skippedFiles ? ` · ${upload.skippedFiles} übersprungen` : ''; + const overwritten = upload.overwrittenFiles ? ` · ${upload.overwrittenFiles} überschrieben` : ''; + return ` +
+ Upload #${escapeHtml(upload.id)} +
${escapeHtml(upload.status)} · ${escapeHtml(fileText)} · ${escapeHtml(byteText)}${escapeHtml(detail)}${escapeHtml(skipped)}${escapeHtml(overwritten)}
+
+
+ `; +} + function debugMarkup() { const debugMarkup = state.debug.length ? state.debug.slice(-12).reverse().map((item) => `
@@ -726,7 +749,9 @@ function scheduleTransferAutoHide() { } function activeTransferCount() { - return [...state.jobs.values()].filter((job) => job.type !== 'size' && ['queued', 'running', 'cancel_requested'].includes(job.status)).length; + const activeJobs = [...state.jobs.values()].filter((job) => job.type !== 'size' && ['queued', 'running', 'cancel_requested'].includes(job.status)).length; + const activeUploads = [...state.uploadTransfers.values()].filter((upload) => ['queued', 'running'].includes(upload.status)).length; + return activeJobs + activeUploads; } function renderTransferPanel() { @@ -902,29 +927,68 @@ async function uploadFileList(win, files, targetPath) { } if (!files.length) return; const uploadItems = normalizeUploadItems(files); + const transfer = createUploadTransfer(targetPath, uploadItems); for (const [index, item] of uploadItems.entries()) { + updateUploadTransfer(transfer, { + status: 'running', + currentFile: item.path, + currentIndex: index + 1 + }); recordDebug('upload.file.request', { path: item.path, size: item.file.size, index: index + 1, total: uploadItems.length }); try { - await uploadFileChunked(targetPath, item.file, item.path, false); + await uploadFileChunked(targetPath, item.file, item.path, false, (bytes) => { + addUploadProgress(transfer, bytes); + }); + updateUploadTransfer(transfer, { + completedFiles: transfer.completedFiles + 1, + currentFile: item.path, + progress: uploadProgressPercent(transfer) + }); recordDebug('upload.file.done', { path: item.path, index: index + 1, total: uploadItems.length, overwrite: false }); } catch (error) { if (error.status !== 409) { + updateUploadTransfer(transfer, { + status: 'failed', + message: error.message || 'Upload fehlgeschlagen', + currentFile: item.path, + progress: uploadProgressPercent(transfer) + }, true); throw error; } const overwrite = window.confirm(`"${item.path}" existiert bereits.\n\nVorhandene Datei überschreiben?`); recordDebug('upload.overwrite.prompt', { path: item.path, overwrite }); if (!overwrite) { + updateUploadTransfer(transfer, { + skippedFiles: transfer.skippedFiles + 1, + currentFile: item.path, + progress: uploadProgressPercent(transfer) + }); recordDebug('upload.file.skipped', { path: item.path, index: index + 1, total: uploadItems.length }); continue; } - await uploadFileChunked(targetPath, item.file, item.path, true); + await uploadFileChunked(targetPath, item.file, item.path, true, (bytes) => { + addUploadProgress(transfer, bytes); + }); + updateUploadTransfer(transfer, { + completedFiles: transfer.completedFiles + 1, + overwrittenFiles: transfer.overwrittenFiles + 1, + currentFile: item.path, + progress: uploadProgressPercent(transfer) + }); recordDebug('upload.file.done', { path: item.path, index: index + 1, total: uploadItems.length, overwrite: true }); } } + updateUploadTransfer(transfer, { + status: 'completed', + message: 'Abgeschlossen', + currentFile: '', + progress: 100 + }, true); + scheduleTransferAutoHide(); await loadExplorer(win, targetPath); } -async function uploadFileChunked(targetPath, file, relativePath, overwrite = false) { +async function uploadFileChunked(targetPath, file, relativePath, overwrite = false, onChunkUploaded = null) { const chunkSize = 256 * 1024; const totalChunks = Math.max(1, Math.ceil(file.size / chunkSize)); let finalPayload = null; @@ -963,6 +1027,7 @@ async function uploadFileChunked(targetPath, file, relativePath, overwrite = fal error.path = relativePath; throw error; } + onChunkUploaded?.(chunk.size); try { finalPayload = JSON.parse(text); } catch (error) { @@ -974,6 +1039,59 @@ async function uploadFileChunked(targetPath, file, relativePath, overwrite = fal } } +function createUploadTransfer(targetPath, uploadItems) { + const transfer = { + id: `upload-${state.nextUploadTransferId++}`, + status: 'queued', + message: 'Wartet', + targetPath, + totalFiles: uploadItems.length, + completedFiles: 0, + skippedFiles: 0, + overwrittenFiles: 0, + totalBytes: uploadItems.reduce((sum, item) => sum + Number(item.file?.size || 0), 0), + uploadedBytes: 0, + currentFile: '', + currentIndex: 0, + progress: 0, + updatedAt: Date.now(), + lastRenderAt: 0 + }; + state.uploadTransfers.set(transfer.id, transfer); + showTransferPanel(); + updateTransferPanel(); + return transfer; +} + +function updateUploadTransfer(transfer, patch, force = false) { + Object.assign(transfer, patch, { + message: patch.message ?? `${transfer.currentIndex || 0}/${transfer.totalFiles}`, + updatedAt: Date.now() + }); + const now = Date.now(); + if (force || now - transfer.lastRenderAt > 180) { + transfer.lastRenderAt = now; + updateTransferPanel(); + } +} + +function addUploadProgress(transfer, bytes) { + transfer.uploadedBytes += Number(bytes || 0); + updateUploadTransfer(transfer, { + progress: uploadProgressPercent(transfer) + }); +} + +function uploadProgressPercent(transfer) { + if (transfer.totalBytes > 0) { + return (transfer.uploadedBytes / transfer.totalBytes) * 100; + } + if (transfer.totalFiles > 0) { + return ((transfer.completedFiles + transfer.skippedFiles) / transfer.totalFiles) * 100; + } + return 0; +} + function arrayBufferToBase64(buffer) { const bytes = new Uint8Array(buffer); let binary = ''; diff --git a/server/public/styles.css b/server/public/styles.css index fad81f4..a20eaab 100644 --- a/server/public/styles.css +++ b/server/public/styles.css @@ -809,6 +809,14 @@ padding: 10px; } +.u-nav .job .muted { + overflow-wrap: anywhere; +} + +.u-nav .upload-job { + border-color: color-mix(in srgb, var(--accent-2) 45%, var(--line)); +} + .u-nav .progress { background: var(--surface-2); border-radius: 999px; diff --git a/u-navigator.plg b/u-navigator.plg index 7d958bf..a533996 100644 --- a/u-navigator.plg +++ b/u-navigator.plg @@ -1,7 +1,7 @@ - + @@ -24,7 +24,7 @@ https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package; -3bf3f4752ed9cd8b86fd27f9e17fbe14 +5d36280f152114104067f4d3e3a97234