mirror of
https://github.com/lights0123/n-link.git
synced 2024-12-23 02:35:27 +00:00
Error messages
This commit is contained in:
parent
5d5b98dd59
commit
ac1a26a797
|
@ -13,6 +13,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'no-console': process.env.NODE_ENV === 'production' ? ['warn', { allow: ['warn', 'error'] }] : 'off',
|
'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',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="header border-b px-2 py-2 flex w-full">
|
<div class="header border-b px-2 py-2 flex w-full">
|
||||||
<button @click="$devices.enumerate()" class="flex-shrink-0 mr-2 focus:outline-none" :class="$devices.enumerating && 'cursor-not-allowed opacity-25'" :disabled="$devices.enumerating">
|
<button @click="$devices.enumerate()" class="flex-shrink-0 mr-2 focus:outline-none"
|
||||||
|
:class="$devices.enumerating && 'cursor-not-allowed opacity-25'" :disabled="$devices.enumerating">
|
||||||
<img src="~feather-icons/dist/icons/refresh-cw.svg" class="w-5"/>
|
<img src="~feather-icons/dist/icons/refresh-cw.svg" class="w-5"/>
|
||||||
|
<div v-if="scanHint && Object.keys($devices.devices).length === 0" class="p-4 refresh-popup">
|
||||||
|
Click to connect a device
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<el-popover width="239" :visible-arrow="false" popper-class="focus:outline-none dev-select-pop" v-model="active" class="w-full overflow-hidden">
|
<el-popover width="239" :visible-arrow="false" popper-class="focus:outline-none dev-select-pop" v-model="active"
|
||||||
|
class="w-full overflow-hidden">
|
||||||
<div slot="reference" class="relative w-full focus:outline-none">
|
<div slot="reference" class="relative w-full focus:outline-none">
|
||||||
<div
|
<div
|
||||||
class="block w-full bg-white border border-gray-400 hover:border-gray-500 px-4 py-3/2 pr-8 rounded shadow leading-tight focus:outline-none focus:shadow-outline h-8 truncate">
|
class="block w-full bg-white border border-gray-400 hover:border-gray-500 px-4 py-3/2 pr-8 rounded shadow leading-tight focus:outline-none focus:shadow-outline h-8 truncate">
|
||||||
|
@ -38,13 +43,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Component, PropSync, Vue} from 'vue-property-decorator';
|
import {Component, Prop, PropSync, Vue} from 'vue-property-decorator';
|
||||||
import ElPopover from 'element-ui/packages/popover/src/main.vue';
|
import ElPopover from 'element-ui/packages/popover/src/main.vue';
|
||||||
import 'element-ui/lib/theme-chalk/popover.css';
|
import 'element-ui/lib/theme-chalk/popover.css';
|
||||||
|
|
||||||
@Component({components: {ElPopover}})
|
@Component({components: {ElPopover}})
|
||||||
export default class DeviceSelect extends Vue {
|
export default class DeviceSelect extends Vue {
|
||||||
@PropSync('selected', {type: [String]}) selectedCalculator!: string | null;
|
@PropSync('selected', {type: [String]}) selectedCalculator!: string | null;
|
||||||
|
@Prop({type: Boolean, default: false}) scanHint!: boolean;
|
||||||
active = false;
|
active = false;
|
||||||
|
|
||||||
select(dev: string) {
|
select(dev: string) {
|
||||||
|
@ -66,6 +72,25 @@ export default class DeviceSelect extends Vue {
|
||||||
.header {
|
.header {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.refresh-popup {
|
||||||
|
$offset: 5px;
|
||||||
|
$size: 9px;
|
||||||
|
margin-left: -4.5px;
|
||||||
|
margin-top: 10px;
|
||||||
|
@apply absolute bg-blue-600 text-white;
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: $offset;
|
||||||
|
top: $size * -2;
|
||||||
|
border-top: $size solid transparent;
|
||||||
|
border-right: $size solid transparent;
|
||||||
|
border-bottom: $size solid theme('colors.blue.600');
|
||||||
|
|
||||||
|
border-left: $size solid transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
@ -46,6 +46,9 @@ module.exports = {
|
||||||
blockquote: 'var(--color-ui-blockquote)',
|
blockquote: 'var(--color-ui-blockquote)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
height: {
|
||||||
|
min: 'min-content',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
variants: {},
|
variants: {},
|
||||||
|
|
|
@ -16,5 +16,7 @@ module.exports = {
|
||||||
// add your custom rules here
|
// add your custom rules here
|
||||||
rules: {
|
rules: {
|
||||||
'import/no-webpack-loader-syntax': 0,
|
'import/no-webpack-loader-syntax': 0,
|
||||||
|
'require-await': 0,
|
||||||
|
'unicorn/number-literal-case': 0,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
82
web/components/ErrorMessage.vue
Normal file
82
web/components/ErrorMessage.vue
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
<template>
|
||||||
|
<client-only>
|
||||||
|
<vue-final-modal
|
||||||
|
v-model="isOpen"
|
||||||
|
classes="flex justify-center h-screen overflow-auto"
|
||||||
|
content-class="mt-10 mb-8 xl:mx-10 w-full h-min bg-white rounded p-4"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col bg-white rounded p-4">
|
||||||
|
<button v-if="isOpen" class="self-end" @click="isOpen = false">
|
||||||
|
<img src="~feather-icons/dist/icons/x.svg" class="w-5" />
|
||||||
|
</button>
|
||||||
|
<div v-if="category === 'udev'">
|
||||||
|
<h1 class="text-3xl">Permission Error</h1>
|
||||||
|
<p>
|
||||||
|
A permissions error was encountered when trying to access the
|
||||||
|
device.
|
||||||
|
<a
|
||||||
|
href="https://lights0123.com/n-link/#linux"
|
||||||
|
class="text-blue-600 underline"
|
||||||
|
>
|
||||||
|
Follow the Linux installation steps</a
|
||||||
|
>
|
||||||
|
to configure udev rules.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="category === 'reset'">
|
||||||
|
<h1 class="text-3xl">Resetting Error</h1>
|
||||||
|
<p>
|
||||||
|
Unfortunately, you've run into a known bug affecting calculators on
|
||||||
|
Windows and macOS. There's currently a bug in Chrome that prevents
|
||||||
|
this from working. Feel free to grab the
|
||||||
|
<a
|
||||||
|
href="https://lights0123.com/n-link/"
|
||||||
|
class="text-blue-600 underline"
|
||||||
|
>
|
||||||
|
desktop version</a
|
||||||
|
>
|
||||||
|
instead, though.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<h1 class="text-3xl">Unknown Error</h1>
|
||||||
|
<p>
|
||||||
|
An unknown error occurred with the message:
|
||||||
|
{{ error && error.message }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</vue-final-modal>
|
||||||
|
</client-only>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class ErrorMessage extends Vue {
|
||||||
|
isOpen = false;
|
||||||
|
category = '';
|
||||||
|
error: DOMException | null = null;
|
||||||
|
|
||||||
|
handleError(error: DOMException, phase: 'connection' | 'operation') {
|
||||||
|
if (
|
||||||
|
phase === 'connection' &&
|
||||||
|
error.name === 'SecurityError' &&
|
||||||
|
navigator.platform.includes('Linux')
|
||||||
|
) {
|
||||||
|
this.category = 'udev';
|
||||||
|
} else if (
|
||||||
|
phase === 'connection' &&
|
||||||
|
error.message === 'Unable to reset the device.' &&
|
||||||
|
['Win', 'Mac'].some((platform) => navigator.platform.includes(platform))
|
||||||
|
) {
|
||||||
|
this.category = 'reset';
|
||||||
|
} else {
|
||||||
|
this.category = '';
|
||||||
|
}
|
||||||
|
this.isOpen = true;
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,35 +0,0 @@
|
||||||
<template>
|
|
||||||
<svg
|
|
||||||
class="NuxtLogo"
|
|
||||||
width="245"
|
|
||||||
height="180"
|
|
||||||
viewBox="0 0 452 342"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M139 330l-1-2c-2-4-2-8-1-13H29L189 31l67 121 22-16-67-121c-1-2-9-14-22-14-6 0-15 2-22 15L5 303c-1 3-8 16-2 27 4 6 10 12 24 12h136c-14 0-21-6-24-12z"
|
|
||||||
fill="#00C58E"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M447 304L317 70c-2-2-9-15-22-15-6 0-15 3-22 15l-17 28v54l39-67 129 230h-49a23 23 0 0 1-2 14l-1 1c-6 11-21 12-23 12h76c3 0 17-1 24-12 3-5 5-14-2-26z"
|
|
||||||
fill="#108775"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M376 330v-1l1-2c1-4 2-8 1-12l-4-12-102-178-15-27h-1l-15 27-102 178-4 12a24 24 0 0 0 2 15c4 6 10 12 24 12h190c3 0 18-1 25-12zM256 152l93 163H163l93-163z"
|
|
||||||
fill="#2F495E"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.NuxtLogo {
|
|
||||||
animation: 1s appear;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes appear {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -3,18 +3,22 @@ import {RpcProvider} from 'worker-rpc';
|
||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
import UsbWorker from 'worker-loader!@/components/usb.worker.ts';
|
import UsbWorker from 'worker-loader!@/components/usb.worker.ts';
|
||||||
import UsbCompat from '@/components/impl';
|
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.
|
/// The USB vendor ID used by all Nspire calculators.
|
||||||
const VID = 0x0451;
|
const VID = 0x0451;
|
||||||
/// The USB vendor ID used by all non-CX and original CX calculators.
|
/// The USB vendor ID used by all non-CX and original CX calculators.
|
||||||
const PID = 0xe012;
|
const PID = 0xe012;
|
||||||
/// The USB vendor ID used by all CX II calculators.
|
/// The USB vendor ID used by all CX II calculators.
|
||||||
const PID_CX2 = 0xe022;
|
const PID_CX2 = 0xe022;
|
||||||
|
type Rpc = Pick<RpcProvider, 'rpc'>;
|
||||||
async function promisified(...a: any[]): Promise<any> {
|
type WorkerExt = Worker & { rpc: Rpc; compat: UsbCompat };
|
||||||
}
|
|
||||||
|
|
||||||
type WorkerExt = Worker & { rpc: RpcProvider };
|
|
||||||
export type Device = {
|
export type Device = {
|
||||||
device: USBDevice;
|
device: USBDevice;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -27,47 +31,44 @@ export type Device = {
|
||||||
running?: boolean;
|
running?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function downloadFile(
|
async function downloadFile(dev: Rpc, path: [string, number]) {
|
||||||
dev: RpcProvider,
|
|
||||||
path: [string, number]
|
|
||||||
) {
|
|
||||||
const data: Uint8Array = await dev.rpc('downloadFile', { path });
|
const data: Uint8Array = await dev.rpc('downloadFile', { path });
|
||||||
saveAs(new Blob([data]), path[0].split('/').pop());
|
saveAs(new Blob([data]), path[0].split('/').pop());
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadFile(dev: RpcProvider, path: string, data: Uint8Array) {
|
async function uploadFile(dev: Rpc, path: string, data: Uint8Array) {
|
||||||
await dev.rpc('uploadFile', { path, data });
|
await dev.rpc('uploadFile', { path, data });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadOs(dev: RpcProvider, data: Uint8Array) {
|
async function uploadOs(dev: Rpc, data: Uint8Array) {
|
||||||
await dev.rpc('uploadOs', { data });
|
await dev.rpc('uploadOs', { data });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteFile(dev: RpcProvider, path: string) {
|
async function deleteFile(dev: Rpc, path: string) {
|
||||||
await dev.rpc('deleteFile', { path });
|
await dev.rpc('deleteFile', { path });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteDir(dev: RpcProvider, path: string) {
|
async function deleteDir(dev: Rpc, path: string) {
|
||||||
await dev.rpc('deleteDir', { path });
|
await dev.rpc('deleteDir', { path });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createDir(dev: RpcProvider, path: string) {
|
async function createDir(dev: Rpc, path: string) {
|
||||||
await dev.rpc('createDir', { path });
|
await dev.rpc('createDir', { path });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function move(dev: RpcProvider, src: string, dest: string) {
|
async function move(dev: Rpc, src: string, dest: string) {
|
||||||
await dev.rpc('move', { src, dest });
|
await dev.rpc('move', { src, dest });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copy(dev: RpcProvider, src: string, dest: string) {
|
async function copy(dev: Rpc, src: string, dest: string) {
|
||||||
await dev.rpc('copy', { src, dest });
|
await dev.rpc('copy', { src, dest });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listDir(dev: RpcProvider, path: string) {
|
async function listDir(dev: Rpc, path: string) {
|
||||||
return (await dev.rpc('listDir', { path })) as FileInfo[];
|
return (await dev.rpc('listDir', { path })) as FileInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listAll(dev: RpcProvider, path: FileInfo): Promise<FileInfo[]> {
|
async function listAll(dev: Rpc, path: FileInfo): Promise<FileInfo[]> {
|
||||||
if (!path.isDir) return [path];
|
if (!path.isDir) return [path];
|
||||||
try {
|
try {
|
||||||
const contents = await listDir(dev, path.path);
|
const contents = await listDir(dev, path.path);
|
||||||
|
@ -93,8 +94,7 @@ class Devices extends Vue implements GenericDevices {
|
||||||
hasEnumerated = false;
|
hasEnumerated = false;
|
||||||
devices: Record<string, Device> = {};
|
devices: Record<string, Device> = {};
|
||||||
|
|
||||||
created() {
|
created() {}
|
||||||
}
|
|
||||||
|
|
||||||
async runQueue(dev: string) {
|
async runQueue(dev: string) {
|
||||||
const device = this.devices[dev];
|
const device = this.devices[dev];
|
||||||
|
@ -116,7 +116,11 @@ class Devices extends Vue implements GenericDevices {
|
||||||
await downloadFile(rpc, cmd.path);
|
await downloadFile(rpc, cmd.path);
|
||||||
} else if (cmd.action === 'upload') {
|
} else if (cmd.action === 'upload') {
|
||||||
if (!('file' in cmd)) return;
|
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') {
|
} else if (cmd.action === 'uploadOs') {
|
||||||
if (!('file' in cmd)) return;
|
if (!('file' in cmd)) return;
|
||||||
await uploadOs(rpc, new Uint8Array(await cmd.file.arrayBuffer()));
|
await uploadOs(rpc, new Uint8Array(await cmd.file.arrayBuffer()));
|
||||||
|
@ -194,7 +198,18 @@ class Devices extends Vue implements GenericDevices {
|
||||||
const rpc = new RpcProvider((message, transfer: any) =>
|
const rpc = new RpcProvider((message, transfer: any) =>
|
||||||
worker.postMessage(message, transfer)
|
worker.postMessage(message, transfer)
|
||||||
);
|
);
|
||||||
worker.rpc = rpc;
|
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 }) => {
|
worker.onmessage = ({ data }) => {
|
||||||
if ('usbCmd' in data) return compat.processCmd(data);
|
if ('usbCmd' in data) return compat.processCmd(data);
|
||||||
if ('total' in data) {
|
if ('total' in data) {
|
||||||
|
@ -203,9 +218,15 @@ class Devices extends Vue implements GenericDevices {
|
||||||
}
|
}
|
||||||
rpc.dispatch(data);
|
rpc.dispatch(data);
|
||||||
};
|
};
|
||||||
|
worker.compat = compat;
|
||||||
this.$set(this.devices[dev], 'worker', worker as WorkerExt);
|
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);
|
await this.update(dev);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,11 +256,11 @@ class Devices extends Vue implements GenericDevices {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async promptUploadFiles(dev: string, path: string) {
|
async promptUploadFiles(_dev: string, _path: string) {
|
||||||
throw new Error('Unimplemented');
|
throw new Error('Unimplemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadOs(dev: string, filter: string) {
|
async uploadOs(_dev: string, _filter: string) {
|
||||||
throw new Error('Unimplemented');
|
throw new Error('Unimplemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,14 +10,14 @@ export enum UsbError {
|
||||||
Unknown = 'Unknown',
|
Unknown = 'Unknown',
|
||||||
}
|
}
|
||||||
|
|
||||||
const exceptionMap = globalThis.DOMException
|
const exceptionMap: Record<string, string> = globalThis.DOMException
|
||||||
? {
|
? {
|
||||||
[DOMException.NOT_FOUND_ERR]: UsbError.NotFound,
|
NotFoundError: UsbError.NotFound,
|
||||||
[DOMException.SECURITY_ERR]: UsbError.Security,
|
SecurityError: UsbError.Security,
|
||||||
[DOMException.NETWORK_ERR]: UsbError.Network,
|
NetworkError: UsbError.Network,
|
||||||
[DOMException.ABORT_ERR]: UsbError.Abort,
|
AbortError: UsbError.Abort,
|
||||||
[DOMException.INVALID_STATE_ERR]: UsbError.InvalidState,
|
InvalidStateError: UsbError.InvalidState,
|
||||||
[DOMException.INVALID_ACCESS_ERR]: UsbError.InvalidAccess,
|
InvalidAccessError: UsbError.InvalidAccess,
|
||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
|
@ -85,6 +85,7 @@ let count = 0;
|
||||||
export default class UsbCompat {
|
export default class UsbCompat {
|
||||||
devices: Record<number, USBDevice> = {};
|
devices: Record<number, USBDevice> = {};
|
||||||
arr: SharedArrayBuffer;
|
arr: SharedArrayBuffer;
|
||||||
|
lastError?: DOMException;
|
||||||
|
|
||||||
constructor(arr: SharedArrayBuffer) {
|
constructor(arr: SharedArrayBuffer) {
|
||||||
this.arr = arr;
|
this.arr = arr;
|
||||||
|
@ -149,7 +150,10 @@ export default class UsbCompat {
|
||||||
return reply;
|
return reply;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
return { Err: exceptionMap[e.name] || UsbError.Unknown };
|
this.lastError = e;
|
||||||
|
return {
|
||||||
|
Err: { [exceptionMap[e.name as string] || UsbError.Unknown]: null },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,6 @@ export default {
|
||||||
|
|
||||||
// Build Configuration (https://go.nuxtjs.dev/config-build)
|
// Build Configuration (https://go.nuxtjs.dev/config-build)
|
||||||
build: {
|
build: {
|
||||||
transpile: ['n-link-core', 'element-ui'],
|
transpile: ['n-link-core', 'element-ui', 'vue-final-modal'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
"nuxt": "^2.14.5",
|
"nuxt": "^2.14.5",
|
||||||
"vue-async-computed": "^3.9.0",
|
"vue-async-computed": "^3.9.0",
|
||||||
"vue-property-decorator": "^9.0.2",
|
"vue-property-decorator": "^9.0.2",
|
||||||
"web-libnspire": "^0.1.4",
|
"web-libnspire": "^0.1.5",
|
||||||
"worker-rpc": "^0.2.0"
|
"worker-rpc": "^0.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -37,6 +37,7 @@
|
||||||
"eslint-plugin-nuxt": "^1.0.0",
|
"eslint-plugin-nuxt": "^1.0.0",
|
||||||
"eslint-plugin-prettier": "^3.1.4",
|
"eslint-plugin-prettier": "^3.1.4",
|
||||||
"prettier": "^2.1.1",
|
"prettier": "^2.1.1",
|
||||||
|
"vue-final-modal": "^2.1.0",
|
||||||
"worker-loader": "^3.0.3"
|
"worker-loader": "^3.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="home h-full overflow-hidden">
|
<div class="home h-full overflow-hidden">
|
||||||
|
<ErrorMessage ref="errorMessage" />
|
||||||
<div class="flex flex-row h-full">
|
<div class="flex flex-row h-full">
|
||||||
<div class="flex flex-col flex-shrink-0 border-r w-64">
|
<div class="flex flex-col flex-shrink-0 border-r w-64">
|
||||||
<device-select
|
<device-select
|
||||||
|
:scan-hint="webUSB"
|
||||||
:selected.sync="selectedCalculator"
|
:selected.sync="selectedCalculator"
|
||||||
:class="webUSB || 'opacity-50 pointer-events-none'"
|
:class="webUSB || 'opacity-50 pointer-events-none'"
|
||||||
/>
|
/>
|
||||||
|
@ -76,12 +78,14 @@
|
||||||
class="flex flex-col items-center justify-center h-full select-text"
|
class="flex flex-col items-center justify-center h-full select-text"
|
||||||
>
|
>
|
||||||
<p class="text-3xl">Your browser doesn't support WebUSB</p>
|
<p class="text-3xl">Your browser doesn't support WebUSB</p>
|
||||||
|
<p class="text-xl">
|
||||||
<a
|
<a
|
||||||
href="https://lights0123.com/n-link/"
|
href="https://lights0123.com/n-link/"
|
||||||
class="text-xl text-blue-600 underline"
|
class="text-blue-600 underline inline"
|
||||||
>
|
>
|
||||||
Check out the desktop version instead
|
Check out the desktop version instead</a
|
||||||
</a>
|
>, or switch to a Chrome-based browser
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -89,11 +93,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Watch } from 'vue-property-decorator';
|
import { Component, Vue, Watch, Ref } from 'vue-property-decorator';
|
||||||
import CalcInfo from 'n-link-core/components/CalcInfo.vue';
|
import CalcInfo from 'n-link-core/components/CalcInfo.vue';
|
||||||
import FileBrowser from 'n-link-core/components/FileBrowser.vue';
|
import FileBrowser from 'n-link-core/components/FileBrowser.vue';
|
||||||
import DeviceSelect from 'n-link-core/components/DeviceSelect.vue';
|
import DeviceSelect from 'n-link-core/components/DeviceSelect.vue';
|
||||||
import '@/components/devices';
|
import '@/components/devices';
|
||||||
|
import ErrorMessage from '@/components/ErrorMessage.vue';
|
||||||
|
|
||||||
function sleep(ms: number) {
|
function sleep(ms: number) {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
@ -101,6 +106,7 @@ function sleep(ms: number) {
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
|
ErrorMessage,
|
||||||
DeviceSelect,
|
DeviceSelect,
|
||||||
FileBrowser,
|
FileBrowser,
|
||||||
CalcInfo,
|
CalcInfo,
|
||||||
|
@ -110,6 +116,7 @@ export default class Home extends Vue {
|
||||||
selectedCalculator: string | null = null;
|
selectedCalculator: string | null = null;
|
||||||
showHidden = false;
|
showHidden = false;
|
||||||
webUSB = true;
|
webUSB = true;
|
||||||
|
@Ref() readonly errorMessage!: ErrorMessage;
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.webUSB = !!(navigator as any).usb;
|
this.webUSB = !!(navigator as any).usb;
|
||||||
|
@ -147,7 +154,8 @@ export default class Home extends Vue {
|
||||||
try {
|
try {
|
||||||
await this.$devices.open(dev);
|
await this.$devices.open(dev);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error({ e });
|
||||||
|
this.errorMessage.handleError(e, 'connection');
|
||||||
this.selectedCalculator = null;
|
this.selectedCalculator = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import AsyncComputed from 'vue-async-computed';
|
import AsyncComputed from 'vue-async-computed';
|
||||||
|
import VueFinalModal from 'vue-final-modal';
|
||||||
|
|
||||||
|
if (process.client) Vue.use(VueFinalModal());
|
||||||
Vue.use(AsyncComputed);
|
Vue.use(AsyncComputed);
|
||||||
|
|
6
web/types/vue-final-modal/index.d.ts
vendored
Normal file
6
web/types/vue-final-modal/index.d.ts
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
declare module 'vue-final-modal' {
|
||||||
|
import { PluginFunction } from 'vue';
|
||||||
|
|
||||||
|
const install: () => PluginFunction<undefined>;
|
||||||
|
export default install;
|
||||||
|
}
|
15
yarn.lock
15
yarn.lock
|
@ -13017,6 +13017,13 @@ vue-eslint-parser@^7.0.0:
|
||||||
esquery "^1.0.1"
|
esquery "^1.0.1"
|
||||||
lodash "^4.17.15"
|
lodash "^4.17.15"
|
||||||
|
|
||||||
|
vue-final-modal@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/vue-final-modal/-/vue-final-modal-2.1.0.tgz#6cb6820b37a4fb063ac3450d81f60c4153f07acf"
|
||||||
|
integrity sha512-QRZEVx6Omt2PVugIoz0k7URMJUB8bkoQ8dkROg1Ig5cXQTQaMiLonWnf/AaUHMmBh22HtG24LawwW9onv0FOZg==
|
||||||
|
dependencies:
|
||||||
|
vue "^2.6.12"
|
||||||
|
|
||||||
vue-hot-reload-api@^2.3.0:
|
vue-hot-reload-api@^2.3.0:
|
||||||
version "2.3.4"
|
version "2.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2"
|
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2"
|
||||||
|
@ -13155,10 +13162,10 @@ wcwidth@^1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
defaults "^1.0.3"
|
defaults "^1.0.3"
|
||||||
|
|
||||||
web-libnspire@^0.1.4:
|
web-libnspire@^0.1.5:
|
||||||
version "0.1.4"
|
version "0.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/web-libnspire/-/web-libnspire-0.1.4.tgz#3ceee8dc3640f33806269b7ee55f379cbe8c7f7a"
|
resolved "https://registry.yarnpkg.com/web-libnspire/-/web-libnspire-0.1.5.tgz#9e7c518f594b3b11b6cba826bf89c757779fd04d"
|
||||||
integrity sha512-N4e/G4HWWjz6cZDwxpKseU/tqZ2qY6+mz+9OPBmYgVQ7NEZ+4PPdFu4pl9aNd7vGo0DqbrEz87raVK84KgoLOA==
|
integrity sha512-njGq7G6bol3DElbPbshxwRKq9gQa9G5XN+Lw+jCfD9bdmmk+vhn8XkR9B/ko/xU7AgamvWfilVx2bXGrVv45ig==
|
||||||
|
|
||||||
webidl-conversions@^5.0.0:
|
webidl-conversions@^5.0.0:
|
||||||
version "5.0.0"
|
version "5.0.0"
|
||||||
|
|
Loading…
Reference in a new issue