diff --git a/n-link-core/.eslintrc.js b/n-link-core/.eslintrc.js index e77ac92..a880a71 100644 --- a/n-link-core/.eslintrc.js +++ b/n-link-core/.eslintrc.js @@ -13,6 +13,7 @@ module.exports = { }, rules: { 'no-console': process.env.NODE_ENV === 'production' ? ['warn', { allow: ['warn', 'error'] }] : 'off', - 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' + 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', } } diff --git a/n-link-core/components/DeviceSelect.vue b/n-link-core/components/DeviceSelect.vue index 9d2a0b9..a4d5fe9 100644 --- a/n-link-core/components/DeviceSelect.vue +++ b/n-link-core/components/DeviceSelect.vue @@ -1,9 +1,14 @@ diff --git a/web/components/Logo.vue b/web/components/Logo.vue deleted file mode 100644 index bf7c01a..0000000 --- a/web/components/Logo.vue +++ /dev/null @@ -1,35 +0,0 @@ - - - diff --git a/web/components/devices.ts b/web/components/devices.ts index 38a7b12..0f38c16 100644 --- a/web/components/devices.ts +++ b/web/components/devices.ts @@ -1,20 +1,24 @@ -import {Component, Vue} from 'vue-property-decorator'; -import {RpcProvider} from 'worker-rpc'; -import {saveAs} from 'file-saver'; +import { Component, Vue } from 'vue-property-decorator'; +import { RpcProvider } from 'worker-rpc'; +import { saveAs } from 'file-saver'; import UsbWorker from 'worker-loader!@/components/usb.worker.ts'; import UsbCompat from '@/components/impl'; -import {Cmd, FileInfo, GenericDevices, Info, PartialCmd, Progress,} from 'n-link-core/components/devices'; +import { + Cmd, + FileInfo, + GenericDevices, + Info, + PartialCmd, + Progress, +} from 'n-link-core/components/devices'; /// The USB vendor ID used by all Nspire calculators. const VID = 0x0451; /// The USB vendor ID used by all non-CX and original CX calculators. const PID = 0xe012; /// The USB vendor ID used by all CX II calculators. const PID_CX2 = 0xe022; - -async function promisified(...a: any[]): Promise { -} - -type WorkerExt = Worker & { rpc: RpcProvider }; +type Rpc = Pick; +type WorkerExt = Worker & { rpc: Rpc; compat: UsbCompat }; export type Device = { device: USBDevice; name: string; @@ -27,54 +31,51 @@ export type Device = { running?: boolean; }; -async function downloadFile( - dev: RpcProvider, - path: [string, number] -) { - const data: Uint8Array = await dev.rpc('downloadFile', {path}); +async function downloadFile(dev: Rpc, path: [string, number]) { + const data: Uint8Array = await dev.rpc('downloadFile', { path }); saveAs(new Blob([data]), path[0].split('/').pop()); } -async function uploadFile(dev: RpcProvider, path: string, data: Uint8Array) { - await dev.rpc('uploadFile', {path, data}); +async function uploadFile(dev: Rpc, path: string, data: Uint8Array) { + await dev.rpc('uploadFile', { path, data }); } -async function uploadOs(dev: RpcProvider, data: Uint8Array) { - await dev.rpc('uploadOs', {data}); +async function uploadOs(dev: Rpc, data: Uint8Array) { + await dev.rpc('uploadOs', { data }); } -async function deleteFile(dev: RpcProvider, path: string) { - await dev.rpc('deleteFile', {path}); +async function deleteFile(dev: Rpc, path: string) { + await dev.rpc('deleteFile', { path }); } -async function deleteDir(dev: RpcProvider, path: string) { - await dev.rpc('deleteDir', {path}); +async function deleteDir(dev: Rpc, path: string) { + await dev.rpc('deleteDir', { path }); } -async function createDir(dev: RpcProvider, path: string) { - await dev.rpc('createDir', {path}); +async function createDir(dev: Rpc, path: string) { + await dev.rpc('createDir', { path }); } -async function move(dev: RpcProvider, src: string, dest: string) { - await dev.rpc('move', {src, dest}); +async function move(dev: Rpc, src: string, dest: string) { + await dev.rpc('move', { src, dest }); } -async function copy(dev: RpcProvider, src: string, dest: string) { - await dev.rpc('copy', {src, dest}); +async function copy(dev: Rpc, src: string, dest: string) { + await dev.rpc('copy', { src, dest }); } -async function listDir(dev: RpcProvider, path: string) { - return (await dev.rpc('listDir', {path})) as FileInfo[]; +async function listDir(dev: Rpc, path: string) { + return (await dev.rpc('listDir', { path })) as FileInfo[]; } -async function listAll(dev: RpcProvider, path: FileInfo): Promise { +async function listAll(dev: Rpc, path: FileInfo): Promise { if (!path.isDir) return [path]; try { const contents = await listDir(dev, path.path); const parts: FileInfo[] = []; for (const file of contents) { parts.push( - ...(await listAll(dev, {...file, path: `${path.path}/${file.path}`})) + ...(await listAll(dev, { ...file, path: `${path.path}/${file.path}` })) ); } parts.push(path); @@ -93,13 +94,12 @@ class Devices extends Vue implements GenericDevices { hasEnumerated = false; devices: Record = {}; - created() { - } + created() {} async runQueue(dev: string) { const device = this.devices[dev]; if (!device?.queue || !device.worker || device.running) return; - const {rpc} = device.worker; + const { rpc } = device.worker; this.$set(device, 'running', true); // eslint-disable-next-line no-constant-condition while (true) { @@ -116,7 +116,11 @@ class Devices extends Vue implements GenericDevices { await downloadFile(rpc, cmd.path); } else if (cmd.action === 'upload') { if (!('file' in cmd)) return; - await uploadFile(rpc, `${cmd.path}/${cmd.file.name}`, new Uint8Array(await cmd.file.arrayBuffer())); + await uploadFile( + rpc, + `${cmd.path}/${cmd.file.name}`, + new Uint8Array(await cmd.file.arrayBuffer()) + ); } else if (cmd.action === 'uploadOs') { if (!('file' in cmd)) return; await uploadOs(rpc, new Uint8Array(await cmd.file.arrayBuffer())); @@ -147,7 +151,7 @@ class Devices extends Vue implements GenericDevices { this.$set(device, 'queue', []); } device.queue?.push( - ...cmds.map((cmd) => ({...cmd, id: queueId++} as Cmd)) + ...cmds.map((cmd) => ({ ...cmd, id: queueId++ } as Cmd)) ); this.runQueue(dev); } @@ -157,7 +161,7 @@ class Devices extends Vue implements GenericDevices { if (!navigator.usb) return; const device = await navigator.usb.requestDevice({ filters: [ - {vendorId: VID, productId: PID}, + { vendorId: VID, productId: PID }, { vendorId: VID, productId: PID_CX2, @@ -166,9 +170,9 @@ class Devices extends Vue implements GenericDevices { }); navigator.usb.ondisconnect = (e) => { const [key] = - Object.entries(this.devices).find( - ([_, {device}]) => device === e.device - ) || []; + Object.entries(this.devices).find( + ([_, { device }]) => device === e.device + ) || []; if (key) { this.$delete(this.devices, key); } @@ -194,18 +198,35 @@ class Devices extends Vue implements GenericDevices { const rpc = new RpcProvider((message, transfer: any) => worker.postMessage(message, transfer) ); - worker.rpc = rpc; - worker.onmessage = ({data}) => { + worker.rpc = { + async rpc(id, payload, transfer) { + compat.lastError = undefined; + try { + return await rpc.rpc(id, payload, transfer); + } catch (e) { + console.log(e, compat.lastError); + if (compat.lastError) throw compat.lastError; + throw new DOMException(e.toString()); + } + }, + }; + worker.onmessage = ({ data }) => { if ('usbCmd' in data) return compat.processCmd(data); - if('total' in data) { + if ('total' in data) { this.$set(this.devices[dev], 'progress', data); return; } rpc.dispatch(data); }; + worker.compat = compat; this.$set(this.devices[dev], 'worker', worker as WorkerExt); - await rpc.rpc('init', {id, sab, vid: device.vendorId, pid: device.productId}); + await worker.rpc.rpc('init', { + id, + sab, + vid: device.vendorId, + pid: device.productId, + }); await this.update(dev); } @@ -231,25 +252,25 @@ class Devices extends Vue implements GenericDevices { async uploadFiles(dev: string, path: string, files: File[]) { for (const file of files) { - this.addToQueue(dev, {action: 'upload', path, file}); + this.addToQueue(dev, { action: 'upload', path, file }); } } - async promptUploadFiles(dev: string, path: string) { + async promptUploadFiles(_dev: string, _path: string) { throw new Error('Unimplemented'); } - async uploadOs(dev: string, filter: string) { + async uploadOs(_dev: string, _filter: string) { throw new Error('Unimplemented'); } async uploadOsFile(dev: string, file: File): Promise { - this.addToQueue(dev, {action: 'uploadOs', file}); + this.addToQueue(dev, { action: 'uploadOs', file }); } async downloadFiles(dev: string, files: [string, number][]) { for (const path of files) { - this.addToQueue(dev, {action: 'download', path}); + this.addToQueue(dev, { action: 'download', path }); } } @@ -269,15 +290,15 @@ class Devices extends Vue implements GenericDevices { } async createDir(dev: string, path: string) { - this.addToQueue(dev, {action: 'createDir', path}); + this.addToQueue(dev, { action: 'createDir', path }); } async copy(dev: string, src: string, dest: string) { - this.addToQueue(dev, {action: 'copy', src, dest}); + this.addToQueue(dev, { action: 'copy', src, dest }); } async move(dev: string, src: string, dest: string) { - this.addToQueue(dev, {action: 'move', src, dest}); + this.addToQueue(dev, { action: 'move', src, dest }); } } diff --git a/web/components/impl.ts b/web/components/impl.ts index 44c1197..a8fef80 100644 --- a/web/components/impl.ts +++ b/web/components/impl.ts @@ -10,14 +10,14 @@ export enum UsbError { Unknown = 'Unknown', } -const exceptionMap = globalThis.DOMException +const exceptionMap: Record = globalThis.DOMException ? { - [DOMException.NOT_FOUND_ERR]: UsbError.NotFound, - [DOMException.SECURITY_ERR]: UsbError.Security, - [DOMException.NETWORK_ERR]: UsbError.Network, - [DOMException.ABORT_ERR]: UsbError.Abort, - [DOMException.INVALID_STATE_ERR]: UsbError.InvalidState, - [DOMException.INVALID_ACCESS_ERR]: UsbError.InvalidAccess, + NotFoundError: UsbError.NotFound, + SecurityError: UsbError.Security, + NetworkError: UsbError.Network, + AbortError: UsbError.Abort, + InvalidStateError: UsbError.InvalidState, + InvalidAccessError: UsbError.InvalidAccess, } : {}; @@ -85,6 +85,7 @@ let count = 0; export default class UsbCompat { devices: Record = {}; arr: SharedArrayBuffer; + lastError?: DOMException; constructor(arr: SharedArrayBuffer) { this.arr = arr; @@ -149,7 +150,10 @@ export default class UsbCompat { return reply; } catch (e) { console.error(e); - return { Err: exceptionMap[e.name] || UsbError.Unknown }; + this.lastError = e; + return { + Err: { [exceptionMap[e.name as string] || UsbError.Unknown]: null }, + }; } } diff --git a/web/nuxt.config.js b/web/nuxt.config.js index 29efb45..1a80787 100644 --- a/web/nuxt.config.js +++ b/web/nuxt.config.js @@ -40,6 +40,6 @@ export default { // Build Configuration (https://go.nuxtjs.dev/config-build) build: { - transpile: ['n-link-core', 'element-ui'], + transpile: ['n-link-core', 'element-ui', 'vue-final-modal'], }, }; diff --git a/web/package.json b/web/package.json index 9d10a9b..23e89c8 100644 --- a/web/package.json +++ b/web/package.json @@ -19,7 +19,7 @@ "nuxt": "^2.14.5", "vue-async-computed": "^3.9.0", "vue-property-decorator": "^9.0.2", - "web-libnspire": "^0.1.4", + "web-libnspire": "^0.1.5", "worker-rpc": "^0.2.0" }, "devDependencies": { @@ -37,6 +37,7 @@ "eslint-plugin-nuxt": "^1.0.0", "eslint-plugin-prettier": "^3.1.4", "prettier": "^2.1.1", + "vue-final-modal": "^2.1.0", "worker-loader": "^3.0.3" } } diff --git a/web/pages/index.vue b/web/pages/index.vue index 509e411..2966507 100644 --- a/web/pages/index.vue +++ b/web/pages/index.vue @@ -1,8 +1,10 @@