1
0

More tweaks
All checks were successful
Build & Test WASM / build-and-test (push) Successful in 1m42s

This commit is contained in:
2026-03-16 15:02:45 +01:00
parent 3b5faf9696
commit 583ad4e52c
5 changed files with 104 additions and 47 deletions

View File

@@ -8,6 +8,7 @@
let manualMode = false; let manualMode = false;
let selectedPrefix = null; let selectedPrefix = null;
let patchesLoaded = false; let patchesLoaded = false;
let isRestore = false;
// DOM elements // DOM elements
const stepNav = document.getElementById('step-nav'); const stepNav = document.getElementById('step-nav');
@@ -27,10 +28,10 @@
const manualModel = document.getElementById('manual-model'); const manualModel = document.getElementById('manual-model');
const manualChromeHint = document.getElementById('manual-chrome-hint'); const manualChromeHint = document.getElementById('manual-chrome-hint');
const btnDeviceNext = document.getElementById('btn-device-next'); const btnDeviceNext = document.getElementById('btn-device-next');
const btnDeviceRestore = document.getElementById('btn-device-restore');
const btnPatchesBack = document.getElementById('btn-patches-back'); const btnPatchesBack = document.getElementById('btn-patches-back');
const btnPatchesNext = document.getElementById('btn-patches-next'); const btnPatchesNext = document.getElementById('btn-patches-next');
const btnBuildBack = document.getElementById('btn-build-back'); const btnBuildBack = document.getElementById('btn-build-back');
const btnBuild = document.getElementById('btn-build');
const btnWrite = document.getElementById('btn-write'); const btnWrite = document.getElementById('btn-write');
const btnDownload = document.getElementById('btn-download'); const btnDownload = document.getElementById('btn-download');
const btnRetry = document.getElementById('btn-retry'); const btnRetry = document.getElementById('btn-retry');
@@ -41,6 +42,7 @@
const deviceStatus = document.getElementById('device-status'); const deviceStatus = document.getElementById('device-status');
const patchContainer = document.getElementById('patch-container'); const patchContainer = document.getElementById('patch-container');
const buildStatus = document.getElementById('build-status'); const buildStatus = document.getElementById('build-status');
const existingTgzWarning = document.getElementById('existing-tgz-warning');
const writeInstructions = document.getElementById('write-instructions'); const writeInstructions = document.getElementById('write-instructions');
const downloadInstructions = document.getElementById('download-instructions'); const downloadInstructions = document.getElementById('download-instructions');
const firmwareVersionLabel = document.getElementById('firmware-version-label'); const firmwareVersionLabel = document.getElementById('firmware-version-label');
@@ -74,12 +76,12 @@
// --- Patch count --- // --- Patch count ---
function updatePatchCount() { function updatePatchCount() {
const count = patchUI.getEnabledCount(); const count = patchUI.getEnabledCount();
btnPatchesNext.disabled = count === 0; btnPatchesNext.disabled = false;
patchCountHint.textContent = count === 0 if (count === 0) {
? 'Select at least one patch to continue.' patchCountHint.textContent = 'No patches selected — continuing will restore the original unpatched firmware.';
: count === 1 } else {
? '1 patch selected.' patchCountHint.textContent = count === 1 ? '1 patch selected.' : count + ' patches selected.';
: count + ' patches selected.'; }
} }
patchUI.onChange = updatePatchCount; patchUI.onChange = updatePatchCount;
@@ -134,8 +136,10 @@
const version = manualVersion.value; const version = manualVersion.value;
selectedPrefix = null; selectedPrefix = null;
const modelHint = document.getElementById('manual-model-hint');
if (!version) { if (!version) {
manualModel.hidden = true; manualModel.hidden = true;
modelHint.hidden = true;
btnManualConfirm.disabled = true; btnManualConfirm.disabled = true;
return; return;
} }
@@ -149,6 +153,7 @@
manualModel.appendChild(opt); manualModel.appendChild(opt);
} }
manualModel.hidden = false; manualModel.hidden = false;
modelHint.hidden = false;
btnManualConfirm.disabled = true; btnManualConfirm.disabled = true;
}); });
@@ -191,8 +196,10 @@
const match = available.find(p => p.version === info.firmware); const match = available.find(p => p.version === info.firmware);
if (match) { if (match) {
deviceStatus.className = 'status-supported'; deviceStatus.className = '';
deviceStatus.textContent = 'Patches available for firmware ' + info.firmware + '.'; deviceStatus.textContent =
'KoboPatch Web UI currently supports this version of the firmware. ' +
'You can choose to customize it or simply restore the original software.';
await patchUI.loadFromURL('patches/' + match.filename); await patchUI.loadFromURL('patches/' + match.filename);
patchUI.render(patchContainer); patchUI.render(patchContainer);
@@ -200,6 +207,8 @@
patchesLoaded = true; patchesLoaded = true;
configureFirmwareStep(info.firmware, info.serialPrefix); configureFirmwareStep(info.firmware, info.serialPrefix);
btnDeviceNext.hidden = false;
btnDeviceRestore.hidden = false;
showStep(stepDevice); showStep(stepDevice);
} else { } else {
deviceStatus.className = 'status-unsupported'; deviceStatus.className = 'status-unsupported';
@@ -207,6 +216,7 @@
'No patches available for firmware ' + info.firmware + '. ' + 'No patches available for firmware ' + info.firmware + '. ' +
'Supported versions: ' + available.map(p => p.version).join(', '); 'Supported versions: ' + available.map(p => p.version).join(', ');
btnDeviceNext.hidden = true; btnDeviceNext.hidden = true;
btnDeviceRestore.hidden = true;
showStep(stepDevice); showStep(stepDevice);
} }
} catch (err) { } catch (err) {
@@ -220,6 +230,12 @@
if (patchesLoaded) goToPatches(); if (patchesLoaded) goToPatches();
}); });
btnDeviceRestore.addEventListener('click', () => {
if (!patchesLoaded) return;
isRestore = true;
goToBuild();
});
async function loadPatchesForVersion(version, available) { async function loadPatchesForVersion(version, available) {
const match = available.find(p => p.version === version); const match = available.find(p => p.version === version);
if (!match) return false; if (!match) return false;
@@ -247,12 +263,24 @@
}); });
btnPatchesNext.addEventListener('click', () => { btnPatchesNext.addEventListener('click', () => {
if (patchUI.getEnabledCount() === 0) return; isRestore = patchUI.getEnabledCount() === 0;
goToBuild(); goToBuild();
}); });
// --- Step 3: Review & Build --- // --- Step 3: Review & Build ---
const btnBuild = document.getElementById('btn-build');
const firmwareDescription = document.getElementById('firmware-description');
function goToBuild() { function goToBuild() {
if (isRestore) {
firmwareDescription.textContent =
'will be downloaded and extracted without modifications to restore the original unpatched software:';
btnBuild.textContent = 'Restore Original Firmware';
} else {
firmwareDescription.textContent =
'will be downloaded automatically from Kobo\u2019s servers and will be patched after the download completes:';
btnBuild.textContent = 'Build Patched Firmware';
}
setNavStep(3); setNavStep(3);
showStep(stepFirmware); showStep(stepFirmware);
} }
@@ -320,7 +348,7 @@
const firmwareBytes = await downloadFirmware(firmwareURL); const firmwareBytes = await downloadFirmware(firmwareURL);
appendLog('Firmware downloaded: ' + (firmwareBytes.length / 1024 / 1024).toFixed(1) + ' MB'); appendLog('Firmware downloaded: ' + (firmwareBytes.length / 1024 / 1024).toFixed(1) + ' MB');
buildProgress.textContent = 'Applying patches...'; buildProgress.textContent = isRestore ? 'Extracting firmware...' : 'Applying patches...';
const configYAML = patchUI.generateConfig(); const configYAML = patchUI.generateConfig();
const patchFiles = patchUI.getPatchFileBytes(); const patchFiles = patchUI.getPatchFileBytes();
@@ -334,9 +362,17 @@
}); });
resultTgz = result.tgz; resultTgz = result.tgz;
buildStatus.textContent = const sizeTxt = (resultTgz.length / 1024 / 1024).toFixed(1) + ' MB';
'Patching complete. KoboRoot.tgz is ' + const action = isRestore ? 'Firmware extracted' : 'Patching complete';
(resultTgz.length / 1024).toFixed(0) + ' KB.'; const description = isRestore
? 'This will restore the original unpatched software.'
: '';
buildStatus.innerHTML =
action + '. <strong>KoboRoot.tgz</strong> (' + sizeTxt + ') is ready. ' +
(description ? description + ' ' : '') +
(manualMode
? 'Download the file and copy it to your Kobo.'
: 'Write it directly to your connected Kobo, or download for manual installation.');
const doneLog = document.getElementById('done-log'); const doneLog = document.getElementById('done-log');
doneLog.textContent = buildLog.textContent; doneLog.textContent = buildLog.textContent;
@@ -349,6 +385,18 @@
btnDownload.disabled = false; btnDownload.disabled = false;
writeInstructions.hidden = true; writeInstructions.hidden = true;
downloadInstructions.hidden = true; downloadInstructions.hidden = true;
existingTgzWarning.hidden = true;
// Check if a KoboRoot.tgz already exists on the device.
if (!manualMode && device.directoryHandle) {
try {
const koboDir = await device.directoryHandle.getDirectoryHandle('.kobo');
await koboDir.getFileHandle('KoboRoot.tgz');
existingTgzWarning.hidden = false;
} catch {
// No existing file — that's fine.
}
}
setNavStep(4); setNavStep(4);
showStep(stepDone); showStep(stepDone);
@@ -421,7 +469,9 @@
manualMode = false; manualMode = false;
selectedPrefix = null; selectedPrefix = null;
patchesLoaded = false; patchesLoaded = false;
isRestore = false;
btnDeviceNext.hidden = false; btnDeviceNext.hidden = false;
btnDeviceRestore.hidden = false;
if (hasFileSystemAccess) { if (hasFileSystemAccess) {
setNavStep(1); setNavStep(1);

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KoboPatch Web UI</title> <title>KoboPatch Web UI</title>
<link rel="stylesheet" href="style.css?ts=1773666969"> <link rel="stylesheet" href="style.css?ts=1773669670">
<script src="https://cdn.jsdelivr.net/npm/jszip@3/dist/jszip.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/jszip@3/dist/jszip.min.js"></script>
</head> </head>
<body> <body>
@@ -27,9 +27,8 @@
<!-- Step 1a: Connect device (automatic, Chromium only) --> <!-- Step 1a: Connect device (automatic, Chromium only) -->
<section id="step-connect" class="step" hidden> <section id="step-connect" class="step" hidden>
<div class="warning"> <div class="warning">
Patching modifies system files on your Kobo. If something goes wrong, <b>Patching modifies system files on your Kobo, and <u>will void your warranty</u>.</b> This process allows for custom modifications to be applied, or undone. If something has gone wrong,
you may need to <a href="https://help.kobo.com/hc/en-us/articles/360017605314-Manual-reset-your-Kobo-Clara-HD-Kobo-Nia-Kobo-Elipsa-Kobo-Clara-2E-Kobo-Elipsa-2E" target="_blank">manually reset your device</a>. you may need to <a href="https://help.kobo.com/hc/en-us/articles/360017605314-Manual-reset-your-Kobo-Clara-HD-Kobo-Nia-Kobo-Elipsa-Kobo-Clara-2E-Kobo-Elipsa-2E" target="_blank">manually reset your device</a>.
Proceed with care.
</div> </div>
<p> <p>
Connect your Kobo e-reader via USB. It should appear as a removable drive. Connect your Kobo e-reader via USB. It should appear as a removable drive.
@@ -53,13 +52,21 @@
Tip: if you use Chrome or Edge, this tool can auto-detect your device Tip: if you use Chrome or Edge, this tool can auto-detect your device
and write patched files directly to it. Even easier, but sadly requires a Chromium based browser. and write patched files directly to it. Even easier, but sadly requires a Chromium based browser.
</p> </p>
<p>Select your Kobo model and firmware version.</p> <p>Select your Kobo model and firmware version.
Not sure which model you have?
<a href="https://help.kobo.com/hc/en-us/articles/360019676973-Identify-your-Kobo-eReader-or-Kobo-tablet" target="_blank">Identify your Kobo eReader</a>.
</p>
<select id="manual-version"> <select id="manual-version">
<option value="">-- Select firmware version --</option> <option value="">-- Select firmware version --</option>
</select> </select>
<select id="manual-model" hidden> <select id="manual-model" hidden>
<option value="">-- Select your Kobo model --</option> <option value="">-- Select your Kobo model --</option>
</select> </select>
<p id="manual-model-hint" class="fallback-hint" hidden>
You can identify your model by the serial number prefix shown on your Kobo
under <strong>Settings &gt; Device information</strong>.
Match the first characters (e.g. N428) to the list above.
</p>
<button id="btn-manual-confirm" class="primary" disabled>Continue &#x203A;</button> <button id="btn-manual-confirm" class="primary" disabled>Continue &#x203A;</button>
</section> </section>
@@ -79,8 +86,9 @@
<span id="device-firmware" class="value">--</span> <span id="device-firmware" class="value">--</span>
</div> </div>
</div> </div>
<div id="device-status"></div> <p id="device-status"></p>
<div class="step-actions"> <div class="step-actions">
<button id="btn-device-restore" class="secondary">Restore Unpatched Software</button>
<button id="btn-device-next" class="primary">Configure Patches &#x203A;</button> <button id="btn-device-next" class="primary">Configure Patches &#x203A;</button>
</div> </div>
</section> </section>
@@ -100,8 +108,8 @@
<section id="step-firmware" class="step" hidden> <section id="step-firmware" class="step" hidden>
<p id="firmware-auto-info"> <p id="firmware-auto-info">
Firmware <strong id="firmware-version-label"></strong> for Firmware <strong id="firmware-version-label"></strong> for
<strong id="firmware-device-label"></strong> will be downloaded <strong id="firmware-device-label"></strong>
automatically from Kobo's servers and will be patched after the download completes:<br> <span id="firmware-description">will be downloaded automatically from Kobo's servers and will be patched after the download completes:</span><br>
<code id="firmware-download-url"></code><br> <code id="firmware-download-url"></code><br>
<span id="firmware-verify-notice"> <span id="firmware-verify-notice">
You can verify if this URL matches your Kobo's model on You can verify if this URL matches your Kobo's model on
@@ -131,11 +139,15 @@
<p id="build-progress">Starting...</p> <p id="build-progress">Starting...</p>
</div> </div>
<pre id="build-log" class="build-log"></pre> <pre id="build-log" class="build-log"></pre>
<p class="fallback-hint">Please wait while the patch is being applied...</p>
</section> </section>
<!-- Step 5: Install --> <!-- Step 5: Install -->
<section id="step-done" class="step" hidden> <section id="step-done" class="step" hidden>
<div id="build-status" class="info-banner"></div> <p id="build-status" class="install-summary"></p>
<div id="existing-tgz-warning" class="warning" hidden>
An existing KoboRoot.tgz file was found on your Kobo and has not been applied yet. It will be overwritten.
</div>
<details class="log-details"> <details class="log-details">
<summary>Build log</summary> <summary>Build log</summary>
<pre id="done-log" class="build-log done-log"></pre> <pre id="done-log" class="build-log done-log"></pre>
@@ -170,9 +182,9 @@
</main> </main>
<!-- wasm_exec.js loaded by patch-worker.js inside the Web Worker --> <!-- wasm_exec.js loaded by patch-worker.js inside the Web Worker -->
<script src="kobo-device.js?ts=1773666969"></script> <script src="kobo-device.js?ts=1773669670"></script>
<script src="kobopatch.js?ts=1773666969"></script> <script src="kobopatch.js?ts=1773669670"></script>
<script src="patch-ui.js?ts=1773666969"></script> <script src="patch-ui.js?ts=1773669670"></script>
<script src="app.js?ts=1773666969"></script> <script src="app.js?ts=1773669670"></script>
</body> </body>
</html> </html>

View File

@@ -76,19 +76,9 @@ function getDevicesForVersion(version) {
const versionMap = FIRMWARE_DOWNLOADS[version]; const versionMap = FIRMWARE_DOWNLOADS[version];
if (!versionMap) return []; if (!versionMap) return [];
const devices = []; const devices = [];
const seen = {};
for (const prefix of Object.keys(versionMap)) { for (const prefix of Object.keys(versionMap)) {
const model = KOBO_MODELS[prefix] || 'Unknown (' + prefix + ')'; const model = KOBO_MODELS[prefix] || 'Unknown';
// Track duplicates to disambiguate with serial prefix devices.push({ prefix, model: model + ' (' + prefix + ')' });
if (seen[model]) seen[model].push(prefix);
else seen[model] = [prefix];
}
for (const prefix of Object.keys(versionMap)) {
const model = KOBO_MODELS[prefix] || 'Unknown (' + prefix + ')';
const label = seen[model].length > 1
? model + ' (serial ' + prefix + '...)'
: model;
devices.push({ prefix, model: label });
} }
return devices; return devices;
} }

View File

@@ -10,7 +10,7 @@ async function loadWasm() {
const go = new Go(); const go = new Go();
const result = await WebAssembly.instantiateStreaming( const result = await WebAssembly.instantiateStreaming(
fetch('kobopatch.wasm?ts=1773666969'), fetch('kobopatch.wasm?ts=1773669670'),
go.importObject go.importObject
); );
go.run(result.instance); go.run(result.instance);

View File

@@ -213,17 +213,13 @@ button:disabled {
box-shadow: none; box-shadow: none;
} }
button.btn-success { button.btn-success,
background: var(--success-text);
border-color: var(--success-text);
color: #fff;
opacity: 1;
cursor: default;
}
button.btn-success:hover { button.btn-success:hover {
background: var(--success-text); background: var(--success-text);
border-color: var(--success-text); border-color: var(--success-text);
color: #fff;
cursor: default;
opacity: 0.7;
} }
/* Cards */ /* Cards */
@@ -433,6 +429,7 @@ input[type="file"] {
#build-actions { #build-actions {
display: flex; display: flex;
justify-content: space-between;
gap: 0.75rem; gap: 0.75rem;
margin-top: 1.25rem; margin-top: 1.25rem;
} }
@@ -541,6 +538,14 @@ input[type="file"] {
color: var(--text-secondary); color: var(--text-secondary);
} }
/* Install summary */
.install-summary {
font-size: 0.93rem;
color: var(--text);
line-height: 1.6;
margin-bottom: 0.5rem;
}
/* Install instructions */ /* Install instructions */
.install-instructions { .install-instructions {
margin-top: 1rem; margin-top: 1rem;