1
0

Add KOReader option
All checks were successful
Build and test project / build-and-test (push) Successful in 1m32s

This commit is contained in:
2026-03-21 16:32:15 +01:00
parent 620d8a1929
commit aaf3bf8749
19 changed files with 659 additions and 344 deletions

View File

@@ -898,6 +898,10 @@ select + .fallback-hint {
/* Install instructions */
.install-instructions {
margin-top: 1rem;
background: var(--card-bg);
border: 1px solid var(--border-light);
border-radius: 10px;
padding: 1rem 1.25rem;
}
.install-instructions .warning {
@@ -909,14 +913,25 @@ select + .fallback-hint {
}
.install-steps {
margin: 0.5rem 0 0 1.25rem;
margin: 0.25rem 0 0 1.25rem;
font-size: 0.88rem;
color: var(--text-secondary);
line-height: 1.7;
}
.install-steps li {
padding: 0.1rem 0;
padding: 0.15rem 0;
}
.install-steps code {
display: inline-block;
background: var(--bg);
border: 1px solid var(--border-light);
border-radius: 4px;
padding: 0.15rem 0.4rem;
font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
font-size: 0.82rem;
word-break: break-all;
}
.step .info-banner {

View File

@@ -191,6 +191,13 @@
<span class="nm-config-desc">Adds the Readerly font family. These fonts are optically similar to Bookerly. When you are reading a book, you will be able to select this font from the dropdown as "KF Readerly".</span>
</div>
</label>
<label class="nm-config-item" id="nm-cfg-koreader-label">
<input type="checkbox" name="nm-cfg-koreader">
<div class="nm-config-text">
<span>Install KOReader <span id="koreader-version"></span> (optional)</span>
<span class="nm-config-desc">Installs <a href="https://koreader.rocks" target="_blank">KOReader</a>, an alternative e-book reader with advanced features like PDF reflow, customizable fonts, and more.</span>
</div>
</label>
<label class="nm-config-item">
<input type="checkbox" name="nm-cfg-simplify-tabs">
<div class="nm-config-text">
@@ -269,12 +276,12 @@
<div id="nm-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>Extract the downloaded ZIP to the <strong>root</strong> of the Kobo drive, preserving the folder structure.</li>
<li id="nm-download-conf-step" hidden>
Open <strong>.kobo/Kobo/Kobo eReader.conf</strong> in a text editor.<br>
<li id="nm-download-conf-step" hidden>Open <strong>.kobo/Kobo/Kobo eReader.conf</strong> in a text editor.<br>
Find the <code>[FeatureSettings]</code> section (or add it at the end) and add the following line:<br>
<code>ExcludeSyncFolders=(calibre|\.(?!kobo|adobe|calibre).+|([^.][^/]*/)+\..+)</code><br>
This prevents the Kobo from discovering books in these folders during a sync.</li>
This prevents the Kobo from incorrectly identifying certain files as books in your library.</li>
<li id="nm-download-reboot-step" hidden><strong>Safely eject</strong> the Kobo, then power it off by holding the power button until it says "Powered off". Press the power button again to boot it back up. The config change takes effect after reboot. Reconnect the Kobo via USB afterwards.</li>
<li>Extract the downloaded ZIP to the <strong>root</strong> of the Kobo drive, preserving the folder structure. Make sure hidden folders like <code>.kobo</code> and <code>.adds</code> are also copied.</li>
<li><strong>Safely eject</strong> the Kobo &mdash; do not just unplug the cable.</li>
<li>The device will reboot and install NickelMenu automatically.</li>
</ol>

View File

@@ -23,16 +23,27 @@ import JSZip from 'jszip';
let selectedMode = null; // 'nickelmenu' | 'patches'
let nickelMenuOption = null; // 'sample' | 'nickelmenu-only' | 'remove'
// Fetch data eagerly so it's ready when needed.
const softwareUrlsReady = loadSoftwareUrls();
const availablePatchesReady = scanAvailablePatches().then(p => { availablePatches = p; });
// --- Helpers ---
const $ = (id) => document.getElementById(id);
const $q = (sel, ctx = document) => ctx.querySelector(sel);
const $qa = (sel, ctx = document) => ctx.querySelectorAll(sel);
// Fetch data eagerly so it's ready when needed.
const softwareUrlsReady = loadSoftwareUrls();
const availablePatchesReady = scanAvailablePatches().then(p => { availablePatches = p; });
// Show KOReader version in the UI (best-effort, non-blocking).
fetch('/koreader/release.json').then(r => r.ok ? r.json() : null).then(meta => {
if (meta && meta.version) {
$('koreader-version').textContent = meta.version;
} else {
$('nm-cfg-koreader-label').style.display = 'none';
}
}).catch(() => {
$('nm-cfg-koreader-label').style.display = 'none';
});
function formatMB(bytes) {
return (bytes / 1024 / 1024).toFixed(1) + ' MB';
}
@@ -502,6 +513,7 @@ import JSZip from 'jszip';
screensaver: $q('input[name="nm-cfg-screensaver"]').checked,
simplifyTabs: $q('input[name="nm-cfg-simplify-tabs"]').checked,
simplifyHome: $q('input[name="nm-cfg-simplify-home"]').checked,
koreader: $q('input[name="nm-cfg-koreader"]').checked,
};
}
@@ -558,6 +570,7 @@ import JSZip from 'jszip';
if (cfg.screensaver) items.push(TL.NICKEL_MENU_ITEMS.SCREENSAVER);
if (cfg.simplifyTabs) items.push(TL.NICKEL_MENU_ITEMS.SIMPLIFY_TABS);
if (cfg.simplifyHome) items.push(TL.NICKEL_MENU_ITEMS.SIMPLIFY_HOME);
if (cfg.koreader) items.push(TL.NICKEL_MENU_ITEMS.KOREADER);
for (const text of items) {
const li = document.createElement('li');
li.textContent = text;
@@ -638,8 +651,10 @@ import JSZip from 'jszip';
nmDoneStatus.textContent = TL.STATUS.NM_DOWNLOAD_READY;
triggerDownload(resultNmZip, 'NickelMenu-install.zip', 'application/zip');
$('nm-download-instructions').hidden = false;
// Show eReader.conf step only when sample config is included
$('nm-download-conf-step').hidden = nickelMenuOption !== 'sample';
// Show eReader.conf + reboot steps only when sample config is included
const showConfStep = nickelMenuOption === 'sample';
$('nm-download-conf-step').hidden = !showConfStep;
$('nm-download-reboot-step').hidden = !showConfStep;
}
setNavStep(5);

View File

@@ -16,11 +16,13 @@ import JSZip from 'jszip';
* screensaver: bool — include custom screensaver
* simplifyTabs: bool — comment out experimental tab items in config
* simplifyHome: bool — append homescreen simplification lines
* koreader: bool — download and install latest KOReader from GitHub
*/
class NickelMenuInstaller {
constructor() {
this.nickelMenuZip = null; // JSZip instance
this.koboConfigZip = null; // JSZip instance
this.koreaderZip = null; // JSZip instance
}
/**
@@ -44,6 +46,25 @@ class NickelMenuInstaller {
}
}
/**
* Download and cache KOReader for Kobo (served from the app's own domain
* to avoid CORS issues with GitHub release downloads).
* @param {function} progressFn
*/
async loadKoreader(progressFn) {
if (this.koreaderZip) return;
progressFn('Fetching KOReader release info...');
const metaResp = await fetch('/koreader/release.json');
if (!metaResp.ok) throw new Error('KOReader assets not available (run koreader/setup.sh)');
const meta = await metaResp.json();
progressFn('Downloading KOReader ' + meta.version + '...');
const zipResp = await fetch('/koreader/koreader-kobo.zip');
if (!zipResp.ok) throw new Error('Failed to download KOReader: HTTP ' + zipResp.status);
this.koreaderZip = await JSZip.loadAsync(await zipResp.arrayBuffer());
}
/**
* Get the KoboRoot.tgz from the NickelMenu zip.
*/
@@ -53,6 +74,37 @@ class NickelMenuInstaller {
return new Uint8Array(await file.async('arraybuffer'));
}
/**
* Get KOReader files from the downloaded zip, remapped to .adds/koreader/.
* The zip contains a top-level koreader/ directory that needs to be placed
* under .adds/ on the device. Also includes a NickelMenu launcher config.
* Returns { path: string[], data: Uint8Array } entries.
*/
async getKoreaderFiles() {
const files = [];
for (const [relativePath, zipEntry] of Object.entries(this.koreaderZip.files)) {
if (zipEntry.dir) continue;
// Remap koreader/... to .adds/koreader/...
const devicePath = relativePath.startsWith('koreader/')
? '.adds/' + relativePath
: '.adds/koreader/' + relativePath;
const data = new Uint8Array(await zipEntry.async('arraybuffer'));
files.push({
path: devicePath.split('/'),
data,
});
}
// Add NickelMenu launcher config
const launcherConfig = 'menu_item:main:KOReader:cmd_spawn:quiet:exec /mnt/onboard/.adds/koreader/koreader.sh\n';
files.push({
path: ['.adds', 'nm', 'koreader'],
data: new TextEncoder().encode(launcherConfig),
});
return files;
}
/**
* Get config files from kobo-config.zip filtered by cfg flags.
* Returns { path: string[], data: Uint8Array } entries.
@@ -135,6 +187,16 @@ class NickelMenuInstaller {
await device.writeFile(path, data);
}
// Install KOReader if selected
if (cfg.koreader) {
await this.loadKoreader(progressFn);
progressFn('Writing KOReader files...');
const koreaderFiles = await this.getKoreaderFiles();
for (const { path, data } of koreaderFiles) {
await device.writeFile(path, data);
}
}
// Modify Kobo eReader.conf
progressFn('Updating Kobo eReader.conf...');
await this.updateEReaderConf(device);
@@ -193,6 +255,16 @@ class NickelMenuInstaller {
for (const { path, data } of configFiles) {
zip.file(path.join('/'), data);
}
// Include KOReader if selected
if (cfg.koreader) {
await this.loadKoreader(progressFn);
progressFn('Adding KOReader to package...');
const koreaderFiles = await this.getKoreaderFiles();
for (const { path, data } of koreaderFiles) {
zip.file(path.join('/'), data);
}
}
}
progressFn('Compressing...');

View File

@@ -55,5 +55,6 @@ export const TL = {
SCREENSAVER: 'Custom screensaver',
SIMPLIFY_TABS: 'Simplified tab menu',
SIMPLIFY_HOME: 'Simplified homescreen',
KOREADER: 'KOReader',
},
};