mirror of
https://github.com/lights0123/n-link.git
synced 2024-12-22 10:15:27 +00:00
Error messages
This commit is contained in:
parent
5d5b98dd59
commit
ac1a26a797
|
@ -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',
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
<template>
|
||||
<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">
|
||||
<img src="~feather-icons/dist/icons/refresh-cw.svg" class="w-5"/>
|
||||
<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"/>
|
||||
<div v-if="scanHint && Object.keys($devices.devices).length === 0" class="p-4 refresh-popup">
|
||||
Click to connect a device
|
||||
</div>
|
||||
</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
|
||||
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>
|
||||
|
||||
<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 'element-ui/lib/theme-chalk/popover.css';
|
||||
|
||||
@Component({components: {ElPopover}})
|
||||
export default class DeviceSelect extends Vue {
|
||||
@PropSync('selected', {type: [String]}) selectedCalculator!: string | null;
|
||||
@Prop({type: Boolean, default: false}) scanHint!: boolean;
|
||||
active = false;
|
||||
|
||||
select(dev: string) {
|
||||
|
@ -66,6 +72,25 @@ export default class DeviceSelect extends Vue {
|
|||
.header {
|
||||
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 lang="scss">
|
||||
|
|
|
@ -46,6 +46,9 @@ module.exports = {
|
|||
blockquote: 'var(--color-ui-blockquote)',
|
||||
},
|
||||
},
|
||||
height: {
|
||||
min: 'min-content',
|
||||
},
|
||||
},
|
||||
},
|
||||
variants: {},
|
||||
|
|
|
@ -16,5 +16,7 @@ module.exports = {
|
|||
// add your custom rules here
|
||||
rules: {
|
||||
'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>
|
|
@ -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<any> {
|
||||
}
|
||||
|
||||
type WorkerExt = Worker & { rpc: RpcProvider };
|
||||
type Rpc = Pick<RpcProvider, 'rpc'>;
|
||||
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<FileInfo[]> {
|
||||
async function listAll(dev: Rpc, path: FileInfo): Promise<FileInfo[]> {
|
||||
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<string, Device> = {};
|
||||
|
||||
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<void> {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,14 +10,14 @@ export enum UsbError {
|
|||
Unknown = 'Unknown',
|
||||
}
|
||||
|
||||
const exceptionMap = globalThis.DOMException
|
||||
const exceptionMap: Record<string, string> = 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<number, USBDevice> = {};
|
||||
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 },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
<template>
|
||||
<div class="home h-full overflow-hidden">
|
||||
<ErrorMessage ref="errorMessage" />
|
||||
<div class="flex flex-row h-full">
|
||||
<div class="flex flex-col flex-shrink-0 border-r w-64">
|
||||
<device-select
|
||||
:scan-hint="webUSB"
|
||||
:selected.sync="selectedCalculator"
|
||||
:class="webUSB || 'opacity-50 pointer-events-none'"
|
||||
/>
|
||||
|
@ -76,12 +78,14 @@
|
|||
class="flex flex-col items-center justify-center h-full select-text"
|
||||
>
|
||||
<p class="text-3xl">Your browser doesn't support WebUSB</p>
|
||||
<a
|
||||
href="https://lights0123.com/n-link/"
|
||||
class="text-xl text-blue-600 underline"
|
||||
>
|
||||
Check out the desktop version instead
|
||||
</a>
|
||||
<p class="text-xl">
|
||||
<a
|
||||
href="https://lights0123.com/n-link/"
|
||||
class="text-blue-600 underline inline"
|
||||
>
|
||||
Check out the desktop version instead</a
|
||||
>, or switch to a Chrome-based browser
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -89,11 +93,12 @@
|
|||
</template>
|
||||
|
||||
<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 FileBrowser from 'n-link-core/components/FileBrowser.vue';
|
||||
import DeviceSelect from 'n-link-core/components/DeviceSelect.vue';
|
||||
import '@/components/devices';
|
||||
import ErrorMessage from '@/components/ErrorMessage.vue';
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
@ -101,6 +106,7 @@ function sleep(ms: number) {
|
|||
|
||||
@Component({
|
||||
components: {
|
||||
ErrorMessage,
|
||||
DeviceSelect,
|
||||
FileBrowser,
|
||||
CalcInfo,
|
||||
|
@ -110,6 +116,7 @@ export default class Home extends Vue {
|
|||
selectedCalculator: string | null = null;
|
||||
showHidden = false;
|
||||
webUSB = true;
|
||||
@Ref() readonly errorMessage!: ErrorMessage;
|
||||
|
||||
mounted() {
|
||||
this.webUSB = !!(navigator as any).usb;
|
||||
|
@ -147,7 +154,8 @@ export default class Home extends Vue {
|
|||
try {
|
||||
await this.$devices.open(dev);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.error({ e });
|
||||
this.errorMessage.handleError(e, 'connection');
|
||||
this.selectedCalculator = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import AsyncComputed from 'vue-async-computed';
|
||||
import VueFinalModal from 'vue-final-modal';
|
||||
|
||||
if (process.client) Vue.use(VueFinalModal());
|
||||
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"
|
||||
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:
|
||||
version "2.3.4"
|
||||
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:
|
||||
defaults "^1.0.3"
|
||||
|
||||
web-libnspire@^0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/web-libnspire/-/web-libnspire-0.1.4.tgz#3ceee8dc3640f33806269b7ee55f379cbe8c7f7a"
|
||||
integrity sha512-N4e/G4HWWjz6cZDwxpKseU/tqZ2qY6+mz+9OPBmYgVQ7NEZ+4PPdFu4pl9aNd7vGo0DqbrEz87raVK84KgoLOA==
|
||||
web-libnspire@^0.1.5:
|
||||
version "0.1.5"
|
||||
resolved "https://registry.yarnpkg.com/web-libnspire/-/web-libnspire-0.1.5.tgz#9e7c518f594b3b11b6cba826bf89c757779fd04d"
|
||||
integrity sha512-njGq7G6bol3DElbPbshxwRKq9gQa9G5XN+Lw+jCfD9bdmmk+vhn8XkR9B/ko/xU7AgamvWfilVx2bXGrVv45ig==
|
||||
|
||||
webidl-conversions@^5.0.0:
|
||||
version "5.0.0"
|
||||
|
|
Loading…
Reference in a new issue