(() => {
const device = new KoboDevice();
const patchUI = new PatchUI();
const runner = new KobopatchRunner();
let firmwareURL = null;
// let firmwareFile = null; // fallback: manual file input
let resultTgz = null;
let manualMode = false;
let selectedPrefix = null;
// DOM elements
const stepConnect = document.getElementById('step-connect');
const stepManual = document.getElementById('step-manual');
const stepDevice = document.getElementById('step-device');
const stepPatches = document.getElementById('step-patches');
const stepFirmware = document.getElementById('step-firmware');
const stepBuilding = document.getElementById('step-building');
const stepDone = document.getElementById('step-done');
const stepError = document.getElementById('step-error');
const btnConnect = document.getElementById('btn-connect');
const btnManualFromAuto = document.getElementById('btn-manual-from-auto');
const btnManualConfirm = document.getElementById('btn-manual-confirm');
const manualVersion = document.getElementById('manual-version');
const manualModel = document.getElementById('manual-model');
const manualChromeHint = document.getElementById('manual-chrome-hint');
const btnBuild = document.getElementById('btn-build');
const btnWrite = document.getElementById('btn-write');
const btnDownload = document.getElementById('btn-download');
const btnRetry = document.getElementById('btn-retry');
// const firmwareInput = document.getElementById('firmware-input'); // fallback
const firmwareAutoInfo = document.getElementById('firmware-auto-info');
// const firmwareManualInfo = document.getElementById('firmware-manual-info'); // fallback
const errorMessage = document.getElementById('error-message');
const errorLog = document.getElementById('error-log');
const deviceStatus = document.getElementById('device-status');
const patchContainer = document.getElementById('patch-container');
const buildStatus = document.getElementById('build-status');
const writeSuccess = document.getElementById('write-success');
const firmwareVersionLabel = document.getElementById('firmware-version-label');
// const firmwareVersionLabelManual = document.getElementById('firmware-version-label-manual'); // fallback
const allSteps = [stepConnect, stepManual, stepDevice, stepPatches, stepFirmware, stepBuilding, stepDone, stepError];
// Decide initial step based on browser support
const hasFileSystemAccess = KoboDevice.isSupported();
if (hasFileSystemAccess) {
showSteps(stepConnect);
} else {
// Skip straight to manual mode
enterManualMode();
}
function showSteps(...steps) {
for (const s of allSteps) {
s.hidden = !steps.includes(s);
}
}
function showError(message, log) {
errorMessage.textContent = message;
if (log) {
errorLog.textContent = log;
errorLog.hidden = false;
} else {
errorLog.hidden = true;
}
showSteps(stepError);
}
/**
* Configure the firmware step for auto-download.
*/
function configureFirmwareStep(version, prefix) {
firmwareURL = prefix ? getFirmwareURL(prefix, version) : null;
firmwareVersionLabel.textContent = version;
document.getElementById('firmware-download-url').textContent = firmwareURL || '';
}
async function enterManualMode() {
manualMode = true;
// Show the Chrome hint only if the browser actually supports it
// (i.e., user chose manual mode voluntarily)
if (hasFileSystemAccess) {
manualChromeHint.hidden = false;
}
// Populate version dropdown from available patches
const available = await scanAvailablePatches();
manualVersion.innerHTML = '';
for (const p of available) {
const opt = document.createElement('option');
opt.value = p.version;
opt.textContent = p.version;
opt.dataset.filename = p.filename;
manualVersion.appendChild(opt);
}
// Reset model dropdown
manualModel.innerHTML = '';
manualModel.hidden = true;
showSteps(stepManual);
}
async function loadPatchesForVersion(version, available) {
const match = available.find(p => p.version === version);
if (!match) return false;
await patchUI.loadFromURL('patches/' + match.filename);
patchUI.render(patchContainer);
return true;
}
// Switch to manual mode from auto mode
btnManualFromAuto.addEventListener('click', (e) => {
e.preventDefault();
enterManualMode();
});
// Manual mode: version selected → populate model dropdown
manualVersion.addEventListener('change', () => {
const version = manualVersion.value;
selectedPrefix = null;
if (!version) {
manualModel.hidden = true;
btnManualConfirm.disabled = true;
return;
}
// Populate device dropdown for this firmware version
const devices = getDevicesForVersion(version);
manualModel.innerHTML = '';
for (const d of devices) {
const opt = document.createElement('option');
opt.value = d.prefix;
opt.textContent = d.model;
manualModel.appendChild(opt);
}
manualModel.hidden = false;
btnManualConfirm.disabled = true;
});
// Manual mode: model selected
manualModel.addEventListener('change', () => {
selectedPrefix = manualModel.value || null;
btnManualConfirm.disabled = !manualVersion.value || !manualModel.value;
});
// Manual mode: confirm selection
btnManualConfirm.addEventListener('click', async () => {
const version = manualVersion.value;
if (!version || !selectedPrefix) return;
try {
const available = await scanAvailablePatches();
const loaded = await loadPatchesForVersion(version, available);
if (!loaded) {
showError('Could not load patches for firmware ' + version);
return;
}
configureFirmwareStep(version, selectedPrefix);
showSteps(stepPatches, stepFirmware);
} catch (err) {
showError(err.message);
}
});
// Auto mode: connect device
btnConnect.addEventListener('click', async () => {
try {
const info = await device.connect();
document.getElementById('device-model').textContent = info.model;
document.getElementById('device-serial').textContent = info.serial;
document.getElementById('device-firmware').textContent = info.firmware;
selectedPrefix = info.serialPrefix;
const available = await scanAvailablePatches();
const match = available.find(p => p.version === info.firmware);
if (match) {
deviceStatus.className = 'status-supported';
deviceStatus.textContent = 'Patches available for firmware ' + info.firmware + '.';
await patchUI.loadFromURL('patches/' + match.filename);
patchUI.render(patchContainer);
configureFirmwareStep(info.firmware, info.serialPrefix);
showSteps(stepDevice, stepPatches, stepFirmware);
} else {
deviceStatus.className = 'status-unsupported';
deviceStatus.textContent =
'No patches available for firmware ' + info.firmware + '. ' +
'Supported versions: ' + available.map(p => p.version).join(', ');
showSteps(stepDevice);
}
} catch (err) {
if (err.name === 'AbortError') return;
showError(err.message);
}
});
// // Firmware file selected (fallback for devices without auto-download URL)
// firmwareInput.addEventListener('change', () => {
// firmwareFile = firmwareInput.files[0] || null;
// });
const buildProgress = document.getElementById('build-progress');
const buildLog = document.getElementById('build-log');
/**
* Download firmware zip from Kobo's servers with progress tracking.
* Returns Uint8Array of the zip file.
*/
async function downloadFirmware(url) {
const resp = await fetch(url);
if (!resp.ok) {
throw new Error('Firmware download failed: HTTP ' + resp.status);
}
const contentLength = resp.headers.get('Content-Length');
if (!contentLength || !resp.body) {
// Fallback: no streaming progress
buildProgress.textContent = 'Downloading firmware...';
return new Uint8Array(await resp.arrayBuffer());
}
const total = parseInt(contentLength, 10);
const reader = resp.body.getReader();
const chunks = [];
let received = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
received += value.length;
const pct = ((received / total) * 100).toFixed(0);
const mb = (received / 1024 / 1024).toFixed(1);
const totalMB = (total / 1024 / 1024).toFixed(1);
buildProgress.textContent = `Downloading firmware... ${mb} / ${totalMB} MB (${pct}%)`;
}
// Concatenate chunks into single Uint8Array
const result = new Uint8Array(received);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return result;
}
function appendLog(msg) {
buildLog.textContent += msg + '\n';
buildLog.scrollTop = buildLog.scrollHeight;
}
// Build
btnBuild.addEventListener('click', async () => {
const stepsToShow = manualMode ? [stepBuilding] : [stepDevice, stepBuilding];
showSteps(...stepsToShow);
buildLog.textContent = '';
buildProgress.textContent = 'Starting...';
try {
if (!firmwareURL) {
showError('No firmware download URL available for this device.');
return;
}
const firmwareBytes = await downloadFirmware(firmwareURL);
appendLog('Firmware downloaded: ' + (firmwareBytes.length / 1024 / 1024).toFixed(1) + ' MB');
buildProgress.textContent = 'Loading WASM patcher...';
await runner.load();
appendLog('WASM module loaded');
buildProgress.textContent = 'Applying patches...';
const configYAML = patchUI.generateConfig();
const patchFiles = patchUI.getPatchFileBytes();
const result = await runner.patchFirmware(configYAML, firmwareBytes, patchFiles, (msg) => {
appendLog(msg);
// Update headline with the latest high-level step
if (msg.startsWith('Patching ') || msg.startsWith('Extracting ') || msg.startsWith('Verifying ')) {
buildProgress.textContent = msg;
}
});
resultTgz = result.tgz;
buildStatus.textContent =
'Patching complete. KoboRoot.tgz is ' +
(resultTgz.length / 1024).toFixed(0) + ' KB.';
writeSuccess.hidden = true;
// Copy log to done step
const doneLog = document.getElementById('done-log');
doneLog.textContent = buildLog.textContent;
doneLog.scrollTop = doneLog.scrollHeight;
// In manual mode, hide the "Write to Kobo" button
btnWrite.hidden = manualMode;
const doneSteps = manualMode ? [stepDone] : [stepDevice, stepDone];
showSteps(...doneSteps);
} catch (err) {
showError('Build failed: ' + err.message, buildLog.textContent);
}
});
// Write to device (auto mode only)
btnWrite.addEventListener('click', async () => {
if (!resultTgz || !device.directoryHandle) return;
try {
const koboDir = await device.directoryHandle.getDirectoryHandle('.kobo');
const fileHandle = await koboDir.getFileHandle('KoboRoot.tgz', { create: true });
const writable = await fileHandle.createWritable();
await writable.write(resultTgz);
await writable.close();
writeSuccess.hidden = false;
} catch (err) {
showError('Failed to write KoboRoot.tgz: ' + err.message);
}
});
// Download
btnDownload.addEventListener('click', () => {
if (!resultTgz) return;
const blob = new Blob([resultTgz], { type: 'application/gzip' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'KoboRoot.tgz';
a.click();
URL.revokeObjectURL(url);
});
// Retry
btnRetry.addEventListener('click', () => {
device.disconnect();
firmwareURL = null;
resultTgz = null;
manualMode = false;
selectedPrefix = null;
btnWrite.hidden = false;
if (hasFileSystemAccess) {
showSteps(stepConnect);
} else {
enterManualMode();
}
});
})();