Improve device and version detection
This commit is contained in:
@@ -110,16 +110,20 @@ function setupFirmwareSymlink() {
|
|||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
* @param {object} opts
|
* @param {object} opts
|
||||||
* @param {boolean} [opts.hasNickelMenu=false] - Whether .adds/nm/ exists on device
|
* @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 = {}) {
|
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
|
// In-memory filesystem for the mock device
|
||||||
const filesystem = {
|
const filesystem = {
|
||||||
'.kobo': {
|
'.kobo': {
|
||||||
_type: 'dir',
|
_type: 'dir',
|
||||||
'version': {
|
'version': {
|
||||||
_type: 'file',
|
_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': {
|
'Kobo': {
|
||||||
_type: 'dir',
|
_type: 'dir',
|
||||||
@@ -205,7 +209,7 @@ async function injectMockDevice(page, opts = {}) {
|
|||||||
|
|
||||||
// Override showDirectoryPicker
|
// Override showDirectoryPicker
|
||||||
window.showDirectoryPicker = async () => rootHandle;
|
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);
|
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 }) => {
|
test('no device — both modes available in manual mode', async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
|
|
||||||
|
|||||||
@@ -522,6 +522,22 @@ button.btn-success:hover {
|
|||||||
font-size: 0.88rem;
|
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 */
|
/* Status banners */
|
||||||
.warning {
|
.warning {
|
||||||
background: var(--warning-bg);
|
background: var(--warning-bg);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
@keyframes spin { to { transform: rotate(360deg); } }
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
[hidden] { display: none !important; }
|
[hidden] { display: none !important; }
|
||||||
</style>
|
</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>
|
<script src="js/jszip.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -122,6 +122,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p id="device-status"></p>
|
<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">
|
<div class="step-actions">
|
||||||
<button id="btn-device-restore" class="secondary">Restore Software</button>
|
<button id="btn-device-restore" class="secondary">Restore Software</button>
|
||||||
<button id="btn-device-next" class="primary">Continue ›</button>
|
<button id="btn-device-next" class="primary">Continue ›</button>
|
||||||
@@ -434,10 +443,10 @@
|
|||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<!-- wasm_exec.js loaded by patch-worker.js inside the Web Worker -->
|
<!-- wasm_exec.js loaded by patch-worker.js inside the Web Worker -->
|
||||||
<script src="js/kobo-device.js?ts=1773771588"></script>
|
<script src="js/kobo-device.js?ts=1773916731"></script>
|
||||||
<script src="js/kobopatch.js?ts=1773771588"></script>
|
<script src="js/kobopatch.js?ts=1773916731"></script>
|
||||||
<script src="js/patch-ui.js?ts=1773771588"></script>
|
<script src="js/patch-ui.js?ts=1773916731"></script>
|
||||||
<script src="js/nickelmenu.js?ts=1773771588"></script>
|
<script src="js/nickelmenu.js?ts=1773916731"></script>
|
||||||
<script src="js/app.js?ts=1773771588"></script>
|
<script src="js/app.js?ts=1773916731"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -97,6 +97,9 @@
|
|||||||
const errorMessage = $('error-message');
|
const errorMessage = $('error-message');
|
||||||
const errorLog = $('error-log');
|
const errorLog = $('error-log');
|
||||||
const deviceStatus = $('device-status');
|
const deviceStatus = $('device-status');
|
||||||
|
const deviceUnknownWarning = $('device-unknown-warning');
|
||||||
|
const deviceUnknownAck = $('device-unknown-ack');
|
||||||
|
const deviceUnknownCheckbox = $('device-unknown-checkbox');
|
||||||
const patchContainer = $('patch-container');
|
const patchContainer = $('patch-container');
|
||||||
const buildStatus = $('build-status');
|
const buildStatus = $('build-status');
|
||||||
const existingTgzWarning = $('existing-tgz-warning');
|
const existingTgzWarning = $('existing-tgz-warning');
|
||||||
@@ -264,37 +267,66 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Auto connect -> show device info
|
// 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 () => {
|
btnConnect.addEventListener('click', async () => {
|
||||||
try {
|
try {
|
||||||
const info = await device.connect();
|
const info = await device.connect();
|
||||||
|
|
||||||
$('device-model').textContent = info.model;
|
displayDeviceInfo(info);
|
||||||
const serialEl = $('device-serial');
|
|
||||||
serialEl.textContent = '';
|
if (info.isIncompatible) {
|
||||||
const prefixLen = info.serialPrefix.length;
|
deviceStatus.textContent =
|
||||||
const u = document.createElement('u');
|
'You seem to have an incompatible Kobo software version installed. ' +
|
||||||
u.textContent = info.serial.slice(0, prefixLen);
|
'NickelMenu does not support it, and the custom patches are incompatible with this version.';
|
||||||
serialEl.appendChild(u);
|
deviceStatus.classList.add('error');
|
||||||
serialEl.appendChild(document.createTextNode(info.serial.slice(prefixLen)));
|
btnDeviceNext.hidden = true;
|
||||||
$('device-firmware').textContent = info.firmware;
|
btnDeviceRestore.hidden = true;
|
||||||
|
showStep(stepDevice);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
selectedPrefix = info.serialPrefix;
|
selectedPrefix = info.serialPrefix;
|
||||||
|
|
||||||
await availablePatchesReady;
|
await availablePatchesReady;
|
||||||
const match = availablePatches.find(p => p.version === info.firmware);
|
const match = availablePatches.find(p => p.version === info.firmware);
|
||||||
|
|
||||||
|
configureFirmwareStep(info.firmware, info.serialPrefix);
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
await patchUI.loadFromURL('patches/' + match.filename);
|
await patchUI.loadFromURL('patches/' + match.filename);
|
||||||
patchUI.render(patchContainer);
|
patchUI.render(patchContainer);
|
||||||
updatePatchCount();
|
updatePatchCount();
|
||||||
patchesLoaded = true;
|
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;
|
btnDeviceNext.hidden = false;
|
||||||
showStep(stepDevice);
|
showStep(stepDevice);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -308,6 +340,10 @@
|
|||||||
goToModeSelection();
|
goToModeSelection();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
deviceUnknownCheckbox.addEventListener('change', () => {
|
||||||
|
btnDeviceNext.disabled = !deviceUnknownCheckbox.checked;
|
||||||
|
});
|
||||||
|
|
||||||
btnDeviceRestore.addEventListener('click', () => {
|
btnDeviceRestore.addEventListener('click', () => {
|
||||||
if (!patchesLoaded) return;
|
if (!patchesLoaded) return;
|
||||||
selectedMode = 'patches';
|
selectedMode = 'patches';
|
||||||
@@ -329,10 +365,10 @@
|
|||||||
|
|
||||||
// --- Step 2: Mode selection ---
|
// --- Step 2: Mode selection ---
|
||||||
function goToModeSelection() {
|
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 patchesRadio = $q('input[value="patches"]', stepMode);
|
||||||
const patchesCard = patchesRadio.closest('.mode-card');
|
const patchesCard = patchesRadio.closest('.mode-card');
|
||||||
const autoModeNoPatchesAvailable = !manualMode && !patchesLoaded;
|
const autoModeNoPatchesAvailable = !manualMode && (!patchesLoaded || !firmwareURL);
|
||||||
|
|
||||||
const patchesHint = $('mode-patches-hint');
|
const patchesHint = $('mode-patches-hint');
|
||||||
if (autoModeNoPatchesAvailable) {
|
if (autoModeNoPatchesAvailable) {
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ class KoboDevice {
|
|||||||
: serial.substring(0, 3);
|
: serial.substring(0, 3);
|
||||||
const model = KOBO_MODELS[serialPrefix] || 'Unknown Kobo (' + serial.substring(0, 4) + ')';
|
const model = KOBO_MODELS[serialPrefix] || 'Unknown Kobo (' + serial.substring(0, 4) + ')';
|
||||||
const isSupported = SUPPORTED_FIRMWARE.includes(firmware);
|
const isSupported = SUPPORTED_FIRMWARE.includes(firmware);
|
||||||
|
const isIncompatible = firmware.startsWith('5.');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
serial,
|
serial,
|
||||||
@@ -162,6 +163,7 @@ class KoboDevice {
|
|||||||
hardwareId,
|
hardwareId,
|
||||||
model,
|
model,
|
||||||
isSupported,
|
isSupported,
|
||||||
|
isIncompatible,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ async function loadWasm() {
|
|||||||
|
|
||||||
const go = new Go();
|
const go = new Go();
|
||||||
const result = await WebAssembly.instantiateStreaming(
|
const result = await WebAssembly.instantiateStreaming(
|
||||||
fetch('../wasm/kobopatch.wasm?ts=1773771588'),
|
fetch('../wasm/kobopatch.wasm?ts=1773916731'),
|
||||||
go.importObject
|
go.importObject
|
||||||
);
|
);
|
||||||
go.run(result.instance);
|
go.run(result.instance);
|
||||||
|
|||||||
Reference in New Issue
Block a user