Add fourth install step
This commit is contained in:
@@ -5,8 +5,6 @@
|
|||||||
|
|
||||||
A web application that provides a GUI for applying custom [kobopatch](https://github.com/pgaskin/kobopatch) patches to Kobo e-readers. It uses the File System Access API (Chromium) to interface with connected Kobo devices, or falls back to manual model/firmware selection on other browsers.
|
A web application that provides a GUI for applying custom [kobopatch](https://github.com/pgaskin/kobopatch) patches to Kobo e-readers. It uses the File System Access API (Chromium) to interface with connected Kobo devices, or falls back to manual model/firmware selection on other browsers.
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
The app makes it easy to configure which patches to apply, downloads the correct firmware from Kobo's servers, runs the patcher (compiled to WebAssembly), and places the resulting `KoboRoot.tgz` on the device. The user then reboots to apply.
|
The app makes it easy to configure which patches to apply, downloads the correct firmware from Kobo's servers, runs the patcher (compiled to WebAssembly), and places the resulting `KoboRoot.tgz` on the device. The user then reboots to apply.
|
||||||
|
|
||||||
Fully client-side — no backend needed, can be hosted as a static site. Patches are community-contributed via [MobileRead](https://www.mobileread.com/) and need to be manually updated when new Kobo firmware versions come out.
|
Fully client-side — no backend needed, can be hosted as a static site. Patches are community-contributed via [MobileRead](https://www.mobileread.com/) and need to be manually updated when new Kobo firmware versions come out.
|
||||||
|
|||||||
@@ -23,8 +23,10 @@ echo "Copying artifacts to $PUBLIC_DIR..."
|
|||||||
cp kobopatch.wasm "$PUBLIC_DIR/kobopatch.wasm"
|
cp kobopatch.wasm "$PUBLIC_DIR/kobopatch.wasm"
|
||||||
cp wasm_exec.js "$PUBLIC_DIR/wasm_exec.js"
|
cp wasm_exec.js "$PUBLIC_DIR/wasm_exec.js"
|
||||||
|
|
||||||
# Update the cache-busting timestamp in the worker
|
# Update cache-busting timestamps
|
||||||
sed -i "s|kobopatch\.wasm?ts=[0-9]*|kobopatch.wasm?ts=$TS|g" "$PUBLIC_DIR/patch-worker.js"
|
sed -i "s|kobopatch\.wasm?ts=[0-9]*|kobopatch.wasm?ts=$TS|g" "$PUBLIC_DIR/patch-worker.js"
|
||||||
|
sed -i "s|\.js?ts=[0-9]*|.js?ts=$TS|g" "$PUBLIC_DIR/index.html"
|
||||||
|
sed -i "s|\.css?ts=[0-9]*|.css?ts=$TS|g" "$PUBLIC_DIR/index.html"
|
||||||
|
|
||||||
echo "Build timestamp: $TS"
|
echo "Build timestamp: $TS"
|
||||||
echo "Done."
|
echo "Done."
|
||||||
|
|||||||
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
|
Before Width: | Height: | Size: 84 KiB |
@@ -41,7 +41,8 @@
|
|||||||
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 writeSuccess = document.getElementById('write-success');
|
const writeInstructions = document.getElementById('write-instructions');
|
||||||
|
const downloadInstructions = document.getElementById('download-instructions');
|
||||||
const firmwareVersionLabel = document.getElementById('firmware-version-label');
|
const firmwareVersionLabel = document.getElementById('firmware-version-label');
|
||||||
const firmwareDeviceLabel = document.getElementById('firmware-device-label');
|
const firmwareDeviceLabel = document.getElementById('firmware-device-label');
|
||||||
const patchCountHint = document.getElementById('patch-count-hint');
|
const patchCountHint = document.getElementById('patch-count-hint');
|
||||||
@@ -306,7 +307,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
btnBuild.addEventListener('click', async () => {
|
btnBuild.addEventListener('click', async () => {
|
||||||
hideNav();
|
|
||||||
showStep(stepBuilding);
|
showStep(stepBuilding);
|
||||||
buildLog.textContent = '';
|
buildLog.textContent = '';
|
||||||
buildProgress.textContent = 'Starting...';
|
buildProgress.textContent = 'Starting...';
|
||||||
@@ -337,32 +337,52 @@
|
|||||||
buildStatus.textContent =
|
buildStatus.textContent =
|
||||||
'Patching complete. KoboRoot.tgz is ' +
|
'Patching complete. KoboRoot.tgz is ' +
|
||||||
(resultTgz.length / 1024).toFixed(0) + ' KB.';
|
(resultTgz.length / 1024).toFixed(0) + ' KB.';
|
||||||
writeSuccess.hidden = true;
|
|
||||||
|
|
||||||
const doneLog = document.getElementById('done-log');
|
const doneLog = document.getElementById('done-log');
|
||||||
doneLog.textContent = buildLog.textContent;
|
doneLog.textContent = buildLog.textContent;
|
||||||
doneLog.scrollTop = doneLog.scrollHeight;
|
|
||||||
|
|
||||||
|
// Reset install step state.
|
||||||
btnWrite.hidden = manualMode;
|
btnWrite.hidden = manualMode;
|
||||||
hideNav();
|
btnWrite.disabled = false;
|
||||||
|
btnWrite.className = 'primary';
|
||||||
|
btnWrite.textContent = 'Write to Kobo';
|
||||||
|
btnDownload.disabled = false;
|
||||||
|
writeInstructions.hidden = true;
|
||||||
|
downloadInstructions.hidden = true;
|
||||||
|
|
||||||
|
setNavStep(4);
|
||||||
showStep(stepDone);
|
showStep(stepDone);
|
||||||
|
|
||||||
|
// Scroll log to bottom after the step becomes visible.
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
doneLog.scrollTop = doneLog.scrollHeight;
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError('Build failed: ' + err.message, buildLog.textContent);
|
showError('Build failed: ' + err.message, buildLog.textContent);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Done step ---
|
// --- Install step ---
|
||||||
btnWrite.addEventListener('click', async () => {
|
btnWrite.addEventListener('click', async () => {
|
||||||
if (!resultTgz || !device.directoryHandle) return;
|
if (!resultTgz || !device.directoryHandle) return;
|
||||||
|
|
||||||
|
btnWrite.disabled = true;
|
||||||
|
btnWrite.textContent = 'Writing...';
|
||||||
|
downloadInstructions.hidden = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const koboDir = await device.directoryHandle.getDirectoryHandle('.kobo');
|
const koboDir = await device.directoryHandle.getDirectoryHandle('.kobo');
|
||||||
const fileHandle = await koboDir.getFileHandle('KoboRoot.tgz', { create: true });
|
const fileHandle = await koboDir.getFileHandle('KoboRoot.tgz', { create: true });
|
||||||
const writable = await fileHandle.createWritable();
|
const writable = await fileHandle.createWritable();
|
||||||
await writable.write(resultTgz);
|
await writable.write(resultTgz);
|
||||||
await writable.close();
|
await writable.close();
|
||||||
writeSuccess.hidden = false;
|
|
||||||
|
btnWrite.textContent = 'Written';
|
||||||
|
btnWrite.className = 'btn-success';
|
||||||
|
writeInstructions.hidden = false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
btnWrite.disabled = false;
|
||||||
|
btnWrite.textContent = 'Write to Kobo';
|
||||||
showError('Failed to write KoboRoot.tgz: ' + err.message);
|
showError('Failed to write KoboRoot.tgz: ' + err.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -376,6 +396,9 @@
|
|||||||
a.download = 'KoboRoot.tgz';
|
a.download = 'KoboRoot.tgz';
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
writeInstructions.hidden = true;
|
||||||
|
downloadInstructions.hidden = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Error / Retry ---
|
// --- Error / Retry ---
|
||||||
@@ -398,7 +421,6 @@
|
|||||||
manualMode = false;
|
manualMode = false;
|
||||||
selectedPrefix = null;
|
selectedPrefix = null;
|
||||||
patchesLoaded = false;
|
patchesLoaded = false;
|
||||||
btnWrite.hidden = false;
|
|
||||||
btnDeviceNext.hidden = false;
|
btnDeviceNext.hidden = false;
|
||||||
|
|
||||||
if (hasFileSystemAccess) {
|
if (hasFileSystemAccess) {
|
||||||
|
|||||||
@@ -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">
|
<link rel="stylesheet" href="style.css?ts=1773666969">
|
||||||
<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>
|
||||||
@@ -20,11 +20,17 @@
|
|||||||
<li data-step="1" class="active">Device</li>
|
<li data-step="1" class="active">Device</li>
|
||||||
<li data-step="2">Patches</li>
|
<li data-step="2">Patches</li>
|
||||||
<li data-step="3">Build</li>
|
<li data-step="3">Build</li>
|
||||||
|
<li data-step="4">Install</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- 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">
|
||||||
|
Patching modifies system files on your Kobo. If something goes 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>.
|
||||||
|
Proceed with care.
|
||||||
|
</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.
|
||||||
Then click the button below and select the root of the Kobo drive.
|
Then click the button below and select the root of the Kobo drive.
|
||||||
@@ -38,6 +44,11 @@
|
|||||||
|
|
||||||
<!-- Step 1b (manual): Select device model + firmware -->
|
<!-- Step 1b (manual): Select device model + firmware -->
|
||||||
<section id="step-manual" class="step" hidden>
|
<section id="step-manual" class="step" hidden>
|
||||||
|
<div class="warning">
|
||||||
|
Patching modifies system files on your Kobo. If something goes 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>.
|
||||||
|
Proceed with care.
|
||||||
|
</div>
|
||||||
<p id="manual-chrome-hint" class="info-banner" hidden>
|
<p id="manual-chrome-hint" class="info-banner" hidden>
|
||||||
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.
|
||||||
@@ -122,22 +133,31 @@
|
|||||||
<pre id="build-log" class="build-log"></pre>
|
<pre id="build-log" class="build-log"></pre>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Step 5: Done -->
|
<!-- 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>
|
<div id="build-status" class="info-banner"></div>
|
||||||
|
<details class="log-details">
|
||||||
|
<summary>Build log</summary>
|
||||||
<pre id="done-log" class="build-log done-log"></pre>
|
<pre id="done-log" class="build-log done-log"></pre>
|
||||||
|
</details>
|
||||||
<div id="build-actions">
|
<div id="build-actions">
|
||||||
<button id="btn-write" class="primary">Write to Kobo</button>
|
<button id="btn-write" class="primary">Write to Kobo</button>
|
||||||
<button id="btn-download" class="secondary">Download KoboRoot.tgz</button>
|
<button id="btn-download" class="secondary">Download KoboRoot.tgz</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="fallback-hint" id="download-hint">
|
<div id="write-instructions" class="install-instructions" hidden>
|
||||||
Place the downloaded <strong>KoboRoot.tgz</strong> in the <strong>.kobo</strong> directory
|
<p class="hint">
|
||||||
on your Kobo's USB drive, then safely eject and reboot the device.
|
|
||||||
</p>
|
|
||||||
<p class="hint" hidden id="write-success">
|
|
||||||
KoboRoot.tgz has been written to your Kobo.
|
KoboRoot.tgz has been written to your Kobo.
|
||||||
Safely eject the device and reboot it to apply the patches.
|
<strong>Safely eject</strong> the device before unplugging the USB cable — it will reboot and apply the patches automatically.
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
<div id="download-instructions" class="install-instructions" hidden>
|
||||||
|
<ol class="install-steps">
|
||||||
|
<li>Connect your Kobo via USB so it appears as a removable drive.</li>
|
||||||
|
<li>Copy <strong>KoboRoot.tgz</strong> into the <strong>.kobo</strong> folder on the Kobo drive.</li>
|
||||||
|
<li><strong>Safely eject</strong> the Kobo — do not just unplug the cable, as this can corrupt data.</li>
|
||||||
|
<li>The device will reboot and apply the patches automatically.</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Error state -->
|
<!-- Error state -->
|
||||||
@@ -150,9 +170,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"></script>
|
<script src="kobo-device.js?ts=1773666969"></script>
|
||||||
<script src="kobopatch.js"></script>
|
<script src="kobopatch.js?ts=1773666969"></script>
|
||||||
<script src="patch-ui.js"></script>
|
<script src="patch-ui.js?ts=1773666969"></script>
|
||||||
<script src="app.js"></script>
|
<script src="app.js?ts=1773666969"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -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=1773611308'),
|
fetch('kobopatch.wasm?ts=1773666969'),
|
||||||
go.importObject
|
go.importObject
|
||||||
);
|
);
|
||||||
go.run(result.instance);
|
go.run(result.instance);
|
||||||
|
|||||||
@@ -213,6 +213,19 @@ button:disabled {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.btn-success {
|
||||||
|
background: var(--success-text);
|
||||||
|
border-color: var(--success-text);
|
||||||
|
color: #fff;
|
||||||
|
opacity: 1;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.btn-success:hover {
|
||||||
|
background: var(--success-text);
|
||||||
|
border-color: var(--success-text);
|
||||||
|
}
|
||||||
|
|
||||||
/* Cards */
|
/* Cards */
|
||||||
.info-card {
|
.info-card {
|
||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
@@ -468,6 +481,23 @@ input[type="file"] {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Collapsible log on install step */
|
||||||
|
.log-details {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-details summary {
|
||||||
|
font-size: 0.83rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-details summary:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
/* Done screen log is shorter */
|
/* Done screen log is shorter */
|
||||||
.done-log {
|
.done-log {
|
||||||
height: calc(7 * 1.5em + 1.5rem);
|
height: calc(7 * 1.5em + 1.5rem);
|
||||||
@@ -511,6 +541,30 @@ input[type="file"] {
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Install instructions */
|
||||||
|
.install-instructions {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-instructions .warning {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-instructions .hint {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-steps {
|
||||||
|
margin: 0.5rem 0 0 1.25rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-steps li {
|
||||||
|
padding: 0.1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
.info-banner {
|
.info-banner {
|
||||||
background: var(--primary-light);
|
background: var(--primary-light);
|
||||||
border: 1px solid #bfdbfe;
|
border: 1px solid #bfdbfe;
|
||||||
|
|||||||
Reference in New Issue
Block a user