Add NickelMenu installation options
This commit is contained in:
195
web/public/js/nickelmenu.js
Normal file
195
web/public/js/nickelmenu.js
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* NickelMenu installer module.
|
||||
*
|
||||
* Handles downloading bundled NickelMenu + kobo-config assets,
|
||||
* and either writing them directly to a connected Kobo (auto mode)
|
||||
* or building a zip for manual download.
|
||||
*
|
||||
* Options:
|
||||
* 'nickelmenu-only' — just NickelMenu (KoboRoot.tgz)
|
||||
* 'sample' — NickelMenu + config based on cfg flags
|
||||
*
|
||||
* Config flags (when option is 'sample'):
|
||||
* fonts: bool — include Readerly fonts
|
||||
* screensaver: bool — include custom screensaver
|
||||
* simplifyTabs: bool — comment out experimental tab items in config
|
||||
* simplifyHome: bool — append homescreen simplification lines
|
||||
*/
|
||||
class NickelMenuInstaller {
|
||||
constructor() {
|
||||
this.nickelMenuZip = null; // JSZip instance
|
||||
this.koboConfigZip = null; // JSZip instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and cache the bundled assets.
|
||||
*/
|
||||
async loadAssets(progressFn) {
|
||||
if (this.nickelMenuZip && this.koboConfigZip) return;
|
||||
|
||||
progressFn('Downloading NickelMenu...');
|
||||
const nmResp = await fetch('nickelmenu/NickelMenu.zip');
|
||||
if (!nmResp.ok) throw new Error('Failed to download NickelMenu.zip: HTTP ' + nmResp.status);
|
||||
this.nickelMenuZip = await JSZip.loadAsync(await nmResp.arrayBuffer());
|
||||
|
||||
progressFn('Downloading configuration files...');
|
||||
const cfgResp = await fetch('nickelmenu/kobo-config.zip');
|
||||
if (!cfgResp.ok) throw new Error('Failed to download kobo-config.zip: HTTP ' + cfgResp.status);
|
||||
this.koboConfigZip = await JSZip.loadAsync(await cfgResp.arrayBuffer());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the KoboRoot.tgz from the NickelMenu zip.
|
||||
*/
|
||||
async getKoboRootTgz() {
|
||||
const file = this.nickelMenuZip.file('KoboRoot.tgz');
|
||||
if (!file) throw new Error('KoboRoot.tgz not found in NickelMenu.zip');
|
||||
return new Uint8Array(await file.async('arraybuffer'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get config files from kobo-config.zip filtered by cfg flags.
|
||||
* Returns { path: string[], data: Uint8Array } entries.
|
||||
*/
|
||||
async getConfigFiles(cfg) {
|
||||
const files = [];
|
||||
|
||||
for (const [relativePath, zipEntry] of Object.entries(this.koboConfigZip.files)) {
|
||||
if (zipEntry.dir) continue;
|
||||
|
||||
// Filter by cfg flags
|
||||
if (relativePath.startsWith('fonts/') && !cfg.fonts) continue;
|
||||
if (relativePath.startsWith('.kobo/screensaver/') && !cfg.screensaver) continue;
|
||||
|
||||
// Only include relevant directories
|
||||
if (!relativePath.startsWith('.adds/') &&
|
||||
!relativePath.startsWith('.kobo/screensaver/') &&
|
||||
!relativePath.startsWith('fonts/')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let data = new Uint8Array(await zipEntry.async('arraybuffer'));
|
||||
|
||||
// Modify the NickelMenu items file based on config
|
||||
if (relativePath === '.adds/nm/items') {
|
||||
let text = new TextDecoder().decode(data);
|
||||
|
||||
// Comment out experimental lines at top if simplifyTabs is off
|
||||
if (!cfg.simplifyTabs) {
|
||||
text = text.split('\n').map(line => {
|
||||
if (line.startsWith('experimental:') && !line.startsWith('experimental:hide_home')) {
|
||||
return '#' + line;
|
||||
}
|
||||
return line;
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
// Append homescreen simplification lines
|
||||
if (cfg.simplifyHome) {
|
||||
text += '\nexperimental:hide_home_row1col2_enabled:1\nexperimental:hide_home_row3_enabled:1\n';
|
||||
}
|
||||
|
||||
data = new TextEncoder().encode(text);
|
||||
}
|
||||
|
||||
files.push({
|
||||
path: relativePath.split('/'),
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install to a connected Kobo device via File System Access API.
|
||||
* @param {KoboDevice} device
|
||||
* @param {string} option - 'sample' or 'nickelmenu-only'
|
||||
* @param {object|null} cfg - config flags (when option is 'sample')
|
||||
* @param {function} progressFn
|
||||
*/
|
||||
async installToDevice(device, option, cfg, progressFn) {
|
||||
await this.loadAssets(progressFn);
|
||||
|
||||
// Always install KoboRoot.tgz
|
||||
progressFn('Writing KoboRoot.tgz...');
|
||||
const tgz = await this.getKoboRootTgz();
|
||||
await device.writeFile(['.kobo', 'KoboRoot.tgz'], tgz);
|
||||
|
||||
if (option === 'nickelmenu-only') {
|
||||
progressFn('Done.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Install config files
|
||||
progressFn('Writing configuration files...');
|
||||
const configFiles = await this.getConfigFiles(cfg);
|
||||
for (const { path, data } of configFiles) {
|
||||
await device.writeFile(path, data);
|
||||
}
|
||||
|
||||
// Modify Kobo eReader.conf
|
||||
progressFn('Updating Kobo eReader.conf...');
|
||||
await this.updateEReaderConf(device);
|
||||
|
||||
progressFn('Done.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add ExcludeSyncFolders to Kobo eReader.conf if not already present.
|
||||
*/
|
||||
async updateEReaderConf(device) {
|
||||
const confPath = ['.kobo', 'Kobo', 'Kobo eReader.conf'];
|
||||
let content = await device.readFile(confPath) || '';
|
||||
|
||||
const settingLine = 'ExcludeSyncFolders=(calibre|\\.(?!kobo|adobe|calibre).+|([^.][^/]*/)+\\..+)';
|
||||
|
||||
if (content.includes('ExcludeSyncFolders')) {
|
||||
// Already has the setting, don't duplicate
|
||||
return;
|
||||
}
|
||||
|
||||
// Add under [FeatureSettings], creating the section if needed
|
||||
if (content.includes('[FeatureSettings]')) {
|
||||
content = content.replace(
|
||||
'[FeatureSettings]',
|
||||
'[FeatureSettings]\n' + settingLine
|
||||
);
|
||||
} else {
|
||||
content += '\n[FeatureSettings]\n' + settingLine + '\n';
|
||||
}
|
||||
|
||||
await device.writeFile(confPath, new TextEncoder().encode(content));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a zip for manual download containing all files to copy to the Kobo.
|
||||
* @param {string} option - 'sample' or 'nickelmenu-only'
|
||||
* @param {object|null} cfg - config flags (when option is 'sample')
|
||||
* @param {function} progressFn
|
||||
* @returns {Uint8Array} zip contents
|
||||
*/
|
||||
async buildDownloadZip(option, cfg, progressFn) {
|
||||
await this.loadAssets(progressFn);
|
||||
|
||||
progressFn('Building download package...');
|
||||
const zip = new JSZip();
|
||||
|
||||
// Always include KoboRoot.tgz
|
||||
const tgz = await this.getKoboRootTgz();
|
||||
zip.file('.kobo/KoboRoot.tgz', tgz);
|
||||
|
||||
if (option !== 'nickelmenu-only') {
|
||||
// Include config files
|
||||
const configFiles = await this.getConfigFiles(cfg);
|
||||
for (const { path, data } of configFiles) {
|
||||
zip.file(path.join('/'), data);
|
||||
}
|
||||
}
|
||||
|
||||
progressFn('Compressing...');
|
||||
const result = await zip.generateAsync({ type: 'uint8array' });
|
||||
progressFn('Done.');
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user