From 04cd929c9cd7471296f37c60a10bb2adff9f2adb Mon Sep 17 00:00:00 2001 From: Kroese Date: Sun, 9 Aug 2026 13:33:33 +0200 Subject: [PATCH] feat: Show progress while extracting DSM image (#1362) --- src/install.sh | 5 ++ web/css/style.css | 62 +++++++++++++ web/js/script.js | 215 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 282 insertions(+) diff --git a/src/install.sh b/src/install.sh index 068901a..a184683 100644 --- a/src/install.sh +++ b/src/install.sh @@ -227,6 +227,8 @@ fi MSG="Extracting installation image..." info "Install: $MSG" && html "$MSG" +/run/progress.sh "$TMP" "$SIZE" "$MSG ([P])..." & + # Newer PAT files are normal tar archives; older encrypted/proprietary forms # require the bundled extractor as a compatibility fallback. if { tar tf "$PAT"; } >/dev/null 2>&1; then @@ -238,12 +240,15 @@ else { (cd "$TMP" && python3 /run/extract.py -i "$PAT" -d 2>/run/extract.log); rc=$?; } || : if (( rc != 0 )); then + fKill "progress.sh" cat /run/extract.log error "Failed to extract PAT file, reason $rc" && exit 63 fi fi +fKill "progress.sh" + MSG="Preparing system partition..." info "Install: $MSG" && html "$MSG" diff --git a/web/css/style.css b/web/css/style.css index 20960f1..f18dc69 100644 --- a/web/css/style.css +++ b/web/css/style.css @@ -17,6 +17,68 @@ body { margin-top: 50px; } +#progress { + width: 0; + min-width: 250px; + max-width: 80vw; + height: 15px; + overflow: hidden; + margin: 20px auto 0; + border-radius: 999px; + background: rgba(0, 0, 0, 0.17); + box-shadow: + inset 0 1px 2px rgba(0, 0, 0, 0.22), + 0 1px 0 rgba(255, 255, 255, 0.08); +} + +#progress[hidden] { + display: none; +} + +#progress-fill { + position: relative; + width: 0; + height: 100%; + overflow: hidden; + background: rgba(255, 255, 255, 0.94); + transition: width 0.25s ease; +} + +#progress-fill:after { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: -40%; + width: 34%; + background: linear-gradient( + 90deg, + rgba(255, 255, 255, 0), + rgba(189, 227, 255, 0.15) 25%, + rgba(255, 255, 255, 0.70) 50%, + rgba(189, 227, 255, 0.15) 75%, + rgba(255, 255, 255, 0) + ); + animation: progress-sheen 1.9s ease-in-out infinite; +} + +@keyframes progress-sheen { + 0% { + left: -40%; + opacity: 0; + } + + 12% { + opacity: 1; + } + + 60%, + 100% { + left: 112%; + opacity: 1; + } +} + footer { width: 98%; position: fixed; diff --git a/web/js/script.js b/web/js/script.js index 1f3b7e5..16c67e8 100644 --- a/web/js/script.js +++ b/web/js/script.js @@ -259,6 +259,210 @@ function rememberStatus(msg) { return true; } +function parseSize(value, unit) { + + var powers = { + "B": 0, + "KB": 1, + "KIB": 1, + "MB": 2, + "MIB": 2, + "GB": 3, + "GIB": 3, + "TB": 4, + "TIB": 4, + "PB": 5, + "PIB": 5, + "EB": 6, + "EIB": 6 + }; + + unit = unit.toUpperCase(); + + if (!Object.prototype.hasOwnProperty.call(powers, unit)) { + return null; + } + + var bytes = Number(value) * Math.pow(1024, powers[unit]); + + if (!Number.isFinite(bytes) || bytes < 0) { + return null; + } + + return bytes; +} + +function estimateProgress(bytes) { + + var boundary = 512 * 1024 * 1024; + var previousBoundary = 0; + + while (bytes > boundary) { + previousBoundary = boundary; + boundary *= 2; + } + + if (previousBoundary === 0) { + return Math.min(bytes / boundary * 100, 100); + } + + var rangeProgress = + (bytes - previousBoundary) / + (boundary - previousBoundary); + + return 30 + Math.pow(rangeProgress, 2) * 70; +} + +function parseProgress(msg) { + + var container = document.createElement("div"); + container.innerHTML = msg; + + var walker = document.createTreeWalker( + container, + NodeFilter.SHOW_TEXT + ); + + var node; + var lastNode = null; + + while ((node = walker.nextNode())) { + if (node.nodeValue.trim() !== "") { + lastNode = node; + } + } + + if (!lastNode) { + return { + message: msg, + progress: null, + size: null + }; + } + + var percentMatch = lastNode.nodeValue.match( + /\s+\((\d+(?:\.\d+)?)%\)\s*$/ + ); + + if (percentMatch) { + var progress = Number(percentMatch[1]); + + if (Number.isFinite(progress) && progress >= 0 && progress <= 100) { + lastNode.nodeValue = lastNode.nodeValue.slice( + 0, + percentMatch.index + ); + + return { + message: container.innerHTML, + progress: progress, + size: null + }; + } + } + + var sizeMatch = lastNode.nodeValue.match( + /\s+\((\d+(?:\.\d+)?)\s+([KMGTPE]?i?B)\)\s*$/i + ); + + if (sizeMatch) { + var bytes = parseSize(sizeMatch[1], sizeMatch[2]); + + if (bytes != null) { + var size = sizeMatch[1] + " " + sizeMatch[2]; + + return { + message: msg, + progress: estimateProgress(bytes), + size: size + }; + } + } + + return { + message: msg, + progress: null, + size: null + }; +} + +function resizeProgress() { + + var info = document.getElementById("info"); + var progress = document.getElementById("progress"); + + if (!info || !progress || progress.hidden) { + return false; + } + + var style = window.getComputedStyle(info); + var measurement = document.createElement("span"); + + measurement.textContent = info.innerText; + measurement.style.position = "fixed"; + measurement.style.left = "-9999px"; + measurement.style.top = "-9999px"; + measurement.style.visibility = "hidden"; + measurement.style.whiteSpace = "nowrap"; + measurement.style.fontFamily = style.fontFamily; + measurement.style.fontSize = style.fontSize; + measurement.style.fontWeight = style.fontWeight; + measurement.style.fontStyle = style.fontStyle; + measurement.style.letterSpacing = style.letterSpacing; + measurement.style.textTransform = style.textTransform; + + document.body.appendChild(measurement); + + var textWidth = measurement.getBoundingClientRect().width; + var loading = info.getElementsByClassName("loading").length > 0; + var dotsWidth = 0; + + if (loading) { + dotsWidth = parseFloat(style.fontSize) * 0.75; + } + + measurement.remove(); + + var maximumWidth = window.innerWidth * 0.8; + var width = Math.min(textWidth + dotsWidth + 100, maximumWidth); + + progress.style.width = width + "px"; + progress.style.transform = loading + ? "translateX(" + (dotsWidth / 2) + "px)" + : ""; + + return true; +} + +function setProgress(value, size) { + + var progress = document.getElementById("progress"); + var fill = document.getElementById("progress-fill"); + + if (value == null) { + progress.hidden = true; + progress.removeAttribute("title"); + progress.removeAttribute("aria-valuenow"); + progress.removeAttribute("aria-valuetext"); + fill.style.width = "0%"; + return true; + } + + progress.hidden = false; + progress.title = size != null ? size : value + "%"; + progress.setAttribute("aria-valuenow", value); + + if (size != null) { + progress.setAttribute("aria-valuetext", size); + } else { + progress.removeAttribute("aria-valuetext"); + } + + fill.style.width = value + "%"; + + return true; +} + function setInfo(msg, loading, error) { try { @@ -266,9 +470,17 @@ function setInfo(msg, loading, error) { return false; } + var parsed = parseProgress(msg); + msg = parsed.message; + setProgress(parsed.progress, parsed.size); + var el = document.getElementById("info"); if (el.innerText == msg || el.innerHTML == msg) { + var progressEl = document.getElementById("progress"); + if (progressEl && !progressEl.hidden && !progressEl.style.width) { + resizeProgress(); + } return true; } @@ -290,11 +502,13 @@ function setInfo(msg, loading, error) { if (msg.includes(p)) { if (el.innerHTML.includes(p)) { el.getElementsByClassName('loading')[0].textContent = extractContent(msg); + resizeProgress(); return true; } } el.innerHTML = msg; + resizeProgress(); return true; } catch (e) { @@ -413,6 +627,7 @@ function connect() { }; } +window.addEventListener("resize", resizeProgress); document.addEventListener("visibilitychange", visibilityChanged); rememberStatus(document.getElementById("info").innerHTML);