Restructured JS files
This commit is contained in:
@@ -60,17 +60,17 @@ async function build() {
|
||||
logLevel: 'warning',
|
||||
});
|
||||
|
||||
// Copy worker files from src/js/ (not bundled, served separately)
|
||||
mkdirSync(join(distDir, 'js'), { recursive: true });
|
||||
// Copy worker files from src/js/workers/ (not bundled, served separately)
|
||||
mkdirSync(join(distDir, 'js', 'workers'), { recursive: true });
|
||||
|
||||
// Copy wasm_exec.js as-is
|
||||
const wasmExecSrc = join(srcDir, 'js', 'wasm_exec.js');
|
||||
if (existsSync(wasmExecSrc)) {
|
||||
cpSync(wasmExecSrc, join(distDir, 'js', 'wasm_exec.js'));
|
||||
cpSync(wasmExecSrc, join(distDir, 'js', 'workers', 'wasm_exec.js'));
|
||||
}
|
||||
|
||||
// Copy patch-worker.js with WASM hash injected
|
||||
const workerSrc = join(srcDir, 'js', 'patch-worker.js');
|
||||
const workerSrc = join(srcDir, 'js', 'workers', 'patch-worker.js');
|
||||
if (existsSync(workerSrc)) {
|
||||
let workerContent = readFileSync(workerSrc, 'utf-8');
|
||||
const wasmFile = join(distDir, 'wasm', 'kobopatch.wasm');
|
||||
@@ -81,7 +81,7 @@ async function build() {
|
||||
`kobopatch.wasm?h=${wasmHash}'`
|
||||
);
|
||||
}
|
||||
writeFileSync(join(distDir, 'js', 'patch-worker.js'), workerContent);
|
||||
writeFileSync(join(distDir, 'js', 'workers', 'patch-worker.js'), workerContent);
|
||||
}
|
||||
|
||||
// Get git version string
|
||||
|
||||
@@ -38,7 +38,7 @@ export default [
|
||||
},
|
||||
{
|
||||
// Worker script uses importScripts, self, Go, globalThis, WebAssembly
|
||||
files: ['src/js/patch-worker.js'],
|
||||
files: ['src/js/workers/patch-worker.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
self: 'readonly',
|
||||
|
||||
@@ -18,17 +18,17 @@
|
||||
* `state.showError()` when they need to cross module boundaries.
|
||||
*/
|
||||
|
||||
import { KoboDevice } 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 { KoboDevice } from './services/kobo-device.js';
|
||||
import { loadSoftwareUrls, getSoftwareUrl, getDevicesForVersion } from './services/kobo-software-urls.js';
|
||||
import { PatchUI, scanAvailablePatches } from './ui/patch-ui.js';
|
||||
import { KoboPatchRunner } from './services/patch-runner.js';
|
||||
import { NickelMenuInstaller, ALL_FEATURES } from '../nickelmenu/installer.js';
|
||||
import { TL } from './strings.js';
|
||||
import { isEnabled as analyticsEnabled, track } from './analytics.js';
|
||||
import { $, $q, populateSelect } from './dom.js';
|
||||
import { showStep, setNavLabels, setNavStep, hideNav, showNav, stepHistory, setupCardRadios } from './nav.js';
|
||||
import { initNickelMenu } from './nickelmenu-flow.js';
|
||||
import { initPatchesFlow } from './patches-flow.js';
|
||||
import { initNickelMenu } from './flows/nickelmenu-flow.js';
|
||||
import { initPatchesFlow } from './flows/patches-flow.js';
|
||||
|
||||
// =============================================================================
|
||||
// Shared state
|
||||
|
||||
@@ -44,6 +44,59 @@ export function populateSelect(selectEl, placeholder, items) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a list of checkbox items into a container.
|
||||
* @param {HTMLElement} container
|
||||
* @param {Array<{name: string, title: string, description: string, checked: boolean, disabled?: boolean}>} items
|
||||
*/
|
||||
export function renderNmCheckboxList(container, items) {
|
||||
container.innerHTML = '';
|
||||
for (const item of items) {
|
||||
const label = document.createElement('label');
|
||||
label.className = 'nm-config-item';
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'checkbox';
|
||||
input.name = item.name;
|
||||
input.checked = item.checked;
|
||||
if (item.disabled) input.disabled = true;
|
||||
|
||||
const textDiv = document.createElement('div');
|
||||
textDiv.className = 'nm-config-text';
|
||||
|
||||
const titleSpan = document.createElement('span');
|
||||
titleSpan.className = 'nm-config-title';
|
||||
titleSpan.textContent = item.title;
|
||||
|
||||
const descSpan = document.createElement('span');
|
||||
descSpan.className = 'nm-config-desc';
|
||||
descSpan.textContent = item.description;
|
||||
|
||||
textDiv.appendChild(titleSpan);
|
||||
textDiv.appendChild(descSpan);
|
||||
label.appendChild(input);
|
||||
label.appendChild(textDiv);
|
||||
container.appendChild(label);
|
||||
}
|
||||
}
|
||||
|
||||
/** Populate a <ul>/<ol> with text items, clearing existing content. */
|
||||
export function populateList(listEl, items) {
|
||||
listEl.innerHTML = '';
|
||||
for (const text of items) {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = text;
|
||||
listEl.appendChild(li);
|
||||
}
|
||||
}
|
||||
|
||||
/** Fetch with automatic error throwing on non-OK responses. */
|
||||
export async function fetchOrThrow(url, errorPrefix = 'Fetch failed') {
|
||||
const resp = await fetch(url);
|
||||
if (!resp.ok) throw new Error(`${errorPrefix}: HTTP ${resp.status}`);
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a browser download of in-memory data.
|
||||
* Creates a temporary object URL, clicks a hidden <a>, then revokes it.
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
* `resetNickelMenuState`.
|
||||
*/
|
||||
|
||||
import { $, $q, $qa, triggerDownload } from './dom.js';
|
||||
import { showStep, setNavStep } from './nav.js';
|
||||
import { ALL_FEATURES } from '../nickelmenu/installer.js';
|
||||
import { TL } from './strings.js';
|
||||
import { track } from './analytics.js';
|
||||
import { $, $q, $qa, triggerDownload, renderNmCheckboxList, populateList } from '../dom.js';
|
||||
import { showStep, setNavStep } from '../nav.js';
|
||||
import { ALL_FEATURES } from '../../nickelmenu/installer.js';
|
||||
import { TL } from '../strings.js';
|
||||
import { track } from '../analytics.js';
|
||||
|
||||
export function initNickelMenu(state) {
|
||||
|
||||
@@ -47,42 +47,16 @@ export function initNickelMenu(state) {
|
||||
// Required features are checked and disabled; others use their default.
|
||||
|
||||
function renderFeatureCheckboxes() {
|
||||
nmConfigOptions.innerHTML = '';
|
||||
for (const feature of ALL_FEATURES) {
|
||||
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');
|
||||
titleSpan.className = 'nm-config-title';
|
||||
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);
|
||||
}
|
||||
const items = ALL_FEATURES
|
||||
.filter(f => f.available !== false)
|
||||
.map(f => ({
|
||||
name: 'nm-cfg-' + f.id,
|
||||
title: f.title + (f.required ? ' (required)' : '') + (f.version ? ' ' + f.version : ''),
|
||||
description: f.description,
|
||||
checked: f.required || f.default,
|
||||
disabled: f.required,
|
||||
}));
|
||||
renderNmCheckboxList(nmConfigOptions, items);
|
||||
}
|
||||
|
||||
// --- Uninstall checkboxes ---
|
||||
@@ -90,35 +64,17 @@ export function initNickelMenu(state) {
|
||||
// (like KOReader) so the user can opt into cleaning those up too.
|
||||
|
||||
function renderUninstallCheckboxes() {
|
||||
nmUninstallOptions.innerHTML = '';
|
||||
if (detectedUninstallFeatures.length === 0) return;
|
||||
|
||||
for (const feature of detectedUninstallFeatures) {
|
||||
const label = document.createElement('label');
|
||||
label.className = 'nm-config-item';
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'checkbox';
|
||||
input.name = 'nm-uninstall-' + feature.id;
|
||||
input.checked = true;
|
||||
|
||||
const textDiv = document.createElement('div');
|
||||
textDiv.className = 'nm-config-text';
|
||||
|
||||
const titleSpan = document.createElement('span');
|
||||
titleSpan.className = 'nm-config-title';
|
||||
titleSpan.textContent = 'Also remove ' + feature.uninstall.title;
|
||||
|
||||
const descSpan = document.createElement('span');
|
||||
descSpan.className = 'nm-config-desc';
|
||||
descSpan.textContent = feature.uninstall.description;
|
||||
|
||||
textDiv.appendChild(titleSpan);
|
||||
textDiv.appendChild(descSpan);
|
||||
label.appendChild(input);
|
||||
label.appendChild(textDiv);
|
||||
nmUninstallOptions.appendChild(label);
|
||||
if (detectedUninstallFeatures.length === 0) {
|
||||
nmUninstallOptions.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
const items = detectedUninstallFeatures.map(f => ({
|
||||
name: 'nm-uninstall-' + f.id,
|
||||
title: 'Also remove ' + f.uninstall.title,
|
||||
description: f.uninstall.description,
|
||||
checked: true,
|
||||
}));
|
||||
renderNmCheckboxList(nmUninstallOptions, items);
|
||||
}
|
||||
|
||||
/** Clear removal state when returning to mode selection. */
|
||||
@@ -263,39 +219,24 @@ export function initNickelMenu(state) {
|
||||
function goToNmReview() {
|
||||
const summary = $('nm-review-summary');
|
||||
const list = $('nm-review-list');
|
||||
list.innerHTML = '';
|
||||
|
||||
if (state.nickelMenuOption === 'remove') {
|
||||
summary.textContent = TL.STATUS.NM_WILL_BE_REMOVED;
|
||||
const featuresToRemove = getSelectedUninstallFeatures();
|
||||
for (const feature of featuresToRemove) {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = feature.uninstall.title + ' will also be removed';
|
||||
list.appendChild(li);
|
||||
}
|
||||
populateList(list, featuresToRemove.map(f => f.uninstall.title + ' will also be removed'));
|
||||
btnNmWrite.hidden = state.manualMode;
|
||||
btnNmWrite.textContent = TL.BUTTON.REMOVE_FROM_KOBO;
|
||||
btnNmDownload.hidden = true;
|
||||
} else if (state.nickelMenuOption === 'nickelmenu-only') {
|
||||
summary.textContent = TL.STATUS.NM_WILL_BE_INSTALLED;
|
||||
const li = document.createElement('li');
|
||||
li.textContent = TL.STATUS.NM_NICKEL_ROOT_TGZ;
|
||||
list.appendChild(li);
|
||||
btnNmWrite.hidden = false;
|
||||
btnNmWrite.textContent = TL.BUTTON.WRITE_TO_KOBO;
|
||||
btnNmDownload.hidden = false;
|
||||
} else {
|
||||
// "preset" — list NickelMenu plus all selected features.
|
||||
// "nickelmenu-only" or "preset" — both install NickelMenu.
|
||||
summary.textContent = TL.STATUS.NM_WILL_BE_INSTALLED;
|
||||
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;
|
||||
list.appendChild(li);
|
||||
if (state.nickelMenuOption === 'preset') {
|
||||
for (const feature of getSelectedFeatures()) {
|
||||
items.push(feature.title);
|
||||
}
|
||||
}
|
||||
populateList(list, items);
|
||||
btnNmWrite.hidden = false;
|
||||
btnNmWrite.textContent = TL.BUTTON.WRITE_TO_KOBO;
|
||||
btnNmDownload.hidden = false;
|
||||
@@ -14,11 +14,11 @@
|
||||
* `updatePatchCount`, and `configureFirmwareStep`.
|
||||
*/
|
||||
|
||||
import { $, formatMB, triggerDownload } from './dom.js';
|
||||
import { showStep, setNavLabels, setNavStep } from './nav.js';
|
||||
import { KoboModels } from './kobo-device.js';
|
||||
import { TL } from './strings.js';
|
||||
import { track } from './analytics.js';
|
||||
import { $, formatMB, triggerDownload, populateList } from '../dom.js';
|
||||
import { showStep, setNavLabels, setNavStep } from '../nav.js';
|
||||
import { KoboModels } from '../services/kobo-device.js';
|
||||
import { TL } from '../strings.js';
|
||||
import { track } from '../analytics.js';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
export function initPatchesFlow(state) {
|
||||
@@ -99,13 +99,8 @@ export function initPatchesFlow(state) {
|
||||
|
||||
function populateSelectedPatchesList() {
|
||||
const patchList = $('selected-patches-list');
|
||||
patchList.innerHTML = '';
|
||||
const enabled = state.patchUI.getEnabledPatches();
|
||||
for (const name of enabled) {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = name;
|
||||
patchList.appendChild(li);
|
||||
}
|
||||
populateList(patchList, enabled);
|
||||
const hasPatches = enabled.length > 0;
|
||||
patchList.hidden = !hasPatches;
|
||||
$('selected-patches-heading').hidden = !hasPatches;
|
||||
@@ -1,4 +1,5 @@
|
||||
import { KoboModels } from './kobo-device.js';
|
||||
import { fetchOrThrow } from '../dom.js';
|
||||
|
||||
let _data = null;
|
||||
|
||||
@@ -8,8 +9,7 @@ let _data = null;
|
||||
*/
|
||||
async function loadSoftwareUrls() {
|
||||
if (_data) return _data;
|
||||
const resp = await fetch('/patches/downloads.json');
|
||||
if (!resp.ok) throw new Error('Failed to load download URLs');
|
||||
const resp = await fetchOrThrow('/patches/downloads.json', 'Failed to load download URLs');
|
||||
_data = await resp.json();
|
||||
window.FIRMWARE_DOWNLOADS = _data;
|
||||
return _data;
|
||||
@@ -17,7 +17,7 @@ class KoboPatchRunner {
|
||||
*/
|
||||
patchFirmware(configYAML, firmwareZip, patchFiles, onProgress) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker('js/patch-worker.js');
|
||||
const worker = new Worker('js/workers/patch-worker.js');
|
||||
this._worker = worker;
|
||||
|
||||
worker.onmessage = (e) => {
|
||||
@@ -1,5 +1,6 @@
|
||||
import JSZip from 'jszip';
|
||||
import { TL } from './strings.js';
|
||||
import { TL } from '../strings.js';
|
||||
import { fetchOrThrow } from '../dom.js';
|
||||
|
||||
/**
|
||||
* Friendly display names for patch files.
|
||||
@@ -208,10 +209,7 @@ class PatchUI {
|
||||
* Load patches from a URL pointing to a zip file.
|
||||
*/
|
||||
async loadFromURL(url) {
|
||||
const resp = await fetch(url);
|
||||
if (!resp.ok) {
|
||||
throw new Error('Failed to fetch patch zip: ' + resp.statusText);
|
||||
}
|
||||
const resp = await fetchOrThrow(url, 'Failed to fetch patch zip');
|
||||
const data = await resp.arrayBuffer();
|
||||
await this.loadFromZip(data);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ async function loadWasm() {
|
||||
|
||||
const go = new Go();
|
||||
const result = await WebAssembly.instantiateStreaming(
|
||||
fetch('../wasm/kobopatch.wasm'),
|
||||
fetch('../../wasm/kobopatch.wasm'),
|
||||
go.importObject
|
||||
);
|
||||
go.run(result.instance);
|
||||
23
web/src/nickelmenu/features/helpers.js
Normal file
23
web/src/nickelmenu/features/helpers.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Shared helpers for NickelMenu features that modify .adds/nm/items.
|
||||
*/
|
||||
|
||||
/** Create a postProcess function that appends a line to .adds/nm/items. */
|
||||
export function appendToNmConfig(line) {
|
||||
return function postProcess(files) {
|
||||
const items = files.find(f => f.path === '.adds/nm/items');
|
||||
if (!items || typeof items.data !== 'string') return files;
|
||||
items.data += '\n' + line + '\n';
|
||||
return files;
|
||||
};
|
||||
}
|
||||
|
||||
/** Create a postProcess function that prepends a line to .adds/nm/items. */
|
||||
export function prependToNmConfig(line) {
|
||||
return function postProcess(files) {
|
||||
const items = files.find(f => f.path === '.adds/nm/items');
|
||||
if (!items || typeof items.data !== 'string') return files;
|
||||
items.data = line + '\n\n' + items.data;
|
||||
return files;
|
||||
};
|
||||
}
|
||||
@@ -1,15 +1,10 @@
|
||||
import { appendToNmConfig } from '../helpers.js';
|
||||
|
||||
export default {
|
||||
id: 'hide-notices',
|
||||
title: 'Hide home screen notices',
|
||||
description: 'Hides the third row on the home screen that shows notices below your books, such as reading time, release notes for updates, and Kobo Plus or Store promotions.',
|
||||
default: false,
|
||||
|
||||
postProcess(files) {
|
||||
const items = files.find(f => f.path === '.adds/nm/items');
|
||||
if (!items || typeof items.data !== 'string') return files;
|
||||
|
||||
items.data += '\nexperimental:hide_home_row3_enabled:1\n';
|
||||
|
||||
return files;
|
||||
},
|
||||
postProcess: appendToNmConfig('experimental:hide_home_row3_enabled:1'),
|
||||
};
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import { appendToNmConfig } from '../helpers.js';
|
||||
|
||||
export default {
|
||||
id: 'hide-recommendations',
|
||||
title: 'Hide home screen recommendations',
|
||||
description: 'Hides the recommendations next to your current read on the home screen.',
|
||||
default: false,
|
||||
|
||||
postProcess(files) {
|
||||
const items = files.find(f => f.path === '.adds/nm/items');
|
||||
if (!items || typeof items.data !== 'string') return files;
|
||||
|
||||
items.data += '\nexperimental:hide_home_row1col2_enabled:1\n';
|
||||
|
||||
return files;
|
||||
},
|
||||
postProcess: appendToNmConfig('experimental:hide_home_row1col2_enabled:1'),
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import JSZip from 'jszip';
|
||||
import { prependToNmConfig } from '../helpers.js';
|
||||
|
||||
export default {
|
||||
id: 'koreader',
|
||||
@@ -43,12 +44,5 @@ export default {
|
||||
return files;
|
||||
},
|
||||
|
||||
postProcess(files) {
|
||||
const items = files.find(f => f.path === '.adds/nm/items');
|
||||
if (!items || typeof items.data !== 'string') return files;
|
||||
|
||||
items.data = 'menu_item:main:KOReader:cmd_spawn:quiet:exec /mnt/onboard/.adds/koreader/koreader.sh\n\n' + items.data;
|
||||
|
||||
return files;
|
||||
},
|
||||
postProcess: prependToNmConfig('menu_item:main:KOReader:cmd_spawn:quiet:exec /mnt/onboard/.adds/koreader/koreader.sh'),
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { prependToNmConfig } from '../helpers.js';
|
||||
|
||||
const TAB_CONFIG = [
|
||||
'experimental :menu_main_15505_0_enabled: 1',
|
||||
'experimental :menu_main_15505_1_label: Books',
|
||||
@@ -17,12 +19,5 @@ export default {
|
||||
description: 'Hides the "My Notebooks" and "Discover" tabs from the bottom navigation tab bar.',
|
||||
default: false,
|
||||
|
||||
postProcess(files) {
|
||||
const items = files.find(f => f.path === '.adds/nm/items');
|
||||
if (!items || typeof items.data !== 'string') return files;
|
||||
|
||||
items.data = TAB_CONFIG + '\n\n' + items.data;
|
||||
|
||||
return files;
|
||||
},
|
||||
postProcess: prependToNmConfig(TAB_CONFIG),
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import JSZip from 'jszip';
|
||||
import { fetchOrThrow } from '../js/dom.js';
|
||||
|
||||
import customMenu from './features/custom-menu/index.js';
|
||||
import readerlyFonts from './features/readerly-fonts/index.js';
|
||||
@@ -32,8 +33,7 @@ function createContext(feature, progressFn) {
|
||||
return {
|
||||
async asset(relativePath) {
|
||||
const url = basePath + relativePath;
|
||||
const resp = await fetch(url);
|
||||
if (!resp.ok) throw new Error(`Failed to load asset ${url}: HTTP ${resp.status}`);
|
||||
const resp = await fetchOrThrow(url, `Failed to load asset ${url}`);
|
||||
return new Uint8Array(await resp.arrayBuffer());
|
||||
},
|
||||
progress(msg) {
|
||||
@@ -53,8 +53,7 @@ export class NickelMenuInstaller {
|
||||
async loadNickelMenu(progressFn) {
|
||||
if (this.nickelMenuZip) return;
|
||||
progressFn('Downloading NickelMenu...');
|
||||
const resp = await fetch('nickelmenu/NickelMenu.zip');
|
||||
if (!resp.ok) throw new Error('Failed to download NickelMenu.zip: HTTP ' + resp.status);
|
||||
const resp = await fetchOrThrow('nickelmenu/NickelMenu.zip', 'Failed to download NickelMenu.zip');
|
||||
this.nickelMenuZip = await JSZip.loadAsync(await resp.arrayBuffer());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user