Change foundational structure
Some checks failed
Build & Test WASM / build-and-test (push) Failing after 1m12s
Some checks failed
Build & Test WASM / build-and-test (push) Failing after 1m12s
This commit is contained in:
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -2,10 +2,8 @@ name: Build & Test WASM
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main, develop]
|
||||||
tags: ['v*']
|
tags: ['v*']
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-test:
|
build-and-test:
|
||||||
@@ -73,5 +71,5 @@ jobs:
|
|||||||
- name: Full integration test (Playwright)
|
- name: Full integration test (Playwright)
|
||||||
if: steps.check-wasm.outputs.run == 'true' && env.GITEA_ACTIONS != 'true'
|
if: steps.check-wasm.outputs.run == 'true' && env.GITEA_ACTIONS != 'true'
|
||||||
run: |
|
run: |
|
||||||
cd e2e
|
cd tests/e2e
|
||||||
./run-e2e.sh
|
./run-e2e.sh
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -19,9 +19,9 @@ web/public/wasm/kobopatch.wasm
|
|||||||
web/public/js/wasm_exec.js
|
web/public/js/wasm_exec.js
|
||||||
|
|
||||||
# E2E tests
|
# E2E tests
|
||||||
e2e/node_modules/
|
tests/e2e/node_modules/
|
||||||
e2e/test-results/
|
tests/e2e/test-results/
|
||||||
e2e/playwright-report/
|
tests/e2e/playwright-report/
|
||||||
|
|
||||||
# Claude
|
# Claude
|
||||||
.claude
|
.claude
|
||||||
52
README.md
52
README.md
@@ -14,29 +14,32 @@ Fully client-side — no backend needed, can be hosted as a static site. Patches
|
|||||||
|
|
||||||
## User flow
|
## User flow
|
||||||
|
|
||||||
1. Select device (auto-detect via File System Access API on Chromium, or manual dropdowns on any browser)
|
1. **Select device** — auto-detect via File System Access API on Chromium, or manual dropdowns on any browser
|
||||||
2. Configure patches (enable/disable, PatchGroup mutual exclusion via radio buttons) — or select none to restore original software
|
2. **Configure patches** — enable/disable patches (PatchGroup mutual exclusion via radio buttons), or select none to restore original unpatched software
|
||||||
3. Build — software update auto-downloaded from Kobo's CDN (`ereaderfiles.kobo.com`, CORS open), patched via WASM in a Web Worker
|
3. **Build** — software update is auto-downloaded from Kobo's CDN (`ereaderfiles.kobo.com`, CORS open) and either patched via WASM in a Web Worker, or the original `KoboRoot.tgz` is extracted as-is for restoring
|
||||||
4. Write `KoboRoot.tgz` to device (Chromium auto mode) or download manually
|
4. **Install** — write `KoboRoot.tgz` directly to the device (Chromium auto mode) or download for manual installation
|
||||||
|
|
||||||
## File structure
|
## File structure
|
||||||
|
|
||||||
```
|
```
|
||||||
web/public/ # Webroot — serve this directory
|
web/public/ # Webroot — serve this directory
|
||||||
index.html # Single-page app, 3-step wizard (Device → Patches → Build)
|
index.html # Single-page app, 4-step wizard (Device → Patches → Build → Install)
|
||||||
style.css
|
css/
|
||||||
app.js # Step navigation, flow orchestration, firmware download with progress
|
style.css
|
||||||
kobo-device.js # KOBO_MODELS (serial prefix → name), FIRMWARE_DOWNLOADS (version+prefix → URL),
|
js/
|
||||||
|
app.js # Step navigation, flow orchestration, firmware download with progress
|
||||||
|
kobo-device.js # KOBO_MODELS (serial prefix → name), FIRMWARE_DOWNLOADS (version+prefix → URL),
|
||||||
# getDevicesForVersion(), getFirmwareURL(), KoboDevice class (File System Access API)
|
# getDevicesForVersion(), getFirmwareURL(), KoboDevice class (File System Access API)
|
||||||
patch-ui.js # PatchUI class: loads patch zips (JSZip), parses YAML, renders toggle UI,
|
patch-ui.js # PatchUI class: loads patch zips (JSZip), parses YAML, renders toggle UI,
|
||||||
# generates kobopatch.yaml config with overrides
|
# generates kobopatch.yaml config with overrides
|
||||||
kobopatch.js # KobopatchRunner: spawns Web Worker per build, handles progress/done/error messages
|
kobopatch.js # KobopatchRunner: spawns Web Worker per build, handles progress/done/error messages
|
||||||
patch-worker.js # Web Worker: loads wasm_exec.js + kobopatch.wasm, runs patchFirmware(),
|
patch-worker.js # Web Worker: loads wasm_exec.js + kobopatch.wasm, runs patchFirmware(),
|
||||||
# posts progress back, transfers result buffer zero-copy
|
# posts progress back, transfers result buffer zero-copy
|
||||||
wasm_exec.js # Go WASM support runtime (copied from Go SDK by setup.sh, gitignored)
|
wasm_exec.js # Go WASM support runtime (copied from Go SDK by setup.sh, gitignored)
|
||||||
kobopatch.wasm # Compiled WASM binary (built by build.sh, gitignored)
|
wasm/
|
||||||
|
kobopatch.wasm # Compiled WASM binary (built by build.sh, gitignored)
|
||||||
patches/
|
patches/
|
||||||
index.json # [{ "version": "4.45.23646", "filename": "patches_4.45.23646.zip" }]
|
index.json # Contains a list of available patches
|
||||||
patches_*.zip # Each contains kobopatch.yaml + src/*.yaml patch files
|
patches_*.zip # Each contains kobopatch.yaml + src/*.yaml patch files
|
||||||
|
|
||||||
kobopatch-wasm/ # WASM build
|
kobopatch-wasm/ # WASM build
|
||||||
@@ -45,14 +48,23 @@ kobopatch-wasm/ # WASM build
|
|||||||
# Returns { tgz: Uint8Array, log: string }
|
# Returns { tgz: Uint8Array, log: string }
|
||||||
go.mod
|
go.mod
|
||||||
setup.sh # Clones kobopatch source, copies wasm_exec.js
|
setup.sh # Clones kobopatch source, copies wasm_exec.js
|
||||||
build.sh # GOOS=js GOARCH=wasm go build, copies .wasm to web/public/,
|
build.sh # GOOS=js GOARCH=wasm go build, copies .wasm to web/public/wasm/,
|
||||||
# sets ?ts= cache-bust timestamp in patch-worker.js
|
# sets ?ts= cache-bust timestamp in js/patch-worker.js
|
||||||
|
integration_test.go # Go integration test: validates SHA1 checksums of patched binaries
|
||||||
|
test-integration.sh # Downloads firmware and runs integration_test.go
|
||||||
|
|
||||||
|
tests/
|
||||||
|
e2e/ # Playwright E2E tests
|
||||||
|
integration.spec.js # Full browser pipeline test (patch + restore)
|
||||||
|
playwright.config.js
|
||||||
|
run-e2e.sh # Headless E2E runner (downloads firmware, installs browser)
|
||||||
|
run-e2e-local.sh # Headed E2E runner (visible browser window)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Adding a new software version
|
## Adding a new software version
|
||||||
|
|
||||||
1. Add the patch zip to `web/public/patches/` and update `index.json`
|
1. Add the patch zip to `web/public/patches/` and update `index.json`
|
||||||
2. Add download URLs to `FIRMWARE_DOWNLOADS` in `kobo-device.js` (keyed by version then serial prefix)
|
2. Add download URLs to `FIRMWARE_DOWNLOADS` in `js/kobo-device.js` (keyed by version then serial prefix)
|
||||||
3. The Kobo CDN prefix per device family (e.g. `kobo12`, `kobo13`) is stable; the date path segment changes per release
|
3. The Kobo CDN prefix per device family (e.g. `kobo12`, `kobo13`) is stable; the date path segment changes per release
|
||||||
|
|
||||||
## Building the WASM binary
|
## Building the WASM binary
|
||||||
@@ -62,7 +74,7 @@ Requires Go 1.21+.
|
|||||||
```bash
|
```bash
|
||||||
cd kobopatch-wasm
|
cd kobopatch-wasm
|
||||||
./setup.sh # first time only
|
./setup.sh # first time only
|
||||||
./build.sh # compiles WASM, copies to web/public/
|
./build.sh # compiles WASM, copies to web/public/wasm/
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running locally
|
## Running locally
|
||||||
@@ -89,14 +101,14 @@ cd kobopatch-wasm
|
|||||||
**Playwright E2E test** — drives the full browser UI (manual mode, headless):
|
**Playwright E2E test** — drives the full browser UI (manual mode, headless):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd e2e
|
cd tests/e2e
|
||||||
./run-e2e.sh
|
./run-e2e.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
To run the Playwright test with a visible browser window:
|
To run the Playwright test with a visible browser window:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd e2e
|
cd tests/e2e
|
||||||
./run-e2e-local.sh
|
./run-e2e-local.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ const EXPECTED_SHA1 = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const FIRMWARE_PATH = process.env.FIRMWARE_ZIP
|
const FIRMWARE_PATH = process.env.FIRMWARE_ZIP
|
||||||
|| path.resolve(__dirname, '..', 'kobopatch-wasm', 'testdata', 'kobo-update-4.45.23646.zip');
|
|| path.resolve(__dirname, '..', '..', 'kobopatch-wasm', 'testdata', 'kobo-update-4.45.23646.zip');
|
||||||
|
|
||||||
const WEBROOT_FIRMWARE = path.resolve(__dirname, '..', 'web', 'public', '_test_firmware.zip');
|
const WEBROOT_FIRMWARE = path.resolve(__dirname, '..', '..', 'web', 'public', '_test_firmware.zip');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a tar archive (uncompressed) and return a map of entry name -> Buffer.
|
* Parse a tar archive (uncompressed) and return a map of entry name -> Buffer.
|
||||||
@@ -16,7 +16,7 @@ module.exports = defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'python3 -m http.server -d ../web/public 8889',
|
command: 'python3 -m http.server -d ../../web/public 8889',
|
||||||
port: 8889,
|
port: 8889,
|
||||||
reuseExistingServer: true,
|
reuseExistingServer: true,
|
||||||
},
|
},
|
||||||
@@ -8,10 +8,10 @@ cd "$(dirname "$0")"
|
|||||||
|
|
||||||
FIRMWARE_VERSION="4.45.23646"
|
FIRMWARE_VERSION="4.45.23646"
|
||||||
FIRMWARE_URL="https://ereaderfiles.kobo.com/firmwares/kobo13/Mar2026/kobo-update-${FIRMWARE_VERSION}.zip"
|
FIRMWARE_URL="https://ereaderfiles.kobo.com/firmwares/kobo13/Mar2026/kobo-update-${FIRMWARE_VERSION}.zip"
|
||||||
FIRMWARE_DIR="../kobopatch-wasm/testdata"
|
FIRMWARE_DIR="../../kobopatch-wasm/testdata"
|
||||||
FIRMWARE_FILE="${FIRMWARE_DIR}/kobo-update-${FIRMWARE_VERSION}.zip"
|
FIRMWARE_FILE="${FIRMWARE_DIR}/kobo-update-${FIRMWARE_VERSION}.zip"
|
||||||
|
|
||||||
if [ ! -f "../web/public/wasm/kobopatch.wasm" ]; then
|
if [ ! -f "../../web/public/wasm/kobopatch.wasm" ]; then
|
||||||
echo "ERROR: kobopatch.wasm not found. Run kobopatch-wasm/build.sh first."
|
echo "ERROR: kobopatch.wasm not found. Run kobopatch-wasm/build.sh first."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -34,5 +34,5 @@ npm install --silent 2>/dev/null
|
|||||||
npx playwright install chromium 2>/dev/null
|
npx playwright install chromium 2>/dev/null
|
||||||
|
|
||||||
echo "Running E2E integration test (headed)..."
|
echo "Running E2E integration test (headed)..."
|
||||||
FIRMWARE_ZIP="$(cd .. && pwd)/kobopatch-wasm/testdata/kobo-update-${FIRMWARE_VERSION}.zip" \
|
FIRMWARE_ZIP="$(cd ../.. && pwd)/kobopatch-wasm/testdata/kobo-update-${FIRMWARE_VERSION}.zip" \
|
||||||
npx playwright test --reporter=list --headed
|
npx playwright test --reporter=list --headed
|
||||||
@@ -14,11 +14,11 @@ cd "$(dirname "$0")"
|
|||||||
|
|
||||||
FIRMWARE_VERSION="4.45.23646"
|
FIRMWARE_VERSION="4.45.23646"
|
||||||
FIRMWARE_URL="https://ereaderfiles.kobo.com/firmwares/kobo13/Mar2026/kobo-update-${FIRMWARE_VERSION}.zip"
|
FIRMWARE_URL="https://ereaderfiles.kobo.com/firmwares/kobo13/Mar2026/kobo-update-${FIRMWARE_VERSION}.zip"
|
||||||
FIRMWARE_DIR="../kobopatch-wasm/testdata"
|
FIRMWARE_DIR="../../kobopatch-wasm/testdata"
|
||||||
FIRMWARE_FILE="${FIRMWARE_DIR}/kobo-update-${FIRMWARE_VERSION}.zip"
|
FIRMWARE_FILE="${FIRMWARE_DIR}/kobo-update-${FIRMWARE_VERSION}.zip"
|
||||||
|
|
||||||
# Check WASM is built.
|
# Check WASM is built.
|
||||||
if [ ! -f "../web/public/wasm/kobopatch.wasm" ]; then
|
if [ ! -f "../../web/public/wasm/kobopatch.wasm" ]; then
|
||||||
echo "ERROR: kobopatch.wasm not found. Run kobopatch-wasm/build.sh first."
|
echo "ERROR: kobopatch.wasm not found. Run kobopatch-wasm/build.sh first."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -45,5 +45,5 @@ npx playwright install chromium --with-deps 2>/dev/null || npx playwright instal
|
|||||||
|
|
||||||
# Run the test.
|
# Run the test.
|
||||||
echo "Running E2E integration test..."
|
echo "Running E2E integration test..."
|
||||||
FIRMWARE_ZIP="$(cd .. && pwd)/kobopatch-wasm/testdata/kobo-update-${FIRMWARE_VERSION}.zip" \
|
FIRMWARE_ZIP="$(cd ../.. && pwd)/kobopatch-wasm/testdata/kobo-update-${FIRMWARE_VERSION}.zip" \
|
||||||
npx playwright test --reporter=list
|
npx playwright test --reporter=list
|
||||||
@@ -14,7 +14,42 @@
|
|||||||
// Fetch patch index immediately so it's ready when needed.
|
// Fetch patch index immediately so it's ready when needed.
|
||||||
const availablePatchesReady = scanAvailablePatches().then(p => { availablePatches = p; });
|
const availablePatchesReady = scanAvailablePatches().then(p => { availablePatches = p; });
|
||||||
|
|
||||||
// DOM elements
|
// --- Helpers ---
|
||||||
|
|
||||||
|
function formatMB(bytes) {
|
||||||
|
return (bytes / 1024 / 1024).toFixed(1) + ' MB';
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateSelect(selectEl, placeholder, items) {
|
||||||
|
selectEl.innerHTML = '';
|
||||||
|
const defaultOpt = document.createElement('option');
|
||||||
|
defaultOpt.value = '';
|
||||||
|
defaultOpt.textContent = placeholder;
|
||||||
|
selectEl.appendChild(defaultOpt);
|
||||||
|
for (const { value, text, data } of items) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = value;
|
||||||
|
opt.textContent = text;
|
||||||
|
if (data) {
|
||||||
|
for (const [k, v] of Object.entries(data)) {
|
||||||
|
opt.dataset[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectEl.appendChild(opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerDownload(data, filename, mimeType) {
|
||||||
|
const blob = new Blob([data], { type: mimeType });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DOM elements ---
|
||||||
const stepNav = document.getElementById('step-nav');
|
const stepNav = document.getElementById('step-nav');
|
||||||
const stepConnect = document.getElementById('step-connect');
|
const stepConnect = document.getElementById('step-connect');
|
||||||
const stepManual = document.getElementById('step-manual');
|
const stepManual = document.getElementById('step-manual');
|
||||||
@@ -40,7 +75,6 @@
|
|||||||
const btnDownload = document.getElementById('btn-download');
|
const btnDownload = document.getElementById('btn-download');
|
||||||
const btnRetry = document.getElementById('btn-retry');
|
const btnRetry = document.getElementById('btn-retry');
|
||||||
|
|
||||||
const firmwareAutoInfo = document.getElementById('firmware-auto-info');
|
|
||||||
const errorMessage = document.getElementById('error-message');
|
const errorMessage = document.getElementById('error-message');
|
||||||
const errorLog = document.getElementById('error-log');
|
const errorLog = document.getElementById('error-log');
|
||||||
const deviceStatus = document.getElementById('device-status');
|
const deviceStatus = document.getElementById('device-status');
|
||||||
@@ -82,7 +116,7 @@
|
|||||||
const count = patchUI.getEnabledCount();
|
const count = patchUI.getEnabledCount();
|
||||||
btnPatchesNext.disabled = false;
|
btnPatchesNext.disabled = false;
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
patchCountHint.textContent = 'No patches selected — continuing will restore the original unpatched software.';
|
patchCountHint.textContent = 'No patches selected \u2014 continuing will restore the original unpatched software.';
|
||||||
} else {
|
} else {
|
||||||
patchCountHint.textContent = count === 1 ? '1 patch selected.' : count + ' patches selected.';
|
patchCountHint.textContent = count === 1 ? '1 patch selected.' : count + ' patches selected.';
|
||||||
}
|
}
|
||||||
@@ -113,16 +147,11 @@
|
|||||||
manualChromeHint.hidden = false;
|
manualChromeHint.hidden = false;
|
||||||
|
|
||||||
await availablePatchesReady;
|
await availablePatchesReady;
|
||||||
manualVersion.innerHTML = '<option value="">-- Select software version --</option>';
|
populateSelect(manualVersion, '-- Select software version --',
|
||||||
for (const p of availablePatches) {
|
availablePatches.map(p => ({ value: p.version, text: p.version, data: { filename: p.filename } }))
|
||||||
const opt = document.createElement('option');
|
);
|
||||||
opt.value = p.version;
|
|
||||||
opt.textContent = p.version;
|
|
||||||
opt.dataset.filename = p.filename;
|
|
||||||
manualVersion.appendChild(opt);
|
|
||||||
}
|
|
||||||
|
|
||||||
manualModel.innerHTML = '<option value="">-- Select your Kobo model --</option>';
|
populateSelect(manualModel, '-- Select your Kobo model --', []);
|
||||||
manualModel.hidden = true;
|
manualModel.hidden = true;
|
||||||
|
|
||||||
setNavStep(1);
|
setNavStep(1);
|
||||||
@@ -147,13 +176,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const devices = getDevicesForVersion(version);
|
const devices = getDevicesForVersion(version);
|
||||||
manualModel.innerHTML = '<option value="">-- Select your Kobo model --</option>';
|
populateSelect(manualModel, '-- Select your Kobo model --',
|
||||||
for (const d of devices) {
|
devices.map(d => ({ value: d.prefix, text: d.model }))
|
||||||
const opt = document.createElement('option');
|
);
|
||||||
opt.value = d.prefix;
|
|
||||||
opt.textContent = d.model;
|
|
||||||
manualModel.appendChild(opt);
|
|
||||||
}
|
|
||||||
manualModel.hidden = false;
|
manualModel.hidden = false;
|
||||||
modelHint.hidden = false;
|
modelHint.hidden = false;
|
||||||
btnManualConfirm.disabled = true;
|
btnManualConfirm.disabled = true;
|
||||||
@@ -164,7 +189,7 @@
|
|||||||
btnManualConfirm.disabled = !manualVersion.value || !manualModel.value;
|
btnManualConfirm.disabled = !manualVersion.value || !manualModel.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Manual confirm → load patches → go to step 2
|
// Manual confirm -> load patches -> go to step 2
|
||||||
btnManualConfirm.addEventListener('click', async () => {
|
btnManualConfirm.addEventListener('click', async () => {
|
||||||
const version = manualVersion.value;
|
const version = manualVersion.value;
|
||||||
if (!version || !selectedPrefix) return;
|
if (!version || !selectedPrefix) return;
|
||||||
@@ -182,7 +207,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto connect → show device info
|
// Auto connect -> show device info
|
||||||
btnConnect.addEventListener('click', async () => {
|
btnConnect.addEventListener('click', async () => {
|
||||||
try {
|
try {
|
||||||
const info = await device.connect();
|
const info = await device.connect();
|
||||||
@@ -231,7 +256,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Device info → patches
|
// Device info -> patches
|
||||||
btnDeviceNext.addEventListener('click', () => {
|
btnDeviceNext.addEventListener('click', () => {
|
||||||
if (patchesLoaded) goToPatches();
|
if (patchesLoaded) goToPatches();
|
||||||
});
|
});
|
||||||
@@ -261,11 +286,7 @@
|
|||||||
|
|
||||||
btnPatchesBack.addEventListener('click', () => {
|
btnPatchesBack.addEventListener('click', () => {
|
||||||
setNavStep(1);
|
setNavStep(1);
|
||||||
if (manualMode) {
|
showStep(manualMode ? stepManual : stepDevice);
|
||||||
showStep(stepManual);
|
|
||||||
} else {
|
|
||||||
showStep(stepDevice);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
btnPatchesNext.addEventListener('click', () => {
|
btnPatchesNext.addEventListener('click', () => {
|
||||||
@@ -277,6 +298,20 @@
|
|||||||
const btnBuild = document.getElementById('btn-build');
|
const btnBuild = document.getElementById('btn-build');
|
||||||
const firmwareDescription = document.getElementById('firmware-description');
|
const firmwareDescription = document.getElementById('firmware-description');
|
||||||
|
|
||||||
|
function populateSelectedPatchesList() {
|
||||||
|
const patchList = document.getElementById('selected-patches-list');
|
||||||
|
patchList.innerHTML = '';
|
||||||
|
const enabled = patchUI.getEnabledPatches();
|
||||||
|
for (const name of enabled) {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = name;
|
||||||
|
patchList.appendChild(li);
|
||||||
|
}
|
||||||
|
const hasPatches = enabled.length > 0;
|
||||||
|
patchList.hidden = !hasPatches;
|
||||||
|
document.getElementById('selected-patches-heading').hidden = !hasPatches;
|
||||||
|
}
|
||||||
|
|
||||||
function goToBuild() {
|
function goToBuild() {
|
||||||
if (isRestore) {
|
if (isRestore) {
|
||||||
firmwareDescription.textContent =
|
firmwareDescription.textContent =
|
||||||
@@ -287,21 +322,7 @@
|
|||||||
'will be downloaded automatically from Kobo\u2019s servers and will be patched after the download completes.';
|
'will be downloaded automatically from Kobo\u2019s servers and will be patched after the download completes.';
|
||||||
btnBuild.textContent = 'Build Patched Software';
|
btnBuild.textContent = 'Build Patched Software';
|
||||||
}
|
}
|
||||||
// Populate selected patches list.
|
populateSelectedPatchesList();
|
||||||
const patchList = document.getElementById('selected-patches-list');
|
|
||||||
patchList.innerHTML = '';
|
|
||||||
const enabled = patchUI.getEnabledPatches();
|
|
||||||
if (enabled.length > 0) {
|
|
||||||
for (const name of enabled) {
|
|
||||||
const li = document.createElement('li');
|
|
||||||
li.textContent = name;
|
|
||||||
patchList.appendChild(li);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const hasPatches = enabled.length > 0;
|
|
||||||
patchList.hidden = !hasPatches;
|
|
||||||
document.getElementById('selected-patches-heading').hidden = !hasPatches;
|
|
||||||
|
|
||||||
setNavStep(3);
|
setNavStep(3);
|
||||||
showStep(stepFirmware);
|
showStep(stepFirmware);
|
||||||
}
|
}
|
||||||
@@ -313,6 +334,11 @@
|
|||||||
const buildProgress = document.getElementById('build-progress');
|
const buildProgress = document.getElementById('build-progress');
|
||||||
const buildLog = document.getElementById('build-log');
|
const buildLog = document.getElementById('build-log');
|
||||||
|
|
||||||
|
function appendLog(msg) {
|
||||||
|
buildLog.textContent += msg + '\n';
|
||||||
|
buildLog.scrollTop = buildLog.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
async function downloadFirmware(url) {
|
async function downloadFirmware(url) {
|
||||||
const resp = await fetch(url);
|
const resp = await fetch(url);
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
@@ -336,9 +362,8 @@
|
|||||||
chunks.push(value);
|
chunks.push(value);
|
||||||
received += value.length;
|
received += value.length;
|
||||||
const pct = ((received / total) * 100).toFixed(0);
|
const pct = ((received / total) * 100).toFixed(0);
|
||||||
const mb = (received / 1024 / 1024).toFixed(1);
|
buildProgress.textContent =
|
||||||
const totalMB = (total / 1024 / 1024).toFixed(1);
|
`Downloading software update... ${formatMB(received)} / ${formatMB(total)} (${pct}%)`;
|
||||||
buildProgress.textContent = `Downloading software update... ${mb} / ${totalMB} MB (${pct}%)`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = new Uint8Array(received);
|
const result = new Uint8Array(received);
|
||||||
@@ -350,9 +375,76 @@
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendLog(msg) {
|
async function extractOriginalTgz(firmwareBytes) {
|
||||||
buildLog.textContent += msg + '\n';
|
buildProgress.textContent = 'Extracting KoboRoot.tgz...';
|
||||||
buildLog.scrollTop = buildLog.scrollHeight;
|
appendLog('Extracting original KoboRoot.tgz from software update...');
|
||||||
|
const zip = await JSZip.loadAsync(firmwareBytes);
|
||||||
|
const koboRoot = zip.file('KoboRoot.tgz');
|
||||||
|
if (!koboRoot) throw new Error('KoboRoot.tgz not found in software update');
|
||||||
|
const tgz = new Uint8Array(await koboRoot.async('arraybuffer'));
|
||||||
|
appendLog('Extracted KoboRoot.tgz: ' + formatMB(tgz.length));
|
||||||
|
return tgz;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runPatcher(firmwareBytes) {
|
||||||
|
buildProgress.textContent = 'Applying patches...';
|
||||||
|
const configYAML = patchUI.generateConfig();
|
||||||
|
const patchFiles = patchUI.getPatchFileBytes();
|
||||||
|
|
||||||
|
const result = await runner.patchFirmware(configYAML, firmwareBytes, patchFiles, (msg) => {
|
||||||
|
appendLog(msg);
|
||||||
|
const trimmed = msg.trimStart();
|
||||||
|
if (trimmed.startsWith('Patching ') || trimmed.startsWith('Checking ') ||
|
||||||
|
trimmed.startsWith('Loading WASM') || trimmed.startsWith('WASM module')) {
|
||||||
|
buildProgress.textContent = trimmed;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.tgz;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showBuildResult() {
|
||||||
|
const action = isRestore ? 'Software extracted' : 'Patching complete';
|
||||||
|
const description = isRestore ? 'This will restore the original unpatched software.' : '';
|
||||||
|
const deviceName = KOBO_MODELS[selectedPrefix] || 'Kobo';
|
||||||
|
const installHint = manualMode
|
||||||
|
? 'Download the file and copy it to your ' + deviceName + '.'
|
||||||
|
: 'Write it directly to your connected Kobo, or download for manual installation.';
|
||||||
|
|
||||||
|
buildStatus.innerHTML =
|
||||||
|
action + '. <strong>KoboRoot.tgz</strong> (' + formatMB(resultTgz.length) + ') is ready. ' +
|
||||||
|
(description ? description + ' ' : '') + installHint;
|
||||||
|
|
||||||
|
const doneLog = document.getElementById('done-log');
|
||||||
|
doneLog.textContent = buildLog.textContent;
|
||||||
|
|
||||||
|
// Reset install step state.
|
||||||
|
btnWrite.hidden = manualMode;
|
||||||
|
btnWrite.disabled = false;
|
||||||
|
btnWrite.className = 'primary';
|
||||||
|
btnWrite.textContent = 'Write to Kobo';
|
||||||
|
btnDownload.disabled = false;
|
||||||
|
writeInstructions.hidden = true;
|
||||||
|
downloadInstructions.hidden = true;
|
||||||
|
existingTgzWarning.hidden = true;
|
||||||
|
|
||||||
|
setNavStep(4);
|
||||||
|
showStep(stepDone);
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
doneLog.scrollTop = doneLog.scrollHeight;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkExistingTgz() {
|
||||||
|
if (manualMode || !device.directoryHandle) return;
|
||||||
|
try {
|
||||||
|
const koboDir = await device.directoryHandle.getDirectoryHandle('.kobo');
|
||||||
|
await koboDir.getFileHandle('KoboRoot.tgz');
|
||||||
|
existingTgzWarning.hidden = false;
|
||||||
|
} catch {
|
||||||
|
// No existing file — that's fine.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
btnBuild.addEventListener('click', async () => {
|
btnBuild.addEventListener('click', async () => {
|
||||||
@@ -370,75 +462,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const firmwareBytes = await downloadFirmware(firmwareURL);
|
const firmwareBytes = await downloadFirmware(firmwareURL);
|
||||||
appendLog('Download complete: ' + (firmwareBytes.length / 1024 / 1024).toFixed(1) + ' MB');
|
appendLog('Download complete: ' + formatMB(firmwareBytes.length));
|
||||||
|
|
||||||
if (isRestore) {
|
resultTgz = isRestore
|
||||||
buildProgress.textContent = 'Extracting KoboRoot.tgz...';
|
? await extractOriginalTgz(firmwareBytes)
|
||||||
appendLog('Extracting original KoboRoot.tgz from software update...');
|
: await runPatcher(firmwareBytes);
|
||||||
const zip = await JSZip.loadAsync(firmwareBytes);
|
|
||||||
const koboRoot = zip.file('KoboRoot.tgz');
|
|
||||||
if (!koboRoot) throw new Error('KoboRoot.tgz not found in software update');
|
|
||||||
resultTgz = new Uint8Array(await koboRoot.async('arraybuffer'));
|
|
||||||
appendLog('Extracted KoboRoot.tgz: ' + (resultTgz.length / 1024 / 1024).toFixed(1) + ' MB');
|
|
||||||
} else {
|
|
||||||
buildProgress.textContent = 'Applying patches...';
|
|
||||||
const configYAML = patchUI.generateConfig();
|
|
||||||
const patchFiles = patchUI.getPatchFileBytes();
|
|
||||||
|
|
||||||
const result = await runner.patchFirmware(configYAML, firmwareBytes, patchFiles, (msg) => {
|
showBuildResult();
|
||||||
appendLog(msg);
|
await checkExistingTgz();
|
||||||
const trimmed = msg.trimStart();
|
|
||||||
if (trimmed.startsWith('Patching ') || trimmed.startsWith('Checking ') ||
|
|
||||||
trimmed.startsWith('Loading WASM') || trimmed.startsWith('WASM module')) {
|
|
||||||
buildProgress.textContent = trimmed;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
resultTgz = result.tgz;
|
|
||||||
}
|
|
||||||
const sizeTxt = (resultTgz.length / 1024 / 1024).toFixed(1) + ' MB';
|
|
||||||
const action = isRestore ? 'Software extracted' : 'Patching complete';
|
|
||||||
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_MODELS[selectedPrefix] || 'Kobo') + '.'
|
|
||||||
: 'Write it directly to your connected Kobo, or download for manual installation.');
|
|
||||||
|
|
||||||
const doneLog = document.getElementById('done-log');
|
|
||||||
doneLog.textContent = buildLog.textContent;
|
|
||||||
|
|
||||||
// Reset install step state.
|
|
||||||
btnWrite.hidden = manualMode;
|
|
||||||
btnWrite.disabled = false;
|
|
||||||
btnWrite.className = 'primary';
|
|
||||||
btnWrite.textContent = 'Write to Kobo';
|
|
||||||
btnDownload.disabled = false;
|
|
||||||
writeInstructions.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);
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -471,14 +502,7 @@
|
|||||||
|
|
||||||
btnDownload.addEventListener('click', () => {
|
btnDownload.addEventListener('click', () => {
|
||||||
if (!resultTgz) return;
|
if (!resultTgz) return;
|
||||||
const blob = new Blob([resultTgz], { type: 'application/gzip' });
|
triggerDownload(resultTgz, 'KoboRoot.tgz', 'application/gzip');
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = 'KoboRoot.tgz';
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
|
|
||||||
writeInstructions.hidden = true;
|
writeInstructions.hidden = true;
|
||||||
downloadInstructions.hidden = false;
|
downloadInstructions.hidden = false;
|
||||||
document.getElementById('download-device-name').textContent = KOBO_MODELS[selectedPrefix] || 'Kobo';
|
document.getElementById('download-device-name').textContent = KOBO_MODELS[selectedPrefix] || 'Kobo';
|
||||||
|
|||||||
Reference in New Issue
Block a user