diff --git a/nixpacks.toml b/nixpacks.toml new file mode 100644 index 0000000..78322ab --- /dev/null +++ b/nixpacks.toml @@ -0,0 +1,11 @@ +[phases.setup] +nixPkgs = ["go", "git"] + +[phases.build] +cmds = [ + "cd kobopatch-wasm && bash setup.sh", + "cd kobopatch-wasm && bash build.sh", +] + +[staticAssets] +root = "web/public" diff --git a/web/public/app.js b/web/public/app.js index f33768d..6a51577 100644 --- a/web/public/app.js +++ b/web/public/app.js @@ -274,11 +274,11 @@ function goToBuild() { if (isRestore) { firmwareDescription.textContent = - 'will be downloaded and extracted without modifications to restore the original unpatched software:'; + '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:'; + 'will be downloaded automatically from Kobo\u2019s servers and will be patched after the download completes.'; btnBuild.textContent = 'Build Patched Firmware'; } setNavStep(3); @@ -338,6 +338,9 @@ showStep(stepBuilding); buildLog.textContent = ''; buildProgress.textContent = 'Starting...'; + document.getElementById('build-wait-hint').textContent = isRestore + ? 'Please wait while the original firmware is being downloaded and extracted...' + : 'Please wait while the patch is being applied...'; try { if (!firmwareURL) { @@ -348,20 +351,30 @@ const firmwareBytes = await downloadFirmware(firmwareURL); appendLog('Firmware downloaded: ' + (firmwareBytes.length / 1024 / 1024).toFixed(1) + ' MB'); - buildProgress.textContent = isRestore ? 'Extracting firmware...' : 'Applying patches...'; - const configYAML = patchUI.generateConfig(); - const patchFiles = patchUI.getPatchFileBytes(); + if (isRestore) { + buildProgress.textContent = 'Extracting KoboRoot.tgz...'; + appendLog('Extracting original KoboRoot.tgz from firmware...'); + const zip = await JSZip.loadAsync(firmwareBytes); + const koboRoot = zip.file('KoboRoot.tgz'); + if (!koboRoot) throw new Error('KoboRoot.tgz not found in firmware zip'); + 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) => { - appendLog(msg); - const trimmed = msg.trimStart(); - if (trimmed.startsWith('Patching ') || trimmed.startsWith('Checking ') || - trimmed.startsWith('Loading WASM') || trimmed.startsWith('WASM module')) { - buildProgress.textContent = trimmed; - } - }); + 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; + } + }); - resultTgz = result.tgz; + resultTgz = result.tgz; + } const sizeTxt = (resultTgz.length / 1024 / 1024).toFixed(1) + ' MB'; const action = isRestore ? 'Firmware extracted' : 'Patching complete'; const description = isRestore @@ -480,4 +493,17 @@ enterManualMode(); } }); + + // --- How it works dialog --- + const dialog = document.getElementById('how-it-works-dialog'); + document.getElementById('btn-how-it-works').addEventListener('click', (e) => { + e.preventDefault(); + dialog.showModal(); + }); + document.getElementById('btn-close-dialog').addEventListener('click', () => { + dialog.close(); + }); + dialog.addEventListener('click', (e) => { + if (e.target === dialog) dialog.close(); + }); })(); diff --git a/web/public/index.html b/web/public/index.html index 81c9a6f..14e8955 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -4,7 +4,10 @@ KoboPatch Web UI - + + + + @@ -109,13 +112,16 @@

Firmware for - will be downloaded automatically from Kobo's servers and will be patched after the download completes:
-
- + will be downloaded automatically from Kobo's servers and will be patched after the download completes. +

+
+ Firmware download URL + +

You can verify if this URL matches your Kobo's model on Kobo's support page. The most important bit is that "koboXX" matches, for example "kobo13" for Kobo Libra Color. - -

+

+
+ + + + + + - - - - + + + + diff --git a/web/public/patch-ui.js b/web/public/patch-ui.js index 7aa2bdf..3e17249 100644 --- a/web/public/patch-ui.js +++ b/web/public/patch-ui.js @@ -386,4 +386,5 @@ class PatchUI { } return files; } + } diff --git a/web/public/patch-worker.js b/web/public/patch-worker.js index bc0bbb2..6eeb4c7 100644 --- a/web/public/patch-worker.js +++ b/web/public/patch-worker.js @@ -10,7 +10,7 @@ async function loadWasm() { const go = new Go(); const result = await WebAssembly.instantiateStreaming( - fetch('kobopatch.wasm?ts=1773669670'), + fetch('kobopatch.wasm?ts=1773670636'), go.importObject ); go.run(result.instance); diff --git a/web/public/style.css b/web/public/style.css index 26a16a2..393fbc9 100644 --- a/web/public/style.css +++ b/web/public/style.css @@ -30,7 +30,7 @@ } body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif; + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; @@ -619,3 +619,122 @@ button:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; } + +/* Footer */ +.site-footer { + max-width: 640px; + margin: 0 auto; + padding: 1.5rem 1.5rem 2rem; + border-top: 1px solid var(--border-light); + text-align: center; + font-size: 0.8rem; + color: var(--text-secondary); +} + +.site-footer a { + color: var(--text-secondary); + text-decoration: underline; +} + +.site-footer a:hover { + color: var(--text); +} + +/* Modal dialog */ +.modal { + border: none; + border-radius: 12px; + padding: 0; + max-width: 560px; + width: calc(100% - 2rem); + max-height: 80vh; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + margin: 0; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05); +} + +.modal::backdrop { + background: rgba(0, 0, 0, 0.4); +} + +.modal-content { + display: flex; + flex-direction: column; + max-height: 80vh; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 1.25rem; + border-bottom: 1px solid var(--border-light); + flex-shrink: 0; +} + +.modal-header h2 { + margin-bottom: 0; + font-size: 1rem; +} + +.modal-close { + background: none; + border: none; + font-size: 1.4rem; + color: var(--text-secondary); + cursor: pointer; + padding: 0 0.25rem; + line-height: 1; + box-shadow: none; +} + +.modal-close:hover { + color: var(--text); +} + +.modal-body { + padding: 1.25rem; + overflow-y: auto; + font-size: 0.88rem; + color: var(--text-secondary); + line-height: 1.7; +} + +.modal-body h3 { + font-size: 0.88rem; + font-weight: 600; + color: var(--text); + margin-top: 1.25rem; + margin-bottom: 0.5rem; +} + +.modal-body p { + margin-bottom: 0.75rem; +} + +.modal-body ol { + margin: 0 0 0.75rem 1.25rem; +} + +.modal-body li { + margin-bottom: 0.5rem; +} + +.modal-body a { + color: var(--primary); + text-decoration: none; +} + +.modal-body a:hover { + text-decoration: underline; +} + +.modal-body code { + font-size: 0.8rem; + background: #f1f5f9; + padding: 0.1rem 0.35rem; + border-radius: 3px; +}