Improve device and version detection
This commit is contained in:
@@ -110,16 +110,20 @@ function setupFirmwareSymlink() {
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {object} opts
|
||||
* @param {boolean} [opts.hasNickelMenu=false] - Whether .adds/nm/ exists on device
|
||||
* @param {string} [opts.firmware='4.45.23646'] - Firmware version to report
|
||||
* @param {string} [opts.serial='N4280A0000000'] - Serial number to report
|
||||
*/
|
||||
async function injectMockDevice(page, opts = {}) {
|
||||
await page.evaluate(({ hasNickelMenu }) => {
|
||||
const firmware = opts.firmware || '4.45.23646';
|
||||
const serial = opts.serial || 'N4280A0000000';
|
||||
await page.evaluate(({ hasNickelMenu, firmware, serial }) => {
|
||||
// In-memory filesystem for the mock device
|
||||
const filesystem = {
|
||||
'.kobo': {
|
||||
_type: 'dir',
|
||||
'version': {
|
||||
_type: 'file',
|
||||
content: 'N4280A0000000,4.9.77,4.45.23646,4.9.77,4.9.77,00000000-0000-0000-0000-000000000390',
|
||||
content: serial + ',4.9.77,' + firmware + ',4.9.77,4.9.77,00000000-0000-0000-0000-000000000390',
|
||||
},
|
||||
'Kobo': {
|
||||
_type: 'dir',
|
||||
@@ -205,7 +209,7 @@ async function injectMockDevice(page, opts = {}) {
|
||||
|
||||
// Override showDirectoryPicker
|
||||
window.showDirectoryPicker = async () => rootHandle;
|
||||
}, { hasNickelMenu: opts.hasNickelMenu || false });
|
||||
}, { hasNickelMenu: opts.hasNickelMenu || false, firmware: firmware, serial: serial });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -686,6 +690,59 @@ test.describe('Custom patches', () => {
|
||||
expect(actualHash, 'restored KoboRoot.tgz SHA1 mismatch').toBe(ORIGINAL_TGZ_SHA1);
|
||||
});
|
||||
|
||||
test('with device — incompatible version 5.x shows error', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await injectMockDevice(page, { firmware: '5.0.0' });
|
||||
await page.click('#btn-connect');
|
||||
|
||||
// Device info should be displayed
|
||||
await expect(page.locator('#step-device')).not.toBeHidden();
|
||||
await expect(page.locator('#device-model')).toHaveText('Kobo Libra Colour');
|
||||
await expect(page.locator('#device-firmware')).toHaveText('5.0.0');
|
||||
|
||||
// Status message should show incompatibility warning
|
||||
await expect(page.locator('#device-status')).toContainText('incompatible');
|
||||
await expect(page.locator('#device-status')).toContainText('NickelMenu does not support it');
|
||||
await expect(page.locator('#device-status')).toHaveClass(/error/);
|
||||
|
||||
// Continue and restore buttons should be hidden
|
||||
await expect(page.locator('#btn-device-next')).toBeHidden();
|
||||
await expect(page.locator('#btn-device-restore')).toBeHidden();
|
||||
});
|
||||
|
||||
test('with device — unknown model shows warning and requires checkbox', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await injectMockDevice(page, { serial: 'X9990A0000000' });
|
||||
await page.click('#btn-connect');
|
||||
|
||||
// Device info should be displayed with unknown model
|
||||
await expect(page.locator('#step-device')).not.toBeHidden();
|
||||
await expect(page.locator('#device-model')).toContainText('Unknown');
|
||||
await expect(page.locator('#device-firmware')).toHaveText('4.45.23646');
|
||||
|
||||
// Warning should be visible with GitHub link
|
||||
await expect(page.locator('#device-unknown-warning')).not.toBeHidden();
|
||||
await expect(page.locator('#device-unknown-warning')).toContainText('file an issue on GitHub');
|
||||
await expect(page.locator('#device-unknown-warning a')).toHaveAttribute('href', 'https://github.com/nicoverbruggen/kobopatch-webui/issues/new');
|
||||
|
||||
// Checkbox should be visible, Continue should be disabled
|
||||
await expect(page.locator('#device-unknown-ack')).not.toBeHidden();
|
||||
await expect(page.locator('#btn-device-next')).toBeVisible();
|
||||
await expect(page.locator('#btn-device-next')).toBeDisabled();
|
||||
|
||||
// Restore Software should be hidden (no firmware URL for unknown model)
|
||||
await expect(page.locator('#btn-device-restore')).toBeHidden();
|
||||
|
||||
// Checking the checkbox enables Continue
|
||||
await page.check('#device-unknown-checkbox');
|
||||
await expect(page.locator('#btn-device-next')).toBeEnabled();
|
||||
|
||||
// Custom patches should be disabled in mode selection (no firmware URL)
|
||||
await page.click('#btn-device-next');
|
||||
await expect(page.locator('#step-mode')).not.toBeHidden();
|
||||
await expect(page.locator('input[name="mode"][value="patches"]')).toBeDisabled();
|
||||
});
|
||||
|
||||
test('no device — both modes available in manual mode', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
|
||||
@@ -522,6 +522,22 @@ button.btn-success:hover {
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.device-unknown-ack {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.6rem;
|
||||
padding: 0.5rem 0;
|
||||
font-size: 0.83rem;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.device-unknown-ack input[type="checkbox"] {
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.15rem;
|
||||
accent-color: var(--primary);
|
||||
}
|
||||
|
||||
/* Status banners */
|
||||
.warning {
|
||||
background: var(--warning-bg);
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
[hidden] { display: none !important; }
|
||||
</style>
|
||||
<link rel="stylesheet" href="css/style.css?ts=1773771588">
|
||||
<link rel="stylesheet" href="css/style.css?ts=1773916731">
|
||||
<script src="js/jszip.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -122,6 +122,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<p id="device-status"></p>
|
||||
<p id="device-unknown-warning" class="warning" hidden>
|
||||
You seem to have a Kobo device that isn't currently being detected. While you can continue since a supported software version seems to be installed, please
|
||||
<a href="https://github.com/nicoverbruggen/kobopatch-webui/issues/new" target="_blank">file an issue on GitHub</a>
|
||||
so the developer can add this device to the list.
|
||||
</p>
|
||||
<label id="device-unknown-ack" class="device-unknown-ack" hidden>
|
||||
<input type="checkbox" id="device-unknown-checkbox">
|
||||
<span>I understand that this model is likely not officially supported by NickelMenu yet, but I wish to continue regardless, and I understand it may not work correctly.</span>
|
||||
</label>
|
||||
<div class="step-actions">
|
||||
<button id="btn-device-restore" class="secondary">Restore Software</button>
|
||||
<button id="btn-device-next" class="primary">Continue ›</button>
|
||||
@@ -434,10 +443,10 @@
|
||||
</dialog>
|
||||
|
||||
<!-- wasm_exec.js loaded by patch-worker.js inside the Web Worker -->
|
||||
<script src="js/kobo-device.js?ts=1773771588"></script>
|
||||
<script src="js/kobopatch.js?ts=1773771588"></script>
|
||||
<script src="js/patch-ui.js?ts=1773771588"></script>
|
||||
<script src="js/nickelmenu.js?ts=1773771588"></script>
|
||||
<script src="js/app.js?ts=1773771588"></script>
|
||||
<script src="js/kobo-device.js?ts=1773916731"></script>
|
||||
<script src="js/kobopatch.js?ts=1773916731"></script>
|
||||
<script src="js/patch-ui.js?ts=1773916731"></script>
|
||||
<script src="js/nickelmenu.js?ts=1773916731"></script>
|
||||
<script src="js/app.js?ts=1773916731"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -97,6 +97,9 @@
|
||||
const errorMessage = $('error-message');
|
||||
const errorLog = $('error-log');
|
||||
const deviceStatus = $('device-status');
|
||||
const deviceUnknownWarning = $('device-unknown-warning');
|
||||
const deviceUnknownAck = $('device-unknown-ack');
|
||||
const deviceUnknownCheckbox = $('device-unknown-checkbox');
|
||||
const patchContainer = $('patch-container');
|
||||
const buildStatus = $('build-status');
|
||||
const existingTgzWarning = $('existing-tgz-warning');
|
||||
@@ -264,37 +267,66 @@
|
||||
});
|
||||
|
||||
// Auto connect -> show device info
|
||||
function displayDeviceInfo(info) {
|
||||
$('device-model').textContent = info.model;
|
||||
const serialEl = $('device-serial');
|
||||
serialEl.textContent = '';
|
||||
const prefixLen = info.serialPrefix.length;
|
||||
const u = document.createElement('u');
|
||||
u.textContent = info.serial.slice(0, prefixLen);
|
||||
serialEl.appendChild(u);
|
||||
serialEl.appendChild(document.createTextNode(info.serial.slice(prefixLen)));
|
||||
$('device-firmware').textContent = info.firmware;
|
||||
}
|
||||
|
||||
btnConnect.addEventListener('click', async () => {
|
||||
try {
|
||||
const info = await device.connect();
|
||||
|
||||
$('device-model').textContent = info.model;
|
||||
const serialEl = $('device-serial');
|
||||
serialEl.textContent = '';
|
||||
const prefixLen = info.serialPrefix.length;
|
||||
const u = document.createElement('u');
|
||||
u.textContent = info.serial.slice(0, prefixLen);
|
||||
serialEl.appendChild(u);
|
||||
serialEl.appendChild(document.createTextNode(info.serial.slice(prefixLen)));
|
||||
$('device-firmware').textContent = info.firmware;
|
||||
displayDeviceInfo(info);
|
||||
|
||||
if (info.isIncompatible) {
|
||||
deviceStatus.textContent =
|
||||
'You seem to have an incompatible Kobo software version installed. ' +
|
||||
'NickelMenu does not support it, and the custom patches are incompatible with this version.';
|
||||
deviceStatus.classList.add('error');
|
||||
btnDeviceNext.hidden = true;
|
||||
btnDeviceRestore.hidden = true;
|
||||
showStep(stepDevice);
|
||||
return;
|
||||
}
|
||||
|
||||
selectedPrefix = info.serialPrefix;
|
||||
|
||||
await availablePatchesReady;
|
||||
const match = availablePatches.find(p => p.version === info.firmware);
|
||||
|
||||
configureFirmwareStep(info.firmware, info.serialPrefix);
|
||||
|
||||
if (match) {
|
||||
await patchUI.loadFromURL('patches/' + match.filename);
|
||||
patchUI.render(patchContainer);
|
||||
updatePatchCount();
|
||||
patchesLoaded = true;
|
||||
configureFirmwareStep(info.firmware, info.serialPrefix);
|
||||
btnDeviceRestore.hidden = false;
|
||||
} else {
|
||||
btnDeviceRestore.hidden = true;
|
||||
}
|
||||
|
||||
deviceStatus.textContent = 'Your device has been recognized. You can continue to the next step!';
|
||||
btnDeviceRestore.hidden = !patchesLoaded || !firmwareURL;
|
||||
|
||||
deviceStatus.classList.remove('error');
|
||||
const isUnknownModel = info.model.startsWith('Unknown');
|
||||
if (isUnknownModel) {
|
||||
deviceStatus.textContent = '';
|
||||
deviceUnknownWarning.hidden = false;
|
||||
deviceUnknownAck.hidden = false;
|
||||
deviceUnknownCheckbox.checked = false;
|
||||
btnDeviceNext.disabled = true;
|
||||
} else {
|
||||
deviceStatus.textContent = 'Your device has been recognized. You can continue to the next step!';
|
||||
deviceUnknownWarning.hidden = true;
|
||||
deviceUnknownAck.hidden = true;
|
||||
deviceUnknownCheckbox.checked = false;
|
||||
btnDeviceNext.disabled = false;
|
||||
}
|
||||
btnDeviceNext.hidden = false;
|
||||
showStep(stepDevice);
|
||||
} catch (err) {
|
||||
@@ -308,6 +340,10 @@
|
||||
goToModeSelection();
|
||||
});
|
||||
|
||||
deviceUnknownCheckbox.addEventListener('change', () => {
|
||||
btnDeviceNext.disabled = !deviceUnknownCheckbox.checked;
|
||||
});
|
||||
|
||||
btnDeviceRestore.addEventListener('click', () => {
|
||||
if (!patchesLoaded) return;
|
||||
selectedMode = 'patches';
|
||||
@@ -329,10 +365,10 @@
|
||||
|
||||
// --- Step 2: Mode selection ---
|
||||
function goToModeSelection() {
|
||||
// In auto mode, disable custom patches if firmware isn't supported
|
||||
// In auto mode, disable custom patches if firmware or download URL isn't available
|
||||
const patchesRadio = $q('input[value="patches"]', stepMode);
|
||||
const patchesCard = patchesRadio.closest('.mode-card');
|
||||
const autoModeNoPatchesAvailable = !manualMode && !patchesLoaded;
|
||||
const autoModeNoPatchesAvailable = !manualMode && (!patchesLoaded || !firmwareURL);
|
||||
|
||||
const patchesHint = $('mode-patches-hint');
|
||||
if (autoModeNoPatchesAvailable) {
|
||||
|
||||
@@ -154,6 +154,7 @@ class KoboDevice {
|
||||
: serial.substring(0, 3);
|
||||
const model = KOBO_MODELS[serialPrefix] || 'Unknown Kobo (' + serial.substring(0, 4) + ')';
|
||||
const isSupported = SUPPORTED_FIRMWARE.includes(firmware);
|
||||
const isIncompatible = firmware.startsWith('5.');
|
||||
|
||||
return {
|
||||
serial,
|
||||
@@ -162,6 +163,7 @@ class KoboDevice {
|
||||
hardwareId,
|
||||
model,
|
||||
isSupported,
|
||||
isIncompatible,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ async function loadWasm() {
|
||||
|
||||
const go = new Go();
|
||||
const result = await WebAssembly.instantiateStreaming(
|
||||
fetch('../wasm/kobopatch.wasm?ts=1773771588'),
|
||||
fetch('../wasm/kobopatch.wasm?ts=1773916731'),
|
||||
go.importObject
|
||||
);
|
||||
go.run(result.instance);
|
||||
|
||||
Reference in New Issue
Block a user