Actually add web workspace

This commit is contained in:
lights0123 2020-10-06 11:01:36 -04:00
parent 10756ff9e8
commit 9868fa3634
No known key found for this signature in database
GPG key ID: 28F315322E37972F
19 changed files with 1022 additions and 1 deletions

View file

@ -93,4 +93,4 @@ jobs:
- name: Build
working-directory: ./web
run: |
yarn generate
yarn workspace web run generate

13
web/.editorconfig Normal file
View file

@ -0,0 +1,13 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

20
web/.eslintrc.js Normal file
View file

@ -0,0 +1,20 @@
module.exports = {
root: true,
env: {
es2020: true,
browser: true,
node: true,
},
extends: [
'@nuxtjs/eslint-config-typescript',
'prettier',
'prettier/vue',
'plugin:prettier/recommended',
'plugin:nuxt/recommended',
],
plugins: ['prettier'],
// add your custom rules here
rules: {
'import/no-webpack-loader-syntax': 0,
},
};

90
web/.gitignore vendored Normal file
View file

@ -0,0 +1,90 @@
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
/logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# Nuxt generate
dist
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE / Editor
.idea
# Service worker
sw.*
# macOS
.DS_Store
# Vim swap files
*.swp

4
web/.prettierrc Normal file
View file

@ -0,0 +1,4 @@
{
"semi": true,
"singleQuote": true
}

20
web/README.md Normal file
View file

@ -0,0 +1,20 @@
# web
## Build Setup
```bash
# install dependencies
$ yarn install
# serve with hot reload at localhost:3000
$ yarn dev
# build for production and launch server
$ yarn build
$ yarn start
# generate static project
$ yarn generate
```
For detailed explanation on how things work, check out [Nuxt.js docs](https://nuxtjs.org).

35
web/components/Logo.vue Normal file
View file

@ -0,0 +1,35 @@
<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>

286
web/components/devices.ts Normal file
View file

@ -0,0 +1,286 @@
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';
/// 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 };
export type Device = {
device: USBDevice;
name: string;
isCxIi: boolean;
needsDrivers: boolean;
worker?: WorkerExt;
info?: Info;
progress?: Progress;
queue?: Cmd[];
running?: boolean;
};
async function downloadFile(
dev: RpcProvider,
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 uploadOs(dev: RpcProvider, data: Uint8Array) {
await dev.rpc('uploadOs', {data});
}
async function deleteFile(dev: RpcProvider, path: string) {
await dev.rpc('deleteFile', {path});
}
async function deleteDir(dev: RpcProvider, path: string) {
await dev.rpc('deleteDir', {path});
}
async function createDir(dev: RpcProvider, path: string) {
await dev.rpc('createDir', {path});
}
async function move(dev: RpcProvider, 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 listDir(dev: RpcProvider, path: string) {
return (await dev.rpc('listDir', {path})) as FileInfo[];
}
async function listAll(dev: RpcProvider, 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}`}))
);
}
parts.push(path);
return parts;
} catch (e) {
console.error(path, e);
return [];
}
}
let queueId = 0;
@Component
class Devices extends Vue implements GenericDevices {
enumerating = false;
hasEnumerated = false;
devices: Record<string, Device> = {};
created() {
}
async runQueue(dev: string) {
const device = this.devices[dev];
if (!device?.queue || !device.worker || device.running) return;
const {rpc} = device.worker;
this.$set(device, 'running', true);
// eslint-disable-next-line no-constant-condition
while (true) {
// The device has been removed
if (!this.devices[dev]) return;
const cmd = device.queue[0];
if (!cmd) {
device.running = false;
return;
}
try {
if (cmd.action === 'download') {
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()));
} else if (cmd.action === 'uploadOs') {
if (!('file' in cmd)) return;
await uploadOs(rpc, new Uint8Array(await cmd.file.arrayBuffer()));
} else if (cmd.action === 'deleteFile') {
await deleteFile(rpc, cmd.path);
} else if (cmd.action === 'deleteDir') {
await deleteDir(rpc, cmd.path);
} else if (cmd.action === 'createDir') {
await createDir(rpc, cmd.path);
} else if (cmd.action === 'move') {
await move(rpc, cmd.src, cmd.dest);
} else if (cmd.action === 'copy') {
await copy(rpc, cmd.src, cmd.dest);
}
} catch (e) {
console.error(e);
}
if ('progress' in device) this.$delete(device, 'progress');
device.queue.shift();
await this.update(dev);
}
}
private addToQueue(dev: string, ...cmds: PartialCmd[]) {
const device = this.devices[dev];
if (!device) return;
if (!device.queue) {
this.$set(device, 'queue', []);
}
device.queue?.push(
...cmds.map((cmd) => ({...cmd, id: queueId++} as Cmd))
);
this.runQueue(dev);
}
async enumerate() {
try {
if (!navigator.usb) return;
const device = await navigator.usb.requestDevice({
filters: [
{vendorId: VID, productId: PID},
{
vendorId: VID,
productId: PID_CX2,
},
],
});
navigator.usb.ondisconnect = (e) => {
const [key] =
Object.entries(this.devices).find(
([_, {device}]) => device === e.device
) || [];
if (key) {
this.$delete(this.devices, key);
}
};
this.$set(this.devices, queueId++, {
device,
name: device.productName,
isCxIi: device.productId === PID_CX2,
needsDrivers: false,
} as Device);
} catch (e) {
console.error(e);
}
}
async open(dev: string) {
const device = this.devices[dev].device;
await device.open();
const worker: Worker & Partial<WorkerExt> = new UsbWorker();
const sab = new SharedArrayBuffer(10000);
const compat = new UsbCompat(sab);
const id = compat.addDevice(device);
const rpc = new RpcProvider((message, transfer: any) =>
worker.postMessage(message, transfer)
);
worker.rpc = rpc;
worker.onmessage = ({data}) => {
if ('usbCmd' in data) return compat.processCmd(data);
if('total' in data) {
this.$set(this.devices[dev], 'progress', data);
return;
}
rpc.dispatch(data);
};
this.$set(this.devices[dev], 'worker', worker as WorkerExt);
await rpc.rpc('init', {id, sab, vid: device.vendorId, pid: device.productId});
await this.update(dev);
}
async close(dev: string) {
const device = this.devices[dev];
device.worker?.terminate();
device.device.close();
this.$delete(this.devices, dev);
}
async update(dev: string) {
console.log('up');
const info = await this.devices[dev].worker?.rpc.rpc('updateDevice');
console.log(info);
this.$set(this.devices[dev], 'info', info);
}
async listDir(dev: string, path: string) {
const worker = this.devices[dev].worker;
if (!worker) return [];
return await listDir(worker.rpc, path);
}
async uploadFiles(dev: string, path: string, files: File[]) {
for (const file of files) {
this.addToQueue(dev, {action: 'upload', path, file});
}
}
async promptUploadFiles(dev: string, path: string) {
throw new Error('Unimplemented');
}
async uploadOs(dev: string, filter: string) {
throw new Error('Unimplemented');
}
async uploadOsFile(dev: string, file: File): Promise<void> {
this.addToQueue(dev, {action: 'uploadOs', file});
}
async downloadFiles(dev: string, files: [string, number][]) {
for (const path of files) {
this.addToQueue(dev, {action: 'download', path});
}
}
async delete(dev: string, files: FileInfo[]) {
const rpc = this.devices[dev].worker?.rpc;
if (!rpc) return;
const toDelete: FileInfo[] = [];
for (const file of files) {
toDelete.push(...(await listAll(rpc, file)));
}
for (const file of toDelete) {
this.addToQueue(dev, {
action: file.isDir ? 'deleteDir' : 'deleteFile',
path: file.path,
});
}
}
async createDir(dev: string, path: string) {
this.addToQueue(dev, {action: 'createDir', path});
}
async copy(dev: string, src: string, dest: string) {
this.addToQueue(dev, {action: 'copy', src, dest});
}
async move(dev: string, src: string, dest: string) {
this.addToQueue(dev, {action: 'move', src, dest});
}
}
const devices = new Devices();
export default devices;
Vue.prototype.$devices = devices;

168
web/components/impl.ts Normal file
View file

@ -0,0 +1,168 @@
import { Encoder } from '@msgpack/msgpack';
export enum UsbError {
NotFound = 'NotFound',
Security = 'Security',
Network = 'Network',
Abort = 'Abort',
InvalidState = 'InvalidState',
InvalidAccess = 'InvalidAccess',
Unknown = 'Unknown',
}
const exceptionMap = 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,
}
: {};
export type Cmd =
| ({ usbCmd: 'bulkTransferOut' } & BulkTransferOut)
| ({ usbCmd: 'bulkTransferIn' } & BulkTransferIn)
| ({ usbCmd: 'selectConfiguration' } & SelectConfiguration)
| ({ usbCmd: 'claimInterface' } & ClaimInterface)
| ({ usbCmd: 'releaseInterface' } & ReleaseInterface)
| ({ usbCmd: 'resetDevice' } & ResetDevice)
| ({ usbCmd: 'activeConfigDescriptor' } & ActiveConfigDescriptor);
export type NullReply = { Ok: null } | { Err: UsbError };
export type BulkTransferOut = {
device: number;
endpoint: number;
data: Uint8Array;
};
export type BulkTransferOutReply = { Ok: number } | { Err: UsbError };
export type BulkTransferIn = {
device: number;
endpoint: number;
length: number;
};
export type Data = Uint8Array;
export type BulkTransferInReply = { Ok: Data } | { Err: UsbError };
export type SelectConfiguration = { device: number; config: number };
export type ClaimInterface = { device: number; number: number };
export type ReleaseInterface = { device: number; number: number };
export type ResetDevice = { device: number };
export type ActiveConfigDescriptor = { device: number };
export type USBEndpoint = { address: number; packetSize: number };
export type USBAlternateInterface = {
alternateSetting: number;
interfaceClass: number;
interfaceSubclass: number;
interfaceProtocol: number;
endpoints: USBEndpoint[];
};
export type USBConfiguration = {
configurationValue: number;
interfaces: USBAlternateInterface[][];
};
export type ActiveConfigDescriptorReply =
| { Ok: USBConfiguration }
| { Err: UsbError };
const encoder = new Encoder();
let count = 0;
export default class UsbCompat {
devices: Record<number, USBDevice> = {};
arr: SharedArrayBuffer;
constructor(arr: SharedArrayBuffer) {
this.arr = arr;
}
addDevice(dev: USBDevice) {
const i = count++;
this.devices[i] = dev;
return i;
}
private async _processCmd(cmd: Cmd) {
try {
if (cmd.usbCmd === 'bulkTransferOut') {
const res = await this.devices[cmd.device].transferOut(
cmd.endpoint & ~0x80,
cmd.data
);
const reply: BulkTransferOutReply = { Ok: res.bytesWritten };
return reply;
} else if (cmd.usbCmd === 'bulkTransferIn') {
const res = await this.devices[cmd.device].transferIn(
cmd.endpoint & ~0x80,
cmd.length
);
const reply: BulkTransferInReply = {
Ok: new Uint8Array(res.data!.buffer),
};
return reply;
} else if (cmd.usbCmd === 'selectConfiguration') {
await this.devices[cmd.device].selectConfiguration(cmd.config);
} else if (cmd.usbCmd === 'claimInterface') {
await this.devices[cmd.device].claimInterface(cmd.number);
} else if (cmd.usbCmd === 'releaseInterface') {
await this.devices[cmd.device].releaseInterface(cmd.number);
} else if (cmd.usbCmd === 'resetDevice') {
await this.devices[cmd.device].reset();
} else if (cmd.usbCmd === 'activeConfigDescriptor') {
const configuration = this.devices[cmd.device].configuration!;
const reply: ActiveConfigDescriptorReply = {
Ok: {
configurationValue: configuration.configurationValue,
interfaces: configuration.interfaces.map(({ alternates }) => {
return alternates.map((alternate) => ({
alternateSetting: alternate.alternateSetting,
interfaceClass: alternate.interfaceClass,
interfaceSubclass: alternate.interfaceSubclass,
interfaceProtocol: alternate.interfaceProtocol,
endpoints: alternate.endpoints.map((endpoint) => ({
address:
endpoint.endpointNumber |
(endpoint.direction === 'in' ? 0x80 : 0),
packetSize: endpoint.packetSize,
})),
}));
}),
},
};
return reply;
}
const reply: NullReply = { Ok: null };
return reply;
} catch (e) {
console.error(e);
return { Err: exceptionMap[e.name] || UsbError.Unknown };
}
}
async processCmd(cmd: Cmd) {
const msg = await this._processCmd(cmd);
const encoded = encoder.encode(msg);
if (encoded.length > this.arr.length - 4) {
throw new Error('too long');
}
new Uint8Array(this.arr).set(encoded, 4);
const notify = new Int32Array(this.arr);
Atomics.store(notify, 0, encoded.length);
Atomics.notify(notify, 0, Infinity);
}
}

View file

@ -0,0 +1,66 @@
/* eslint camelcase: 0, require-await: 0 */
import { RpcProvider } from 'worker-rpc';
// eslint-disable-next-line import/no-absolute-path
import type { Calculator } from 'web-libnspire';
console.log('worker!');
const ctx: Worker = self as any;
const module = import('web-libnspire');
let calc: Calculator | undefined;
const rpcProvider = new RpcProvider((message, transfer: any) =>
ctx.postMessage(message, transfer)
);
ctx.onmessage = (e) => rpcProvider.dispatch(e.data);
type Path = { path: string };
type Data = { data: Uint8Array };
type SrcDest = { src: string; dest: string };
rpcProvider.registerRpcHandler<{id: number, sab: SharedArrayBuffer, vid: number, pid: number}>('init', async ({ id, sab, vid, pid }) => {
if (calc) calc.free();
calc = new (await module).Calculator(id, vid, pid, new Int32Array(sab));
});
rpcProvider.registerRpcHandler('updateDevice', async () => {
return calc?.update();
});
rpcProvider.registerRpcHandler<{path: [string, number]}, Uint8Array | undefined>('downloadFile', async ({ path }) => {
return calc?.download_file(path[0], path[1]);
});
rpcProvider.registerRpcHandler<Path & Data>(
'uploadFile',
async ({ path, data }) => {
calc?.upload_file(path, data);
}
);
rpcProvider.registerRpcHandler<Data>('uploadOs', async ({ data }) => {
calc?.upload_os(data);
});
rpcProvider.registerRpcHandler<Path>('deleteFile', async ({ path }) => {
calc?.delete_file(path);
});
rpcProvider.registerRpcHandler<Path>('deleteDir', async ({ path }) => {
calc?.delete_dir(path);
});
rpcProvider.registerRpcHandler<Path>('createDir', async ({ path }) => {
calc?.create_dir(path);
});
rpcProvider.registerRpcHandler<SrcDest>('move', async ({ src, dest }) => {
calc?.move_file(src, dest);
});
rpcProvider.registerRpcHandler<SrcDest>('copy', async ({ src, dest }) => {
calc?.copy_file(src, dest);
});
rpcProvider.registerRpcHandler<Path>('listDir', async ({ path }) => {
return calc?.list_dir(path);
});

27
web/layouts/default.vue Normal file
View file

@ -0,0 +1,27 @@
<template>
<div class="h-screen">
<Nuxt />
</div>
</template>
<style>
html {
color: #2c3e50;
user-select: none;
font-family: Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
word-spacing: 1px;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
box-sizing: border-box;
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
}
</style>

40
web/nuxt.config.js Normal file
View file

@ -0,0 +1,40 @@
export default {
// Target (https://go.nuxtjs.dev/config-target)
target: 'static',
// Global page headers (https://go.nuxtjs.dev/config-head)
head: {
title: 'web',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' },
],
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
},
// Global CSS (https://go.nuxtjs.dev/config-css)
css: [],
// Plugins to run before rendering page (https://go.nuxtjs.dev/config-plugins)
plugins: ['@/plugins/index.ts'],
// Auto import components (https://go.nuxtjs.dev/config-components)
components: true,
// Modules for dev and build (recommended) (https://go.nuxtjs.dev/config-modules)
buildModules: [
// https://go.nuxtjs.dev/typescript
'@nuxt/typescript-build',
// https://go.nuxtjs.dev/tailwindcss
'@nuxtjs/tailwindcss',
],
// Modules (https://go.nuxtjs.dev/config-modules)
modules: [],
// Build Configuration (https://go.nuxtjs.dev/config-build)
build: {
transpile: ['n-link-core', 'element-ui'],
},
};

42
web/package.json Normal file
View file

@ -0,0 +1,42 @@
{
"name": "web",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "nuxt-ts",
"build": "nuxt-ts build",
"start": "nuxt-ts start",
"generate": "nuxt-ts generate",
"lint:js": "eslint --ext .js,.vue --ignore-path .gitignore .",
"lint": "yarn lint:js"
},
"dependencies": {
"@msgpack/msgpack": "^2.1.0",
"@nuxt/typescript-runtime": "^2.0.0",
"core-js": "^3.6.5",
"file-saver": "^2.0.2",
"n-link-core": "0.0.0",
"nuxt": "^2.14.5",
"vue-async-computed": "^3.9.0",
"vue-property-decorator": "^9.0.2",
"web-libnspire": "^0.1.3",
"worker-rpc": "^0.2.0"
},
"devDependencies": {
"@nuxt/types": "^2.14.5",
"@nuxt/typescript-build": "^2.0.3",
"@nuxtjs/eslint-config": "^3.1.0",
"@nuxtjs/eslint-config-typescript": "^3.0.0",
"@nuxtjs/eslint-module": "^2.0.0",
"@nuxtjs/tailwindcss": "^3.0.2",
"@types/file-saver": "^2.0.1",
"@types/w3c-web-usb": "^1.0.4",
"babel-eslint": "^10.1.0",
"eslint": "^7.8.1",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-nuxt": "^1.0.0",
"eslint-plugin-prettier": "^3.1.4",
"prettier": "^2.1.1",
"worker-loader": "^3.0.3"
}
}

158
web/pages/index.vue Normal file
View file

@ -0,0 +1,158 @@
<template>
<div class="home h-full overflow-hidden">
<div class="flex flex-row h-full">
<div class="flex-shrink-0 border-r w-64">
<device-select :selected.sync="selectedCalculator" />
<div class="overflow-auto h-full px-4 py-4">
<div v-if="needsDrivers">
<h1 class="text-3xl">Drivers required</h1>
<p>The WinUSB driver is required to use this device.</p>
<p class="text-center mt-2">
<a href="#" class="text-blue-600" @click.prevent="installDrivers"
>See installation instructions</a
>
</p>
</div>
<div
v-else-if="calculator && !calculator.info"
class="flex items-center justify-center h-full"
>
<div class="lds-dual-ring" />
</div>
<div v-else-if="calculator && calculator.info">
<calc-info :info="calculator.info" :dev="selectedCalculator" native-upload />
<label class="inline-flex items-center cursor-pointer mr-2 mt-4">
<input
v-model="showHidden"
type="checkbox"
class="form-checkbox h-5 w-5 text-blue-600 cursor-pointer"
/>
<span class="mx-2 text-gray-700 select-none"
>Include hidden files</span
>
</label>
</div>
</div>
</div>
<div class="w-full">
<div class="h-full">
<file-browser
v-if="calculator && calculator.info"
:dev="selectedCalculator"
:show-hidden="showHidden"
native-upload
/>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Watch } 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';
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
@Component({
components: {
DeviceSelect,
FileBrowser,
CalcInfo,
},
})
export default class Home extends Vue {
selectedCalculator: string | null = null;
showHidden = false;
@Watch('$devices.hasEnumerated')
onEnumerated() {
const first = Object.keys(this.$devices.devices)[0];
if (first) this.selectedCalculator = first;
}
@Watch('$devices.devices')
async onDeviceChange() {
if (!this.selectedCalculator) {
await sleep(1000);
if (this.selectedCalculator) return;
const first = Object.keys(this.$devices.devices)[0];
if (first) this.selectedCalculator = first;
} else if (
!Object.keys(this.$devices.devices).includes(this.selectedCalculator)
) {
this.selectedCalculator = null;
// go back and choose the first if available
this.onDeviceChange();
}
}
@Watch('selectedCalculator')
async onSelectCalculator(dev: string | null) {
if (
dev &&
!this.$devices.devices[dev].info &&
!this.$devices.devices[dev].needsDrivers
) {
try {
await this.$devices.open(dev);
} catch (e) {
console.error(e);
this.selectedCalculator = null;
}
}
}
get calculator() {
return (
this.selectedCalculator && this.$devices.devices[this.selectedCalculator]
);
}
get needsDrivers() {
return (
this.selectedCalculator &&
this.$devices.devices[this.selectedCalculator]?.needsDrivers
);
}
installDrivers() {
open('https://lights0123.com/n-link/#windows');
}
}
</script>
<style lang="scss" scoped>
.lds-dual-ring {
display: inline-block;
width: 80px;
height: 80px;
}
.lds-dual-ring:after {
$color: theme('colors.gray.400');
content: ' ';
display: block;
width: 64px;
height: 64px;
margin: 8px;
border-radius: 50%;
border: 6px solid $color;
border-color: $color transparent $color transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

4
web/plugins/index.ts Normal file
View file

@ -0,0 +1,4 @@
import Vue from 'vue';
import AsyncComputed from 'vue-async-computed';
Vue.use(AsyncComputed);

BIN
web/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

1
web/tailwind.config.js Normal file
View file

@ -0,0 +1 @@
module.exports = require('n-link-core/tailwind.config');

37
web/tsconfig.json Normal file
View file

@ -0,0 +1,37 @@
{
"compilerOptions": {
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "Node",
"lib": [
"ESNext",
"ESNext.AsyncIterable",
"DOM"
],
"esModuleInterop": true,
"allowJs": true,
"sourceMap": true,
"strict": true,
"noEmit": true,
"experimentalDecorators": true,
"baseUrl": ".",
"paths": {
"~/*": [
"./*"
],
"@/*": [
"./*"
]
},
"types": [
"@types/node",
"@nuxt/types",
"@types/w3c-web-usb"
]
},
"exclude": [
"node_modules",
".nuxt",
"dist"
]
}

10
web/types/shims-vue.d.ts vendored Normal file
View file

@ -0,0 +1,10 @@
declare module 'worker-loader!*' {
// You need to change `Worker`, if you specified a different value for the `workerType` option
class WebpackWorker extends Worker {
constructor();
}
// Uncomment this if you set the `esModule` option to `false`
// export = WebpackWorker;
export default WebpackWorker;
}