(appRoot)
| 89 | } |
| 90 | |
| 91 | async function installRuntimePack(appRoot) { |
| 92 | const status = await invoke("runtime_pack_status"); |
| 93 | if (!status.manifestReady) { |
| 94 | throw Object.assign( |
| 95 | new Error(`Python runtime not found under ${appRoot}.`), |
| 96 | { hint: "Try reinstalling StemDeck. If the problem persists, check that your disk has at least 2 GB free." } |
| 97 | ); |
| 98 | } |
| 99 | |
| 100 | const progressWrap = document.getElementById("progress-wrap"); |
| 101 | const progressFill = document.getElementById("progress-fill"); |
| 102 | |
| 103 | // lastProgressAt is updated by the SSE handler below; only meaningful |
| 104 | // during a network download, so it is reset just before download starts. |
| 105 | let lastReceived = 0; |
| 106 | let lastProgressAt = Date.now(); |
| 107 | const STALL_WARN_MS = 30_000; |
| 108 | |
| 109 | // Guard against stacked listeners on rapid retry (#146): unlisten any |
| 110 | // previous registration before creating a new one. |
| 111 | if (_runtimeUnlisten) { _runtimeUnlisten(); _runtimeUnlisten = null; } |
| 112 | |
| 113 | const unlisten = await window.__TAURI__.event.listen( |
| 114 | "runtime-download-progress", |
| 115 | (event) => { |
| 116 | const { received, total } = event.payload; |
| 117 | if (received !== lastReceived) { |
| 118 | lastReceived = received; |
| 119 | lastProgressAt = Date.now(); |
| 120 | } |
| 121 | const mb = (received / 1e6).toFixed(0); |
| 122 | if (total && total > 0) { |
| 123 | const pct = Math.min(100, Math.round((received / total) * 100)); |
| 124 | progressFill.style.width = `${pct}%`; |
| 125 | progressFill.classList.remove("indeterminate"); |
| 126 | setStatus(`Downloading StemDeck runtime... ${mb} / ${(total / 1e6).toFixed(0)} MB`); |
| 127 | } else { |
| 128 | progressFill.classList.add("indeterminate"); |
| 129 | setStatus(`Downloading StemDeck runtime... ${mb} MB received`); |
| 130 | } |
| 131 | } |
| 132 | ); |
| 133 | _runtimeUnlisten = unlisten; |
| 134 | |
| 135 | progressWrap.classList.remove("hidden"); |
| 136 | progressFill.style.width = "0%"; |
| 137 | progressFill.classList.remove("indeterminate"); |
| 138 | |
| 139 | try { |
| 140 | let verified = false; |
| 141 | if (status.archiveReady) { |
| 142 | try { |
| 143 | setStatus("Runtime archive found locally, verifying..."); |
| 144 | progressWrap.classList.add("hidden"); |
| 145 | await invoke("verify_runtime_pack"); |
| 146 | verified = true; |
| 147 | } catch { |
| 148 | // Stale or corrupt archive — fall through to re-download |
no test coverage detected