1
0

Refactor how NickelMenu is set up
All checks were successful
Build and test project / build-and-test (push) Successful in 1m32s

This commit is contained in:
2026-03-21 17:57:04 +01:00
parent 6f902090c1
commit 7b62c2e166
27 changed files with 553 additions and 404 deletions

View File

@@ -2,7 +2,7 @@ import { KoboDevice, KoboModels } from './kobo-device.js';
import { loadSoftwareUrls, getSoftwareUrl, getDevicesForVersion } from './kobo-software-urls.js';
import { PatchUI, scanAvailablePatches } from './patch-ui.js';
import { KoboPatchRunner } from './patch-runner.js';
import { NickelMenuInstaller } from './nickelmenu.js';
import { NickelMenuInstaller, ALL_FEATURES } from '../nickelmenu/installer.js';
import { TL } from './strings.js';
import { isEnabled as analyticsEnabled, track } from './analytics.js';
import JSZip from 'jszip';
@@ -34,16 +34,17 @@ import JSZip from 'jszip';
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';
});
// Check KOReader availability and mark the feature (best-effort, non-blocking).
const koreaderFeature = ALL_FEATURES.find(f => f.id === 'koreader');
const koreaderVersionReady = fetch('/koreader/release.json')
.then(r => r.ok ? r.json() : null)
.then(meta => {
if (meta && meta.version) {
koreaderFeature.available = true;
koreaderFeature.version = meta.version;
}
})
.catch(() => {});
function formatMB(bytes) {
return (bytes / 1024 / 1024).toFixed(1) + ' MB';
@@ -474,6 +475,46 @@ import JSZip from 'jszip';
// --- Step 2b: NickelMenu configuration ---
const nmConfigOptions = $('nm-config-options');
// Render feature checkboxes dynamically from ALL_FEATURES
function renderFeatureCheckboxes() {
nmConfigOptions.innerHTML = '';
for (const feature of ALL_FEATURES) {
// Hide unavailable features (e.g. KOReader when assets missing)
if (feature.available === false) continue;
const label = document.createElement('label');
label.className = 'nm-config-item';
const input = document.createElement('input');
input.type = 'checkbox';
input.name = 'nm-cfg-' + feature.id;
input.checked = feature.default;
if (feature.required) {
input.checked = true;
input.disabled = true;
}
const textDiv = document.createElement('div');
textDiv.className = 'nm-config-text';
const titleSpan = document.createElement('span');
let titleText = feature.title;
if (feature.required) titleText += ' (required)';
if (feature.version) titleText += ' ' + feature.version;
titleSpan.textContent = titleText;
const descSpan = document.createElement('span');
descSpan.className = 'nm-config-desc';
descSpan.textContent = feature.description;
textDiv.appendChild(titleSpan);
textDiv.appendChild(descSpan);
label.appendChild(input);
label.appendChild(textDiv);
nmConfigOptions.appendChild(label);
}
}
// Show/hide config checkboxes based on radio selection, enable Continue
for (const radio of $qa('input[name="nm-option"]', stepNickelMenu)) {
radio.addEventListener('change', () => {
@@ -504,24 +545,24 @@ import JSZip from 'jszip';
removeOption.classList.add('nm-option-disabled');
removeDesc.textContent = TL.STATUS.NM_REMOVAL_DISABLED;
if (removeRadio.checked) {
const sampleRadio = $q('input[value="sample"]', stepNickelMenu);
sampleRadio.checked = true;
sampleRadio.dispatchEvent(new Event('change'));
const presetRadio = $q('input[value="preset"]', stepNickelMenu);
presetRadio.checked = true;
presetRadio.dispatchEvent(new Event('change'));
}
}
function getNmConfig() {
return {
fonts: $q('input[name="nm-cfg-fonts"]').checked,
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,
};
function getSelectedFeatures() {
return ALL_FEATURES.filter(f => {
if (f.available === false) return false;
if (f.required) return true;
const checkbox = $q(`input[name="nm-cfg-${f.id}"]`);
return checkbox && checkbox.checked;
});
}
function goToNickelMenuConfig() {
checkNickelMenuInstalled();
renderFeatureCheckboxes();
const currentOption = $q('input[name="nm-option"]:checked', stepNickelMenu);
nmConfigOptions.hidden = !currentOption || currentOption.value !== 'preset';
btnNmNext.disabled = !currentOption;
@@ -563,13 +604,10 @@ import JSZip from 'jszip';
btnNmDownload.hidden = false;
} else {
summary.textContent = TL.STATUS.NM_WILL_BE_INSTALLED;
const items = [TL.STATUS.NM_NICKEL_ROOT_TGZ, 'Custom menu configuration'];
const cfg = getNmConfig();
if (cfg.fonts) items.push(TL.NICKEL_MENU_ITEMS.FONTS);
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);
const items = [TL.STATUS.NM_NICKEL_ROOT_TGZ];
for (const feature of getSelectedFeatures()) {
items.push(feature.title);
}
for (const text of items) {
const li = document.createElement('li');
li.textContent = text;
@@ -599,11 +637,12 @@ import JSZip from 'jszip';
async function executeNmInstall(writeToDevice) {
const nmProgress = $('nm-progress');
const progressFn = (msg) => { nmProgress.textContent = msg; };
showStep(stepNmInstalling);
try {
if (nickelMenuOption === 'remove') {
await nmInstaller.loadAssets((msg) => { nmProgress.textContent = msg; }, false);
await nmInstaller.loadNickelMenu(progressFn);
nmProgress.textContent = 'Writing KoboRoot.tgz...';
const tgz = await nmInstaller.getKoboRootTgz();
await device.writeFile(['.kobo', 'KoboRoot.tgz'], tgz);
@@ -613,17 +652,13 @@ import JSZip from 'jszip';
return;
}
const cfg = nickelMenuOption === 'preset' ? getNmConfig() : null;
const features = nickelMenuOption === 'preset' ? getSelectedFeatures() : [];
if (writeToDevice && device.directoryHandle) {
await nmInstaller.installToDevice(device, nickelMenuOption, cfg, (msg) => {
nmProgress.textContent = msg;
});
await nmInstaller.installToDevice(device, features, progressFn);
showNmDone('written');
} else {
resultNmZip = await nmInstaller.buildDownloadZip(nickelMenuOption, cfg, (msg) => {
nmProgress.textContent = msg;
});
resultNmZip = await nmInstaller.buildDownloadZip(features, progressFn);
showNmDone('download');
}
} catch (err) {