1
0
Files
kobopatch-webui/web/public/js/nickelmenu.js

196 lines
6.9 KiB
JavaScript

/**
* 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;
}
}