mirror of
https://github.com/m1k1o/neko.git
synced 2024-07-24 14:40:50 +12:00
Merge remote-tracking branch 'origin/demodesk-client-v3' into v3
This commit is contained in:
commit
ac76894800
95
.github/workflows/client_tags.yml
vendored
Normal file
95
.github/workflows/client_tags.yml
vendored
Normal file
@ -0,0 +1,95 @@
|
||||
name: Build and Publish package on tags
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
env:
|
||||
NODE_VERSION: '16.x'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16.x'
|
||||
registry-url: 'https://npm.pkg.github.com'
|
||||
|
||||
- name: Install dependencies and build
|
||||
run: cd ./client/ && npm ci && npm run build
|
||||
|
||||
- name: Prepare Artifact
|
||||
run: |
|
||||
cd ./client/
|
||||
mkdir artifact
|
||||
cp LICENSE README.md package.json artifact
|
||||
cp -r dist artifact/dist
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: package
|
||||
path: client/artifact
|
||||
|
||||
publish-to-npm-pkg-github-com:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
needs: [ build ]
|
||||
|
||||
steps:
|
||||
- name: Download Artifact
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: package
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
registry-url: 'https://npm.pkg.github.com'
|
||||
|
||||
- name: Publish package to npm.pkg.github.com
|
||||
run: cd ./client/ && npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
publish-to-registry-npmjs-org:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
needs: [ build ]
|
||||
|
||||
steps:
|
||||
- name: Download Artifact
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: package
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Replace npm.pkg.github.com with registry.npmjs.org
|
||||
run: sed -i 's/npm\.pkg\.github\.com/registry\.npmjs\.org/' client/package.json
|
||||
|
||||
- name: Publish package to registry.npmjs.org
|
||||
run: cd ./client/ &&npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
22
.github/workflows/pull_requests.yml
vendored
22
.github/workflows/pull_requests.yml
vendored
@ -6,7 +6,7 @@ on:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build-image:
|
||||
build-server:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@ -18,4 +18,22 @@ jobs:
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
|
||||
with:
|
||||
context: .
|
||||
context: ./server
|
||||
|
||||
build-client:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install dependencies and build
|
||||
run: cd ./client && npm install && npm run build
|
||||
|
@ -38,7 +38,7 @@ jobs:
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
|
||||
with:
|
||||
context: .
|
||||
context: ./server/
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
@ -46,7 +46,7 @@ jobs:
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
|
||||
with:
|
||||
context: .
|
||||
context: ./server/
|
||||
file: ${{ matrix.dockerfile }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
40
.gitignore
vendored
40
.gitignore
vendored
@ -1,8 +1,36 @@
|
||||
runtime/fonts/*
|
||||
!runtime/fonts/.gitkeep
|
||||
# OS files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
runtime/icon-theme/*
|
||||
!runtime/icon-theme/.gitkeep
|
||||
# Log/Temp files
|
||||
.tmp/
|
||||
tmp/
|
||||
*.tmp
|
||||
.logs/
|
||||
logs/
|
||||
*.log
|
||||
core
|
||||
.build
|
||||
|
||||
plugins/*
|
||||
!plugins/.gitkeep
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# TypeScript incremental compilation cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Node modules
|
||||
node_modules
|
||||
dist
|
||||
bin
|
||||
|
||||
# Environment files
|
||||
*.env
|
||||
.env.development
|
||||
|
12
.vscode/settings.json
vendored
12
.vscode/settings.json
vendored
@ -1,6 +1,5 @@
|
||||
{
|
||||
"go.inferGopath": false,
|
||||
"go.autocompleteUnimportedPackages": true,
|
||||
"go.delveConfig": {
|
||||
"useApiV1": false
|
||||
},
|
||||
@ -9,5 +8,14 @@
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit"
|
||||
}
|
||||
}
|
||||
},
|
||||
"editor.tabSize": 2,
|
||||
"editor.insertSpaces": true,
|
||||
"editor.detectIndentation": false,
|
||||
"files.encoding": "utf8",
|
||||
"files.eol": "\n",
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
}
|
||||
|
3
LICENSE
3
LICENSE
@ -186,7 +186,8 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2020 Nurdism <nurdism.io@gmail.com>, 2020- https://github.com/m1k1o
|
||||
Copyright 2020 Nurdism <nurdism.io@gmail.com>
|
||||
Copyright 2020-2024 m1k1o
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
122
README.md
122
README.md
@ -37,3 +37,125 @@ Notable differences to the [m1k1o/neko](https://github.com/m1k1o/neko) are:
|
||||
## Docs
|
||||
|
||||
*TBD.*
|
||||
|
||||
# neko-client
|
||||
Connect to [demodesk/neko](https://github.com/demodesk/neko) backend with self contained vue component.
|
||||
|
||||
For **community edition** neko with GUI and _plug & play_ deployment visit [m1k1o/neko](https://github.com/m1k1o/neko).
|
||||
|
||||
## Installation
|
||||
Code is published to public NPM registry and GitHub npm repository.
|
||||
|
||||
```bash
|
||||
# npm command
|
||||
npm i @demodesk/neko
|
||||
# yarn command
|
||||
yarn add @demodesk/neko
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
You can set keyboard provider at build time, either `novnc` or the default `guacamole`.
|
||||
|
||||
```bash
|
||||
# by default uses guacamole keyboard
|
||||
npm run build
|
||||
# uses novnc keyboard
|
||||
KEYBOARD=novnc npm run build
|
||||
```
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Run Unit Tests with [Vitest](https://vitest.dev/)
|
||||
|
||||
```sh
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
### Run End-to-End Tests with [Playwright](https://playwright.dev)
|
||||
|
||||
```sh
|
||||
# Install browsers for the first run
|
||||
npx playwright install
|
||||
|
||||
# When testing on CI, must build the project first
|
||||
npm run build
|
||||
|
||||
# Runs the end-to-end tests
|
||||
npm run test:e2e
|
||||
# Runs the tests only on Chromium
|
||||
npm run test:e2e -- --project=chromium
|
||||
# Runs the tests of a specific file
|
||||
npm run test:e2e -- tests/example.spec.ts
|
||||
# Runs the tests in debug mode
|
||||
npm run test:e2e -- --debug
|
||||
```
|
||||
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
|
||||
```sh
|
||||
npm run lint
|
||||
```
|
||||
|
||||
|
||||
### Example
|
||||
API consists of accessing Vue reactive state, calling various methods and subscribing to events. Simple usage:
|
||||
|
||||
```html
|
||||
<!-- import vue -->
|
||||
<script src="https://unpkg.com/vue"></script>
|
||||
|
||||
<!-- import neko -->
|
||||
<script src="./neko.umd.js"></script>
|
||||
<link rel="stylesheet" href="./neko.css">
|
||||
|
||||
<div id="app">
|
||||
<neko ref="neko" server="http://127.0.0.1:3000/api" autologin autoplay />
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
components: { neko },
|
||||
mounted() {
|
||||
// access state
|
||||
// this.$refs.neko.state.session_id
|
||||
|
||||
// call methods
|
||||
// this.$refs.neko.setUrl('http://127.0.0.1:3000/api')
|
||||
// this.$refs.neko.login('username', 'password')
|
||||
// this.$refs.neko.logout()
|
||||
|
||||
// subscribe to events
|
||||
// this.$refs.neko.events.on('room.control.host', (id) => { })
|
||||
},
|
||||
}).$mount('#app')
|
||||
</script>
|
||||
```
|
||||
|
26
client/.eslintrc.cjs
Normal file
26
client/.eslintrc.cjs
Normal file
@ -0,0 +1,26 @@
|
||||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
'extends': [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'@vue/eslint-config-typescript',
|
||||
'@vue/eslint-config-prettier/skip-formatting'
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'e2e/**/*.{test,spec}.{js,ts,jsx,tsx}'
|
||||
],
|
||||
'extends': [
|
||||
'plugin:playwright/recommended'
|
||||
]
|
||||
}
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest'
|
||||
},
|
||||
ignorePatterns: ["**/*.js"]
|
||||
}
|
7
client/.gitignore
vendored
Normal file
7
client/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
test-results/
|
||||
playwright-report/
|
||||
|
||||
/src/page/plugins/*
|
||||
!/src/page/plugins/filetransfer
|
||||
!/src/page/plugins/chat
|
||||
!/src/page/plugins/.gitkeep
|
8
client/.prettierrc.json
Normal file
8
client/.prettierrc.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none"
|
||||
}
|
20
client/dev/api-gen
Executable file
20
client/dev/api-gen
Executable file
@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
OPENAPI_URL="https://raw.githubusercontent.com/demodesk/neko/master/openapi.yaml"
|
||||
|
||||
rm -rf "${PWD}/../src/component/api"
|
||||
mkdir "${PWD}/../src/component/api"
|
||||
|
||||
docker run --rm \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
-v "${PWD}/../src/component/api:/local/out" \
|
||||
openapitools/openapi-generator-cli generate \
|
||||
-i "$OPENAPI_URL" \
|
||||
-g typescript-axios \
|
||||
-o /local/out \
|
||||
--additional-properties=enumPropertyNaming=original,modelPropertyNaming=original,withSeparateModelsAndApi=true,modelPackage=models,apiPackage=api
|
||||
|
||||
# Remove not needed git_push.sh
|
||||
rm -f "${PWD}/../src/component/api/git_push.sh"
|
||||
|
||||
# Fix lint errors
|
||||
./npm run lint -- --fix src/component/api
|
12
client/dev/build
Executable file
12
client/dev/build
Executable file
@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
APP_PATH="$(realpath ../)"
|
||||
|
||||
# start component watch
|
||||
docker run --rm -it \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
--volume "$APP_PATH:$APP_PATH" \
|
||||
--entrypoint="npm" \
|
||||
--workdir="$APP_PATH" \
|
||||
node:18-buster-slim run build
|
12
client/dev/exec
Executable file
12
client/dev/exec
Executable file
@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
APP_PATH="$(realpath ../)"
|
||||
|
||||
# start component watch
|
||||
docker run --rm -it \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
--volume "$APP_PATH:$APP_PATH" \
|
||||
--entrypoint="/bin/bash" \
|
||||
--workdir="$APP_PATH" \
|
||||
node:18-buster-slim
|
12
client/dev/npm
Executable file
12
client/dev/npm
Executable file
@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
APP_PATH="$(realpath ../)"
|
||||
|
||||
# npm
|
||||
docker run --rm -it \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
--volume "$APP_PATH:$APP_PATH" \
|
||||
--entrypoint="npm" \
|
||||
--workdir="$APP_PATH" \
|
||||
node:18-buster-slim "$@"
|
40
client/dev/serve
Executable file
40
client/dev/serve
Executable file
@ -0,0 +1,40 @@
|
||||
#!/bin/sh
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if [ -z $NEKO_PORT ]; then
|
||||
NEKO_PORT="3000"
|
||||
fi
|
||||
|
||||
if [ -z $NEKO_HOST ]; then
|
||||
for i in $(ifconfig -l 2>/dev/null); do
|
||||
NEKO_HOST=$(ipconfig getifaddr $i)
|
||||
if [ ! -z $NEKO_HOST ]; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z $NEKO_HOST ]; then
|
||||
NEKO_HOST=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
fi
|
||||
|
||||
if [ -z $NEKO_HOST ]; then
|
||||
NEKO_HOST=$(hostname -i 2>/dev/null)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using app port: ${NEKO_PORT}"
|
||||
echo "Using IP address: ${NEKO_HOST}"
|
||||
|
||||
APP_PATH="$(realpath ../)"
|
||||
|
||||
# npm run serve
|
||||
docker run --rm -it \
|
||||
-p 3001:3001 \
|
||||
-e "NEKO_HOST=$NEKO_HOST" \
|
||||
-e "NEKO_PORT=$NEKO_PORT" \
|
||||
-e "VUE_APP_LOG_COLOR=true" \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
--volume "$APP_PATH:$APP_PATH" \
|
||||
--entrypoint="npm" \
|
||||
--workdir="$APP_PATH" \
|
||||
node:18-buster-slim run dev -- --port 3001 --host
|
23
client/dev/version
Executable file
23
client/dev/version
Executable file
@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if ! git diff-index --quiet HEAD
|
||||
then
|
||||
echo "Please clean git before publishing."
|
||||
exit
|
||||
fi
|
||||
|
||||
# bump npm version
|
||||
VERSION=$(./npm version "${1-patch}" --no-git-tag-version)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Npm version bump failed."
|
||||
exit
|
||||
fi
|
||||
|
||||
VERSION=$(echo "$VERSION" | head -1 | cut -c 2- | tr -d '\r')
|
||||
echo "New version is: $VERSION"
|
||||
|
||||
git add ../package*
|
||||
git commit -m "version ${VERSION}"
|
||||
git tag -a "v${VERSION}" -m "version ${VERSION}"
|
||||
git push origin "v${VERSION}"
|
4
client/e2e/tsconfig.json
Normal file
4
client/e2e/tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "@tsconfig/node20/tsconfig.json",
|
||||
"include": ["./**/*"]
|
||||
}
|
8
client/e2e/vue.spec.ts
Normal file
8
client/e2e/vue.spec.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
// See here how to get started:
|
||||
// https://playwright.dev/docs/intro
|
||||
test('visits the app root url', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.locator('div.greetings > h1')).toHaveText('You did it!');
|
||||
})
|
1
client/env.d.ts
vendored
Normal file
1
client/env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
12
client/index.html
Normal file
12
client/index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Neko</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
4871
client/package-lock.json
generated
Normal file
4871
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
61
client/package.json
Normal file
61
client/package.json
Normal file
@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "@demodesk/neko",
|
||||
"version": "2.0.0",
|
||||
"description": "Client as reusable Vue.js component for neko streaming server.",
|
||||
"repository": "https://github.com/demodesk/neko-client",
|
||||
"publishConfig": {
|
||||
"registry": "https://npm.pkg.github.com"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "dist/neko.umd.js",
|
||||
"module": "dist/neko.common.js",
|
||||
"typings": "dist/types/main.d.ts",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest",
|
||||
"test:e2e": "playwright test",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build --force",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.8",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"vue": "^3.4.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
"@tsconfig/node20": "^20.1.2",
|
||||
"@types/jsdom": "^21.1.6",
|
||||
"@types/node": "^20.11.25",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/test-utils": "^2.4.4",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-plugin-playwright": "^1.5.2",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"jsdom": "^24.0.0",
|
||||
"npm-run-all2": "^6.1.2",
|
||||
"prettier": "^3.0.3",
|
||||
"sass": "^1.72.0",
|
||||
"typescript": "~5.4.0",
|
||||
"vite": "^5.1.5",
|
||||
"vitest": "^1.3.1",
|
||||
"vue-tsc": "^2.0.6"
|
||||
},
|
||||
"files": [
|
||||
"dist/*"
|
||||
],
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions"
|
||||
]
|
||||
}
|
110
client/playwright.config.ts
Normal file
110
client/playwright.config.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import process from 'node:process'
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 30 * 1000,
|
||||
expect: {
|
||||
/**
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
* For example in `await expect(locator).toHaveText();`
|
||||
*/
|
||||
timeout: 5000
|
||||
},
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
actionTimeout: 0,
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: 'http://localhost:5173',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
|
||||
/* Only on CI systems run the tests headless */
|
||||
headless: !!process.env.CI
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: {
|
||||
...devices['Desktop Firefox']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
use: {
|
||||
...devices['Desktop Safari']
|
||||
}
|
||||
}
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: {
|
||||
// ...devices['Pixel 5'],
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: {
|
||||
// ...devices['iPhone 12'],
|
||||
// },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: {
|
||||
// channel: 'msedge',
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: {
|
||||
// channel: 'chrome',
|
||||
// },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
// outputDir: 'test-results/',
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
/**
|
||||
* Use the dev server by default for faster feedback loop.
|
||||
* Use the preview server on CI for more realistic testing.
|
||||
* Playwright will re-use the local server if there is already a dev-server running.
|
||||
*/
|
||||
command: process.env.CI ? 'vite preview --port 5173' : 'vite dev',
|
||||
port: 5173,
|
||||
reuseExistingServer: !process.env.CI
|
||||
}
|
||||
})
|
4
client/src/component/api/.gitignore
vendored
Normal file
4
client/src/component/api/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
wwwroot/*.js
|
||||
node_modules
|
||||
typings
|
||||
dist
|
1
client/src/component/api/.npmignore
Normal file
1
client/src/component/api/.npmignore
Normal file
@ -0,0 +1 @@
|
||||
# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm
|
23
client/src/component/api/.openapi-generator-ignore
Normal file
23
client/src/component/api/.openapi-generator-ignore
Normal file
@ -0,0 +1,23 @@
|
||||
# OpenAPI Generator Ignore
|
||||
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
|
||||
|
||||
# Use this file to prevent files from being overwritten by the generator.
|
||||
# The patterns follow closely to .gitignore or .dockerignore.
|
||||
|
||||
# As an example, the C# client generator defines ApiClient.cs.
|
||||
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
|
||||
#ApiClient.cs
|
||||
|
||||
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
|
||||
#foo/*/qux
|
||||
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
|
||||
|
||||
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
|
||||
#foo/**/qux
|
||||
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
|
||||
|
||||
# You can also negate patterns with an exclamation (!).
|
||||
# For example, you can ignore all files in a docs folder with the file extension .md:
|
||||
#docs/*.md
|
||||
# Then explicitly reverse the ignore rule for a single file:
|
||||
#!docs/README.md
|
33
client/src/component/api/.openapi-generator/FILES
Normal file
33
client/src/component/api/.openapi-generator/FILES
Normal file
@ -0,0 +1,33 @@
|
||||
.gitignore
|
||||
.npmignore
|
||||
.openapi-generator-ignore
|
||||
api.ts
|
||||
api/default-api.ts
|
||||
api/members-api.ts
|
||||
api/room-api.ts
|
||||
api/sessions-api.ts
|
||||
base.ts
|
||||
common.ts
|
||||
configuration.ts
|
||||
git_push.sh
|
||||
index.ts
|
||||
models/batch-request.ts
|
||||
models/batch-response.ts
|
||||
models/broadcast-status.ts
|
||||
models/clipboard-text.ts
|
||||
models/control-status.ts
|
||||
models/error-message.ts
|
||||
models/index.ts
|
||||
models/keyboard-map.ts
|
||||
models/keyboard-modifiers.ts
|
||||
models/member-bulk-delete.ts
|
||||
models/member-bulk-update.ts
|
||||
models/member-create.ts
|
||||
models/member-data.ts
|
||||
models/member-password.ts
|
||||
models/member-profile.ts
|
||||
models/screen-configuration.ts
|
||||
models/session-data.ts
|
||||
models/session-login.ts
|
||||
models/session-state.ts
|
||||
models/settings.ts
|
1
client/src/component/api/.openapi-generator/VERSION
Normal file
1
client/src/component/api/.openapi-generator/VERSION
Normal file
@ -0,0 +1 @@
|
||||
7.6.0-SNAPSHOT
|
21
client/src/component/api/api.ts
Normal file
21
client/src/component/api/api.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
export * from './api/default-api';
|
||||
export * from './api/members-api';
|
||||
export * from './api/room-api';
|
||||
export * from './api/sessions-api';
|
||||
|
487
client/src/component/api/api/default-api.ts
Normal file
487
client/src/component/api/api/default-api.ts
Normal file
@ -0,0 +1,487 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
import type { Configuration } from '../configuration';
|
||||
import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';
|
||||
import globalAxios from 'axios';
|
||||
// Some imports not used depending on template conditions
|
||||
// @ts-ignore
|
||||
import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';
|
||||
// @ts-ignore
|
||||
import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';
|
||||
// @ts-ignore
|
||||
import type { BatchRequest } from '../models';
|
||||
// @ts-ignore
|
||||
import type { BatchResponse } from '../models';
|
||||
// @ts-ignore
|
||||
import type { ErrorMessage } from '../models';
|
||||
// @ts-ignore
|
||||
import type { SessionData } from '../models';
|
||||
// @ts-ignore
|
||||
import type { SessionLogin } from '../models';
|
||||
/**
|
||||
* DefaultApi - axios parameter creator
|
||||
* @export
|
||||
*/
|
||||
export const DefaultApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @summary batch
|
||||
* @param {Array<BatchRequest>} batchRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
batch: async (batchRequest: Array<BatchRequest>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'batchRequest' is not null or undefined
|
||||
assertParamExists('batch', 'batchRequest', batchRequest)
|
||||
const localVarPath = `/api/batch`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication CookieAuth required
|
||||
|
||||
// authentication TokenAuth required
|
||||
await setApiKeyToObject(localVarQueryParameter, "token", configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(batchRequest, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary healthcheck
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
healthcheck: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/health`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary login
|
||||
* @param {SessionLogin} sessionLogin
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
login: async (sessionLogin: SessionLogin, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'sessionLogin' is not null or undefined
|
||||
assertParamExists('login', 'sessionLogin', sessionLogin)
|
||||
const localVarPath = `/api/login`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(sessionLogin, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary logout
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
logout: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/api/logout`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication CookieAuth required
|
||||
|
||||
// authentication TokenAuth required
|
||||
await setApiKeyToObject(localVarQueryParameter, "token", configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary metrics
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
metrics: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/metrics`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary whoami
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
whoami: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/api/whoami`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication CookieAuth required
|
||||
|
||||
// authentication TokenAuth required
|
||||
await setApiKeyToObject(localVarQueryParameter, "token", configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* DefaultApi - functional programming interface
|
||||
* @export
|
||||
*/
|
||||
export const DefaultApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = DefaultApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @summary batch
|
||||
* @param {Array<BatchRequest>} batchRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async batch(batchRequest: Array<BatchRequest>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<BatchResponse>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.batch(batchRequest, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['DefaultApi.batch']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary healthcheck
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async healthcheck(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.healthcheck(options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['DefaultApi.healthcheck']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary login
|
||||
* @param {SessionLogin} sessionLogin
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async login(sessionLogin: SessionLogin, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SessionData>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.login(sessionLogin, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['DefaultApi.login']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary logout
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async logout(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.logout(options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['DefaultApi.logout']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary metrics
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async metrics(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.metrics(options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['DefaultApi.metrics']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary whoami
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async whoami(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SessionData>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.whoami(options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['DefaultApi.whoami']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* DefaultApi - factory interface
|
||||
* @export
|
||||
*/
|
||||
export const DefaultApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||
const localVarFp = DefaultApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @summary batch
|
||||
* @param {Array<BatchRequest>} batchRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
batch(batchRequest: Array<BatchRequest>, options?: any): AxiosPromise<Array<BatchResponse>> {
|
||||
return localVarFp.batch(batchRequest, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary healthcheck
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
healthcheck(options?: any): AxiosPromise<void> {
|
||||
return localVarFp.healthcheck(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary login
|
||||
* @param {SessionLogin} sessionLogin
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
login(sessionLogin: SessionLogin, options?: any): AxiosPromise<SessionData> {
|
||||
return localVarFp.login(sessionLogin, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary logout
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
logout(options?: any): AxiosPromise<void> {
|
||||
return localVarFp.logout(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary metrics
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
metrics(options?: any): AxiosPromise<void> {
|
||||
return localVarFp.metrics(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary whoami
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
whoami(options?: any): AxiosPromise<SessionData> {
|
||||
return localVarFp.whoami(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* DefaultApi - object-oriented interface
|
||||
* @export
|
||||
* @class DefaultApi
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class DefaultApi extends BaseAPI {
|
||||
/**
|
||||
*
|
||||
* @summary batch
|
||||
* @param {Array<BatchRequest>} batchRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof DefaultApi
|
||||
*/
|
||||
public batch(batchRequest: Array<BatchRequest>, options?: RawAxiosRequestConfig) {
|
||||
return DefaultApiFp(this.configuration).batch(batchRequest, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary healthcheck
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof DefaultApi
|
||||
*/
|
||||
public healthcheck(options?: RawAxiosRequestConfig) {
|
||||
return DefaultApiFp(this.configuration).healthcheck(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary login
|
||||
* @param {SessionLogin} sessionLogin
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof DefaultApi
|
||||
*/
|
||||
public login(sessionLogin: SessionLogin, options?: RawAxiosRequestConfig) {
|
||||
return DefaultApiFp(this.configuration).login(sessionLogin, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary logout
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof DefaultApi
|
||||
*/
|
||||
public logout(options?: RawAxiosRequestConfig) {
|
||||
return DefaultApiFp(this.configuration).logout(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary metrics
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof DefaultApi
|
||||
*/
|
||||
public metrics(options?: RawAxiosRequestConfig) {
|
||||
return DefaultApiFp(this.configuration).metrics(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary whoami
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof DefaultApi
|
||||
*/
|
||||
public whoami(options?: RawAxiosRequestConfig) {
|
||||
return DefaultApiFp(this.configuration).whoami(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
731
client/src/component/api/api/members-api.ts
Normal file
731
client/src/component/api/api/members-api.ts
Normal file
@ -0,0 +1,731 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
import type { Configuration } from '../configuration';
|
||||
import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';
|
||||
import globalAxios from 'axios';
|
||||
// Some imports not used depending on template conditions
|
||||
// @ts-ignore
|
||||
import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';
|
||||
// @ts-ignore
|
||||
import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';
|
||||
// @ts-ignore
|
||||
import type { ErrorMessage } from '../models';
|
||||
// @ts-ignore
|
||||
import type { MemberBulkDelete } from '../models';
|
||||
// @ts-ignore
|
||||
import type { MemberBulkUpdate } from '../models';
|
||||
// @ts-ignore
|
||||
import type { MemberCreate } from '../models';
|
||||
// @ts-ignore
|
||||
import type { MemberData } from '../models';
|
||||
// @ts-ignore
|
||||
import type { MemberPassword } from '../models';
|
||||
// @ts-ignore
|
||||
import type { MemberProfile } from '../models';
|
||||
/**
|
||||
* MembersApi - axios parameter creator
|
||||
* @export
|
||||
*/
|
||||
export const MembersApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @summary bulk delete members
|
||||
* @param {MemberBulkDelete} memberBulkDelete
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
membersBulkDelete: async (memberBulkDelete: MemberBulkDelete, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'memberBulkDelete' is not null or undefined
|
||||
assertParamExists('membersBulkDelete', 'memberBulkDelete', memberBulkDelete)
|
||||
const localVarPath = `/api/members_bulk/delete`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication CookieAuth required
|
||||
|
||||
// authentication TokenAuth required
|
||||
await setApiKeyToObject(localVarQueryParameter, "token", configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(memberBulkDelete, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary bulk update members
|
||||
* @param {MemberBulkUpdate} memberBulkUpdate
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
membersBulkUpdate: async (memberBulkUpdate: MemberBulkUpdate, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'memberBulkUpdate' is not null or undefined
|
||||
assertParamExists('membersBulkUpdate', 'memberBulkUpdate', memberBulkUpdate)
|
||||
const localVarPath = `/api/members_bulk/update`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication CookieAuth required
|
||||
|
||||
// authentication TokenAuth required
|
||||
await setApiKeyToObject(localVarQueryParameter, "token", configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(memberBulkUpdate, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary create new member
|
||||
* @param {MemberCreate} memberCreate
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
membersCreate: async (memberCreate: MemberCreate, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'memberCreate' is not null or undefined
|
||||
assertParamExists('membersCreate', 'memberCreate', memberCreate)
|
||||
const localVarPath = `/api/members`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication CookieAuth required
|
||||
|
||||
// authentication TokenAuth required
|
||||
await setApiKeyToObject(localVarQueryParameter, "token", configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(memberCreate, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary get member\'s profile
|
||||
* @param {string} memberId member identifier
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
membersGetProfile: async (memberId: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'memberId' is not null or undefined
|
||||
assertParamExists('membersGetProfile', 'memberId', memberId)
|
||||
const localVarPath = `/api/members/{memberId}`
|
||||
.replace(`{${"memberId"}}`, encodeURIComponent(String(memberId)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication CookieAuth required
|
||||
|
||||
// authentication TokenAuth required
|
||||
await setApiKeyToObject(localVarQueryParameter, "token", configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary list of members
|
||||
* @param {number} [limit]
|
||||
* @param {number} [offset]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
membersList: async (limit?: number, offset?: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/api/members`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication CookieAuth required
|
||||
|
||||
// authentication TokenAuth required
|
||||
await setApiKeyToObject(localVarQueryParameter, "token", configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (limit !== undefined) {
|
||||
localVarQueryParameter['limit'] = limit;
|
||||
}
|
||||
|
||||
if (offset !== undefined) {
|
||||
localVarQueryParameter['offset'] = offset;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary remove member
|
||||
* @param {string} memberId member identifier
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
membersRemove: async (memberId: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'memberId' is not null or undefined
|
||||
assertParamExists('membersRemove', 'memberId', memberId)
|
||||
const localVarPath = `/api/members/{memberId}`
|
||||
.replace(`{${"memberId"}}`, encodeURIComponent(String(memberId)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication CookieAuth required
|
||||
|
||||
// authentication TokenAuth required
|
||||
await setApiKeyToObject(localVarQueryParameter, "token", configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary update member\'s password
|
||||
* @param {string} memberId member identifier
|
||||
* @param {MemberPassword} memberPassword
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
membersUpdatePassword: async (memberId: string, memberPassword: MemberPassword, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'memberId' is not null or undefined
|
||||
assertParamExists('membersUpdatePassword', 'memberId', memberId)
|
||||
// verify required parameter 'memberPassword' is not null or undefined
|
||||
assertParamExists('membersUpdatePassword', 'memberPassword', memberPassword)
|
||||
const localVarPath = `/api/members/{memberId}/password`
|
||||
.replace(`{${"memberId"}}`, encodeURIComponent(String(memberId)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication CookieAuth required
|
||||
|
||||
// authentication TokenAuth required
|
||||
await setApiKeyToObject(localVarQueryParameter, "token", configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(memberPassword, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary update member\'s profile
|
||||
* @param {string} memberId member identifier
|
||||
* @param {MemberProfile} memberProfile
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
membersUpdateProfile: async (memberId: string, memberProfile: MemberProfile, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'memberId' is not null or undefined
|
||||
assertParamExists('membersUpdateProfile', 'memberId', memberId)
|
||||
// verify required parameter 'memberProfile' is not null or undefined
|
||||
assertParamExists('membersUpdateProfile', 'memberProfile', memberProfile)
|
||||
const localVarPath = `/api/members/{memberId}`
|
||||
.replace(`{${"memberId"}}`, encodeURIComponent(String(memberId)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication CookieAuth required
|
||||
|
||||
// authentication TokenAuth required
|
||||
await setApiKeyToObject(localVarQueryParameter, "token", configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(memberProfile, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* MembersApi - functional programming interface
|
||||
* @export
|
||||
*/
|
||||
export const MembersApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = MembersApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @summary bulk delete members
|
||||
* @param {MemberBulkDelete} memberBulkDelete
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async membersBulkDelete(memberBulkDelete: MemberBulkDelete, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.membersBulkDelete(memberBulkDelete, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['MembersApi.membersBulkDelete']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary bulk update members
|
||||
* @param {MemberBulkUpdate} memberBulkUpdate
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async membersBulkUpdate(memberBulkUpdate: MemberBulkUpdate, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.membersBulkUpdate(memberBulkUpdate, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['MembersApi.membersBulkUpdate']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary create new member
|
||||
* @param {MemberCreate} memberCreate
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async membersCreate(memberCreate: MemberCreate, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<MemberData>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.membersCreate(memberCreate, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['MembersApi.membersCreate']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary get member\'s profile
|
||||
* @param {string} memberId member identifier
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async membersGetProfile(memberId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<MemberProfile>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.membersGetProfile(memberId, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['MembersApi.membersGetProfile']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary list of members
|
||||
* @param {number} [limit]
|
||||
* @param {number} [offset]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async membersList(limit?: number, offset?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MemberData>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.membersList(limit, offset, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['MembersApi.membersList']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary remove member
|
||||
* @param {string} memberId member identifier
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async membersRemove(memberId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.membersRemove(memberId, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['MembersApi.membersRemove']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary update member\'s password
|
||||
* @param {string} memberId member identifier
|
||||
* @param {MemberPassword} memberPassword
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async membersUpdatePassword(memberId: string, memberPassword: MemberPassword, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.membersUpdatePassword(memberId, memberPassword, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['MembersApi.membersUpdatePassword']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary update member\'s profile
|
||||
* @param {string} memberId member identifier
|
||||
* @param {MemberProfile} memberProfile
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async membersUpdateProfile(memberId: string, memberProfile: MemberProfile, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.membersUpdateProfile(memberId, memberProfile, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['MembersApi.membersUpdateProfile']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* MembersApi - factory interface
|
||||
* @export
|
||||
*/
|
||||
export const MembersApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||
const localVarFp = MembersApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @summary bulk delete members
|
||||
* @param {MemberBulkDelete} memberBulkDelete
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
membersBulkDelete(memberBulkDelete: MemberBulkDelete, options?: any): AxiosPromise<void> {
|
||||
return localVarFp.membersBulkDelete(memberBulkDelete, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary bulk update members
|
||||
* @param {MemberBulkUpdate} memberBulkUpdate
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
membersBulkUpdate(memberBulkUpdate: MemberBulkUpdate, options?: any): AxiosPromise<void> {
|
||||
return localVarFp.membersBulkUpdate(memberBulkUpdate, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary create new member
|
||||
* @param {MemberCreate} memberCreate
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
membersCreate(memberCreate: MemberCreate, options?: any): AxiosPromise<MemberData> {
|
||||
return localVarFp.membersCreate(memberCreate, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary get member\'s profile
|
||||
* @param {string} memberId member identifier
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
membersGetProfile(memberId: string, options?: any): AxiosPromise<MemberProfile> {
|
||||
return localVarFp.membersGetProfile(memberId, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary list of members
|
||||
* @param {number} [limit]
|
||||
* @param {number} [offset]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
membersList(limit?: number, offset?: number, options?: any): AxiosPromise<Array<MemberData>> {
|
||||
return localVarFp.membersList(limit, offset, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary remove member
|
||||
* @param {string} memberId member identifier
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
membersRemove(memberId: string, options?: any): AxiosPromise<void> {
|
||||
return localVarFp.membersRemove(memberId, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary update member\'s password
|
||||
* @param {string} memberId member identifier
|
||||
* @param {MemberPassword} memberPassword
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
membersUpdatePassword(memberId: string, memberPassword: MemberPassword, options?: any): AxiosPromise<void> {
|
||||
return localVarFp.membersUpdatePassword(memberId, memberPassword, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary update member\'s profile
|
||||
* @param {string} memberId member identifier
|
||||
* @param {MemberProfile} memberProfile
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
membersUpdateProfile(memberId: string, memberProfile: MemberProfile, options?: any): AxiosPromise<void> {
|
||||
return localVarFp.membersUpdateProfile(memberId, memberProfile, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* MembersApi - object-oriented interface
|
||||
* @export
|
||||
* @class MembersApi
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class MembersApi extends BaseAPI {
|
||||
/**
|
||||
*
|
||||
* @summary bulk delete members
|
||||
* @param {MemberBulkDelete} memberBulkDelete
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof MembersApi
|
||||
*/
|
||||
public membersBulkDelete(memberBulkDelete: MemberBulkDelete, options?: RawAxiosRequestConfig) {
|
||||
return MembersApiFp(this.configuration).membersBulkDelete(memberBulkDelete, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary bulk update members
|
||||
* @param {MemberBulkUpdate} memberBulkUpdate
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof MembersApi
|
||||
*/
|
||||
public membersBulkUpdate(memberBulkUpdate: MemberBulkUpdate, options?: RawAxiosRequestConfig) {
|
||||
return MembersApiFp(this.configuration).membersBulkUpdate(memberBulkUpdate, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary create new member
|
||||
* @param {MemberCreate} memberCreate
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof MembersApi
|
||||
*/
|
||||
public membersCreate(memberCreate: MemberCreate, options?: RawAxiosRequestConfig) {
|
||||
return MembersApiFp(this.configuration).membersCreate(memberCreate, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary get member\'s profile
|
||||
* @param {string} memberId member identifier
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof MembersApi
|
||||
*/
|
||||
public membersGetProfile(memberId: string, options?: RawAxiosRequestConfig) {
|
||||
return MembersApiFp(this.configuration).membersGetProfile(memberId, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary list of members
|
||||
* @param {number} [limit]
|
||||
* @param {number} [offset]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof MembersApi
|
||||
*/
|
||||
public membersList(limit?: number, offset?: number, options?: RawAxiosRequestConfig) {
|
||||
return MembersApiFp(this.configuration).membersList(limit, offset, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary remove member
|
||||
* @param {string} memberId member identifier
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof MembersApi
|
||||
*/
|
||||
public membersRemove(memberId: string, options?: RawAxiosRequestConfig) {
|
||||
return MembersApiFp(this.configuration).membersRemove(memberId, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary update member\'s password
|
||||
* @param {string} memberId member identifier
|
||||
* @param {MemberPassword} memberPassword
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof MembersApi
|
||||
*/
|
||||
public membersUpdatePassword(memberId: string, memberPassword: MemberPassword, options?: RawAxiosRequestConfig) {
|
||||
return MembersApiFp(this.configuration).membersUpdatePassword(memberId, memberPassword, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary update member\'s profile
|
||||
* @param {string} memberId member identifier
|
||||
* @param {MemberProfile} memberProfile
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof MembersApi
|
||||
*/
|
||||
public membersUpdateProfile(memberId: string, memberProfile: MemberProfile, options?: RawAxiosRequestConfig) {
|
||||
return MembersApiFp(this.configuration).membersUpdateProfile(memberId, memberProfile, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
2027
client/src/component/api/api/room-api.ts
Normal file
2027
client/src/component/api/api/room-api.ts
Normal file
File diff suppressed because it is too large
Load Diff
369
client/src/component/api/api/sessions-api.ts
Normal file
369
client/src/component/api/api/sessions-api.ts
Normal file
@ -0,0 +1,369 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
import type { Configuration } from '../configuration';
|
||||
import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';
|
||||
import globalAxios from 'axios';
|
||||
// Some imports not used depending on template conditions
|
||||
// @ts-ignore
|
||||
import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common';
|
||||
// @ts-ignore
|
||||
import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';
|
||||
// @ts-ignore
|
||||
import type { ErrorMessage } from '../models';
|
||||
// @ts-ignore
|
||||
import type { SessionData } from '../models';
|
||||
/**
|
||||
* SessionsApi - axios parameter creator
|
||||
* @export
|
||||
*/
|
||||
export const SessionsApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @summary disconnect session
|
||||
* @param {string} sessionId session identifier
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
sessionDisconnect: async (sessionId: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'sessionId' is not null or undefined
|
||||
assertParamExists('sessionDisconnect', 'sessionId', sessionId)
|
||||
const localVarPath = `/api/sessions/{sessionId}/disconnect`
|
||||
.replace(`{${"sessionId"}}`, encodeURIComponent(String(sessionId)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication CookieAuth required
|
||||
|
||||
// authentication TokenAuth required
|
||||
await setApiKeyToObject(localVarQueryParameter, "token", configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary get session
|
||||
* @param {string} sessionId session identifier
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
sessionGet: async (sessionId: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'sessionId' is not null or undefined
|
||||
assertParamExists('sessionGet', 'sessionId', sessionId)
|
||||
const localVarPath = `/api/sessions/{sessionId}`
|
||||
.replace(`{${"sessionId"}}`, encodeURIComponent(String(sessionId)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication CookieAuth required
|
||||
|
||||
// authentication TokenAuth required
|
||||
await setApiKeyToObject(localVarQueryParameter, "token", configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary remove session
|
||||
* @param {string} sessionId session identifier
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
sessionRemove: async (sessionId: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'sessionId' is not null or undefined
|
||||
assertParamExists('sessionRemove', 'sessionId', sessionId)
|
||||
const localVarPath = `/api/sessions/{sessionId}`
|
||||
.replace(`{${"sessionId"}}`, encodeURIComponent(String(sessionId)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication CookieAuth required
|
||||
|
||||
// authentication TokenAuth required
|
||||
await setApiKeyToObject(localVarQueryParameter, "token", configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary get sessions
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
sessionsGet: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/api/sessions`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication CookieAuth required
|
||||
|
||||
// authentication TokenAuth required
|
||||
await setApiKeyToObject(localVarQueryParameter, "token", configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* SessionsApi - functional programming interface
|
||||
* @export
|
||||
*/
|
||||
export const SessionsApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = SessionsApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @summary disconnect session
|
||||
* @param {string} sessionId session identifier
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async sessionDisconnect(sessionId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.sessionDisconnect(sessionId, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['SessionsApi.sessionDisconnect']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary get session
|
||||
* @param {string} sessionId session identifier
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async sessionGet(sessionId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SessionData>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.sessionGet(sessionId, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['SessionsApi.sessionGet']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary remove session
|
||||
* @param {string} sessionId session identifier
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async sessionRemove(sessionId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.sessionRemove(sessionId, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['SessionsApi.sessionRemove']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary get sessions
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async sessionsGet(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<SessionData>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.sessionsGet(options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['SessionsApi.sessionsGet']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* SessionsApi - factory interface
|
||||
* @export
|
||||
*/
|
||||
export const SessionsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||
const localVarFp = SessionsApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @summary disconnect session
|
||||
* @param {string} sessionId session identifier
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
sessionDisconnect(sessionId: string, options?: any): AxiosPromise<void> {
|
||||
return localVarFp.sessionDisconnect(sessionId, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary get session
|
||||
* @param {string} sessionId session identifier
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
sessionGet(sessionId: string, options?: any): AxiosPromise<SessionData> {
|
||||
return localVarFp.sessionGet(sessionId, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary remove session
|
||||
* @param {string} sessionId session identifier
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
sessionRemove(sessionId: string, options?: any): AxiosPromise<void> {
|
||||
return localVarFp.sessionRemove(sessionId, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary get sessions
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
sessionsGet(options?: any): AxiosPromise<Array<SessionData>> {
|
||||
return localVarFp.sessionsGet(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* SessionsApi - object-oriented interface
|
||||
* @export
|
||||
* @class SessionsApi
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class SessionsApi extends BaseAPI {
|
||||
/**
|
||||
*
|
||||
* @summary disconnect session
|
||||
* @param {string} sessionId session identifier
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof SessionsApi
|
||||
*/
|
||||
public sessionDisconnect(sessionId: string, options?: RawAxiosRequestConfig) {
|
||||
return SessionsApiFp(this.configuration).sessionDisconnect(sessionId, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary get session
|
||||
* @param {string} sessionId session identifier
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof SessionsApi
|
||||
*/
|
||||
public sessionGet(sessionId: string, options?: RawAxiosRequestConfig) {
|
||||
return SessionsApiFp(this.configuration).sessionGet(sessionId, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary remove session
|
||||
* @param {string} sessionId session identifier
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof SessionsApi
|
||||
*/
|
||||
public sessionRemove(sessionId: string, options?: RawAxiosRequestConfig) {
|
||||
return SessionsApiFp(this.configuration).sessionRemove(sessionId, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary get sessions
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof SessionsApi
|
||||
*/
|
||||
public sessionsGet(options?: RawAxiosRequestConfig) {
|
||||
return SessionsApiFp(this.configuration).sessionsGet(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
86
client/src/component/api/base.ts
Normal file
86
client/src/component/api/base.ts
Normal file
@ -0,0 +1,86 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
import type { Configuration } from './configuration';
|
||||
// Some imports not used depending on template conditions
|
||||
// @ts-ignore
|
||||
import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';
|
||||
import globalAxios from 'axios';
|
||||
|
||||
export const BASE_PATH = "http://localhost:3000".replace(/\/+$/, "");
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const COLLECTION_FORMATS = {
|
||||
csv: ",",
|
||||
ssv: " ",
|
||||
tsv: "\t",
|
||||
pipes: "|",
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface RequestArgs
|
||||
*/
|
||||
export interface RequestArgs {
|
||||
url: string;
|
||||
options: RawAxiosRequestConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @class BaseAPI
|
||||
*/
|
||||
export class BaseAPI {
|
||||
protected configuration: Configuration | undefined;
|
||||
|
||||
constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) {
|
||||
if (configuration) {
|
||||
this.configuration = configuration;
|
||||
this.basePath = configuration.basePath ?? basePath;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @class RequiredError
|
||||
* @extends {Error}
|
||||
*/
|
||||
export class RequiredError extends Error {
|
||||
constructor(public field: string, msg?: string) {
|
||||
super(msg);
|
||||
this.name = "RequiredError"
|
||||
}
|
||||
}
|
||||
|
||||
interface ServerMap {
|
||||
[key: string]: {
|
||||
url: string,
|
||||
description: string,
|
||||
}[];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const operationServerMap: ServerMap = {
|
||||
}
|
150
client/src/component/api/common.ts
Normal file
150
client/src/component/api/common.ts
Normal file
@ -0,0 +1,150 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
import type { Configuration } from "./configuration";
|
||||
import type { RequestArgs } from "./base";
|
||||
import type { AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { RequiredError } from "./base";
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const DUMMY_BASE_URL = 'https://example.com'
|
||||
|
||||
/**
|
||||
*
|
||||
* @throws {RequiredError}
|
||||
* @export
|
||||
*/
|
||||
export const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) {
|
||||
if (paramValue === null || paramValue === undefined) {
|
||||
throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) {
|
||||
if (configuration && configuration.apiKey) {
|
||||
const localVarApiKeyValue = typeof configuration.apiKey === 'function'
|
||||
? await configuration.apiKey(keyParamName)
|
||||
: await configuration.apiKey;
|
||||
object[keyParamName] = localVarApiKeyValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const setBasicAuthToObject = function (object: any, configuration?: Configuration) {
|
||||
if (configuration && (configuration.username || configuration.password)) {
|
||||
object["auth"] = { username: configuration.username, password: configuration.password };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) {
|
||||
if (configuration && configuration.accessToken) {
|
||||
const accessToken = typeof configuration.accessToken === 'function'
|
||||
? await configuration.accessToken()
|
||||
: await configuration.accessToken;
|
||||
object["Authorization"] = "Bearer " + accessToken;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) {
|
||||
if (configuration && configuration.accessToken) {
|
||||
const localVarAccessTokenValue = typeof configuration.accessToken === 'function'
|
||||
? await configuration.accessToken(name, scopes)
|
||||
: await configuration.accessToken;
|
||||
object["Authorization"] = "Bearer " + localVarAccessTokenValue;
|
||||
}
|
||||
}
|
||||
|
||||
function setFlattenedQueryParams(urlSearchParams: URLSearchParams, parameter: any, key: string = ""): void {
|
||||
if (parameter == null) return;
|
||||
if (typeof parameter === "object") {
|
||||
if (Array.isArray(parameter)) {
|
||||
(parameter as any[]).forEach(item => setFlattenedQueryParams(urlSearchParams, item, key));
|
||||
}
|
||||
else {
|
||||
Object.keys(parameter).forEach(currentKey =>
|
||||
setFlattenedQueryParams(urlSearchParams, parameter[currentKey], `${key}${key !== '' ? '.' : ''}${currentKey}`)
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (urlSearchParams.has(key)) {
|
||||
urlSearchParams.append(key, parameter);
|
||||
}
|
||||
else {
|
||||
urlSearchParams.set(key, parameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const setSearchParams = function (url: URL, ...objects: any[]) {
|
||||
const searchParams = new URLSearchParams(url.search);
|
||||
setFlattenedQueryParams(searchParams, objects);
|
||||
url.search = searchParams.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) {
|
||||
const nonString = typeof value !== 'string';
|
||||
const needsSerialization = nonString && configuration && configuration.isJsonMime
|
||||
? configuration.isJsonMime(requestOptions.headers['Content-Type'])
|
||||
: nonString;
|
||||
return needsSerialization
|
||||
? JSON.stringify(value !== undefined ? value : {})
|
||||
: (value || "");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const toPathString = function (url: URL) {
|
||||
return url.pathname + url.search + url.hash
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) {
|
||||
return <T = unknown, R = AxiosResponse<T>>(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
|
||||
const axiosRequestArgs = {...axiosArgs.options, url: (axios.defaults.baseURL ? '' : configuration?.basePath ?? basePath) + axiosArgs.url};
|
||||
return axios.request<T, R>(axiosRequestArgs);
|
||||
};
|
||||
}
|
110
client/src/component/api/configuration.ts
Normal file
110
client/src/component/api/configuration.ts
Normal file
@ -0,0 +1,110 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
export interface ConfigurationParameters {
|
||||
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
|
||||
username?: string;
|
||||
password?: string;
|
||||
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
|
||||
basePath?: string;
|
||||
serverIndex?: number;
|
||||
baseOptions?: any;
|
||||
formDataCtor?: new () => any;
|
||||
}
|
||||
|
||||
export class Configuration {
|
||||
/**
|
||||
* parameter for apiKey security
|
||||
* @param name security name
|
||||
* @memberof Configuration
|
||||
*/
|
||||
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
|
||||
/**
|
||||
* parameter for basic security
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Configuration
|
||||
*/
|
||||
username?: string;
|
||||
/**
|
||||
* parameter for basic security
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Configuration
|
||||
*/
|
||||
password?: string;
|
||||
/**
|
||||
* parameter for oauth2 security
|
||||
* @param name security name
|
||||
* @param scopes oauth2 scope
|
||||
* @memberof Configuration
|
||||
*/
|
||||
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
|
||||
/**
|
||||
* override base path
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Configuration
|
||||
*/
|
||||
basePath?: string;
|
||||
/**
|
||||
* override server index
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof Configuration
|
||||
*/
|
||||
serverIndex?: number;
|
||||
/**
|
||||
* base options for axios calls
|
||||
*
|
||||
* @type {any}
|
||||
* @memberof Configuration
|
||||
*/
|
||||
baseOptions?: any;
|
||||
/**
|
||||
* The FormData constructor that will be used to create multipart form data
|
||||
* requests. You can inject this here so that execution environments that
|
||||
* do not support the FormData class can still run the generated client.
|
||||
*
|
||||
* @type {new () => FormData}
|
||||
*/
|
||||
formDataCtor?: new () => any;
|
||||
|
||||
constructor(param: ConfigurationParameters = {}) {
|
||||
this.apiKey = param.apiKey;
|
||||
this.username = param.username;
|
||||
this.password = param.password;
|
||||
this.accessToken = param.accessToken;
|
||||
this.basePath = param.basePath;
|
||||
this.serverIndex = param.serverIndex;
|
||||
this.baseOptions = param.baseOptions;
|
||||
this.formDataCtor = param.formDataCtor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given MIME is a JSON MIME.
|
||||
* JSON MIME examples:
|
||||
* application/json
|
||||
* application/json; charset=UTF8
|
||||
* APPLICATION/JSON
|
||||
* application/vnd.company+json
|
||||
* @param mime - MIME (Multipurpose Internet Mail Extensions)
|
||||
* @return True if the given MIME is JSON, false otherwise.
|
||||
*/
|
||||
public isJsonMime(mime: string): boolean {
|
||||
const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i');
|
||||
return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json');
|
||||
}
|
||||
}
|
18
client/src/component/api/index.ts
Normal file
18
client/src/component/api/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
export * from "./api";
|
||||
export * from "./configuration";
|
||||
export * from "./models";
|
51
client/src/component/api/models/batch-request.ts
Normal file
51
client/src/component/api/models/batch-request.ts
Normal file
@ -0,0 +1,51 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface BatchRequest
|
||||
*/
|
||||
export interface BatchRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof BatchRequest
|
||||
*/
|
||||
'path'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof BatchRequest
|
||||
*/
|
||||
'method'?: BatchRequestMethodEnum;
|
||||
/**
|
||||
* Request body
|
||||
* @type {any}
|
||||
* @memberof BatchRequest
|
||||
*/
|
||||
'body'?: any;
|
||||
}
|
||||
|
||||
export const BatchRequestMethodEnum = {
|
||||
GET: 'GET',
|
||||
POST: 'POST',
|
||||
DELETE: 'DELETE'
|
||||
} as const;
|
||||
|
||||
export type BatchRequestMethodEnum = typeof BatchRequestMethodEnum[keyof typeof BatchRequestMethodEnum];
|
||||
|
||||
|
57
client/src/component/api/models/batch-response.ts
Normal file
57
client/src/component/api/models/batch-response.ts
Normal file
@ -0,0 +1,57 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface BatchResponse
|
||||
*/
|
||||
export interface BatchResponse {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof BatchResponse
|
||||
*/
|
||||
'path'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof BatchResponse
|
||||
*/
|
||||
'method'?: BatchResponseMethodEnum;
|
||||
/**
|
||||
* Response body
|
||||
* @type {any}
|
||||
* @memberof BatchResponse
|
||||
*/
|
||||
'body'?: any;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof BatchResponse
|
||||
*/
|
||||
'status'?: number;
|
||||
}
|
||||
|
||||
export const BatchResponseMethodEnum = {
|
||||
GET: 'GET',
|
||||
POST: 'POST',
|
||||
DELETE: 'DELETE'
|
||||
} as const;
|
||||
|
||||
export type BatchResponseMethodEnum = typeof BatchResponseMethodEnum[keyof typeof BatchResponseMethodEnum];
|
||||
|
||||
|
36
client/src/component/api/models/broadcast-status.ts
Normal file
36
client/src/component/api/models/broadcast-status.ts
Normal file
@ -0,0 +1,36 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface BroadcastStatus
|
||||
*/
|
||||
export interface BroadcastStatus {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof BroadcastStatus
|
||||
*/
|
||||
'url'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof BroadcastStatus
|
||||
*/
|
||||
'is_active'?: boolean;
|
||||
}
|
||||
|
36
client/src/component/api/models/clipboard-text.ts
Normal file
36
client/src/component/api/models/clipboard-text.ts
Normal file
@ -0,0 +1,36 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ClipboardText
|
||||
*/
|
||||
export interface ClipboardText {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ClipboardText
|
||||
*/
|
||||
'text'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ClipboardText
|
||||
*/
|
||||
'html'?: string;
|
||||
}
|
||||
|
36
client/src/component/api/models/control-status.ts
Normal file
36
client/src/component/api/models/control-status.ts
Normal file
@ -0,0 +1,36 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ControlStatus
|
||||
*/
|
||||
export interface ControlStatus {
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof ControlStatus
|
||||
*/
|
||||
'has_host'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ControlStatus
|
||||
*/
|
||||
'host_id'?: string;
|
||||
}
|
||||
|
30
client/src/component/api/models/error-message.ts
Normal file
30
client/src/component/api/models/error-message.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ErrorMessage
|
||||
*/
|
||||
export interface ErrorMessage {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ErrorMessage
|
||||
*/
|
||||
'message'?: string;
|
||||
}
|
||||
|
19
client/src/component/api/models/index.ts
Normal file
19
client/src/component/api/models/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export * from './batch-request';
|
||||
export * from './batch-response';
|
||||
export * from './broadcast-status';
|
||||
export * from './clipboard-text';
|
||||
export * from './control-status';
|
||||
export * from './error-message';
|
||||
export * from './keyboard-map';
|
||||
export * from './keyboard-modifiers';
|
||||
export * from './member-bulk-delete';
|
||||
export * from './member-bulk-update';
|
||||
export * from './member-create';
|
||||
export * from './member-data';
|
||||
export * from './member-password';
|
||||
export * from './member-profile';
|
||||
export * from './screen-configuration';
|
||||
export * from './session-data';
|
||||
export * from './session-login';
|
||||
export * from './session-state';
|
||||
export * from './settings';
|
36
client/src/component/api/models/keyboard-map.ts
Normal file
36
client/src/component/api/models/keyboard-map.ts
Normal file
@ -0,0 +1,36 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface KeyboardMap
|
||||
*/
|
||||
export interface KeyboardMap {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof KeyboardMap
|
||||
*/
|
||||
'layout'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof KeyboardMap
|
||||
*/
|
||||
'variant'?: string;
|
||||
}
|
||||
|
72
client/src/component/api/models/keyboard-modifiers.ts
Normal file
72
client/src/component/api/models/keyboard-modifiers.ts
Normal file
@ -0,0 +1,72 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface KeyboardModifiers
|
||||
*/
|
||||
export interface KeyboardModifiers {
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof KeyboardModifiers
|
||||
*/
|
||||
'shift'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof KeyboardModifiers
|
||||
*/
|
||||
'capslock'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof KeyboardModifiers
|
||||
*/
|
||||
'control'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof KeyboardModifiers
|
||||
*/
|
||||
'alt'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof KeyboardModifiers
|
||||
*/
|
||||
'numlock'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof KeyboardModifiers
|
||||
*/
|
||||
'meta'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof KeyboardModifiers
|
||||
*/
|
||||
'super'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof KeyboardModifiers
|
||||
*/
|
||||
'altgr'?: boolean;
|
||||
}
|
||||
|
30
client/src/component/api/models/member-bulk-delete.ts
Normal file
30
client/src/component/api/models/member-bulk-delete.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface MemberBulkDelete
|
||||
*/
|
||||
export interface MemberBulkDelete {
|
||||
/**
|
||||
*
|
||||
* @type {Array<string>}
|
||||
* @memberof MemberBulkDelete
|
||||
*/
|
||||
'ids'?: Array<string>;
|
||||
}
|
||||
|
39
client/src/component/api/models/member-bulk-update.ts
Normal file
39
client/src/component/api/models/member-bulk-update.ts
Normal file
@ -0,0 +1,39 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { MemberProfile } from './member-profile';
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface MemberBulkUpdate
|
||||
*/
|
||||
export interface MemberBulkUpdate {
|
||||
/**
|
||||
*
|
||||
* @type {Array<string>}
|
||||
* @memberof MemberBulkUpdate
|
||||
*/
|
||||
'ids'?: Array<string>;
|
||||
/**
|
||||
*
|
||||
* @type {MemberProfile}
|
||||
* @memberof MemberBulkUpdate
|
||||
*/
|
||||
'profile'?: MemberProfile;
|
||||
}
|
||||
|
45
client/src/component/api/models/member-create.ts
Normal file
45
client/src/component/api/models/member-create.ts
Normal file
@ -0,0 +1,45 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { MemberProfile } from './member-profile';
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface MemberCreate
|
||||
*/
|
||||
export interface MemberCreate {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof MemberCreate
|
||||
*/
|
||||
'username'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof MemberCreate
|
||||
*/
|
||||
'password'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {MemberProfile}
|
||||
* @memberof MemberCreate
|
||||
*/
|
||||
'profile'?: MemberProfile;
|
||||
}
|
||||
|
39
client/src/component/api/models/member-data.ts
Normal file
39
client/src/component/api/models/member-data.ts
Normal file
@ -0,0 +1,39 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { MemberProfile } from './member-profile';
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface MemberData
|
||||
*/
|
||||
export interface MemberData {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof MemberData
|
||||
*/
|
||||
'id'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {MemberProfile}
|
||||
* @memberof MemberData
|
||||
*/
|
||||
'profile'?: MemberProfile;
|
||||
}
|
||||
|
30
client/src/component/api/models/member-password.ts
Normal file
30
client/src/component/api/models/member-password.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface MemberPassword
|
||||
*/
|
||||
export interface MemberPassword {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof MemberPassword
|
||||
*/
|
||||
'password'?: string;
|
||||
}
|
||||
|
90
client/src/component/api/models/member-profile.ts
Normal file
90
client/src/component/api/models/member-profile.ts
Normal file
@ -0,0 +1,90 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface MemberProfile
|
||||
*/
|
||||
export interface MemberProfile {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof MemberProfile
|
||||
*/
|
||||
'name'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof MemberProfile
|
||||
*/
|
||||
'is_admin'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof MemberProfile
|
||||
*/
|
||||
'can_login'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof MemberProfile
|
||||
*/
|
||||
'can_connect'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof MemberProfile
|
||||
*/
|
||||
'can_watch'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof MemberProfile
|
||||
*/
|
||||
'can_host'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof MemberProfile
|
||||
*/
|
||||
'can_share_media'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof MemberProfile
|
||||
*/
|
||||
'can_access_clipboard'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof MemberProfile
|
||||
*/
|
||||
'sends_inactive_cursor'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof MemberProfile
|
||||
*/
|
||||
'can_see_inactive_cursors'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {{ [key: string]: any; }}
|
||||
* @memberof MemberProfile
|
||||
*/
|
||||
'plugins'?: { [key: string]: any; };
|
||||
}
|
||||
|
42
client/src/component/api/models/screen-configuration.ts
Normal file
42
client/src/component/api/models/screen-configuration.ts
Normal file
@ -0,0 +1,42 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ScreenConfiguration
|
||||
*/
|
||||
export interface ScreenConfiguration {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof ScreenConfiguration
|
||||
*/
|
||||
'width'?: number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof ScreenConfiguration
|
||||
*/
|
||||
'height'?: number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof ScreenConfiguration
|
||||
*/
|
||||
'rate'?: number;
|
||||
}
|
||||
|
54
client/src/component/api/models/session-data.ts
Normal file
54
client/src/component/api/models/session-data.ts
Normal file
@ -0,0 +1,54 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { MemberProfile } from './member-profile';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { SessionState } from './session-state';
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface SessionData
|
||||
*/
|
||||
export interface SessionData {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof SessionData
|
||||
*/
|
||||
'id'?: string;
|
||||
/**
|
||||
* Only if cookie authentication is disabled.
|
||||
* @type {string}
|
||||
* @memberof SessionData
|
||||
*/
|
||||
'token'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {MemberProfile}
|
||||
* @memberof SessionData
|
||||
*/
|
||||
'profile'?: MemberProfile;
|
||||
/**
|
||||
*
|
||||
* @type {SessionState}
|
||||
* @memberof SessionData
|
||||
*/
|
||||
'state'?: SessionState;
|
||||
}
|
||||
|
36
client/src/component/api/models/session-login.ts
Normal file
36
client/src/component/api/models/session-login.ts
Normal file
@ -0,0 +1,36 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface SessionLogin
|
||||
*/
|
||||
export interface SessionLogin {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof SessionLogin
|
||||
*/
|
||||
'username'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof SessionLogin
|
||||
*/
|
||||
'password'?: string;
|
||||
}
|
||||
|
36
client/src/component/api/models/session-state.ts
Normal file
36
client/src/component/api/models/session-state.ts
Normal file
@ -0,0 +1,36 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface SessionState
|
||||
*/
|
||||
export interface SessionState {
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof SessionState
|
||||
*/
|
||||
'is_connected'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof SessionState
|
||||
*/
|
||||
'is_watching'?: boolean;
|
||||
}
|
||||
|
60
client/src/component/api/models/settings.ts
Normal file
60
client/src/component/api/models/settings.ts
Normal file
@ -0,0 +1,60 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* n.eko REST API
|
||||
* Next Gen Renderer.
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface Settings
|
||||
*/
|
||||
export interface Settings {
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof Settings
|
||||
*/
|
||||
'private_mode'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof Settings
|
||||
*/
|
||||
'locked_controls'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof Settings
|
||||
*/
|
||||
'implicit_hosting'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof Settings
|
||||
*/
|
||||
'inactive_cursors'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof Settings
|
||||
*/
|
||||
'merciful_reconnect'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {{ [key: string]: any; }}
|
||||
* @memberof Settings
|
||||
*/
|
||||
'plugins'?: { [key: string]: any; };
|
||||
}
|
||||
|
266
client/src/component/cursors.vue
Normal file
266
client/src/component/cursors.vue
Normal file
@ -0,0 +1,266 @@
|
||||
<template>
|
||||
<canvas ref="overlay" class="neko-cursors" tabindex="0" />
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.neko-cursors {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
outline: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
|
||||
import type { SessionCursors, Cursor, Session } from './types/state'
|
||||
import type { InactiveCursorDrawFunction, Dimension } from './types/cursors'
|
||||
import { getMovementXYatPercent } from './utils/canvas-movement'
|
||||
|
||||
// How often are position data arriving
|
||||
const POS_INTERVAL_MS = 750
|
||||
// How many pixel change is considered as movement
|
||||
const POS_THRESHOLD_PX = 20
|
||||
|
||||
const props = defineProps<{
|
||||
sessions: Record<string, Session>
|
||||
sessionId: string
|
||||
hostId: string | null
|
||||
screenSize: Dimension
|
||||
canvasSize: Dimension
|
||||
cursors: SessionCursors[]
|
||||
cursorDraw: InactiveCursorDrawFunction | null
|
||||
fps: number
|
||||
}>()
|
||||
|
||||
const overlay = ref<HTMLCanvasElement | null>(null)
|
||||
|
||||
let ctx: CanvasRenderingContext2D | null = null
|
||||
let canvasScale = window.devicePixelRatio
|
||||
let unsubscribePixelRatioChange = null as (() => void) | null
|
||||
|
||||
onMounted(() => {
|
||||
// get canvas overlay context
|
||||
const canvas = overlay.value
|
||||
if (canvas != null) {
|
||||
ctx = canvas.getContext('2d')
|
||||
|
||||
// synchronize intrinsic with extrinsic dimensions
|
||||
const { width, height } = canvas.getBoundingClientRect()
|
||||
canvasResize({ width, height })
|
||||
}
|
||||
|
||||
// react to pixel ratio changes
|
||||
onPixelRatioChange()
|
||||
|
||||
// store last drawing points
|
||||
last_points = {}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// stop pixel ratio change listener
|
||||
if (unsubscribePixelRatioChange) {
|
||||
unsubscribePixelRatioChange()
|
||||
}
|
||||
})
|
||||
|
||||
function onPixelRatioChange() {
|
||||
if (unsubscribePixelRatioChange) {
|
||||
unsubscribePixelRatioChange()
|
||||
}
|
||||
|
||||
const media = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
|
||||
media.addEventListener('change', onPixelRatioChange)
|
||||
unsubscribePixelRatioChange = () => {
|
||||
media.removeEventListener('change', onPixelRatioChange)
|
||||
}
|
||||
|
||||
canvasScale = window.devicePixelRatio
|
||||
onCanvasSizeChange(props.canvasSize)
|
||||
}
|
||||
|
||||
function onCanvasSizeChange({ width, height }: Dimension) {
|
||||
canvasResize({ width, height })
|
||||
canvasUpdateCursors()
|
||||
}
|
||||
|
||||
watch(() => props.canvasSize, onCanvasSizeChange)
|
||||
|
||||
function canvasResize({ width, height }: Dimension) {
|
||||
overlay.value!.width = width * canvasScale
|
||||
overlay.value!.height = height * canvasScale
|
||||
ctx?.setTransform(canvasScale, 0, 0, canvasScale, 0, 0)
|
||||
}
|
||||
|
||||
// start as undefined to prevent jumping
|
||||
let last_animation_time = 0
|
||||
// current animation progress (0-1)
|
||||
let percent = 0
|
||||
// points to be animated for each session
|
||||
let points: SessionCursors[] = []
|
||||
// last points coordinates for each session
|
||||
let last_points: Record<string, Cursor> = {}
|
||||
|
||||
function canvasAnimateFrame(now: number = NaN) {
|
||||
// request another frame
|
||||
if (percent <= 1) window.requestAnimationFrame(canvasAnimateFrame)
|
||||
|
||||
// calc elapsed time since last loop
|
||||
const elapsed = now - last_animation_time
|
||||
|
||||
// skip if fps is set and elapsed time is less than fps
|
||||
if (props.fps > 0 && elapsed < 1000 / props.fps) return
|
||||
|
||||
// calc current animation progress
|
||||
const delta = elapsed / POS_INTERVAL_MS
|
||||
last_animation_time = now
|
||||
|
||||
// skip very first delta to prevent jumping
|
||||
if (isNaN(delta)) return
|
||||
|
||||
// set the animation position
|
||||
percent += delta
|
||||
|
||||
// draw points for current frame
|
||||
canvasDrawPoints(percent)
|
||||
}
|
||||
|
||||
function canvasDrawPoints(percent: number = 1) {
|
||||
// clear canvas
|
||||
canvasClear()
|
||||
|
||||
// draw current position
|
||||
for (const p of points) {
|
||||
const { x, y } = getMovementXYatPercent(p.cursors, percent)
|
||||
canvasDrawCursor(x, y, p.id)
|
||||
}
|
||||
}
|
||||
|
||||
function canvasUpdateCursors() {
|
||||
let new_last_points = {} as Record<string, Cursor>
|
||||
|
||||
// track unchanged cursors
|
||||
let unchanged = 0
|
||||
|
||||
// create points for animation
|
||||
points = []
|
||||
for (const { id, cursors } of props.cursors) {
|
||||
if (
|
||||
// if there are no positions
|
||||
cursors.length == 0 ||
|
||||
// ignore own cursor
|
||||
id == props.sessionId ||
|
||||
// ignore host's cursor
|
||||
id == props.hostId
|
||||
) {
|
||||
unchanged++
|
||||
continue
|
||||
}
|
||||
|
||||
// get last point
|
||||
const new_last_point = cursors[cursors.length - 1]
|
||||
|
||||
// add last cursor position to cursors (if available)
|
||||
let pos = { id } as SessionCursors
|
||||
if (id in last_points) {
|
||||
const last_point = last_points[id]
|
||||
|
||||
// if cursor did not move considerably
|
||||
if (
|
||||
Math.abs(last_point.x - new_last_point.x) < POS_THRESHOLD_PX &&
|
||||
Math.abs(last_point.y - new_last_point.y) < POS_THRESHOLD_PX
|
||||
) {
|
||||
// we knew that this cursor did not change, but other
|
||||
// might, so we keep only one point to be drawn
|
||||
pos.cursors = [new_last_point]
|
||||
// and increase unchanged counter
|
||||
unchanged++
|
||||
} else {
|
||||
// if cursor moved, we want to include last point
|
||||
// in the animation, so that movement can be seamless
|
||||
pos.cursors = [last_point, ...cursors]
|
||||
}
|
||||
} else {
|
||||
// if cursor does not have last point, it is not
|
||||
// displayed in canvas and it should be now
|
||||
pos.cursors = [...cursors]
|
||||
}
|
||||
|
||||
new_last_points[id] = new_last_point
|
||||
points.push(pos)
|
||||
}
|
||||
|
||||
// apply new last points
|
||||
last_points = new_last_points
|
||||
|
||||
// no cursors to animate
|
||||
if (points.length == 0) {
|
||||
canvasClear()
|
||||
return
|
||||
}
|
||||
|
||||
// if all cursors are unchanged
|
||||
if (unchanged == props.cursors.length) {
|
||||
// draw only last known position without animation
|
||||
canvasDrawPoints()
|
||||
return
|
||||
}
|
||||
|
||||
// start animation if not running
|
||||
const p = percent
|
||||
percent = 0
|
||||
if (p > 1 || !p) {
|
||||
canvasAnimateFrame()
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.hostId, canvasUpdateCursors)
|
||||
watch(() => props.cursors, canvasUpdateCursors)
|
||||
|
||||
function canvasDrawCursor(x: number, y: number, id: string) {
|
||||
if (!ctx) return
|
||||
|
||||
// get intrinsic dimensions
|
||||
const { width, height } = props.canvasSize
|
||||
x = Math.round((x / props.screenSize.width) * width)
|
||||
y = Math.round((y / props.screenSize.height) * height)
|
||||
|
||||
// reset transformation, X and Y will be 0 again
|
||||
ctx.setTransform(canvasScale, 0, 0, canvasScale, 0, 0)
|
||||
|
||||
// use custom draw function, if available
|
||||
if (props.cursorDraw) {
|
||||
props.cursorDraw(ctx, x, y, id)
|
||||
return
|
||||
}
|
||||
|
||||
// get cursor tag
|
||||
const cursorTag = props.sessions[id]?.profile.name || ''
|
||||
|
||||
// draw inactive cursor tag
|
||||
ctx.font = '14px Arial, sans-serif'
|
||||
ctx.textBaseline = 'top'
|
||||
ctx.shadowColor = 'black'
|
||||
ctx.shadowBlur = 2
|
||||
ctx.lineWidth = 2
|
||||
ctx.fillStyle = 'black'
|
||||
ctx.strokeText(cursorTag, x, y)
|
||||
ctx.shadowBlur = 0
|
||||
ctx.fillStyle = 'white'
|
||||
ctx.fillText(cursorTag, x, y)
|
||||
}
|
||||
|
||||
function canvasClear() {
|
||||
if (!ctx) return
|
||||
|
||||
// reset transformation, X and Y will be 0 again
|
||||
ctx.setTransform(canvasScale, 0, 0, canvasScale, 0, 0)
|
||||
|
||||
const { width, height } = props.canvasSize
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
}
|
||||
</script>
|
41
client/src/component/internal/api.ts
Normal file
41
client/src/component/internal/api.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import * as Api from '../api'
|
||||
|
||||
export class NekoApi {
|
||||
public readonly config = new Api.Configuration({
|
||||
basePath: location.href.replace(/\/+$/, ''),
|
||||
baseOptions: { withCredentials: true },
|
||||
})
|
||||
|
||||
public setUrl(url: string) {
|
||||
this.config.basePath = url.replace(/\/+$/, '')
|
||||
}
|
||||
|
||||
public setToken(token: string) {
|
||||
this.config.accessToken = token
|
||||
}
|
||||
|
||||
get url(): string {
|
||||
return this.config.basePath || location.href.replace(/\/+$/, '')
|
||||
}
|
||||
|
||||
get default(): DefaultApi {
|
||||
return new Api.DefaultApi(this.config)
|
||||
}
|
||||
|
||||
get sessions(): SessionsApi {
|
||||
return new Api.SessionsApi(this.config)
|
||||
}
|
||||
|
||||
get room(): RoomApi {
|
||||
return new Api.RoomApi(this.config)
|
||||
}
|
||||
|
||||
get members(): MembersApi {
|
||||
return new Api.MembersApi(this.config)
|
||||
}
|
||||
}
|
||||
|
||||
export type DefaultApi = Api.DefaultApi
|
||||
export type SessionsApi = Api.SessionsApi
|
||||
export type RoomApi = Api.RoomApi
|
||||
export type MembersApi = Api.MembersApi
|
263
client/src/component/internal/connection.ts
Normal file
263
client/src/component/internal/connection.ts
Normal file
@ -0,0 +1,263 @@
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import * as EVENT from '../types/events'
|
||||
import type * as webrtcTypes from '../types/webrtc'
|
||||
|
||||
import { NekoWebSocket } from './websocket'
|
||||
import { NekoLoggerFactory } from './logger'
|
||||
import { NekoWebRTC } from './webrtc'
|
||||
import type { Connection, WebRTCStats } from '../types/state'
|
||||
|
||||
import { Reconnector } from './reconnector'
|
||||
import { WebsocketReconnector } from './reconnector/websocket'
|
||||
import { WebrtcReconnector } from './reconnector/webrtc'
|
||||
import type { Logger } from '../utils/logger'
|
||||
|
||||
const WEBRTC_RECONN_MAX_LOSS = 25
|
||||
const WEBRTC_RECONN_FAILED_ATTEMPTS = 5
|
||||
|
||||
const WEBRTC_FALLBACK_TIMEOUT_MS = 750
|
||||
|
||||
export interface NekoConnectionEvents {
|
||||
close: (error?: Error) => void
|
||||
}
|
||||
|
||||
export class NekoConnection extends EventEmitter<NekoConnectionEvents> {
|
||||
private _open = false
|
||||
private _closing = false
|
||||
private _peerRequest?: webrtcTypes.PeerRequest
|
||||
|
||||
public websocket = new NekoWebSocket()
|
||||
public logger = new NekoLoggerFactory(this.websocket)
|
||||
public webrtc = new NekoWebRTC(this.logger.new('webrtc'))
|
||||
|
||||
private _reconnector: {
|
||||
websocket: Reconnector
|
||||
webrtc: Reconnector
|
||||
}
|
||||
|
||||
private _onConnectHandle: () => void
|
||||
private _onDisconnectHandle: () => void
|
||||
private _onCloseHandle: (error?: Error) => void
|
||||
|
||||
private _webrtcStatsHandle: (stats: WebRTCStats) => void
|
||||
private _webrtcStableHandle: (isStable: boolean) => void
|
||||
private _webrtcCongestionControlHandle: (stats: WebRTCStats) => void
|
||||
|
||||
// eslint-disable-next-line
|
||||
constructor(
|
||||
private readonly _state: Connection,
|
||||
) {
|
||||
super()
|
||||
|
||||
this._reconnector = {
|
||||
websocket: new Reconnector(new WebsocketReconnector(_state, this.websocket), _state.websocket.config),
|
||||
webrtc: new Reconnector(new WebrtcReconnector(_state, this.websocket, this.webrtc), _state.webrtc.config),
|
||||
}
|
||||
|
||||
this._onConnectHandle = () => {
|
||||
this._state.websocket.connected = this.websocket.connected // TODO: Vue.Set
|
||||
this._state.webrtc.connected = this.webrtc.connected // TODO: Vue.Set
|
||||
|
||||
if (this._state.status !== 'connected' && this.websocket.connected && this.webrtc.connected) {
|
||||
this._state.status = 'connected' // TODO: Vue.Set
|
||||
}
|
||||
|
||||
if (this.websocket.connected && !this.webrtc.connected) {
|
||||
// if custom peer request is set, send custom peer request
|
||||
if (this._peerRequest) {
|
||||
this.websocket.send(EVENT.SIGNAL_REQUEST, this._peerRequest)
|
||||
this._peerRequest = undefined
|
||||
} else {
|
||||
// otherwise use reconnectors connect method
|
||||
this._reconnector.webrtc.connect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._onDisconnectHandle = () => {
|
||||
this._state.websocket.connected = this.websocket.connected // TODO: Vue.Set
|
||||
this._state.webrtc.connected = this.webrtc.connected // TODO: Vue.Set
|
||||
|
||||
if (this._state.webrtc.stable && !this.webrtc.connected) {
|
||||
this._state.webrtc.stable = false // TODO: Vue.Set
|
||||
}
|
||||
|
||||
if (this._state.status === 'connected' && this.activated) {
|
||||
this._state.status = 'connecting' // TODO: Vue.Set
|
||||
}
|
||||
}
|
||||
|
||||
this._onCloseHandle = this.close.bind(this)
|
||||
|
||||
// bind events to all reconnectors
|
||||
Object.values(this._reconnector).forEach((r) => {
|
||||
r.on('connect', this._onConnectHandle)
|
||||
r.on('disconnect', this._onDisconnectHandle)
|
||||
r.on('close', this._onCloseHandle)
|
||||
})
|
||||
|
||||
// synchronize webrtc stats with global state
|
||||
this._webrtcStatsHandle = (stats: WebRTCStats) => {
|
||||
this._state.webrtc.stats = stats // TODO: Vue.Set
|
||||
}
|
||||
this.webrtc.on('stats', this._webrtcStatsHandle)
|
||||
|
||||
// synchronize webrtc stable with global state
|
||||
this._webrtcStableHandle = (isStable: boolean) => {
|
||||
this._state.webrtc.stable = isStable // TODO: Vue.Set
|
||||
}
|
||||
this.webrtc.on('stable', this._webrtcStableHandle)
|
||||
|
||||
//
|
||||
// TODO: Use server side congestion control.
|
||||
//
|
||||
|
||||
let webrtcCongestion: number = 0
|
||||
let webrtcFallbackTimeout: number
|
||||
|
||||
this._webrtcCongestionControlHandle = (stats: WebRTCStats) => {
|
||||
// if automatic quality adjusting is turned off
|
||||
if (this._state.webrtc.video.auto) return
|
||||
|
||||
// when connection is paused or video disabled, 0fps and muted track is expected
|
||||
if (stats.paused || this._state.webrtc.video.disabled) return
|
||||
|
||||
// if automatic quality adjusting is turned off
|
||||
if (!this._reconnector.webrtc.isOpen) return
|
||||
|
||||
// if there are no or just one quality, no switching can be done
|
||||
if (this._state.webrtc.videos.length <= 1) return
|
||||
|
||||
// current quality is not known
|
||||
if (this._state.webrtc.video.id == '') return
|
||||
|
||||
// check if video is not playing smoothly
|
||||
if (stats.fps && stats.packetLoss < WEBRTC_RECONN_MAX_LOSS && !stats.muted) {
|
||||
if (webrtcFallbackTimeout) {
|
||||
window.clearTimeout(webrtcFallbackTimeout)
|
||||
}
|
||||
|
||||
this._state.webrtc.connected = true // TODO: Vue.Set
|
||||
webrtcCongestion = 0
|
||||
return
|
||||
}
|
||||
|
||||
// try to downgrade quality if it happend many times
|
||||
if (++webrtcCongestion >= WEBRTC_RECONN_FAILED_ATTEMPTS) {
|
||||
webrtcFallbackTimeout = window.setTimeout(() => {
|
||||
this._state.webrtc.connected = false // TODO: Vue.Set
|
||||
}, WEBRTC_FALLBACK_TIMEOUT_MS)
|
||||
|
||||
webrtcCongestion = 0
|
||||
|
||||
const quality = this._webrtcQualityDowngrade(this._state.webrtc.video.id)
|
||||
|
||||
// downgrade if lower video quality exists
|
||||
if (quality && this.webrtc.connected) {
|
||||
this.websocket.send(EVENT.SIGNAL_VIDEO, { video: quality })
|
||||
}
|
||||
|
||||
// try to perform ice restart, if available
|
||||
if (this.webrtc.open) {
|
||||
this.websocket.send(EVENT.SIGNAL_RESTART)
|
||||
return
|
||||
}
|
||||
|
||||
// try to reconnect webrtc
|
||||
this._reconnector.webrtc.reconnect()
|
||||
}
|
||||
}
|
||||
this.webrtc.on('stats', this._webrtcCongestionControlHandle)
|
||||
}
|
||||
|
||||
public get activated() {
|
||||
// check if every reconnecter is open
|
||||
return Object.values(this._reconnector).every((r) => r.isOpen)
|
||||
}
|
||||
|
||||
public reloadConfigs() {
|
||||
this._reconnector.websocket.config = this._state.websocket.config
|
||||
this._reconnector.webrtc.config = this._state.webrtc.config
|
||||
}
|
||||
|
||||
public getLogger(scope?: string): Logger {
|
||||
return this.logger.new(scope)
|
||||
}
|
||||
|
||||
public open(peerRequest?: webrtcTypes.PeerRequest) {
|
||||
if (this._open) {
|
||||
throw new Error('connection already open')
|
||||
}
|
||||
|
||||
this._open = true
|
||||
this._peerRequest = peerRequest
|
||||
|
||||
this._state.status = 'connecting' // TODO: Vue.Set
|
||||
|
||||
// open all reconnectors with deferred connection
|
||||
Object.values(this._reconnector).forEach((r) => r.open(true))
|
||||
|
||||
this._reconnector.websocket.connect()
|
||||
}
|
||||
|
||||
public close(error?: Error) {
|
||||
// we want to make sure that close event is only emitted once
|
||||
// and is not intercepted by any other close event
|
||||
const active = this._open && !this._closing
|
||||
|
||||
if (active) {
|
||||
// set state to disconnected
|
||||
this._state.websocket.connected = false // TODO: Vue.Set
|
||||
this._state.webrtc.connected = false // TODO: Vue.Set
|
||||
this._state.status = 'disconnected' // TODO: Vue.Set
|
||||
this._closing = true
|
||||
}
|
||||
|
||||
// close all reconnectors
|
||||
Object.values(this._reconnector).forEach((r) => r.close())
|
||||
|
||||
if (active) {
|
||||
this._open = false
|
||||
this._closing = false
|
||||
this.emit('close', error)
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.logger.destroy()
|
||||
|
||||
this.webrtc.off('stats', this._webrtcStatsHandle)
|
||||
this.webrtc.off('stable', this._webrtcStableHandle)
|
||||
// TODO: Use server side congestion control.
|
||||
this.webrtc.off('stats', this._webrtcCongestionControlHandle)
|
||||
|
||||
// unbind events from all reconnectors
|
||||
Object.values(this._reconnector).forEach((r) => {
|
||||
r.off('connect', this._onConnectHandle)
|
||||
r.off('disconnect', this._onDisconnectHandle)
|
||||
r.off('close', this._onCloseHandle)
|
||||
})
|
||||
|
||||
// destroy all reconnectors
|
||||
Object.values(this._reconnector).forEach((r) => r.destroy())
|
||||
|
||||
// set state to disconnected
|
||||
this._state.websocket.connected = false // TODO: Vue.Set
|
||||
this._state.webrtc.connected = false // TODO: Vue.Set
|
||||
this._state.status = 'disconnected' // TODO: Vue.Set
|
||||
}
|
||||
|
||||
_webrtcQualityDowngrade(quality: string): string | undefined {
|
||||
// get index of selected or surrent quality
|
||||
const index = this._state.webrtc.videos.indexOf(quality)
|
||||
|
||||
// edge case: current quality is not in qualities list
|
||||
if (index === -1) return
|
||||
|
||||
// current quality is the lowest one
|
||||
if (index + 1 == this._state.webrtc.videos.length) return
|
||||
|
||||
// downgrade video quality
|
||||
return this._state.webrtc.videos[index + 1]
|
||||
}
|
||||
}
|
166
client/src/component/internal/control.ts
Normal file
166
client/src/component/internal/control.ts
Normal file
@ -0,0 +1,166 @@
|
||||
import * as EVENT from '../types/events'
|
||||
import type * as message from '../types/messages'
|
||||
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import type { NekoConnection } from './connection'
|
||||
import type { Control } from '../types/state'
|
||||
|
||||
export interface NekoControlEvents {
|
||||
['overlay.click']: (e: MouseEvent) => void
|
||||
['overlay.contextmenu']: (e: MouseEvent) => void
|
||||
}
|
||||
|
||||
export interface ControlPos {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface ControlScroll {
|
||||
delta_x: number
|
||||
delta_y: number
|
||||
control_key?: boolean
|
||||
}
|
||||
|
||||
export class NekoControl extends EventEmitter<NekoControlEvents> {
|
||||
// eslint-disable-next-line
|
||||
constructor(
|
||||
private readonly _connection: NekoConnection,
|
||||
private readonly _state: Control,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
get useWebrtc() {
|
||||
// we want to use webrtc if we're connected and we're the host
|
||||
// because webrtc is faster and it doesn't request control
|
||||
// in contrast to the websocket
|
||||
return this._connection.webrtc.connected && this._state.is_host
|
||||
}
|
||||
|
||||
get enabledTouchEvents() {
|
||||
return this._state.touch.enabled
|
||||
}
|
||||
|
||||
get supportedTouchEvents() {
|
||||
return this._state.touch.supported
|
||||
}
|
||||
|
||||
public lock() {
|
||||
this._state.locked = true // TODO: Vue.Set
|
||||
}
|
||||
|
||||
public unlock() {
|
||||
this._state.locked = false // TODO: Vue.Set
|
||||
}
|
||||
|
||||
public request() {
|
||||
this._connection.websocket.send(EVENT.CONTROL_REQUEST)
|
||||
}
|
||||
|
||||
public release() {
|
||||
this._connection.websocket.send(EVENT.CONTROL_RELEASE)
|
||||
}
|
||||
|
||||
public move(pos: ControlPos) {
|
||||
if (this.useWebrtc) {
|
||||
this._connection.webrtc.send('mousemove', pos)
|
||||
} else {
|
||||
this._connection.websocket.send(EVENT.CONTROL_MOVE, pos as message.ControlPos)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add pos parameter
|
||||
public scroll(scroll: ControlScroll) {
|
||||
if (this.useWebrtc) {
|
||||
this._connection.webrtc.send('wheel', scroll)
|
||||
} else {
|
||||
this._connection.websocket.send(EVENT.CONTROL_SCROLL, scroll as message.ControlScroll)
|
||||
}
|
||||
}
|
||||
|
||||
// buttonpress ensures that only one button is pressed at a time
|
||||
public buttonPress(code: number, pos?: ControlPos) {
|
||||
this._connection.websocket.send(EVENT.CONTROL_BUTTONPRESS, { code, ...pos } as message.ControlButton)
|
||||
}
|
||||
|
||||
public buttonDown(code: number, pos?: ControlPos) {
|
||||
if (this.useWebrtc) {
|
||||
if (pos) this._connection.webrtc.send('mousemove', pos)
|
||||
this._connection.webrtc.send('mousedown', { key: code })
|
||||
} else {
|
||||
this._connection.websocket.send(EVENT.CONTROL_BUTTONDOWN, { code, ...pos } as message.ControlButton)
|
||||
}
|
||||
}
|
||||
|
||||
public buttonUp(code: number, pos?: ControlPos) {
|
||||
if (this.useWebrtc) {
|
||||
if (pos) this._connection.webrtc.send('mousemove', pos)
|
||||
this._connection.webrtc.send('mouseup', { key: code })
|
||||
} else {
|
||||
this._connection.websocket.send(EVENT.CONTROL_BUTTONUP, { code, ...pos } as message.ControlButton)
|
||||
}
|
||||
}
|
||||
|
||||
// keypress ensures that only one key is pressed at a time
|
||||
public keyPress(keysym: number, pos?: ControlPos) {
|
||||
this._connection.websocket.send(EVENT.CONTROL_KEYPRESS, { keysym, ...pos } as message.ControlKey)
|
||||
}
|
||||
|
||||
public keyDown(keysym: number, pos?: ControlPos) {
|
||||
if (this.useWebrtc) {
|
||||
if (pos) this._connection.webrtc.send('mousemove', pos)
|
||||
this._connection.webrtc.send('keydown', { key: keysym })
|
||||
} else {
|
||||
this._connection.websocket.send(EVENT.CONTROL_KEYDOWN, { keysym, ...pos } as message.ControlKey)
|
||||
}
|
||||
}
|
||||
|
||||
public keyUp(keysym: number, pos?: ControlPos) {
|
||||
if (this.useWebrtc) {
|
||||
if (pos) this._connection.webrtc.send('mousemove', pos)
|
||||
this._connection.webrtc.send('keyup', { key: keysym })
|
||||
} else {
|
||||
this._connection.websocket.send(EVENT.CONTROL_KEYUP, { keysym, ...pos } as message.ControlKey)
|
||||
}
|
||||
}
|
||||
|
||||
public touchBegin(touch_id: number, pos: ControlPos, pressure: number) {
|
||||
if (this.useWebrtc) {
|
||||
this._connection.webrtc.send('touchbegin', { touch_id, ...pos, pressure })
|
||||
} else {
|
||||
this._connection.websocket.send(EVENT.CONTROL_TOUCHBEGIN, { touch_id, ...pos, pressure } as message.ControlTouch)
|
||||
}
|
||||
}
|
||||
|
||||
public touchUpdate(touch_id: number, pos: ControlPos, pressure: number) {
|
||||
if (this.useWebrtc) {
|
||||
this._connection.webrtc.send('touchupdate', { touch_id, ...pos, pressure })
|
||||
} else {
|
||||
this._connection.websocket.send(EVENT.CONTROL_TOUCHUPDATE, { touch_id, ...pos, pressure } as message.ControlTouch)
|
||||
}
|
||||
}
|
||||
|
||||
public touchEnd(touch_id: number, pos: ControlPos, pressure: number) {
|
||||
if (this.useWebrtc) {
|
||||
this._connection.webrtc.send('touchend', { touch_id, ...pos, pressure })
|
||||
} else {
|
||||
this._connection.websocket.send(EVENT.CONTROL_TOUCHEND, { touch_id, ...pos, pressure } as message.ControlTouch)
|
||||
}
|
||||
}
|
||||
|
||||
public cut() {
|
||||
this._connection.websocket.send(EVENT.CONTROL_CUT)
|
||||
}
|
||||
|
||||
public copy() {
|
||||
this._connection.websocket.send(EVENT.CONTROL_COPY)
|
||||
}
|
||||
|
||||
public paste(text?: string) {
|
||||
this._connection.websocket.send(EVENT.CONTROL_PASTE, { text } as message.ClipboardData)
|
||||
}
|
||||
|
||||
public selectAll() {
|
||||
this._connection.websocket.send(EVENT.CONTROL_SELECT_ALL)
|
||||
}
|
||||
}
|
130
client/src/component/internal/logger.ts
Normal file
130
client/src/component/internal/logger.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import type { NekoWebSocket } from './websocket'
|
||||
import * as EVENT from '../types/events'
|
||||
import type * as message from '../types/messages'
|
||||
import { Logger } from '../utils/logger'
|
||||
|
||||
const MAX_LOG_MESSAGES = 25
|
||||
const FLUSH_TIMEOUT_MS = 250
|
||||
const RETRY_INTERVAL_MS = 2500
|
||||
|
||||
export class NekoLoggerFactory {
|
||||
private _logs: message.SystemLog[] = []
|
||||
private _timeout: number | null = null
|
||||
private _interval: number | null = null
|
||||
|
||||
// eslint-disable-next-line
|
||||
constructor(
|
||||
private readonly _ws: NekoWebSocket,
|
||||
) {}
|
||||
|
||||
private _flush() {
|
||||
if (this._logs.length > 0) {
|
||||
this._ws.send(EVENT.SYSTEM_LOGS, this._logs)
|
||||
this._logs = []
|
||||
}
|
||||
}
|
||||
|
||||
private _send(level: string, message: string, fields?: Record<string, any>) {
|
||||
for (const key in fields) {
|
||||
const field = fields[key]
|
||||
|
||||
if (field instanceof Error) {
|
||||
fields[key] = (field as Error).message
|
||||
}
|
||||
}
|
||||
|
||||
const payload = { level, message, fields } as message.SystemLog
|
||||
this._logs.push(payload)
|
||||
|
||||
// rotate if exceeded maximum
|
||||
if (this._logs.length > MAX_LOG_MESSAGES) {
|
||||
this._logs.shift()
|
||||
}
|
||||
|
||||
// postpone logs sending
|
||||
if (!this._timeout && !this._interval) {
|
||||
this._timeout = window.setTimeout(() => {
|
||||
if (!this._timeout) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this._ws.connected) {
|
||||
this._flush()
|
||||
} else {
|
||||
this._interval = window.setInterval(() => {
|
||||
if (!this._ws.connected || !this._interval) {
|
||||
return
|
||||
}
|
||||
|
||||
this._flush()
|
||||
window.clearInterval(this._interval)
|
||||
this._interval = null
|
||||
}, RETRY_INTERVAL_MS)
|
||||
}
|
||||
|
||||
window.clearTimeout(this._timeout)
|
||||
this._timeout = null
|
||||
}, FLUSH_TIMEOUT_MS)
|
||||
}
|
||||
}
|
||||
|
||||
public new(submodule?: string): Logger {
|
||||
return new NekoLogger((level: string, message: string, fields?: Record<string, any>) => {
|
||||
if (!fields) {
|
||||
fields = { submodule }
|
||||
} else {
|
||||
fields['submodule'] = submodule
|
||||
}
|
||||
|
||||
this._send(level, message, fields)
|
||||
}, submodule)
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
if (this._ws.connected) {
|
||||
this._flush()
|
||||
}
|
||||
|
||||
if (this._interval) {
|
||||
window.clearInterval(this._interval)
|
||||
this._interval = null
|
||||
}
|
||||
|
||||
if (this._timeout) {
|
||||
window.clearTimeout(this._timeout)
|
||||
this._timeout = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type NekoLoggerMessage = (level: string, message: string, fields?: Record<string, any>) => void
|
||||
|
||||
export class NekoLogger extends Logger {
|
||||
private _on_send: NekoLoggerMessage
|
||||
|
||||
constructor(onSend: NekoLoggerMessage, scope?: string) {
|
||||
super(scope)
|
||||
|
||||
this._on_send = onSend
|
||||
}
|
||||
|
||||
public error(message: string, fields?: Record<string, any>) {
|
||||
this._console('error', message, fields)
|
||||
this._on_send('error', message, fields)
|
||||
}
|
||||
|
||||
public warn(message: string, fields?: Record<string, any>) {
|
||||
this._console('warn', message, fields)
|
||||
this._on_send('warn', message, fields)
|
||||
}
|
||||
|
||||
public info(message: string, fields?: Record<string, any>) {
|
||||
this._console('info', message, fields)
|
||||
this._on_send('info', message, fields)
|
||||
}
|
||||
|
||||
public debug(message: string, fields?: Record<string, any>) {
|
||||
this._console('debug', message, fields)
|
||||
this._on_send('debug', message, fields)
|
||||
}
|
||||
}
|
361
client/src/component/internal/messages.ts
Normal file
361
client/src/component/internal/messages.ts
Normal file
@ -0,0 +1,361 @@
|
||||
import * as EVENT from '../types/events'
|
||||
import type * as message from '../types/messages'
|
||||
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import type { AxiosProgressEvent } from 'axios'
|
||||
import { Logger } from '../utils/logger'
|
||||
import type { NekoConnection } from './connection'
|
||||
import type NekoState from '../types/state'
|
||||
import type { Settings } from '../types/state'
|
||||
|
||||
export interface NekoEvents {
|
||||
// connection events
|
||||
['connection.status']: (status: 'connected' | 'connecting' | 'disconnected') => void
|
||||
['connection.type']: (status: 'fallback' | 'webrtc' | 'none') => void
|
||||
['connection.webrtc.sdp']: (type: 'local' | 'remote', data: string) => void
|
||||
['connection.webrtc.sdp.candidate']: (type: 'local' | 'remote', data: RTCIceCandidateInit) => void
|
||||
['connection.closed']: (error?: Error) => void
|
||||
|
||||
// drag and drop events
|
||||
['upload.drop.started']: () => void
|
||||
['upload.drop.progress']: (progressEvent: AxiosProgressEvent) => void
|
||||
['upload.drop.finished']: (error?: Error) => void
|
||||
|
||||
// upload dialog events
|
||||
['upload.dialog.requested']: () => void
|
||||
['upload.dialog.overlay']: (id: string) => void
|
||||
['upload.dialog.closed']: () => void
|
||||
|
||||
// custom messages events
|
||||
['receive.unicast']: (sender: string, subject: string, body: any) => void
|
||||
['receive.broadcast']: (sender: string, subject: string, body: any) => void
|
||||
|
||||
// session events
|
||||
['session.created']: (id: string) => void
|
||||
['session.deleted']: (id: string) => void
|
||||
['session.updated']: (id: string) => void
|
||||
|
||||
// room events
|
||||
['room.control.host']: (hasHost: boolean, hostID: string | undefined, id: string) => void
|
||||
['room.control.request']: (id: string) => void
|
||||
['room.screen.updated']: (width: number, height: number, rate: number, id: string) => void
|
||||
['room.settings.updated']: (settings: Settings, id: string) => void
|
||||
['room.clipboard.updated']: (text: string) => void
|
||||
['room.broadcast.status']: (isActive: boolean, url?: string) => void
|
||||
|
||||
// external message events
|
||||
['message']: (event: string, payload: any) => void
|
||||
}
|
||||
|
||||
export class NekoMessages extends EventEmitter<NekoEvents> {
|
||||
private _localLog: Logger
|
||||
private _remoteLog: Logger
|
||||
|
||||
// eslint-disable-next-line
|
||||
constructor(
|
||||
private readonly _connection: NekoConnection,
|
||||
private readonly _state: NekoState,
|
||||
) {
|
||||
super()
|
||||
|
||||
this._localLog = new Logger('messages')
|
||||
this._remoteLog = _connection.getLogger('messages')
|
||||
|
||||
this._connection.websocket.on('message', async (event: string, payload: any) => {
|
||||
// @ts-ignore
|
||||
if (typeof this[event] === 'function') {
|
||||
try {
|
||||
// @ts-ignore
|
||||
await this[event](payload)
|
||||
} catch (error: any) {
|
||||
this._remoteLog.error(`error while processing websocket event`, { event, error })
|
||||
}
|
||||
} else {
|
||||
this._remoteLog.debug(`emitting external message`, { event, payload })
|
||||
this.emit('message', event, payload)
|
||||
}
|
||||
})
|
||||
|
||||
this._connection.webrtc.on('candidate', (candidate: RTCIceCandidateInit) => {
|
||||
this._connection.websocket.send(EVENT.SIGNAL_CANDIDATE, candidate)
|
||||
this.emit('connection.webrtc.sdp.candidate', 'local', candidate)
|
||||
})
|
||||
|
||||
this._connection.webrtc.on('negotiation', ({ sdp, type }: RTCSessionDescriptionInit) => {
|
||||
if (!sdp) {
|
||||
this._remoteLog.warn(`sdp empty while negotiation event`)
|
||||
return
|
||||
}
|
||||
|
||||
if (type == 'answer') {
|
||||
this._connection.websocket.send(EVENT.SIGNAL_ANSWER, { sdp })
|
||||
} else if (type == 'offer') {
|
||||
this._connection.websocket.send(EVENT.SIGNAL_OFFER, { sdp })
|
||||
} else {
|
||||
this._remoteLog.warn(`unsupported negotiation type`, { type })
|
||||
}
|
||||
|
||||
// TODO: Return whole signal description (if answer / offer).
|
||||
this.emit('connection.webrtc.sdp', 'local', sdp)
|
||||
})
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// System Events
|
||||
/////////////////////////////
|
||||
|
||||
protected [EVENT.SYSTEM_INIT](conf: message.SystemInit) {
|
||||
this._localLog.debug(`EVENT.SYSTEM_INIT`)
|
||||
this._state.session_id = conf.session_id // TODO: Vue.Set
|
||||
// check if backend supports touch events
|
||||
this._state.control.touch.supported = conf.touch_events // TODO: Vue.Set
|
||||
this._state.connection.screencast = conf.screencast_enabled // TODO: Vue.Set
|
||||
this._state.connection.webrtc.videos = conf.webrtc.videos // TODO: Vue.Set
|
||||
|
||||
for (const id in conf.sessions) {
|
||||
this[EVENT.SESSION_CREATED](conf.sessions[id])
|
||||
}
|
||||
|
||||
const { width, height, rate } = conf.screen_size
|
||||
this._state.screen.size = { width, height, rate } // TODO: Vue.Set
|
||||
this[EVENT.CONTROL_HOST](conf.control_host)
|
||||
this._state.settings = conf.settings // TODO: Vue.Set
|
||||
}
|
||||
|
||||
protected [EVENT.SYSTEM_ADMIN]({ screen_sizes_list, broadcast_status }: message.SystemAdmin) {
|
||||
this._localLog.debug(`EVENT.SYSTEM_ADMIN`)
|
||||
|
||||
const list = screen_sizes_list.sort((a, b) => {
|
||||
if (b.width === a.width && b.height == a.height) {
|
||||
return b.rate - a.rate
|
||||
} else if (b.width === a.width) {
|
||||
return b.height - a.height
|
||||
}
|
||||
return b.width - a.width
|
||||
})
|
||||
|
||||
this._state.screen.configurations = list // TODO: Vue.Set
|
||||
|
||||
this[EVENT.BORADCAST_STATUS](broadcast_status)
|
||||
}
|
||||
|
||||
protected [EVENT.SYSTEM_SETTINGS]({ id, ...settings }: message.SystemSettingsUpdate) {
|
||||
this._localLog.debug(`EVENT.SYSTEM_SETTINGS`)
|
||||
this._state.settings = settings // TODO: Vue.Set
|
||||
this.emit('room.settings.updated', settings, id)
|
||||
}
|
||||
|
||||
protected [EVENT.SYSTEM_DISCONNECT]({ message }: message.SystemDisconnect) {
|
||||
this._localLog.debug(`EVENT.SYSTEM_DISCONNECT`)
|
||||
this._connection.close(new Error(message))
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Signal Events
|
||||
/////////////////////////////
|
||||
|
||||
protected async [EVENT.SIGNAL_PROVIDE]({ sdp, iceservers, video, audio }: message.SignalProvide) {
|
||||
this._localLog.debug(`EVENT.SIGNAL_PROVIDE`)
|
||||
|
||||
// create WebRTC connection
|
||||
await this._connection.webrtc.connect(iceservers)
|
||||
|
||||
// set remote offer
|
||||
await this._connection.webrtc.setOffer(sdp)
|
||||
|
||||
// TODO: Return whole signal description (if answer / offer).
|
||||
this.emit('connection.webrtc.sdp', 'remote', sdp)
|
||||
|
||||
this[EVENT.SIGNAL_VIDEO](video)
|
||||
this[EVENT.SIGNAL_AUDIO](audio)
|
||||
}
|
||||
|
||||
protected async [EVENT.SIGNAL_OFFER]({ sdp }: message.SignalDescription) {
|
||||
this._localLog.debug(`EVENT.SIGNAL_OFFER`)
|
||||
|
||||
// set remote offer
|
||||
await this._connection.webrtc.setOffer(sdp)
|
||||
|
||||
// TODO: Return whole signal description (if answer / offer).
|
||||
this.emit('connection.webrtc.sdp', 'remote', sdp)
|
||||
}
|
||||
|
||||
protected async [EVENT.SIGNAL_ANSWER]({ sdp }: message.SignalDescription) {
|
||||
this._localLog.debug(`EVENT.SIGNAL_ANSWER`)
|
||||
|
||||
// set remote answer
|
||||
await this._connection.webrtc.setAnswer(sdp)
|
||||
|
||||
// TODO: Return whole signal description (if answer / offer).
|
||||
this.emit('connection.webrtc.sdp', 'remote', sdp)
|
||||
}
|
||||
|
||||
// TODO: Use offer event intead.
|
||||
protected async [EVENT.SIGNAL_RESTART]({ sdp }: message.SignalDescription) {
|
||||
this[EVENT.SIGNAL_OFFER]({ sdp })
|
||||
}
|
||||
|
||||
protected async [EVENT.SIGNAL_CANDIDATE](candidate: message.SignalCandidate) {
|
||||
this._localLog.debug(`EVENT.SIGNAL_CANDIDATE`)
|
||||
|
||||
// set remote candidate
|
||||
await this._connection.webrtc.setCandidate(candidate)
|
||||
this.emit('connection.webrtc.sdp.candidate', 'remote', candidate)
|
||||
}
|
||||
|
||||
protected [EVENT.SIGNAL_VIDEO]({ disabled, id, auto }: message.SignalVideo) {
|
||||
this._localLog.debug(`EVENT.SIGNAL_VIDEO`, { disabled, id, auto })
|
||||
this._state.connection.webrtc.video.disabled = disabled // TODO: Vue.Set
|
||||
this._state.connection.webrtc.video.id = id // TODO: Vue.Set
|
||||
this._state.connection.webrtc.video.auto = auto // TODO: Vue.Set
|
||||
}
|
||||
|
||||
protected [EVENT.SIGNAL_AUDIO]({ disabled }: message.SignalAudio) {
|
||||
this._localLog.debug(`EVENT.SIGNAL_AUDIO`, { disabled })
|
||||
this._state.connection.webrtc.audio.disabled = disabled // TODO: Vue.Set
|
||||
}
|
||||
|
||||
protected [EVENT.SIGNAL_CLOSE]() {
|
||||
this._localLog.debug(`EVENT.SIGNAL_CLOSE`)
|
||||
this._connection.webrtc.close()
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Session Events
|
||||
/////////////////////////////
|
||||
|
||||
protected [EVENT.SESSION_CREATED]({ id, profile, ...state }: message.SessionData) {
|
||||
this._localLog.debug(`EVENT.SESSION_CREATED`, { id })
|
||||
this._state.sessions[id] = { id, profile, state } // TODO: Vue.Set
|
||||
this.emit('session.created', id)
|
||||
}
|
||||
|
||||
protected [EVENT.SESSION_DELETED]({ id }: message.SessionID) {
|
||||
this._localLog.debug(`EVENT.SESSION_DELETED`, { id })
|
||||
delete this._state.sessions[id] // TODO: Vue.Delete
|
||||
this.emit('session.deleted', id)
|
||||
}
|
||||
|
||||
protected [EVENT.SESSION_PROFILE]({ id, ...profile }: message.MemberProfile) {
|
||||
if (id in this._state.sessions) {
|
||||
this._localLog.debug(`EVENT.SESSION_PROFILE`, { id })
|
||||
this._state.sessions[id].profile = profile // TODO: Vue.Set
|
||||
this.emit('session.updated', id)
|
||||
}
|
||||
}
|
||||
|
||||
protected [EVENT.SESSION_STATE]({ id, ...state }: message.SessionState) {
|
||||
if (id in this._state.sessions) {
|
||||
this._localLog.debug(`EVENT.SESSION_STATE`, { id })
|
||||
this._state.sessions[id].state = state // TODO: Vue.Set
|
||||
this.emit('session.updated', id)
|
||||
}
|
||||
}
|
||||
|
||||
protected [EVENT.SESSION_CURSORS](cursors: message.SessionCursor[]) {
|
||||
//
|
||||
// TODO: Resolve conflict with state.cursors.
|
||||
//
|
||||
//@ts-ignore
|
||||
this._state.cursors = cursors // TODO: Vue.Set
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Control Events
|
||||
/////////////////////////////
|
||||
|
||||
protected [EVENT.CONTROL_REQUEST]({ id }: message.SessionID) {
|
||||
this._localLog.debug(`EVENT.CONTROL_REQUEST`)
|
||||
this.emit('room.control.request', id)
|
||||
}
|
||||
|
||||
protected [EVENT.CONTROL_HOST]({ has_host, host_id, id }: message.ControlHost) {
|
||||
this._localLog.debug(`EVENT.CONTROL_HOST`)
|
||||
|
||||
if (has_host && host_id) {
|
||||
this._state.control.host_id = host_id // TODO: Vue.Set
|
||||
} else {
|
||||
this._state.control.host_id = null // TODO: Vue.Set
|
||||
}
|
||||
|
||||
// save if user is host
|
||||
this._state.control.is_host = has_host && this._state.control.host_id === this._state.session_id // TODO: Vue.Set
|
||||
|
||||
this.emit('room.control.host', has_host, host_id, id)
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Screen Events
|
||||
/////////////////////////////
|
||||
|
||||
protected [EVENT.SCREEN_UPDATED]({ width, height, rate, id }: message.ScreenSizeUpdate) {
|
||||
this._localLog.debug(`EVENT.SCREEN_UPDATED`)
|
||||
this._state.screen.size = { width, height, rate } // TODO: Vue.Set
|
||||
this.emit('room.screen.updated', width, height, rate, id)
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Clipboard Events
|
||||
/////////////////////////////
|
||||
|
||||
protected [EVENT.CLIPBOARD_UPDATED]({ text }: message.ClipboardData) {
|
||||
this._localLog.debug(`EVENT.CLIPBOARD_UPDATED`)
|
||||
this._state.control.clipboard = { text } // TODO: Vue.Set
|
||||
|
||||
try {
|
||||
navigator.clipboard.writeText(text) // sync user's clipboard
|
||||
} catch (error: any) {
|
||||
this._remoteLog.warn(`unable to write text to client's clipboard`, {
|
||||
error,
|
||||
// works only for HTTPs
|
||||
protocol: location.protocol,
|
||||
clipboard: typeof navigator.clipboard,
|
||||
})
|
||||
}
|
||||
|
||||
this.emit('room.clipboard.updated', text)
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Broadcast Events
|
||||
/////////////////////////////
|
||||
|
||||
protected [EVENT.BORADCAST_STATUS]({ url, is_active }: message.BroadcastStatus) {
|
||||
this._localLog.debug(`EVENT.BORADCAST_STATUS`)
|
||||
// TODO: Handle.
|
||||
this.emit('room.broadcast.status', is_active, url)
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Send Events
|
||||
/////////////////////////////
|
||||
|
||||
protected [EVENT.SEND_UNICAST]({ sender, subject, body }: message.SendMessage) {
|
||||
this._localLog.debug(`EVENT.SEND_UNICAST`)
|
||||
this.emit('receive.unicast', sender, subject, body)
|
||||
}
|
||||
|
||||
protected [EVENT.SEND_BROADCAST]({ sender, subject, body }: message.SendMessage) {
|
||||
this._localLog.debug(`EVENT.BORADCAST_STATUS`)
|
||||
this.emit('receive.broadcast', sender, subject, body)
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// FileChooserDialog Events
|
||||
/////////////////////////////
|
||||
|
||||
protected [EVENT.FILE_CHOOSER_DIALOG_OPENED]({ id }: message.SessionID) {
|
||||
this._localLog.debug(`EVENT.FILE_CHOOSER_DIALOG_OPENED`)
|
||||
|
||||
if (id == this._state.session_id) {
|
||||
this.emit('upload.dialog.requested')
|
||||
} else {
|
||||
this.emit('upload.dialog.overlay', id)
|
||||
}
|
||||
}
|
||||
|
||||
protected [EVENT.FILE_CHOOSER_DIALOG_CLOSED]({}: message.SessionID) {
|
||||
this._localLog.debug(`EVENT.FILE_CHOOSER_DIALOG_CLOSED`)
|
||||
this.emit('upload.dialog.closed')
|
||||
}
|
||||
}
|
222
client/src/component/internal/reconnector/index.ts
Normal file
222
client/src/component/internal/reconnector/index.ts
Normal file
@ -0,0 +1,222 @@
|
||||
import EventEmitter from 'eventemitter3'
|
||||
|
||||
import type { ReconnectorConfig } from '../../types/reconnector'
|
||||
|
||||
export interface ReconnectorAbstractEvents {
|
||||
connect: () => void
|
||||
disconnect: (error?: Error) => void
|
||||
}
|
||||
|
||||
export abstract class ReconnectorAbstract extends EventEmitter<ReconnectorAbstractEvents> {
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
if (this.constructor == ReconnectorAbstract) {
|
||||
throw new Error("Abstract classes can't be instantiated.")
|
||||
}
|
||||
}
|
||||
|
||||
public abstract get connected(): boolean
|
||||
|
||||
public abstract connect(): void
|
||||
public abstract disconnect(): void
|
||||
public abstract destroy(): void
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Reconnector handles reconnection logic according to supplied config for an abstract class. It can reconnect anything that:
|
||||
- can be connected to
|
||||
- can send event once it is connected to
|
||||
- can be disconnected from
|
||||
- can send event once it is disconnected from
|
||||
- can provide information at any moment if it is connected to or not
|
||||
|
||||
Reconnector creates one additional abstract layer for a user. User can open and close a connection. If the connection is open,
|
||||
when connection will be disconnected, reconnector will attempt to connect to it again. Once connection is closed, no further
|
||||
events will be emitted and connection will be disconnected.
|
||||
- When using deferred connection in opening function, reconnector does not try to connect when opening a connection. This is
|
||||
the initial state, when reconnector is not connected but no reconnect attempts are in progress, since there has not been
|
||||
any disconnect even. It is up to user to call initial connect attempt.
|
||||
- Events 'open' and 'close' will be fired exactly once, no matter how many times open() and close() funxtions were called.
|
||||
- Events 'connecŧ' and 'disconnect' can fire throughout open connection at any time.
|
||||
|
||||
*/
|
||||
export interface ReconnectorEvents {
|
||||
open: () => void
|
||||
connect: () => void
|
||||
disconnect: () => void
|
||||
close: (error?: Error) => void
|
||||
}
|
||||
|
||||
export class Reconnector extends EventEmitter<ReconnectorEvents> {
|
||||
private _config: ReconnectorConfig
|
||||
private _timeout?: number
|
||||
|
||||
private _open = false
|
||||
private _total_reconnects = 0
|
||||
private _last_connected?: Date
|
||||
|
||||
private _onConnectHandle: () => void
|
||||
private _onDisconnectHandle: (error?: Error) => void
|
||||
|
||||
// eslint-disable-next-line
|
||||
constructor(
|
||||
private readonly _conn: ReconnectorAbstract,
|
||||
config?: ReconnectorConfig,
|
||||
) {
|
||||
super()
|
||||
|
||||
// setup default config values
|
||||
this._config = {
|
||||
max_reconnects: 10,
|
||||
timeout_ms: 1500,
|
||||
backoff_ms: 750,
|
||||
...config,
|
||||
}
|
||||
|
||||
// register connect and disconnect handlers with current class
|
||||
// as 'this' context, store them to a variable so that they
|
||||
// can be later unregistered
|
||||
|
||||
this._onConnectHandle = this.onConnect.bind(this)
|
||||
this._conn.on('connect', this._onConnectHandle)
|
||||
|
||||
this._onDisconnectHandle = this.onDisconnect.bind(this)
|
||||
this._conn.on('disconnect', this._onDisconnectHandle)
|
||||
}
|
||||
|
||||
private clearTimeout() {
|
||||
if (this._timeout) {
|
||||
window.clearTimeout(this._timeout)
|
||||
this._timeout = undefined
|
||||
}
|
||||
}
|
||||
|
||||
private onConnect() {
|
||||
this.clearTimeout()
|
||||
|
||||
// only if connection is open, fire connect event
|
||||
if (this._open) {
|
||||
this._last_connected = new Date()
|
||||
this._total_reconnects = 0
|
||||
this.emit('connect')
|
||||
} else {
|
||||
// in this case we are connected but this connection
|
||||
// has been closed, so we simply disconnect again
|
||||
this._conn.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private onDisconnect() {
|
||||
this.clearTimeout()
|
||||
|
||||
// only if connection is open, fire disconnect event
|
||||
// and start reconnecteing logic
|
||||
if (this._open) {
|
||||
this.emit('disconnect')
|
||||
this.reconnect()
|
||||
}
|
||||
}
|
||||
|
||||
public get isOpen(): boolean {
|
||||
return this._open
|
||||
}
|
||||
|
||||
public get isConnected(): boolean {
|
||||
return this._conn.connected
|
||||
}
|
||||
|
||||
public get totalReconnects(): number {
|
||||
return this._total_reconnects
|
||||
}
|
||||
|
||||
public get lastConnected(): Date | undefined {
|
||||
return this._last_connected
|
||||
}
|
||||
|
||||
public get config(): ReconnectorConfig {
|
||||
return { ...this._config }
|
||||
}
|
||||
|
||||
public set config(conf: ReconnectorConfig) {
|
||||
this._config = { ...this._config, ...conf }
|
||||
|
||||
if (this._config.max_reconnects <= this._total_reconnects) {
|
||||
this.close(new Error('reconnection config changed'))
|
||||
}
|
||||
}
|
||||
|
||||
// allows future reconnect attempts and connects if not set
|
||||
// deferred connection to true
|
||||
public open(deferredConnection = false): void {
|
||||
this.clearTimeout()
|
||||
|
||||
// assuming open event can fire multiple times, we need to
|
||||
// ensure, that open event get fired only once along with
|
||||
// resetting total reconnects counter
|
||||
|
||||
if (!this._open) {
|
||||
this._open = true
|
||||
this._total_reconnects = 0
|
||||
this.emit('open')
|
||||
}
|
||||
|
||||
if (!deferredConnection && !this._conn.connected) {
|
||||
this.connect()
|
||||
}
|
||||
}
|
||||
|
||||
// disconnects and forbids future reconnect attempts
|
||||
public close(error?: Error): void {
|
||||
this.clearTimeout()
|
||||
|
||||
// assuming close event can fire multiple times, the same
|
||||
// precautions need to be taken as in open event, so that
|
||||
// close event fires only once
|
||||
|
||||
if (this._open) {
|
||||
this._open = false
|
||||
this._last_connected = undefined
|
||||
this.emit('close', error)
|
||||
}
|
||||
|
||||
// if connected, tries to disconnect even if it has been
|
||||
// called multiple times by user
|
||||
|
||||
if (this._conn.connected) {
|
||||
this._conn.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// tries to connect and calls on disconnected if it could not
|
||||
// connect within specified timeout according to config
|
||||
public connect(): void {
|
||||
this.clearTimeout()
|
||||
|
||||
this._conn.connect()
|
||||
this._timeout = window.setTimeout(this.onDisconnect.bind(this), this._config.timeout_ms)
|
||||
}
|
||||
|
||||
// tries to connect with specified backoff time if
|
||||
// maximum reconnect theshold was not exceeded, otherwise
|
||||
// closes the connection with an error message
|
||||
public reconnect(): void {
|
||||
this.clearTimeout()
|
||||
|
||||
if (this._config.max_reconnects > ++this._total_reconnects || this._config.max_reconnects == -1) {
|
||||
this._timeout = window.setTimeout(this.connect.bind(this), this._config.backoff_ms)
|
||||
} else {
|
||||
this.close(new Error('reconnection failed'))
|
||||
}
|
||||
}
|
||||
|
||||
// closes connection and unregisters all events
|
||||
public destroy() {
|
||||
this.close(new Error('connection destroyed'))
|
||||
|
||||
this._conn.off('connect', this._onConnectHandle)
|
||||
this._conn.off('disconnect', this._onDisconnectHandle)
|
||||
this._conn.destroy()
|
||||
}
|
||||
}
|
71
client/src/component/internal/reconnector/webrtc.ts
Normal file
71
client/src/component/internal/reconnector/webrtc.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import * as EVENT from '../../types/events'
|
||||
import type { Connection } from '../../types/state'
|
||||
|
||||
import type { NekoWebSocket } from '../websocket'
|
||||
import type { NekoWebRTC } from '../webrtc'
|
||||
|
||||
import { ReconnectorAbstract } from '.'
|
||||
|
||||
export class WebrtcReconnector extends ReconnectorAbstract {
|
||||
private _onConnectHandle: () => void
|
||||
private _onDisconnectHandle: (error?: Error) => void
|
||||
|
||||
// eslint-disable-next-line
|
||||
constructor(
|
||||
private readonly _state: Connection,
|
||||
private readonly _websocket: NekoWebSocket,
|
||||
private readonly _webrtc: NekoWebRTC,
|
||||
) {
|
||||
super()
|
||||
|
||||
this._onConnectHandle = () => this.emit('connect')
|
||||
this._webrtc.on('connected', this._onConnectHandle)
|
||||
|
||||
this._onDisconnectHandle = (error?: Error) => this.emit('disconnect', error)
|
||||
this._webrtc.on('disconnected', this._onDisconnectHandle)
|
||||
}
|
||||
|
||||
public get connected() {
|
||||
return this._webrtc.connected
|
||||
}
|
||||
|
||||
public connect() {
|
||||
if (!this._webrtc.supported) return
|
||||
|
||||
if (this._webrtc.connected) {
|
||||
this._webrtc.disconnect()
|
||||
}
|
||||
|
||||
if (this._websocket.connected) {
|
||||
// use requests from state to connect with selected values
|
||||
|
||||
let selector = null
|
||||
if (this._state.webrtc.video.id) {
|
||||
selector = {
|
||||
id: this._state.webrtc.video.id,
|
||||
type: 'exact',
|
||||
}
|
||||
}
|
||||
|
||||
this._websocket.send(EVENT.SIGNAL_REQUEST, {
|
||||
video: {
|
||||
disabled: this._state.webrtc.video.disabled,
|
||||
selector,
|
||||
auto: this._state.webrtc.video.auto,
|
||||
},
|
||||
audio: {
|
||||
disabled: this._state.webrtc.audio.disabled,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public disconnect() {
|
||||
this._webrtc.disconnect()
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this._webrtc.off('connected', this._onConnectHandle)
|
||||
this._webrtc.off('disconnected', this._onDisconnectHandle)
|
||||
}
|
||||
}
|
55
client/src/component/internal/reconnector/websocket.ts
Normal file
55
client/src/component/internal/reconnector/websocket.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import type { Connection } from '../../types/state'
|
||||
|
||||
import type { NekoWebSocket } from '../websocket'
|
||||
|
||||
import { ReconnectorAbstract } from '.'
|
||||
|
||||
export class WebsocketReconnector extends ReconnectorAbstract {
|
||||
private _onConnectHandle: () => void
|
||||
private _onDisconnectHandle: (error?: Error) => void
|
||||
|
||||
// eslint-disable-next-line
|
||||
constructor(
|
||||
private readonly _state: Connection,
|
||||
private readonly _websocket: NekoWebSocket,
|
||||
) {
|
||||
super()
|
||||
|
||||
this._onConnectHandle = () => this.emit('connect')
|
||||
this._websocket.on('connected', this._onConnectHandle)
|
||||
|
||||
this._onDisconnectHandle = (error?: Error) => this.emit('disconnect', error)
|
||||
this._websocket.on('disconnected', this._onDisconnectHandle)
|
||||
}
|
||||
|
||||
public get connected() {
|
||||
return this._websocket.connected
|
||||
}
|
||||
|
||||
public connect() {
|
||||
if (!this._websocket.supported) return
|
||||
|
||||
if (this._websocket.connected) {
|
||||
this._websocket.disconnect('connection replaced')
|
||||
}
|
||||
|
||||
let url = this._state.url
|
||||
url = url.replace(/^http/, 'ws').replace(/\/+$/, '') + '/api/ws'
|
||||
|
||||
const token = this._state.token
|
||||
if (token) {
|
||||
url += '?token=' + encodeURIComponent(token)
|
||||
}
|
||||
|
||||
this._websocket.connect(url)
|
||||
}
|
||||
|
||||
public disconnect() {
|
||||
this._websocket.disconnect('manual disconnect')
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this._websocket.off('connected', this._onConnectHandle)
|
||||
this._websocket.off('disconnected', this._onDisconnectHandle)
|
||||
}
|
||||
}
|
30
client/src/component/internal/video.ts
Normal file
30
client/src/component/internal/video.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import type { Video } from '../types/state'
|
||||
|
||||
export function register(el: HTMLVideoElement, state: Video) {
|
||||
el.addEventListener('canplaythrough', () => {
|
||||
state.playable = true
|
||||
})
|
||||
el.addEventListener('playing', () => {
|
||||
state.playing = true
|
||||
})
|
||||
el.addEventListener('pause', () => {
|
||||
state.playing = false
|
||||
})
|
||||
el.addEventListener('emptied', () => {
|
||||
state.playable = false
|
||||
state.playing = false
|
||||
})
|
||||
el.addEventListener('error', () => {
|
||||
state.playable = false
|
||||
state.playing = false
|
||||
})
|
||||
el.addEventListener('volumechange', () => {
|
||||
state.muted = el.muted
|
||||
state.volume = el.volume
|
||||
})
|
||||
|
||||
// Initial state
|
||||
state.muted = el.muted
|
||||
state.volume = el.volume
|
||||
state.playing = !el.paused
|
||||
}
|
626
client/src/component/internal/webrtc.ts
Normal file
626
client/src/component/internal/webrtc.ts
Normal file
@ -0,0 +1,626 @@
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import type { WebRTCStats, CursorPosition, CursorImage } from '../types/webrtc'
|
||||
import { Logger } from '../utils/logger'
|
||||
import { videoSnap } from '../utils/video-snap'
|
||||
|
||||
const maxUint32 = 2 ** 32 - 1
|
||||
|
||||
export const OPCODE = {
|
||||
MOVE: 0x01,
|
||||
SCROLL: 0x02,
|
||||
KEY_DOWN: 0x03,
|
||||
KEY_UP: 0x04,
|
||||
BTN_DOWN: 0x05,
|
||||
BTN_UP: 0x06,
|
||||
PING: 0x07,
|
||||
// touch events
|
||||
TOUCH_BEGIN: 0x08,
|
||||
TOUCH_UPDATE: 0x09,
|
||||
TOUCH_END: 0x0a,
|
||||
} as const
|
||||
|
||||
export interface ICEServer {
|
||||
urls: string[]
|
||||
username: string
|
||||
credential: string
|
||||
}
|
||||
|
||||
export interface NekoWebRTCEvents {
|
||||
connected: () => void
|
||||
disconnected: (error?: Error) => void
|
||||
track: (event: RTCTrackEvent) => void
|
||||
candidate: (candidate: RTCIceCandidateInit) => void
|
||||
negotiation: (description: RTCSessionDescriptionInit) => void
|
||||
stable: (isStable: boolean) => void
|
||||
stats: (stats: WebRTCStats) => void
|
||||
fallback: (image: string) => void // send last frame image URL as fallback
|
||||
['cursor-position']: (data: CursorPosition) => void
|
||||
['cursor-image']: (data: CursorImage) => void
|
||||
}
|
||||
|
||||
export class NekoWebRTC extends EventEmitter<NekoWebRTCEvents> {
|
||||
// used for creating snaps from video for fallback mode
|
||||
public video!: HTMLVideoElement
|
||||
// information for WebRTC that server video has been paused, 0fps is expected
|
||||
public paused = false
|
||||
|
||||
private _peer?: RTCPeerConnection
|
||||
private _channel?: RTCDataChannel
|
||||
private _track?: MediaStreamTrack
|
||||
private _state: RTCIceConnectionState = 'disconnected'
|
||||
private _connected = false
|
||||
private _candidates: RTCIceCandidateInit[] = []
|
||||
private _statsStop?: () => void
|
||||
private _requestLatency = 0
|
||||
private _responseLatency = 0
|
||||
|
||||
// eslint-disable-next-line
|
||||
constructor(
|
||||
private readonly _log: Logger = new Logger('webrtc'),
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
get supported() {
|
||||
return typeof RTCPeerConnection !== 'undefined' && typeof RTCPeerConnection.prototype.addTransceiver !== 'undefined'
|
||||
}
|
||||
|
||||
get open() {
|
||||
return (
|
||||
typeof this._peer !== 'undefined' && typeof this._channel !== 'undefined' && this._channel.readyState == 'open'
|
||||
)
|
||||
}
|
||||
|
||||
get connected() {
|
||||
return this.open && ['connected', 'checking', 'completed'].includes(this._state)
|
||||
}
|
||||
|
||||
public async setCandidate(candidate: RTCIceCandidateInit) {
|
||||
if (!this._peer) {
|
||||
this._candidates.push(candidate)
|
||||
return
|
||||
}
|
||||
|
||||
await this._peer.addIceCandidate(candidate)
|
||||
this._log.debug(`adding remote ICE candidate`, { candidate })
|
||||
}
|
||||
|
||||
public async connect(iceServers: ICEServer[]) {
|
||||
if (!this.supported) {
|
||||
throw new Error('browser does not support webrtc')
|
||||
}
|
||||
|
||||
if (this.connected) {
|
||||
throw new Error('attempting to create peer while connected')
|
||||
}
|
||||
|
||||
this._log.info(`connecting`)
|
||||
|
||||
this._connected = false
|
||||
this._peer = new RTCPeerConnection({ iceServers })
|
||||
|
||||
if (iceServers.length == 0) {
|
||||
this._log.warn(`iceservers are empty`)
|
||||
}
|
||||
|
||||
this._peer.onicecandidate = (event: RTCPeerConnectionIceEvent) => {
|
||||
if (!event.candidate) {
|
||||
this._log.debug(`sent all local ICE candidates`)
|
||||
return
|
||||
}
|
||||
|
||||
const init = event.candidate.toJSON()
|
||||
this.emit('candidate', init)
|
||||
this._log.debug(`sending local ICE candidate`, { init })
|
||||
}
|
||||
|
||||
this._peer.onicecandidateerror = (event: Event) => {
|
||||
const e = event as RTCPeerConnectionIceErrorEvent
|
||||
const fields = { error: e.errorText, code: e.errorCode, port: e.port, url: e.url }
|
||||
this._log.warn(`ICE candidate error`, fields)
|
||||
}
|
||||
|
||||
this._peer.onconnectionstatechange = () => {
|
||||
if (!this._peer) {
|
||||
this._log.warn(`attempting to call 'onconnectionstatechange' for nonexistent peer`)
|
||||
return
|
||||
}
|
||||
|
||||
const state = this._peer.connectionState
|
||||
this._log.info(`peer connection state changed`, { state })
|
||||
|
||||
switch (state) {
|
||||
case 'connected':
|
||||
this.onConnected()
|
||||
break
|
||||
// Chrome sends failed state change only for connectionState and not iceConnectionState, and firefox
|
||||
// does not support connectionState at all.
|
||||
case 'closed':
|
||||
case 'failed':
|
||||
this.onDisconnected(new Error('peer ' + state))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
this._peer.oniceconnectionstatechange = () => {
|
||||
if (!this._peer) {
|
||||
this._log.warn(`attempting to call 'oniceconnectionstatechange' for nonexistent peer`)
|
||||
return
|
||||
}
|
||||
|
||||
this._state = this._peer.iceConnectionState
|
||||
this._log.info(`ice connection state changed`, { state: this._state })
|
||||
|
||||
switch (this._state) {
|
||||
case 'connected':
|
||||
this.onConnected()
|
||||
// Connected event makes connection stable.
|
||||
this.emit('stable', true)
|
||||
break
|
||||
case 'disconnected':
|
||||
// Disconnected event makes connection unstable,
|
||||
// may go back to a connected state after some time.
|
||||
this.emit('stable', false)
|
||||
break
|
||||
// We don't watch the disconnected signaling state here as it can indicate temporary issues and may
|
||||
// go back to a connected state after some time. Watching it would close the video call on any temporary
|
||||
// network issue.
|
||||
case 'closed':
|
||||
case 'failed':
|
||||
this.onDisconnected(new Error('peer ' + this._state))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
this._peer.onsignalingstatechange = () => {
|
||||
if (!this._peer) {
|
||||
this._log.warn(`attempting to call 'onsignalingstatechange' for nonexistent peer`)
|
||||
return
|
||||
}
|
||||
|
||||
const state = this._peer.signalingState
|
||||
this._log.info(`signaling state changed`, { state })
|
||||
|
||||
// The closed signaling state has been deprecated in favor of the closed iceConnectionState.
|
||||
// We are watching for it here to add a bit of backward compatibility.
|
||||
if (state == 'closed') {
|
||||
this.onDisconnected(new Error('signaling state changed to closed'))
|
||||
}
|
||||
}
|
||||
|
||||
let negotiating = false
|
||||
this._peer.onnegotiationneeded = async () => {
|
||||
if (!this._peer) {
|
||||
this._log.warn(`attempting to call 'onsignalingstatechange' for nonexistent peer`)
|
||||
return
|
||||
}
|
||||
|
||||
const state = this._peer.signalingState
|
||||
this._log.warn(`negotiation is needed`, { state })
|
||||
|
||||
if (negotiating) {
|
||||
this._log.info(`negotiation already in progress; skipping...`)
|
||||
return
|
||||
}
|
||||
|
||||
negotiating = true
|
||||
|
||||
try {
|
||||
// If the connection hasn't yet achieved the "stable" state,
|
||||
// return to the caller. Another negotiationneeded event
|
||||
// will be fired when the state stabilizes.
|
||||
|
||||
if (state != 'stable') {
|
||||
this._log.info(`connection isn't stable yet; postponing...`)
|
||||
return
|
||||
}
|
||||
|
||||
const offer = await this._peer.createOffer()
|
||||
await this._peer.setLocalDescription(offer)
|
||||
|
||||
if (offer) {
|
||||
this.emit('negotiation', offer)
|
||||
} else {
|
||||
this._log.warn(`negotiatoion offer is empty`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
this._log.error(`on negotiation needed failed`, { error })
|
||||
} finally {
|
||||
negotiating = false
|
||||
}
|
||||
}
|
||||
|
||||
this._peer.ontrack = this.onTrack.bind(this)
|
||||
this._peer.ondatachannel = this.onDataChannel.bind(this)
|
||||
}
|
||||
|
||||
public async setOffer(sdp: string) {
|
||||
if (!this._peer) {
|
||||
throw new Error('attempting to set offer for nonexistent peer')
|
||||
}
|
||||
|
||||
await this._peer.setRemoteDescription({ type: 'offer', sdp })
|
||||
|
||||
if (this._candidates.length > 0) {
|
||||
let candidates = 0
|
||||
for (const candidate of this._candidates) {
|
||||
try {
|
||||
await this._peer.addIceCandidate(candidate)
|
||||
candidates++
|
||||
} catch (error: any) {
|
||||
this._log.warn(`unable to add remote ICE candidate`, { error })
|
||||
}
|
||||
}
|
||||
|
||||
this._log.debug(`added ${candidates} remote ICE candidates`, { candidates: this._candidates })
|
||||
this._candidates = []
|
||||
}
|
||||
|
||||
const answer = await this._peer.createAnswer()
|
||||
|
||||
// add stereo=1 to answer sdp to enable stereo audio for chromium
|
||||
answer.sdp = answer.sdp?.replace(/(stereo=1;)?useinbandfec=1/, 'useinbandfec=1;stereo=1')
|
||||
|
||||
await this._peer.setLocalDescription(answer)
|
||||
|
||||
if (answer) {
|
||||
this.emit('negotiation', answer)
|
||||
} else {
|
||||
this._log.warn(`negiotation answer is empty`)
|
||||
}
|
||||
}
|
||||
|
||||
public async setAnswer(sdp: string) {
|
||||
if (!this._peer) {
|
||||
throw new Error('attempting to set answer for nonexistent peer')
|
||||
}
|
||||
|
||||
await this._peer.setRemoteDescription({ type: 'answer', sdp })
|
||||
}
|
||||
|
||||
public async close() {
|
||||
if (!this._peer) {
|
||||
throw new Error('attempting to close nonexistent peer')
|
||||
}
|
||||
|
||||
// create and emit video snap before closing connection
|
||||
try {
|
||||
const imageSrc = await videoSnap(this.video)
|
||||
this.emit('fallback', imageSrc)
|
||||
} catch (error: any) {
|
||||
this._log.warn(`unable to generate video snap`, { error })
|
||||
}
|
||||
|
||||
this.onDisconnected(new Error('connection closed'))
|
||||
}
|
||||
|
||||
public addTrack(track: MediaStreamTrack, ...streams: MediaStream[]): RTCRtpSender {
|
||||
if (!this._peer) {
|
||||
throw new Error('attempting to add track for nonexistent peer')
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const isChromium = !!window.chrome
|
||||
|
||||
// TOOD: Ugly workaround, find real cause of this issue.
|
||||
if (isChromium) {
|
||||
return this._peer.addTrack(track, ...streams)
|
||||
} else {
|
||||
return this._peer.addTransceiver(track, { direction: 'sendonly', streams }).sender
|
||||
}
|
||||
}
|
||||
|
||||
public removeTrack(sender: RTCRtpSender) {
|
||||
if (!this._peer) {
|
||||
throw new Error('attempting to add track for nonexistent peer')
|
||||
}
|
||||
|
||||
this._peer.removeTrack(sender)
|
||||
}
|
||||
|
||||
public disconnect() {
|
||||
if (typeof this._channel !== 'undefined') {
|
||||
// unmount all events
|
||||
this._channel.onerror = () => {}
|
||||
this._channel.onmessage = () => {}
|
||||
this._channel.onopen = () => {}
|
||||
this._channel.onclose = () => {}
|
||||
|
||||
try {
|
||||
this._channel.close()
|
||||
} catch {}
|
||||
|
||||
this._channel = undefined
|
||||
}
|
||||
|
||||
if (typeof this._peer != 'undefined') {
|
||||
// unmount all events
|
||||
this._peer.onicecandidate = () => {}
|
||||
this._peer.onicecandidateerror = () => {}
|
||||
this._peer.onconnectionstatechange = () => {}
|
||||
this._peer.oniceconnectionstatechange = () => {}
|
||||
this._peer.onsignalingstatechange = () => {}
|
||||
this._peer.onnegotiationneeded = () => {}
|
||||
this._peer.ontrack = () => {}
|
||||
this._peer.ondatachannel = () => {}
|
||||
|
||||
try {
|
||||
this._peer.close()
|
||||
} catch {}
|
||||
|
||||
this._peer = undefined
|
||||
}
|
||||
|
||||
if (this._statsStop && typeof this._statsStop === 'function') {
|
||||
this._statsStop()
|
||||
this._statsStop = undefined
|
||||
}
|
||||
|
||||
this._track = undefined
|
||||
this._state = 'disconnected'
|
||||
this._connected = false
|
||||
this._candidates = []
|
||||
}
|
||||
|
||||
public send(event: 'wheel', data: { delta_x: number; delta_y: number; control_key?: boolean }): void
|
||||
public send(event: 'mousemove', data: { x: number; y: number }): void
|
||||
public send(event: 'mousedown' | 'mouseup' | 'keydown' | 'keyup', data: { key: number }): void
|
||||
public send(event: 'ping', data: number): void
|
||||
public send(
|
||||
event: 'touchbegin' | 'touchupdate' | 'touchend',
|
||||
data: { touch_id: number; x: number; y: number; pressure: number },
|
||||
): void
|
||||
public send(event: string, data: any): void {
|
||||
if (typeof this._channel === 'undefined' || this._channel.readyState !== 'open') {
|
||||
this._log.warn(`attempting to send data, but data-channel is not open`, { event })
|
||||
return
|
||||
}
|
||||
|
||||
let buffer: ArrayBuffer
|
||||
let payload: DataView
|
||||
switch (event) {
|
||||
case 'mousemove':
|
||||
buffer = new ArrayBuffer(7)
|
||||
payload = new DataView(buffer)
|
||||
payload.setUint8(0, OPCODE.MOVE)
|
||||
payload.setUint16(1, 4)
|
||||
payload.setUint16(3, data.x)
|
||||
payload.setUint16(5, data.y)
|
||||
break
|
||||
case 'wheel':
|
||||
buffer = new ArrayBuffer(8)
|
||||
payload = new DataView(buffer)
|
||||
payload.setUint8(0, OPCODE.SCROLL)
|
||||
payload.setUint16(1, 5)
|
||||
payload.setInt16(3, data.delta_x)
|
||||
payload.setInt16(5, data.delta_y)
|
||||
payload.setUint8(7, data.control_key ? 1 : 0)
|
||||
break
|
||||
case 'keydown':
|
||||
buffer = new ArrayBuffer(7)
|
||||
payload = new DataView(buffer)
|
||||
payload.setUint8(0, OPCODE.KEY_DOWN)
|
||||
payload.setUint16(1, 4)
|
||||
payload.setUint32(3, data.key)
|
||||
break
|
||||
case 'keyup':
|
||||
buffer = new ArrayBuffer(7)
|
||||
payload = new DataView(buffer)
|
||||
payload.setUint8(0, OPCODE.KEY_UP)
|
||||
payload.setUint16(1, 4)
|
||||
payload.setUint32(3, data.key)
|
||||
break
|
||||
case 'mousedown':
|
||||
buffer = new ArrayBuffer(7)
|
||||
payload = new DataView(buffer)
|
||||
payload.setUint8(0, OPCODE.BTN_DOWN)
|
||||
payload.setUint16(1, 4)
|
||||
payload.setUint32(3, data.key)
|
||||
break
|
||||
case 'mouseup':
|
||||
buffer = new ArrayBuffer(7)
|
||||
payload = new DataView(buffer)
|
||||
payload.setUint8(0, OPCODE.BTN_UP)
|
||||
payload.setUint16(1, 4)
|
||||
payload.setUint32(3, data.key)
|
||||
break
|
||||
case 'ping':
|
||||
buffer = new ArrayBuffer(11)
|
||||
payload = new DataView(buffer)
|
||||
payload.setUint8(0, OPCODE.PING)
|
||||
payload.setUint16(1, 8)
|
||||
payload.setUint32(3, Math.trunc(data / maxUint32))
|
||||
payload.setUint32(7, data % maxUint32)
|
||||
break
|
||||
case 'touchbegin':
|
||||
case 'touchupdate':
|
||||
case 'touchend':
|
||||
buffer = new ArrayBuffer(16)
|
||||
payload = new DataView(buffer)
|
||||
if (event === 'touchbegin') {
|
||||
payload.setUint8(0, OPCODE.TOUCH_BEGIN)
|
||||
} else if (event === 'touchupdate') {
|
||||
payload.setUint8(0, OPCODE.TOUCH_UPDATE)
|
||||
} else if (event === 'touchend') {
|
||||
payload.setUint8(0, OPCODE.TOUCH_END)
|
||||
}
|
||||
payload.setUint16(1, 13)
|
||||
payload.setUint32(3, data.touch_id)
|
||||
payload.setInt32(7, data.x)
|
||||
payload.setInt32(11, data.y)
|
||||
payload.setUint8(15, data.pressure)
|
||||
break
|
||||
default:
|
||||
this._log.warn(`unknown data event`, { event })
|
||||
return
|
||||
}
|
||||
|
||||
this._channel.send(buffer)
|
||||
}
|
||||
|
||||
private onTrack(event: RTCTrackEvent) {
|
||||
this._log.debug(`received track from peer`, { label: event.track.label })
|
||||
|
||||
const stream = event.streams[0]
|
||||
if (!stream) {
|
||||
this._log.warn(`no stream provided for track`, { label: event.track.label })
|
||||
return
|
||||
}
|
||||
|
||||
if (event.track.kind === 'video') {
|
||||
this._track = event.track
|
||||
}
|
||||
|
||||
this.emit('track', event)
|
||||
}
|
||||
|
||||
private onDataChannel(event: RTCDataChannelEvent) {
|
||||
this._log.debug(`received data channel from peer`, { label: event.channel.label })
|
||||
|
||||
this._channel = event.channel
|
||||
this._channel.binaryType = 'arraybuffer'
|
||||
this._channel.onerror = this.onDisconnected.bind(this, new Error('peer data channel error'))
|
||||
this._channel.onmessage = this.onData.bind(this)
|
||||
this._channel.onopen = this.onConnected.bind(this)
|
||||
this._channel.onclose = this.onDisconnected.bind(this, new Error('peer data channel closed'))
|
||||
}
|
||||
|
||||
private onData(e: MessageEvent) {
|
||||
const payload = new DataView(e.data)
|
||||
const event = payload.getUint8(0)
|
||||
const length = payload.getUint16(1)
|
||||
|
||||
switch (event) {
|
||||
case 1:
|
||||
this.emit('cursor-position', {
|
||||
x: payload.getUint16(3),
|
||||
y: payload.getUint16(5),
|
||||
})
|
||||
break
|
||||
case 2:
|
||||
const data = e.data.slice(11, length - 11)
|
||||
|
||||
// TODO: get string from server
|
||||
const blob = new Blob([data], { type: 'image/png' })
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
this.emit('cursor-image', {
|
||||
width: payload.getUint16(3),
|
||||
height: payload.getUint16(5),
|
||||
x: payload.getUint16(7),
|
||||
y: payload.getUint16(9),
|
||||
uri: String(e.target!.result),
|
||||
})
|
||||
}
|
||||
reader.readAsDataURL(blob)
|
||||
|
||||
break
|
||||
case 3:
|
||||
const nowTs = Date.now()
|
||||
|
||||
const [clientTs1, clientTs2] = [payload.getUint32(3), payload.getUint32(7)]
|
||||
const clientTs = clientTs1 * maxUint32 + clientTs2
|
||||
const [serverTs1, serverTs2] = [payload.getUint32(11), payload.getUint32(15)]
|
||||
const serverTs = serverTs1 * maxUint32 + serverTs2
|
||||
|
||||
this._requestLatency = serverTs - clientTs
|
||||
this._responseLatency = nowTs - serverTs
|
||||
|
||||
break
|
||||
default:
|
||||
this._log.warn(`unhandled webrtc event`, { event, payload })
|
||||
}
|
||||
}
|
||||
|
||||
private onConnected() {
|
||||
if (!this.connected || this._connected) {
|
||||
return
|
||||
}
|
||||
|
||||
this._log.info(`connected`)
|
||||
this.emit('connected')
|
||||
|
||||
this._statsStop = this.statsEmitter()
|
||||
this._connected = true
|
||||
}
|
||||
|
||||
private onDisconnected(error?: Error) {
|
||||
const wasConnected = this._connected
|
||||
this.disconnect()
|
||||
|
||||
if (wasConnected) {
|
||||
this._log.info(`disconnected`, { error })
|
||||
this.emit('disconnected', error)
|
||||
}
|
||||
}
|
||||
|
||||
private statsEmitter(ms: number = 2000) {
|
||||
let bytesReceived: number
|
||||
let timestamp: number
|
||||
let framesDecoded: number
|
||||
let packetsLost: number
|
||||
let packetsReceived: number
|
||||
|
||||
const timer = window.setInterval(async () => {
|
||||
if (!this._peer) return
|
||||
|
||||
let stats: RTCStatsReport | undefined = undefined
|
||||
if (this._peer.getStats.length === 0) {
|
||||
stats = await this._peer.getStats()
|
||||
} else {
|
||||
// callback browsers support
|
||||
await new Promise((res) => {
|
||||
//@ts-ignore
|
||||
this._peer.getStats((stats) => res(stats))
|
||||
})
|
||||
}
|
||||
|
||||
if (!stats) return
|
||||
|
||||
let report: any = null
|
||||
stats.forEach(function (stat) {
|
||||
if (stat.type === 'inbound-rtp' && stat.kind === 'video') {
|
||||
report = { ...stat }
|
||||
}
|
||||
})
|
||||
|
||||
if (report === null) return
|
||||
|
||||
if (timestamp) {
|
||||
const bytesDiff = (report.bytesReceived - bytesReceived) * 8
|
||||
const tsDiff = report.timestamp - timestamp
|
||||
const framesDecodedDiff = report.framesDecoded - framesDecoded
|
||||
const packetsLostDiff = report.packetsLost - packetsLost
|
||||
const packetsReceivedDiff = report.packetsReceived - packetsReceived
|
||||
|
||||
this.emit('stats', {
|
||||
// Firefox does not emit any event when starting paused
|
||||
// because there is no video report found in stats.
|
||||
paused: this.paused,
|
||||
bitrate: (bytesDiff / tsDiff) * 1000,
|
||||
packetLoss: (packetsLostDiff / (packetsLostDiff + packetsReceivedDiff)) * 100,
|
||||
fps: Number(report.framesPerSecond || framesDecodedDiff / (tsDiff / 1000)),
|
||||
width: report.frameWidth || NaN,
|
||||
height: report.frameHeight || NaN,
|
||||
muted: this._track?.muted,
|
||||
// latency from ping/pong messages
|
||||
latency: this._requestLatency + this._responseLatency,
|
||||
requestLatency: this._requestLatency,
|
||||
responseLatency: this._responseLatency,
|
||||
})
|
||||
}
|
||||
|
||||
bytesReceived = report.bytesReceived
|
||||
timestamp = report.timestamp
|
||||
framesDecoded = report.framesDecoded
|
||||
packetsLost = report.packetsLost
|
||||
packetsReceived = report.packetsReceived
|
||||
|
||||
this.send('ping', Date.now())
|
||||
}, ms)
|
||||
|
||||
return function () {
|
||||
window.clearInterval(timer)
|
||||
}
|
||||
}
|
||||
}
|
164
client/src/component/internal/websocket.ts
Normal file
164
client/src/component/internal/websocket.ts
Normal file
@ -0,0 +1,164 @@
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import { SYSTEM_HEARTBEAT, SYSTEM_LOGS } from '../types/events'
|
||||
import { Logger } from '../utils/logger'
|
||||
|
||||
export interface NekoWebSocketEvents {
|
||||
connected: () => void
|
||||
disconnected: (error?: Error) => void
|
||||
message: (event: string, payload: any) => void
|
||||
}
|
||||
|
||||
// how long can connection be idle before closing
|
||||
const STALE_TIMEOUT_MS = 12_500 // 12.5 seconds
|
||||
|
||||
// how often should stale check be evaluated
|
||||
const STALE_INTERVAL_MS = 7_000 // 7 seconds
|
||||
|
||||
const STATUS_CODE_MAP = {
|
||||
1000: 'Normal Closure',
|
||||
1001: 'Going Away',
|
||||
1002: 'Protocol Error',
|
||||
1003: 'Unsupported Data',
|
||||
1004: '(For future)',
|
||||
1005: 'No Status Received',
|
||||
1006: 'Abnormal Closure',
|
||||
1007: 'Invalid frame payload data',
|
||||
1008: 'Policy Violation',
|
||||
1009: 'Message too big',
|
||||
1010: 'Missing Extension',
|
||||
1011: 'Internal Error',
|
||||
1012: 'Service Restart',
|
||||
1013: 'Try Again Later',
|
||||
1014: 'Bad Gateway',
|
||||
1015: 'TLS Handshake',
|
||||
} as Record<number, string>
|
||||
|
||||
export class NekoWebSocket extends EventEmitter<NekoWebSocketEvents> {
|
||||
private _ws?: WebSocket
|
||||
private _stale_interval?: number
|
||||
private _last_received?: Date
|
||||
|
||||
// eslint-disable-next-line
|
||||
constructor(
|
||||
private readonly _log: Logger = new Logger('websocket'),
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
get supported() {
|
||||
return typeof WebSocket !== 'undefined' && WebSocket.OPEN === 1
|
||||
}
|
||||
|
||||
get connected() {
|
||||
return typeof this._ws !== 'undefined' && this._ws.readyState === WebSocket.OPEN
|
||||
}
|
||||
|
||||
public connect(url: string) {
|
||||
if (!this.supported) {
|
||||
throw new Error('browser does not support websockets')
|
||||
}
|
||||
|
||||
if (this.connected) {
|
||||
throw new Error('attempting to create websocket while connection open')
|
||||
}
|
||||
|
||||
if (typeof this._ws !== 'undefined') {
|
||||
this._log.debug(`previous websocket connection needs to be closed`)
|
||||
this.disconnect('connection replaced')
|
||||
}
|
||||
|
||||
this._ws = new WebSocket(url)
|
||||
|
||||
this._log.info(`connecting`)
|
||||
|
||||
this._ws.onopen = this.onConnected.bind(this)
|
||||
this._ws.onclose = (e: CloseEvent) => {
|
||||
let reason = 'close'
|
||||
|
||||
if (e.code in STATUS_CODE_MAP) {
|
||||
reason = STATUS_CODE_MAP[e.code]
|
||||
}
|
||||
|
||||
this.onDisconnected(reason)
|
||||
}
|
||||
this._ws.onerror = this.onDisconnected.bind(this, 'error')
|
||||
this._ws.onmessage = this.onMessage.bind(this)
|
||||
}
|
||||
|
||||
public disconnect(reason: string) {
|
||||
this._last_received = undefined
|
||||
|
||||
if (this._stale_interval) {
|
||||
window.clearInterval(this._stale_interval)
|
||||
this._stale_interval = undefined
|
||||
}
|
||||
|
||||
if (typeof this._ws !== 'undefined') {
|
||||
// unmount all events
|
||||
this._ws.onopen = () => {}
|
||||
this._ws.onclose = () => {}
|
||||
this._ws.onerror = () => {}
|
||||
this._ws.onmessage = () => {}
|
||||
|
||||
try {
|
||||
this._ws.close(1000, reason)
|
||||
} catch {}
|
||||
|
||||
this._ws = undefined
|
||||
}
|
||||
}
|
||||
|
||||
public send(event: string, payload?: any) {
|
||||
if (!this.connected) {
|
||||
this._log.warn(`attempting to send message while disconnected`)
|
||||
return
|
||||
}
|
||||
|
||||
if (event != SYSTEM_LOGS) this._log.debug(`sending websocket event`, { event, payload })
|
||||
this._ws!.send(JSON.stringify({ event, payload }))
|
||||
}
|
||||
|
||||
private onMessage(e: MessageEvent) {
|
||||
const { event, payload } = JSON.parse(e.data)
|
||||
|
||||
this._last_received = new Date()
|
||||
// heartbeat only updates last_received
|
||||
if (event == SYSTEM_HEARTBEAT) return
|
||||
|
||||
this._log.debug(`received websocket event`, { event, payload })
|
||||
this.emit('message', event, payload)
|
||||
}
|
||||
|
||||
private onConnected() {
|
||||
if (!this.connected) {
|
||||
this._log.warn(`onConnected called while being disconnected`)
|
||||
return
|
||||
}
|
||||
|
||||
// periodically check if connection is stale
|
||||
if (this._stale_interval) window.clearInterval(this._stale_interval)
|
||||
this._stale_interval = window.setInterval(this.onStaleCheck.bind(this), STALE_INTERVAL_MS)
|
||||
|
||||
this._log.info(`connected`)
|
||||
this.emit('connected')
|
||||
}
|
||||
|
||||
private onDisconnected(reason: string) {
|
||||
this.disconnect(reason)
|
||||
|
||||
this._log.info(`disconnected`, { reason })
|
||||
this.emit('disconnected', new Error(`connection ${reason}`))
|
||||
}
|
||||
|
||||
private onStaleCheck() {
|
||||
if (!this._last_received) return
|
||||
|
||||
// if we haven't received a message in specified time,
|
||||
// assume the connection is dead
|
||||
const diff = new Date().getTime() - this._last_received.getTime()
|
||||
if (diff < STALE_TIMEOUT_MS) return
|
||||
|
||||
this._log.warn(`websocket connection is stale, disconnecting`)
|
||||
this.onDisconnected('stale')
|
||||
}
|
||||
}
|
832
client/src/component/main.vue
Normal file
832
client/src/component/main.vue
Normal file
@ -0,0 +1,832 @@
|
||||
<template>
|
||||
<div ref="component" class="neko-component">
|
||||
<div ref="container" class="neko-container">
|
||||
<video ref="video" playsinline></video>
|
||||
<Screencast
|
||||
v-show="screencast && screencastReady"
|
||||
:image="fallbackImage"
|
||||
:enabled="screencast || (!state.connection.webrtc.stable && state.connection.webrtc.connected)"
|
||||
:api="api.room"
|
||||
@imageReady="screencastReady = $event"
|
||||
/>
|
||||
<Cursors
|
||||
v-if="state.settings.inactive_cursors && session && session.profile.can_see_inactive_cursors"
|
||||
:sessions="state.sessions"
|
||||
:sessionId="state.session_id || ''"
|
||||
:hostId="state.control.host_id"
|
||||
:screenSize="state.screen.size"
|
||||
:canvasSize="canvasSize"
|
||||
:cursors="state.cursors"
|
||||
:cursorDraw="inactiveCursorDrawFunction"
|
||||
:fps="fps"
|
||||
/>
|
||||
<Overlay
|
||||
ref="overlay"
|
||||
v-show="!private_mode_enabled && state.connection.status != 'disconnected'"
|
||||
:style="{ pointerEvents: state.control.locked || (session && !session.profile.can_host) ? 'none' : 'auto' }"
|
||||
:control="control"
|
||||
:sessions="state.sessions"
|
||||
:hostId="state.control.host_id || ''"
|
||||
:webrtc="connection.webrtc"
|
||||
:scroll="state.control.scroll"
|
||||
:screenSize="state.screen.size"
|
||||
:canvasSize="canvasSize"
|
||||
:isControling="controlling"
|
||||
:cursorDraw="cursorDrawFunction"
|
||||
:implicitControl="!!(state.settings.implicit_hosting && session && session.profile.can_host)"
|
||||
:inactiveCursors="!!(state.settings.inactive_cursors && session && session.profile.sends_inactive_cursor)"
|
||||
:fps="fps"
|
||||
:hasMobileKeyboard="is_touch_device"
|
||||
@updateKeyboardModifiers="updateKeyboardModifiers($event)"
|
||||
@uploadDrop="uploadDrop($event)"
|
||||
@mobileKeyboardOpen="state.mobile_keyboard_open = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.neko-component {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.neko-container {
|
||||
position: relative;
|
||||
|
||||
video,
|
||||
img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
background: transparent !important;
|
||||
|
||||
&::-webkit-media-controls {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, computed, reactive, onMounted, onBeforeUnmount } from 'vue'
|
||||
|
||||
//export * as ApiModels from './api/models'
|
||||
//export * as StateModels from './types/state'
|
||||
//export * as webrtcTypes from './types/webrtc'
|
||||
|
||||
import type { Configuration } from './api/configuration'
|
||||
import type { AxiosInstance, AxiosProgressEvent } from 'axios'
|
||||
|
||||
import ResizeObserver from 'resize-observer-polyfill'
|
||||
|
||||
import { NekoApi } from './internal/api'
|
||||
import type { SessionsApi, MembersApi, RoomApi } from './internal/api'
|
||||
import { NekoConnection } from './internal/connection'
|
||||
import { NekoMessages } from './internal/messages'
|
||||
import { NekoControl } from './internal/control'
|
||||
import { register as VideoRegister } from './internal/video'
|
||||
|
||||
import type { ReconnectorConfig } from './types/reconnector'
|
||||
import * as EVENT from './types/events'
|
||||
import type * as webrtcTypes from './types/webrtc'
|
||||
import type NekoState from './types/state'
|
||||
import type { CursorDrawFunction, InactiveCursorDrawFunction, Dimension } from './types/cursors'
|
||||
import Overlay from './overlay.vue'
|
||||
import Screencast from './screencast.vue'
|
||||
import Cursors from './cursors.vue'
|
||||
|
||||
const SCREEN_SYNC_THROTTLE = 500 // wait 500ms before reacting to automatic screen size change
|
||||
|
||||
const component = ref<HTMLElement | null>(null)
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
const video = ref<HTMLVideoElement | null>(null)
|
||||
const overlay = ref<typeof Overlay | null>(null)
|
||||
|
||||
// fallback image for webrtc reconnections:
|
||||
// chrome shows black screen when closing webrtc connection, that's why
|
||||
// we need to grab video image before closing connection ans show that
|
||||
// while reconnecting, to not see black screen
|
||||
const fallbackImage = ref('')
|
||||
|
||||
const api = new NekoApi()
|
||||
const observer = new ResizeObserver(onResize)
|
||||
const canvasSize = ref<Dimension>({ width: 0, height: 0 })
|
||||
const cursorDrawFunction = ref<CursorDrawFunction | null>(null)
|
||||
const inactiveCursorDrawFunction = ref<InactiveCursorDrawFunction | null>(null)
|
||||
|
||||
const props = defineProps({
|
||||
server: String,
|
||||
autologin: Boolean,
|
||||
autoconnect: Boolean,
|
||||
autoplay: Boolean,
|
||||
// fps for cursor rendering, 0 for no cap
|
||||
fps: { type: Number, default: 0 },
|
||||
// auto / touch / mouse
|
||||
inputMode: { type: String, default: 'auto' },
|
||||
})
|
||||
|
||||
/////////////////////////////
|
||||
// Public state
|
||||
/////////////////////////////
|
||||
const state = reactive<NekoState>({
|
||||
authenticated: false,
|
||||
connection: {
|
||||
url: location.href,
|
||||
token: undefined,
|
||||
status: 'disconnected',
|
||||
websocket: {
|
||||
connected: false,
|
||||
config: {
|
||||
max_reconnects: 15,
|
||||
timeout_ms: 5000,
|
||||
backoff_ms: 1500,
|
||||
},
|
||||
},
|
||||
webrtc: {
|
||||
connected: false,
|
||||
stable: false,
|
||||
config: {
|
||||
max_reconnects: 15,
|
||||
timeout_ms: 10000,
|
||||
backoff_ms: 1500,
|
||||
},
|
||||
stats: null,
|
||||
video: {
|
||||
disabled: false,
|
||||
id: '',
|
||||
auto: false,
|
||||
},
|
||||
audio: {
|
||||
disabled: false,
|
||||
},
|
||||
videos: [],
|
||||
},
|
||||
screencast: true, // TODO: Should get by API call.
|
||||
type: 'none',
|
||||
},
|
||||
video: {
|
||||
playable: false,
|
||||
playing: false,
|
||||
volume: 0,
|
||||
muted: false,
|
||||
},
|
||||
control: {
|
||||
scroll: {
|
||||
inverse: true,
|
||||
sensitivity: 0,
|
||||
},
|
||||
clipboard: null,
|
||||
keyboard: {
|
||||
layout: 'us',
|
||||
variant: '',
|
||||
},
|
||||
touch: {
|
||||
enabled: true,
|
||||
supported: false,
|
||||
},
|
||||
host_id: null,
|
||||
is_host: false,
|
||||
locked: false,
|
||||
},
|
||||
screen: {
|
||||
size: {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
rate: 30,
|
||||
},
|
||||
configurations: [],
|
||||
sync: {
|
||||
enabled: false,
|
||||
multiplier: 0,
|
||||
rate: 30,
|
||||
},
|
||||
},
|
||||
session_id: null,
|
||||
sessions: {},
|
||||
settings: {
|
||||
private_mode: false,
|
||||
locked_logins: false,
|
||||
locked_controls: false,
|
||||
control_protection: false,
|
||||
implicit_hosting: false,
|
||||
inactive_cursors: false,
|
||||
merciful_reconnect: false,
|
||||
},
|
||||
cursors: [],
|
||||
mobile_keyboard_open: false,
|
||||
})
|
||||
|
||||
/////////////////////////////
|
||||
// Connection manager
|
||||
/////////////////////////////
|
||||
|
||||
const connection = new NekoConnection(state.connection)
|
||||
|
||||
const connected = computed(() => state.connection.status == 'connected')
|
||||
const controlling = computed(() => state.control.host_id !== null && state.session_id === state.control.host_id)
|
||||
const session = computed(() => (state.session_id != null ? state.sessions[state.session_id] : null))
|
||||
const is_admin = computed(() => session.value?.profile.is_admin || false)
|
||||
const private_mode_enabled = computed(() => state.settings.private_mode && !is_admin.value)
|
||||
const is_touch_device = computed(() => {
|
||||
if (props.inputMode == 'mouse') return false
|
||||
if (props.inputMode == 'touch') return true
|
||||
|
||||
return (
|
||||
// check if the device has a touch screen
|
||||
('ontouchstart' in window || navigator.maxTouchPoints > 0) &&
|
||||
// we also check if the device has a pointer
|
||||
!window.matchMedia('(pointer:fine)').matches &&
|
||||
// and is capable of hover, then it probably has a mouse
|
||||
!window.matchMedia('(hover:hover)').matches
|
||||
)
|
||||
})
|
||||
|
||||
watch(private_mode_enabled, (enabled) => {
|
||||
connection.webrtc.paused = enabled
|
||||
})
|
||||
|
||||
const screencastReady = ref(false)
|
||||
const screencast = computed(() => {
|
||||
return (
|
||||
state.authenticated &&
|
||||
state.connection.status != 'disconnected' &&
|
||||
state.connection.screencast &&
|
||||
(!state.connection.webrtc.connected || (state.connection.webrtc.connected && !state.video.playing))
|
||||
)
|
||||
})
|
||||
|
||||
/////////////////////////////
|
||||
// Public events
|
||||
/////////////////////////////
|
||||
const events = new NekoMessages(connection, state)
|
||||
|
||||
/////////////////////////////
|
||||
// Public methods
|
||||
/////////////////////////////
|
||||
|
||||
function setUrl(url?: string) {
|
||||
if (!url) {
|
||||
url = location.href
|
||||
}
|
||||
|
||||
// url can contain ?token=<string>
|
||||
let token = new URL(url).searchParams.get('token') || undefined
|
||||
|
||||
// get URL without query params
|
||||
url = url.split('?')[0]
|
||||
|
||||
const httpURL = url.replace(/^ws/, 'http').replace(/\/$|\/ws\/?$/, '')
|
||||
api.setUrl(httpURL)
|
||||
state.connection.url = httpURL // TODO: Vue.Set
|
||||
|
||||
try {
|
||||
disconnect()
|
||||
} catch {}
|
||||
|
||||
if (state.authenticated) {
|
||||
state.authenticated = false // TODO: Vue.Set
|
||||
}
|
||||
|
||||
// save token to state
|
||||
state.connection.token = token // TODO: Vue.Set
|
||||
|
||||
// try to authenticate and connect
|
||||
if (props.autoconnect) {
|
||||
try {
|
||||
authenticate()
|
||||
connect()
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.server, (url) => {
|
||||
setUrl(url)
|
||||
}, { immediate: true })
|
||||
|
||||
async function authenticate(token?: string) {
|
||||
if (!token) {
|
||||
token = state.connection.token
|
||||
}
|
||||
|
||||
if (!token && props.autologin) {
|
||||
token = localStorage.getItem('neko_session') ?? undefined
|
||||
}
|
||||
|
||||
if (token) {
|
||||
api.setToken(token)
|
||||
state.connection.token = token // TODO: Vue.Set
|
||||
}
|
||||
|
||||
await api.default.whoami()
|
||||
state.authenticated = true // TODO: Vue.Set
|
||||
|
||||
if (token && props.autologin) {
|
||||
localStorage.setItem('neko_session', token)
|
||||
}
|
||||
}
|
||||
|
||||
async function login(username: string, password: string) {
|
||||
if (state.authenticated) {
|
||||
throw new Error('client already authenticated')
|
||||
}
|
||||
|
||||
const res = await api.default.login({ username, password })
|
||||
if (res.data.token) {
|
||||
api.setToken(res.data.token)
|
||||
state.connection.token = res.data.token // TODO: Vue.Set
|
||||
|
||||
if (props.autologin) {
|
||||
localStorage.setItem('neko_session', res.data.token)
|
||||
}
|
||||
}
|
||||
|
||||
state.authenticated = true // TODO: Vue.Set
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
if (!state.authenticated) {
|
||||
throw new Error('client not authenticated')
|
||||
}
|
||||
|
||||
try {
|
||||
disconnect()
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
await api.default.logout()
|
||||
} finally {
|
||||
api.setToken('')
|
||||
delete state.connection.token // TODO: Vue.Delete
|
||||
|
||||
if (props.autologin) {
|
||||
localStorage.removeItem('neko_session')
|
||||
}
|
||||
|
||||
state.authenticated = false // TODO: Vue.Set
|
||||
}
|
||||
}
|
||||
|
||||
function connect(peerRequest?: webrtcTypes.PeerRequest) {
|
||||
if (!state.authenticated) {
|
||||
throw new Error('client not authenticated')
|
||||
}
|
||||
|
||||
if (connected.value) {
|
||||
throw new Error('client is already connected')
|
||||
}
|
||||
|
||||
connection.open(peerRequest)
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
connection.close()
|
||||
}
|
||||
|
||||
function setReconnectorConfig(type: 'websocket' | 'webrtc', config: ReconnectorConfig) {
|
||||
if (type != 'websocket' && type != 'webrtc') {
|
||||
throw new Error('unknown reconnector type')
|
||||
}
|
||||
|
||||
state.connection[type].config = config // TODO: Vue.Set
|
||||
connection.reloadConfigs()
|
||||
}
|
||||
|
||||
async function play() {
|
||||
// if autoplay is disabled, play() will throw an error
|
||||
// and we need to properly save the state otherwise we
|
||||
// would be thinking we're playing when we're not
|
||||
try {
|
||||
await video.value!.play()
|
||||
} catch (e: any) {
|
||||
if (video.value!.muted) {
|
||||
throw e
|
||||
}
|
||||
|
||||
// video.play() can fail if audio is set due restrictive
|
||||
// browsers autoplay policy -> retry with muted audio
|
||||
try {
|
||||
video.value!.muted = true
|
||||
await video.value!.play()
|
||||
// unmute on users first interaction
|
||||
document.addEventListener('click', autoUnmute, { once: true })
|
||||
overlay.value!.once('overlay.click', autoUnmute)
|
||||
} catch (e: any) {
|
||||
// if it still fails, we're not playing anything
|
||||
video.value!.muted = false
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pause() {
|
||||
video.value!.pause()
|
||||
}
|
||||
|
||||
function mute() {
|
||||
video.value!.muted = true
|
||||
}
|
||||
|
||||
function unmute() {
|
||||
video.value!.muted = false
|
||||
}
|
||||
|
||||
// when autoplay fails, we mute the video and wait for the user
|
||||
// to interact with the page to unmute it again
|
||||
function autoUnmute() {
|
||||
unmute()
|
||||
|
||||
// remove listeners
|
||||
document.removeEventListener('click', autoUnmute)
|
||||
overlay.value!.removeListener('overlay.click', autoUnmute)
|
||||
}
|
||||
|
||||
function setVolume(value: number) {
|
||||
if (value < 0 || value > 1) {
|
||||
throw new Error('volume must be between 0 and 1')
|
||||
}
|
||||
|
||||
video.value!.volume = value
|
||||
}
|
||||
|
||||
function setScrollInverse(value: boolean = true) {
|
||||
state.control.scroll.inverse = value // TODO: Vue.Set
|
||||
}
|
||||
|
||||
function setScrollSensitivity(value: number) {
|
||||
state.control.scroll.sensitivity = value // TODO: Vue.Set
|
||||
}
|
||||
|
||||
function setKeyboard(layout: string, variant: string = '') {
|
||||
state.control.keyboard = { layout, variant } // TODO: Vue.Set
|
||||
}
|
||||
|
||||
function setTouchEnabled(value: boolean = true) {
|
||||
state.control.touch.enabled = value // TODO: Vue.Set
|
||||
}
|
||||
|
||||
function mobileKeyboardShow() {
|
||||
overlay.value!.mobileKeyboardShow()
|
||||
}
|
||||
|
||||
function mobileKeyboardHide() {
|
||||
overlay.value!.mobileKeyboardHide()
|
||||
}
|
||||
|
||||
function mobileKeyboardToggle() {
|
||||
if (state.mobile_keyboard_open) {
|
||||
mobileKeyboardHide()
|
||||
} else {
|
||||
mobileKeyboardShow()
|
||||
}
|
||||
}
|
||||
|
||||
function setScreenSync(enabled: boolean = true, multiplier: number = 0, rate: number = 60) {
|
||||
state.screen.sync.enabled = enabled // TODO: Vue.Set
|
||||
state.screen.sync.multiplier = multiplier // TODO: Vue.Set
|
||||
state.screen.sync.rate = rate // TODO: Vue.Set
|
||||
}
|
||||
|
||||
function setCursorDrawFunction(fn?: CursorDrawFunction) {
|
||||
cursorDrawFunction.value = (fn || null)
|
||||
}
|
||||
|
||||
function setInactiveCursorDrawFunction(fn?: InactiveCursorDrawFunction) {
|
||||
inactiveCursorDrawFunction.value = (fn || null)
|
||||
}
|
||||
|
||||
// TODO: Remove? Use REST API only?
|
||||
function setScreenSize(width: number, height: number, rate: number) {
|
||||
connection.websocket.send(EVENT.SCREEN_SET, { width, height, rate })
|
||||
}
|
||||
|
||||
function setWebRTCVideo(peerVideo: webrtcTypes.PeerVideoRequest) {
|
||||
connection.websocket.send(EVENT.SIGNAL_VIDEO, peerVideo)
|
||||
}
|
||||
|
||||
function setWebRTCAudio(peerAudio: webrtcTypes.PeerAudioRequest) {
|
||||
connection.websocket.send(EVENT.SIGNAL_AUDIO, peerAudio)
|
||||
}
|
||||
|
||||
function addTrack(track: MediaStreamTrack, ...streams: MediaStream[]): RTCRtpSender {
|
||||
return connection.webrtc.addTrack(track, ...streams)
|
||||
}
|
||||
|
||||
function removeTrack(sender: RTCRtpSender) {
|
||||
connection.webrtc.removeTrack(sender)
|
||||
}
|
||||
|
||||
function sendUnicast(receiver: string, subject: string, body: any) {
|
||||
connection.websocket.send(EVENT.SEND_UNICAST, { receiver, subject, body })
|
||||
}
|
||||
|
||||
function sendBroadcast(subject: string, body: any) {
|
||||
connection.websocket.send(EVENT.SEND_BROADCAST, { subject, body })
|
||||
}
|
||||
|
||||
function sendMessage(event: string, payload?: any) {
|
||||
connection.websocket.send(event, payload)
|
||||
}
|
||||
|
||||
function withApi<T>(c: new (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) => T): T {
|
||||
return new c(api.config)
|
||||
}
|
||||
|
||||
const control = new NekoControl(connection, state.control)
|
||||
|
||||
const sessions = computed<SessionsApi>(() => api.sessions)
|
||||
const room = computed<RoomApi>(() => api.room)
|
||||
const members = computed<MembersApi>(() => api.members)
|
||||
|
||||
async function uploadDrop({ x, y, files }: { x: number; y: number; files: Array<File> }) {
|
||||
try {
|
||||
events.emit('upload.drop.started')
|
||||
|
||||
await api.room.uploadDrop(x, y, files, {
|
||||
onUploadProgress: (progressEvent: AxiosProgressEvent) => {
|
||||
events.emit('upload.drop.progress', progressEvent)
|
||||
},
|
||||
})
|
||||
|
||||
events.emit('upload.drop.finished')
|
||||
} catch (err: any) {
|
||||
events.emit('upload.drop.finished', err)
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Component lifecycle
|
||||
/////////////////////////////
|
||||
|
||||
onMounted(() => {
|
||||
// component size change
|
||||
observer.observe(component.value!)
|
||||
|
||||
// webrtc needs video tag to capture video snaps for fallback mode
|
||||
connection.webrtc.video = video.value!
|
||||
|
||||
// video events
|
||||
VideoRegister(video.value!, state.video)
|
||||
|
||||
connection.on('close', (error) => {
|
||||
events.emit('connection.closed', error)
|
||||
clear()
|
||||
})
|
||||
|
||||
// when webrtc emits fallback event, it means it is about to reconnect
|
||||
// so we image that it provided (it is last frame of the video), we set
|
||||
// it to the screencast module and pause video in order to show fallback
|
||||
connection.webrtc.on('fallback', (image: string) => {
|
||||
fallbackImage.value = image
|
||||
|
||||
// this ensures that fallback mode starts immediatly
|
||||
pause()
|
||||
})
|
||||
|
||||
connection.webrtc.on('track', (event: RTCTrackEvent) => {
|
||||
const { track, streams } = event
|
||||
if (track.kind === 'audio') return
|
||||
|
||||
// apply track only once it is unmuted
|
||||
track.addEventListener(
|
||||
'unmute',
|
||||
() => {
|
||||
// create stream
|
||||
if ('srcObject' in video.value!) {
|
||||
video.value.srcObject = streams[0]
|
||||
} else {
|
||||
// @ts-ignore
|
||||
video.value.src = window.URL.createObjectURL(streams[0]) // for older browsers
|
||||
}
|
||||
|
||||
if (props.autoplay || connection.activated) {
|
||||
play()
|
||||
}
|
||||
},
|
||||
{ once: true },
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
observer.disconnect()
|
||||
connection.destroy()
|
||||
clear()
|
||||
|
||||
// removes users first interaction events
|
||||
autoUnmute()
|
||||
})
|
||||
|
||||
function updateKeyboard() {
|
||||
if (controlling.value && state.control.keyboard.layout) {
|
||||
connection.websocket.send(EVENT.KEYBOARD_MAP, state.control.keyboard)
|
||||
}
|
||||
}
|
||||
|
||||
watch(controlling, updateKeyboard)
|
||||
watch(() => state.control.keyboard, updateKeyboard)
|
||||
|
||||
function updateKeyboardModifiers(modifiers: { capslock: boolean; numlock: boolean }) {
|
||||
connection.websocket.send(EVENT.KEYBOARD_MODIFIERS, modifiers)
|
||||
}
|
||||
|
||||
function onScreenSyncChange() {
|
||||
if (state.screen.sync.enabled) {
|
||||
syncScreenSize()
|
||||
window.addEventListener('resize', syncScreenSize)
|
||||
} else {
|
||||
window.removeEventListener('resize', syncScreenSize)
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => state.screen.sync.enabled, onScreenSyncChange)
|
||||
|
||||
let syncScreenSizeTimeout = 0
|
||||
|
||||
function syncScreenSize() {
|
||||
if (syncScreenSizeTimeout) {
|
||||
window.clearTimeout(syncScreenSizeTimeout)
|
||||
}
|
||||
syncScreenSizeTimeout = window.setTimeout(() => {
|
||||
const multiplier = state.screen.sync.multiplier || window.devicePixelRatio
|
||||
syncScreenSizeTimeout = 0
|
||||
const { offsetWidth, offsetHeight } = component.value!
|
||||
setScreenSize(
|
||||
Math.round(offsetWidth * multiplier),
|
||||
Math.round(offsetHeight * multiplier),
|
||||
state.screen.sync.rate,
|
||||
)
|
||||
}, SCREEN_SYNC_THROTTLE)
|
||||
}
|
||||
|
||||
function onResize() {
|
||||
const { width, height } = state.screen.size
|
||||
const screenRatio = width / height
|
||||
|
||||
const { offsetWidth, offsetHeight } = component.value!
|
||||
const canvasRatio = offsetWidth / offsetHeight
|
||||
|
||||
// vertical centering
|
||||
if (screenRatio > canvasRatio) {
|
||||
const vertical = offsetWidth / screenRatio
|
||||
container.value!.style.width = `${offsetWidth}px`
|
||||
container.value!.style.height = `${vertical}px`
|
||||
container.value!.style.marginTop = `${(offsetHeight - vertical) / 2}px`
|
||||
container.value!.style.marginLeft = `0px`
|
||||
|
||||
canvasSize.value = {
|
||||
width: offsetWidth,
|
||||
height: vertical,
|
||||
}
|
||||
}
|
||||
// horizontal centering
|
||||
else if (screenRatio < canvasRatio) {
|
||||
const horizontal = screenRatio * offsetHeight
|
||||
container.value!.style.width = `${horizontal}px`
|
||||
container.value!.style.height = `${offsetHeight}px`
|
||||
container.value!.style.marginTop = `0px`
|
||||
container.value!.style.marginLeft = `${(offsetWidth - horizontal) / 2}px`
|
||||
|
||||
canvasSize.value = {
|
||||
width: horizontal,
|
||||
height: offsetHeight,
|
||||
}
|
||||
}
|
||||
// no centering
|
||||
else {
|
||||
container.value!.style.width = `${offsetWidth}px`
|
||||
container.value!.style.height = `${offsetHeight}px`
|
||||
container.value!.style.marginTop = `0px`
|
||||
container.value!.style.marginLeft = `0px`
|
||||
|
||||
canvasSize.value = {
|
||||
width: offsetWidth,
|
||||
height: offsetHeight,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => state.screen.size, onResize)
|
||||
|
||||
function updateConnectionType() {
|
||||
if (screencast.value) {
|
||||
state.connection.type = 'fallback' // TODO: Vue.Set
|
||||
} else if (state.connection.webrtc.connected) {
|
||||
state.connection.type = 'webrtc' // TODO: Vue.Set
|
||||
} else {
|
||||
state.connection.type = 'none' // TODO: Vue.Set
|
||||
}
|
||||
}
|
||||
|
||||
watch(screencast, updateConnectionType)
|
||||
watch(() => state.connection.webrtc.connected, updateConnectionType)
|
||||
|
||||
function onConnectionStatusChange(status: 'connected' | 'connecting' | 'disconnected') {
|
||||
events.emit('connection.status', status)
|
||||
}
|
||||
|
||||
watch(() => state.connection.status, onConnectionStatusChange)
|
||||
|
||||
function onConnectionTypeChange(type: 'fallback' | 'webrtc' | 'none') {
|
||||
events.emit('connection.type', type)
|
||||
}
|
||||
|
||||
watch(() => state.connection.type, onConnectionTypeChange)
|
||||
|
||||
function clear() {
|
||||
// destroy video
|
||||
if (video.value) {
|
||||
if ('srcObject' in video.value) {
|
||||
video.value.srcObject = null
|
||||
} else {
|
||||
// @ts-ignore
|
||||
video.value.removeAttribute('src')
|
||||
}
|
||||
}
|
||||
|
||||
// websocket
|
||||
state.control.clipboard = null // TODO: Vue.Set
|
||||
state.control.host_id = null // TODO: Vue.Set
|
||||
state.control.is_host = false // TODO: Vue.Set
|
||||
state.screen.size = { width: 1280, height: 720, rate: 30 } // TODO: Vue.Set
|
||||
state.screen.configurations = [] // TODO: Vue.Set
|
||||
state.screen.sync.enabled = false // TODO: Vue.Set
|
||||
state.session_id = null // TODO: Vue.Set
|
||||
state.sessions = {} // TODO: Vue.Set
|
||||
state.settings = {
|
||||
private_mode: false,
|
||||
locked_logins: false,
|
||||
locked_controls: false,
|
||||
control_protection: false,
|
||||
implicit_hosting: false,
|
||||
inactive_cursors: false,
|
||||
merciful_reconnect: false,
|
||||
} // TODO: Vue.Set
|
||||
state.cursors = [] // TODO: Vue.Set
|
||||
|
||||
// webrtc
|
||||
state.connection.webrtc.stats = null // TODO: Vue.Set
|
||||
state.connection.webrtc.video.disabled = false // TODO: Vue.Set
|
||||
state.connection.webrtc.video.id = '' // TODO: Vue.Set
|
||||
state.connection.webrtc.video.auto = false // TODO: Vue.Set
|
||||
state.connection.webrtc.audio.disabled = false // TODO: Vue.Set
|
||||
state.connection.webrtc.videos = [] // TODO: Vue.Set
|
||||
state.connection.type = 'none' // TODO: Vue.Set
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
setUrl,
|
||||
authenticate,
|
||||
login,
|
||||
logout,
|
||||
connect,
|
||||
disconnect,
|
||||
setReconnectorConfig,
|
||||
play,
|
||||
pause,
|
||||
mute,
|
||||
unmute,
|
||||
setVolume,
|
||||
setScrollInverse,
|
||||
setScrollSensitivity,
|
||||
setKeyboard,
|
||||
setTouchEnabled,
|
||||
mobileKeyboardShow,
|
||||
mobileKeyboardHide,
|
||||
mobileKeyboardToggle,
|
||||
setScreenSync,
|
||||
setCursorDrawFunction,
|
||||
setInactiveCursorDrawFunction,
|
||||
setScreenSize,
|
||||
setWebRTCVideo,
|
||||
setWebRTCAudio,
|
||||
addTrack,
|
||||
removeTrack,
|
||||
sendUnicast,
|
||||
sendBroadcast,
|
||||
sendMessage,
|
||||
withApi,
|
||||
uploadDrop,
|
||||
// public state
|
||||
state,
|
||||
// computed
|
||||
connected,
|
||||
controlling,
|
||||
session,
|
||||
is_admin,
|
||||
private_mode_enabled,
|
||||
is_touch_device,
|
||||
screencast,
|
||||
// public events
|
||||
events,
|
||||
// public methods
|
||||
control,
|
||||
// public api
|
||||
sessions,
|
||||
room,
|
||||
members,
|
||||
})
|
||||
</script>
|
993
client/src/component/overlay.vue
Normal file
993
client/src/component/overlay.vue
Normal file
@ -0,0 +1,993 @@
|
||||
<template>
|
||||
<div class="neko-overlay-wrap">
|
||||
<canvas ref="overlay" class="neko-overlay" tabindex="0" />
|
||||
<textarea
|
||||
ref="textarea"
|
||||
class="neko-overlay"
|
||||
:style="{ cursor }"
|
||||
v-model="textInput"
|
||||
@click.stop.prevent="control.emit('overlay.click', $event)"
|
||||
@contextmenu.stop.prevent="control.emit('overlay.contextmenu', $event)"
|
||||
@wheel.stop.prevent="onWheel"
|
||||
@mousemove.stop.prevent="onMouseMove"
|
||||
@mousedown.stop.prevent="onMouseDown"
|
||||
@mouseenter.stop.prevent="onMouseEnter"
|
||||
@mouseleave.stop.prevent="onMouseLeave"
|
||||
@dragenter.stop.prevent="onDragEnter"
|
||||
@dragleave.stop.prevent="onDragLeave"
|
||||
@dragover.stop.prevent="onDragOver"
|
||||
@drop.stop.prevent="onDrop"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
/* hide elements around textarea if added by browsers extensions */
|
||||
.neko-overlay-wrap *:not(.neko-overlay) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.neko-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 16px; /* at least 16px to avoid zooming on mobile */
|
||||
resize: none; /* hide textarea resize corner */
|
||||
caret-color: transparent; /* hide caret */
|
||||
outline: 0;
|
||||
border: 0;
|
||||
color: transparent;
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
|
||||
import type { KeyboardInterface } from './utils/keyboard'
|
||||
import type { GestureHandler } from './utils/gesturehandler'
|
||||
import type { Session, Scroll } from './types/state'
|
||||
import type { CursorPosition, CursorImage } from './types/webrtc'
|
||||
import type { CursorDrawFunction, Dimension, KeyboardModifiers } from './types/cursors'
|
||||
|
||||
import { NewKeyboard } from './utils/keyboard'
|
||||
import GestureHandlerInit from './utils/gesturehandler'
|
||||
import { KeyTable, keySymsRemap } from './utils/keyboard-remapping'
|
||||
import { getFilesFromDataTansfer } from './utils/file-upload'
|
||||
import type { NekoControl } from './internal/control'
|
||||
import type { NekoWebRTC } from './internal/webrtc'
|
||||
|
||||
// Wheel thresholds
|
||||
const WHEEL_STEP = 53 // Pixels needed for one step
|
||||
const WHEEL_LINE_HEIGHT = 19 // Assumed pixels for one line step
|
||||
|
||||
// Gesture thresholds
|
||||
const GESTURE_ZOOMSENS = 75
|
||||
const GESTURE_SCRLSENS = 50
|
||||
const DOUBLE_TAP_TIMEOUT = 1000
|
||||
const DOUBLE_TAP_THRESHOLD = 50
|
||||
|
||||
const MOUSE_MOVE_THROTTLE = 1000 / 60 // in ms, 60fps
|
||||
const INACTIVE_CURSOR_INTERVAL = 1000 / 4 // in ms, 4fps
|
||||
|
||||
// refs
|
||||
|
||||
const overlay = ref<HTMLCanvasElement | null>(null)
|
||||
const textarea = ref<HTMLTextAreaElement | null>(null)
|
||||
|
||||
let ctx: CanvasRenderingContext2D | null = null
|
||||
let canvasScale = window.devicePixelRatio
|
||||
|
||||
let keyboard: KeyboardInterface = NewKeyboard()
|
||||
let gestureHandler: GestureHandler = new GestureHandlerInit()
|
||||
|
||||
const focused = ref(false)
|
||||
const textInput = ref('')
|
||||
|
||||
// props and emits
|
||||
|
||||
const props = defineProps<{
|
||||
control: NekoControl
|
||||
sessions: Record<string, Session>
|
||||
hostId: string
|
||||
webrtc: NekoWebRTC
|
||||
scroll: Scroll
|
||||
screenSize: Dimension
|
||||
canvasSize: Dimension
|
||||
cursorDraw: CursorDrawFunction | null
|
||||
isControling: boolean
|
||||
implicitControl: boolean
|
||||
inactiveCursors: boolean
|
||||
fps: number
|
||||
hasMobileKeyboard: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['uploadDrop', 'updateKeyboardModifiers', 'mobileKeyboardOpen'])
|
||||
|
||||
// computed
|
||||
|
||||
const cursor = computed(() => {
|
||||
if (!props.isControling || !cursorImage.value) {
|
||||
return 'default'
|
||||
}
|
||||
|
||||
const { uri, x, y } = cursorImage.value
|
||||
return 'url(' + uri + ') ' + x + ' ' + y + ', default'
|
||||
})
|
||||
|
||||
// lifecycle
|
||||
|
||||
onMounted(() => {
|
||||
// register mouseup globally as user can release mouse button outside of overlay
|
||||
window.addEventListener('mouseup', onMouseUp, true)
|
||||
|
||||
// get canvas overlay context
|
||||
ctx = overlay.value!.getContext('2d')
|
||||
|
||||
// synchronize intrinsic with extrinsic dimensions
|
||||
const { width, height } = overlay.value?.getBoundingClientRect() || { width: 0, height: 0 }
|
||||
canvasResize({ width, height })
|
||||
|
||||
// react to pixel ratio changes
|
||||
onPixelRatioChange()
|
||||
|
||||
let ctrlKey = 0
|
||||
let noKeyUp = {} as Record<number, boolean>
|
||||
|
||||
// Initialize Keyboard
|
||||
keyboard.onkeydown = (key: number) => {
|
||||
key = keySymsRemap(key)
|
||||
|
||||
if (!props.isControling) {
|
||||
noKeyUp[key] = true
|
||||
return true
|
||||
}
|
||||
|
||||
// ctrl+v is aborted
|
||||
if (ctrlKey != 0 && key == KeyTable.XK_v) {
|
||||
keyboard!.release(ctrlKey)
|
||||
noKeyUp[key] = true
|
||||
return true
|
||||
}
|
||||
|
||||
// save information if it is ctrl key event
|
||||
const isCtrlKey = key == KeyTable.XK_Control_L || key == KeyTable.XK_Control_R
|
||||
if (isCtrlKey) ctrlKey = key
|
||||
|
||||
props.control.keyDown(key)
|
||||
return isCtrlKey
|
||||
}
|
||||
keyboard.onkeyup = (key: number) => {
|
||||
key = keySymsRemap(key)
|
||||
|
||||
if (key in noKeyUp) {
|
||||
delete noKeyUp[key]
|
||||
return
|
||||
}
|
||||
|
||||
const isCtrlKey = key == KeyTable.XK_Control_L || key == KeyTable.XK_Control_R
|
||||
if (isCtrlKey) ctrlKey = 0
|
||||
|
||||
props.control.keyUp(key)
|
||||
}
|
||||
keyboard.listenTo(textarea.value!)
|
||||
|
||||
// bind touch handler using `watch` on supportedTouchEvents
|
||||
// because we need to know if touch events are supported
|
||||
// by the server before we can bind touch handler
|
||||
|
||||
// default value is false, so we can bind touch handler
|
||||
bindGestureHandler()
|
||||
|
||||
props.webrtc.addListener('cursor-position', onCursorPosition)
|
||||
props.webrtc.addListener('cursor-image', onCursorImage)
|
||||
props.webrtc.addListener('disconnected', canvasClear)
|
||||
cursorElement.onload = canvasRequestRedraw
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('mouseup', onMouseUp, true)
|
||||
keyboard.removeListener()
|
||||
|
||||
// unbind touch handler
|
||||
unbindTouchHandler()
|
||||
|
||||
// unbind gesture handler
|
||||
unbindGestureHandler()
|
||||
|
||||
props.webrtc.removeListener('cursor-position', onCursorPosition)
|
||||
props.webrtc.removeListener('cursor-image', onCursorImage)
|
||||
props.webrtc.removeListener('disconnected', canvasClear)
|
||||
cursorElement.onload = null
|
||||
|
||||
// stop inactive cursor interval if exists
|
||||
clearInactiveCursorInterval()
|
||||
|
||||
// stop pixel ratio change listener
|
||||
if (unsubscribePixelRatioChange) {
|
||||
unsubscribePixelRatioChange()
|
||||
}
|
||||
})
|
||||
|
||||
//
|
||||
// touch handler for native touch events
|
||||
//
|
||||
|
||||
function bindTouchHandler() {
|
||||
textarea.value?.addEventListener('touchstart', onTouchHandler, { passive: false })
|
||||
textarea.value?.addEventListener('touchmove', onTouchHandler, { passive: false })
|
||||
textarea.value?.addEventListener('touchend', onTouchHandler, { passive: false })
|
||||
textarea.value?.addEventListener('touchcancel', onTouchHandler, { passive: false })
|
||||
}
|
||||
|
||||
function unbindTouchHandler() {
|
||||
textarea.value?.removeEventListener('touchstart', onTouchHandler)
|
||||
textarea.value?.removeEventListener('touchmove', onTouchHandler)
|
||||
textarea.value?.removeEventListener('touchend', onTouchHandler)
|
||||
textarea.value?.removeEventListener('touchcancel', onTouchHandler)
|
||||
}
|
||||
|
||||
function onTouchHandler(ev: TouchEvent) {
|
||||
// we cannot use implicitControlRequest because we don't have mouse event
|
||||
if (!props.isControling) {
|
||||
// if implicitControl is enabled, request control
|
||||
if (props.implicitControl) {
|
||||
props.control.request()
|
||||
}
|
||||
// otherwise, ignore event
|
||||
return
|
||||
}
|
||||
|
||||
ev.stopPropagation()
|
||||
ev.preventDefault()
|
||||
|
||||
for (let i = 0; i < ev.changedTouches.length; i++) {
|
||||
const touch = ev.changedTouches[i]
|
||||
const pos = getMousePos(touch.clientX, touch.clientY)
|
||||
// force is float value between 0 and 1
|
||||
// pressure is integer value between 0 and 255
|
||||
const pressure = Math.round(touch.force * 255)
|
||||
|
||||
switch (ev.type) {
|
||||
case 'touchstart':
|
||||
props.control.touchBegin(touch.identifier, pos, pressure)
|
||||
break
|
||||
case 'touchmove':
|
||||
props.control.touchUpdate(touch.identifier, pos, pressure)
|
||||
break
|
||||
case 'touchend':
|
||||
case 'touchcancel':
|
||||
props.control.touchEnd(touch.identifier, pos, pressure)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// gesture handler for emulated mouse events
|
||||
//
|
||||
|
||||
function bindGestureHandler() {
|
||||
gestureHandler.attach(textarea.value!)
|
||||
textarea.value?.addEventListener('gesturestart', onGestureHandler)
|
||||
textarea.value?.addEventListener('gesturemove', onGestureHandler)
|
||||
textarea.value?.addEventListener('gestureend', onGestureHandler)
|
||||
}
|
||||
|
||||
function unbindGestureHandler() {
|
||||
gestureHandler.detach()
|
||||
textarea.value?.removeEventListener('gesturestart', onGestureHandler)
|
||||
textarea.value?.removeEventListener('gesturemove', onGestureHandler)
|
||||
textarea.value?.removeEventListener('gestureend', onGestureHandler)
|
||||
}
|
||||
|
||||
let gestureLastTapTime: number | null = null
|
||||
let gestureFirstDoubleTapEv: any | null = null
|
||||
let gestureLastMagnitudeX = 0
|
||||
let gestureLastMagnitudeY = 0
|
||||
|
||||
function _handleTapEvent(ev: any, code: number) {
|
||||
let pos = getMousePos(ev.detail.clientX, ev.detail.clientY)
|
||||
|
||||
// If the user quickly taps multiple times we assume they meant to
|
||||
// hit the same spot, so slightly adjust coordinates
|
||||
|
||||
if (
|
||||
gestureLastTapTime !== null &&
|
||||
Date.now() - gestureLastTapTime < DOUBLE_TAP_TIMEOUT &&
|
||||
gestureFirstDoubleTapEv?.detail.type === ev.detail.type
|
||||
) {
|
||||
const dx = gestureFirstDoubleTapEv.detail.clientX - ev.detail.clientX
|
||||
const dy = gestureFirstDoubleTapEv.detail.clientY - ev.detail.clientY
|
||||
const distance = Math.hypot(dx, dy)
|
||||
|
||||
if (distance < DOUBLE_TAP_THRESHOLD) {
|
||||
pos = getMousePos(gestureFirstDoubleTapEv.detail.clientX, gestureFirstDoubleTapEv.detail.clientY)
|
||||
} else {
|
||||
gestureFirstDoubleTapEv = ev
|
||||
}
|
||||
} else {
|
||||
gestureFirstDoubleTapEv = ev
|
||||
}
|
||||
gestureLastTapTime = Date.now()
|
||||
|
||||
props.control.buttonDown(code, pos)
|
||||
props.control.buttonUp(code, pos)
|
||||
}
|
||||
|
||||
function onGestureHandler(ev: any) {
|
||||
// we cannot use implicitControlRequest because we don't have mouse event
|
||||
if (!props.isControling) {
|
||||
// if implicitControl is enabled, request control
|
||||
if (props.implicitControl) {
|
||||
props.control.request()
|
||||
}
|
||||
// otherwise, ignore event
|
||||
return
|
||||
}
|
||||
|
||||
const pos = getMousePos(ev.detail.clientX, ev.detail.clientY)
|
||||
|
||||
let magnitude
|
||||
switch (ev.type) {
|
||||
case 'gesturestart':
|
||||
switch (ev.detail.type) {
|
||||
case 'onetap':
|
||||
_handleTapEvent(ev, 1)
|
||||
break
|
||||
case 'twotap':
|
||||
_handleTapEvent(ev, 3)
|
||||
break
|
||||
case 'threetap':
|
||||
_handleTapEvent(ev, 2)
|
||||
break
|
||||
case 'drag':
|
||||
props.control.buttonDown(1, pos)
|
||||
break
|
||||
case 'longpress':
|
||||
props.control.buttonDown(3, pos)
|
||||
break
|
||||
|
||||
case 'twodrag':
|
||||
gestureLastMagnitudeX = ev.detail.magnitudeX
|
||||
gestureLastMagnitudeY = ev.detail.magnitudeY
|
||||
props.control.move(pos)
|
||||
break
|
||||
case 'pinch':
|
||||
gestureLastMagnitudeX = Math.hypot(ev.detail.magnitudeX, ev.detail.magnitudeY)
|
||||
props.control.move(pos)
|
||||
break
|
||||
}
|
||||
break
|
||||
|
||||
case 'gesturemove':
|
||||
switch (ev.detail.type) {
|
||||
case 'onetap':
|
||||
case 'twotap':
|
||||
case 'threetap':
|
||||
break
|
||||
case 'drag':
|
||||
case 'longpress':
|
||||
props.control.move(pos)
|
||||
break
|
||||
case 'twodrag':
|
||||
// Always scroll in the same position.
|
||||
// We don't know if the mouse was moved so we need to move it
|
||||
// every update.
|
||||
props.control.move(pos)
|
||||
while (ev.detail.magnitudeY - gestureLastMagnitudeY > GESTURE_SCRLSENS) {
|
||||
props.control.scroll({ delta_x: 0, delta_y: 1 })
|
||||
gestureLastMagnitudeY += GESTURE_SCRLSENS
|
||||
}
|
||||
while (ev.detail.magnitudeY - gestureLastMagnitudeY < -GESTURE_SCRLSENS) {
|
||||
props.control.scroll({ delta_x: 0, delta_y: -1 })
|
||||
gestureLastMagnitudeY -= GESTURE_SCRLSENS
|
||||
}
|
||||
while (ev.detail.magnitudeX - gestureLastMagnitudeX > GESTURE_SCRLSENS) {
|
||||
props.control.scroll({ delta_x: 1, delta_y: 0 })
|
||||
gestureLastMagnitudeX+= GESTURE_SCRLSENS
|
||||
}
|
||||
while (ev.detail.magnitudeX - gestureLastMagnitudeX < -GESTURE_SCRLSENS) {
|
||||
props.control.scroll({ delta_x: -1, delta_y: 0 })
|
||||
gestureLastMagnitudeX-= GESTURE_SCRLSENS
|
||||
}
|
||||
break
|
||||
case 'pinch':
|
||||
// Always scroll in the same position.
|
||||
// We don't know if the mouse was moved so we need to move it
|
||||
// every update.
|
||||
props.control.move(pos)
|
||||
magnitude = Math.hypot(ev.detail.magnitudeX, ev.detail.magnitudeY)
|
||||
if (Math.abs(magnitude - gestureLastMagnitudeX) > GESTURE_ZOOMSENS) {
|
||||
while (magnitude - gestureLastMagnitudeX > GESTURE_ZOOMSENS) {
|
||||
props.control.scroll({ delta_x: 0, delta_y: 1, control_key: true })
|
||||
gestureLastMagnitudeX+= GESTURE_ZOOMSENS
|
||||
}
|
||||
while (magnitude - gestureLastMagnitudeX < -GESTURE_ZOOMSENS) {
|
||||
props.control.scroll({ delta_x: 0, delta_y: -1, control_key: true })
|
||||
gestureLastMagnitudeX-= GESTURE_ZOOMSENS
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
|
||||
case 'gestureend':
|
||||
switch (ev.detail.type) {
|
||||
case 'onetap':
|
||||
case 'twotap':
|
||||
case 'threetap':
|
||||
case 'pinch':
|
||||
case 'twodrag':
|
||||
break
|
||||
case 'drag':
|
||||
props.control.buttonUp(1, pos)
|
||||
break
|
||||
case 'longpress':
|
||||
props.control.buttonUp(3, pos)
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// touch and gesture handlers cannot be used together
|
||||
//
|
||||
|
||||
function onTouchEventsChange() {
|
||||
unbindGestureHandler()
|
||||
unbindTouchHandler()
|
||||
|
||||
if (!props.control.enabledTouchEvents) {
|
||||
return
|
||||
}
|
||||
|
||||
if (props.control.supportedTouchEvents) {
|
||||
bindTouchHandler()
|
||||
} else {
|
||||
bindGestureHandler()
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.control.enabledTouchEvents, onTouchEventsChange)
|
||||
watch(() => props.control.supportedTouchEvents, onTouchEventsChange)
|
||||
|
||||
|
||||
function getModifierState(e: MouseEvent): KeyboardModifiers {
|
||||
// we can only use locks, because when someone holds key outside
|
||||
// of the renderer, and releases it inside, keyup event is not fired
|
||||
// by guacamole keyboard and modifier state is not updated
|
||||
|
||||
return {
|
||||
//shift: e.getModifierState('Shift'),
|
||||
capslock: e.getModifierState('CapsLock'),
|
||||
//control: e.getModifierState('Control'),
|
||||
//alt: e.getModifierState('Alt'),
|
||||
numlock: e.getModifierState('NumLock'),
|
||||
//meta: e.getModifierState('Meta'),
|
||||
//super: e.getModifierState('Super'),
|
||||
//altgr: e.getModifierState('AltGraph'),
|
||||
}
|
||||
}
|
||||
|
||||
function getMousePos(clientX: number, clientY: number) {
|
||||
const rect = overlay.value!.getBoundingClientRect()
|
||||
|
||||
return {
|
||||
x: Math.round((props.screenSize.width / rect.width) * (clientX - rect.left)),
|
||||
y: Math.round((props.screenSize.height / rect.height) * (clientY - rect.top)),
|
||||
}
|
||||
}
|
||||
|
||||
function sendMousePos(e: MouseEvent) {
|
||||
const pos = getMousePos(e.clientX, e.clientY)
|
||||
// not using NekoControl here because we want to avoid
|
||||
// sending mousemove events over websocket
|
||||
if (props.webrtc.connected) {
|
||||
props.webrtc.send('mousemove', pos)
|
||||
} // otherwise, no events are sent
|
||||
cursorPosition = pos
|
||||
}
|
||||
|
||||
let wheelX = 0
|
||||
let wheelY = 0
|
||||
let wheelTimeStamp = 0
|
||||
|
||||
// negative sensitivity can be acheived using increased step value
|
||||
const wheelStep = computed(() => {
|
||||
let x = WHEEL_STEP
|
||||
|
||||
if (props.scroll.sensitivity < 0) {
|
||||
x *= Math.abs(props.scroll.sensitivity) + 1
|
||||
}
|
||||
|
||||
return x
|
||||
})
|
||||
|
||||
|
||||
// sensitivity can only be positive
|
||||
const wheelSensitivity = computed(() => {
|
||||
let x = 1
|
||||
|
||||
if (props.scroll.sensitivity > 0) {
|
||||
x = Math.abs(props.scroll.sensitivity) + 1
|
||||
}
|
||||
|
||||
if (props.scroll.inverse) {
|
||||
x *= -1
|
||||
}
|
||||
|
||||
return x
|
||||
})
|
||||
|
||||
// use v-model instead of @input because v-model
|
||||
// doesn't get updated during IME composition
|
||||
function onTextInputChange() {
|
||||
if (textInput.value == '') return
|
||||
props.control.paste(textInput.value)
|
||||
textInput.value = ''
|
||||
}
|
||||
|
||||
watch(textInput, onTextInputChange)
|
||||
|
||||
function onWheel(e: WheelEvent) {
|
||||
if (!props.isControling) {
|
||||
return
|
||||
}
|
||||
|
||||
// when the last scroll was more than 250ms ago
|
||||
const firstScroll = e.timeStamp - wheelTimeStamp > 250
|
||||
|
||||
if (firstScroll) {
|
||||
wheelX = 0
|
||||
wheelY = 0
|
||||
wheelTimeStamp = e.timeStamp
|
||||
}
|
||||
|
||||
let dx = e.deltaX
|
||||
let dy = e.deltaY
|
||||
|
||||
if (e.deltaMode !== 0) {
|
||||
dx *= WHEEL_LINE_HEIGHT
|
||||
dy *= WHEEL_LINE_HEIGHT
|
||||
}
|
||||
|
||||
wheelX += dx
|
||||
wheelY += dy
|
||||
|
||||
let x = 0
|
||||
if (Math.abs(wheelX) >= wheelStep.value || firstScroll) {
|
||||
if (wheelX < 0) {
|
||||
x = wheelSensitivity.value * -1
|
||||
} else if (wheelX > 0) {
|
||||
x = wheelSensitivity.value
|
||||
}
|
||||
|
||||
if (!firstScroll) {
|
||||
wheelX = 0
|
||||
}
|
||||
}
|
||||
|
||||
let y = 0
|
||||
if (Math.abs(wheelY) >= wheelStep.value || firstScroll) {
|
||||
if (wheelY < 0) {
|
||||
y = wheelSensitivity.value * -1
|
||||
} else if (wheelY > 0) {
|
||||
y = wheelSensitivity.value
|
||||
}
|
||||
|
||||
if (!firstScroll) {
|
||||
wheelY = 0
|
||||
}
|
||||
}
|
||||
|
||||
// skip if not scrolled
|
||||
if (x == 0 && y == 0) return
|
||||
|
||||
// TODO: add position for precision scrolling
|
||||
props.control.scroll({
|
||||
delta_x: x,
|
||||
delta_y: y,
|
||||
control_key: e.ctrlKey,
|
||||
})
|
||||
}
|
||||
|
||||
let lastMouseMove = 0
|
||||
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
// throttle mousemove events
|
||||
if (e.timeStamp - lastMouseMove < MOUSE_MOVE_THROTTLE) return
|
||||
lastMouseMove = e.timeStamp
|
||||
|
||||
if (props.isControling) {
|
||||
sendMousePos(e)
|
||||
}
|
||||
|
||||
if (props.inactiveCursors) {
|
||||
saveInactiveMousePos(e)
|
||||
}
|
||||
}
|
||||
|
||||
let isMouseDown = false
|
||||
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
isMouseDown = true
|
||||
|
||||
if (!props.isControling) {
|
||||
implicitControlRequest(e)
|
||||
return
|
||||
}
|
||||
|
||||
const key = e.button + 1
|
||||
const pos = getMousePos(e.clientX, e.clientY)
|
||||
props.control.buttonDown(key, pos)
|
||||
}
|
||||
|
||||
function onMouseUp(e: MouseEvent) {
|
||||
// only if we are the one who started the mouse down
|
||||
if (!isMouseDown) return
|
||||
isMouseDown = false
|
||||
|
||||
if (!props.isControling) {
|
||||
implicitControlRequest(e)
|
||||
return
|
||||
}
|
||||
|
||||
const key = e.button + 1
|
||||
const pos = getMousePos(e.clientX, e.clientY)
|
||||
props.control.buttonUp(key, pos)
|
||||
}
|
||||
|
||||
function onMouseEnter(e: MouseEvent) {
|
||||
// focus opens the keyboard on mobile (only for android)
|
||||
if (!props.hasMobileKeyboard) {
|
||||
textarea.value?.focus()
|
||||
}
|
||||
|
||||
focused.value = true
|
||||
|
||||
if (props.isControling) {
|
||||
updateKeyboardModifiers(e)
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseLeave(e: MouseEvent) {
|
||||
if (props.isControling) {
|
||||
// save current keyboard modifiers state
|
||||
keyboardModifiers = getModifierState(e)
|
||||
}
|
||||
|
||||
focused.value = false
|
||||
}
|
||||
|
||||
function onDragEnter(e: DragEvent) {
|
||||
onMouseEnter(e as MouseEvent)
|
||||
}
|
||||
|
||||
function onDragLeave(e: DragEvent) {
|
||||
onMouseLeave(e as MouseEvent)
|
||||
}
|
||||
|
||||
function onDragOver(e: DragEvent) {
|
||||
onMouseMove(e as MouseEvent)
|
||||
}
|
||||
|
||||
async function onDrop(e: DragEvent) {
|
||||
if (props.isControling || props.implicitControl) {
|
||||
const dt = e.dataTransfer
|
||||
if (!dt) return
|
||||
|
||||
const files = await getFilesFromDataTansfer(dt)
|
||||
if (files.length === 0) return
|
||||
|
||||
const pos = getMousePos(e.clientX, e.clientY)
|
||||
emit('uploadDrop', { ...pos, files })
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// inactive cursor position
|
||||
//
|
||||
|
||||
let inactiveCursorInterval: number | null = null
|
||||
let inactiveCursorPosition: CursorPosition | null = null
|
||||
|
||||
function clearInactiveCursorInterval() {
|
||||
if (inactiveCursorInterval) {
|
||||
window.clearInterval(inactiveCursorInterval)
|
||||
inactiveCursorInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
function restartInactiveCursorInterval() {
|
||||
// clear interval if exists
|
||||
clearInactiveCursorInterval()
|
||||
|
||||
if (props.inactiveCursors && focused.value && !props.isControling) {
|
||||
inactiveCursorInterval = window.setInterval(sendInactiveMousePos, INACTIVE_CURSOR_INTERVAL)
|
||||
}
|
||||
}
|
||||
|
||||
watch(focused, restartInactiveCursorInterval)
|
||||
watch(() => props.isControling, restartInactiveCursorInterval)
|
||||
watch(() => props.inactiveCursors, restartInactiveCursorInterval)
|
||||
|
||||
function saveInactiveMousePos(e: MouseEvent) {
|
||||
const pos = getMousePos(e.clientX, e.clientY)
|
||||
inactiveCursorPosition = pos
|
||||
}
|
||||
|
||||
function sendInactiveMousePos() {
|
||||
if (inactiveCursorPosition && props.webrtc.connected) {
|
||||
// not using NekoControl here, because inactive cursors are
|
||||
// treated differently than moving the mouse while controling
|
||||
props.webrtc.send('mousemove', inactiveCursorPosition)
|
||||
} // if webrtc is not connected, we don't need to send anything
|
||||
}
|
||||
|
||||
//
|
||||
// keyboard modifiers
|
||||
//
|
||||
|
||||
let keyboardModifiers: KeyboardModifiers | null = null
|
||||
|
||||
function updateKeyboardModifiers(e: MouseEvent) {
|
||||
const mods = getModifierState(e)
|
||||
const newMods = Object.values(mods).join()
|
||||
const oldMods = Object.values(keyboardModifiers || {}).join()
|
||||
|
||||
// update keyboard modifiers only if they changed
|
||||
if (newMods !== oldMods) {
|
||||
emit('updateKeyboardModifiers', mods)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// canvas
|
||||
//
|
||||
|
||||
const cursorImage = ref<CursorImage | null>(null)
|
||||
const cursorElement = new Image()
|
||||
|
||||
let cursorPosition: CursorPosition | null = null
|
||||
let cursorLastTime = 0
|
||||
let canvasRequestedFrame = false
|
||||
let canvasRenderTimeout: number | null = null
|
||||
|
||||
let unsubscribePixelRatioChange: (() => void) | null = null
|
||||
|
||||
function onPixelRatioChange() {
|
||||
if (unsubscribePixelRatioChange) {
|
||||
unsubscribePixelRatioChange()
|
||||
}
|
||||
|
||||
const media = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
|
||||
media.addEventListener('change', onPixelRatioChange)
|
||||
unsubscribePixelRatioChange = () => {
|
||||
media.removeEventListener('change', onPixelRatioChange)
|
||||
}
|
||||
|
||||
canvasScale = window.devicePixelRatio
|
||||
onCanvasSizeChange(props.canvasSize)
|
||||
}
|
||||
|
||||
function onCanvasSizeChange({ width, height }: Dimension) {
|
||||
canvasResize({ width, height })
|
||||
canvasRequestRedraw()
|
||||
}
|
||||
|
||||
watch(() => props.canvasSize, onCanvasSizeChange)
|
||||
|
||||
function onCursorPosition(data: CursorPosition) {
|
||||
if (!props.isControling) {
|
||||
cursorPosition = data
|
||||
canvasRequestRedraw()
|
||||
}
|
||||
}
|
||||
|
||||
function onCursorImage(data: CursorImage) {
|
||||
cursorImage.value = data
|
||||
|
||||
if (!props.isControling) {
|
||||
cursorElement.src = data.uri
|
||||
}
|
||||
}
|
||||
|
||||
function canvasResize({ width, height }: Dimension) {
|
||||
if (!ctx || !overlay.value) return
|
||||
|
||||
overlay.value.width = width * canvasScale
|
||||
overlay.value.height = height * canvasScale
|
||||
ctx.setTransform(canvasScale, 0, 0, canvasScale, 0, 0)
|
||||
}
|
||||
|
||||
function canvasRequestRedraw() {
|
||||
if (canvasRequestedFrame) return
|
||||
|
||||
if (props.fps > 0) {
|
||||
if (canvasRenderTimeout) {
|
||||
window.clearTimeout(canvasRenderTimeout)
|
||||
canvasRenderTimeout = null
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
if (now - cursorLastTime < 1000 / props.fps) {
|
||||
canvasRenderTimeout = window.setTimeout(canvasRequestRedraw, 1000 / props.fps)
|
||||
return
|
||||
}
|
||||
|
||||
cursorLastTime = now
|
||||
}
|
||||
|
||||
canvasRequestedFrame = true
|
||||
window.requestAnimationFrame(() => {
|
||||
if (props.isControling) {
|
||||
canvasClear()
|
||||
} else {
|
||||
canvasRedraw()
|
||||
}
|
||||
|
||||
canvasRequestedFrame = false
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => props.hostId, canvasRequestRedraw)
|
||||
watch(() => props.cursorDraw, canvasRequestRedraw)
|
||||
|
||||
function canvasRedraw() {
|
||||
if (!ctx || !cursorPosition || !props.screenSize || !cursorImage.value) return
|
||||
|
||||
// clear drawings
|
||||
canvasClear()
|
||||
|
||||
// ignore hidden cursor
|
||||
if (cursorImage.value.width <= 1 && cursorImage.value.height <= 1) return
|
||||
|
||||
// get intrinsic dimensions
|
||||
const { width, height } = props.canvasSize
|
||||
|
||||
// reset transformation, X and Y will be 0 again
|
||||
ctx.setTransform(canvasScale, 0, 0, canvasScale, 0, 0)
|
||||
|
||||
// get cursor position
|
||||
let x = Math.round((cursorPosition.x / props.screenSize.width) * width)
|
||||
let y = Math.round((cursorPosition.y / props.screenSize.height) * height)
|
||||
|
||||
// use custom draw function, if available
|
||||
if (props.cursorDraw) {
|
||||
props.cursorDraw(ctx, x, y, cursorElement, cursorImage.value, props.hostId)
|
||||
return
|
||||
}
|
||||
|
||||
// draw cursor image
|
||||
ctx.drawImage(
|
||||
cursorElement,
|
||||
x - cursorImage.value.x,
|
||||
y - cursorImage.value.y,
|
||||
cursorImage.value.width,
|
||||
cursorImage.value.height,
|
||||
)
|
||||
|
||||
// draw cursor tag
|
||||
const cursorTag = props.sessions[props.hostId]?.profile.name || ''
|
||||
if (cursorTag) {
|
||||
x += cursorImage.value.width
|
||||
y += cursorImage.value.height
|
||||
|
||||
ctx.font = '14px Arial, sans-serif'
|
||||
ctx.textBaseline = 'top'
|
||||
ctx.shadowColor = 'black'
|
||||
ctx.shadowBlur = 2
|
||||
ctx.lineWidth = 2
|
||||
ctx.fillStyle = 'black'
|
||||
ctx.strokeText(cursorTag, x, y)
|
||||
ctx.shadowBlur = 0
|
||||
ctx.fillStyle = 'white'
|
||||
ctx.fillText(cursorTag, x, y)
|
||||
}
|
||||
}
|
||||
|
||||
function canvasClear() {
|
||||
if (!ctx) return
|
||||
|
||||
// reset transformation, X and Y will be 0 again
|
||||
ctx.setTransform(canvasScale, 0, 0, canvasScale, 0, 0)
|
||||
|
||||
const { width, height } = props.canvasSize
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
}
|
||||
|
||||
//
|
||||
// implicit hosting
|
||||
//
|
||||
|
||||
let reqMouseDown: MouseEvent | null = null
|
||||
let reqMouseUp: MouseEvent | null = null
|
||||
|
||||
function onControlChange(isControling: boolean) {
|
||||
keyboardModifiers = null
|
||||
|
||||
if (isControling && reqMouseDown) {
|
||||
updateKeyboardModifiers(reqMouseDown)
|
||||
onMouseDown(reqMouseDown)
|
||||
}
|
||||
|
||||
if (isControling && reqMouseUp) {
|
||||
onMouseUp(reqMouseUp)
|
||||
}
|
||||
|
||||
canvasRequestRedraw()
|
||||
|
||||
reqMouseDown = null
|
||||
reqMouseUp = null
|
||||
}
|
||||
|
||||
watch(() => props.isControling, onControlChange)
|
||||
|
||||
function implicitControlRequest(e: MouseEvent) {
|
||||
if (props.implicitControl && e.type === 'mousedown') {
|
||||
reqMouseDown = e
|
||||
reqMouseUp = null
|
||||
props.control.request()
|
||||
}
|
||||
|
||||
if (props.implicitControl && e.type === 'mouseup') {
|
||||
reqMouseUp = e
|
||||
}
|
||||
}
|
||||
|
||||
// unused
|
||||
function implicitControlRelease() {
|
||||
if (props.implicitControl) {
|
||||
props.control.release()
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// mobile keyboard
|
||||
//
|
||||
|
||||
let kbdShow = false
|
||||
let kbdOpen = false
|
||||
|
||||
function mobileKeyboardShow() {
|
||||
// skip if not a touch device
|
||||
if (!props.hasMobileKeyboard) return
|
||||
|
||||
kbdShow = true
|
||||
kbdOpen = false
|
||||
|
||||
textarea.value!.focus()
|
||||
window.visualViewport?.addEventListener('resize', onVisualViewportResize)
|
||||
emit('mobileKeyboardOpen', true)
|
||||
}
|
||||
|
||||
function mobileKeyboardHide() {
|
||||
// skip if not a touch device
|
||||
if (!props.hasMobileKeyboard) return
|
||||
|
||||
kbdShow = false
|
||||
kbdOpen = false
|
||||
|
||||
emit('mobileKeyboardOpen', false)
|
||||
window.visualViewport?.removeEventListener('resize', onVisualViewportResize)
|
||||
textarea.value!.blur()
|
||||
}
|
||||
|
||||
// visual viewport resize event is fired when keyboard is opened or closed
|
||||
// android does not blur textarea when keyboard is closed, so we need to do it manually
|
||||
function onVisualViewportResize() {
|
||||
if (!kbdShow) return
|
||||
|
||||
if (!kbdOpen) {
|
||||
kbdOpen = true
|
||||
} else {
|
||||
mobileKeyboardHide()
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
94
client/src/component/screencast.vue
Normal file
94
client/src/component/screencast.vue
Normal file
@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<img :src="imageSrc" @load="onImageLoad" @error="onImageError" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import type { RoomApi } from './api'
|
||||
|
||||
const REFRESH_RATE = 1e3
|
||||
const ERROR_DELAY_MS = 2500
|
||||
|
||||
const imageSrc = ref('')
|
||||
|
||||
const props = defineProps<{
|
||||
image: string
|
||||
enabled: boolean
|
||||
api: RoomApi
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['imageReady'])
|
||||
|
||||
watch(() => props.image, (image) => {
|
||||
imageSrc.value = image
|
||||
})
|
||||
|
||||
let isRunning = false
|
||||
let isStopped = false
|
||||
|
||||
async function loop() {
|
||||
if (isRunning) return
|
||||
isRunning = true
|
||||
|
||||
while (!isStopped) {
|
||||
const lastLoad = Date.now()
|
||||
|
||||
try {
|
||||
const res = await props.api.screenCastImage({ responseType: 'blob' })
|
||||
imageSrc.value = URL.createObjectURL(res.data)
|
||||
|
||||
const delay = lastLoad - Date.now() + REFRESH_RATE
|
||||
if (delay > 0) {
|
||||
await new Promise((res) => setTimeout(res, delay))
|
||||
}
|
||||
} catch {
|
||||
await new Promise((res) => setTimeout(res, ERROR_DELAY_MS))
|
||||
}
|
||||
}
|
||||
|
||||
isRunning = false
|
||||
imageSrc.value = ''
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.enabled) {
|
||||
start()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stop()
|
||||
})
|
||||
|
||||
function start() {
|
||||
isStopped = false
|
||||
|
||||
if (!isRunning) {
|
||||
setTimeout(loop, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function stop() {
|
||||
isStopped = true
|
||||
}
|
||||
|
||||
function onEnabledChanged(enabled: boolean) {
|
||||
if (enabled) {
|
||||
start()
|
||||
} else {
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.enabled, onEnabledChanged)
|
||||
|
||||
function onImageLoad() {
|
||||
URL.revokeObjectURL(imageSrc.value)
|
||||
emit('imageReady', isRunning)
|
||||
}
|
||||
|
||||
function onImageError() {
|
||||
if (imageSrc.value) URL.revokeObjectURL(imageSrc.value)
|
||||
emit('imageReady', false)
|
||||
}
|
||||
</script>
|
33
client/src/component/types/cursors.ts
Normal file
33
client/src/component/types/cursors.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import type { CursorImage } from './webrtc'
|
||||
|
||||
export type CursorDrawFunction = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
cursorElement: HTMLImageElement,
|
||||
cursorImage: CursorImage,
|
||||
sessionId: string,
|
||||
) => void
|
||||
|
||||
export type InactiveCursorDrawFunction = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
sessionId: string,
|
||||
) => void
|
||||
|
||||
export interface Dimension {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface KeyboardModifiers {
|
||||
shift?: boolean
|
||||
capslock?: boolean
|
||||
control?: boolean
|
||||
alt?: boolean
|
||||
numlock?: boolean
|
||||
meta?: boolean
|
||||
super?: boolean
|
||||
altgr?: boolean
|
||||
}
|
62
client/src/component/types/events.ts
Normal file
62
client/src/component/types/events.ts
Normal file
@ -0,0 +1,62 @@
|
||||
export const SYSTEM_INIT = 'system/init'
|
||||
export const SYSTEM_ADMIN = 'system/admin'
|
||||
export const SYSTEM_SETTINGS = 'system/settings'
|
||||
export const SYSTEM_LOGS = 'system/logs'
|
||||
export const SYSTEM_DISCONNECT = 'system/disconnect'
|
||||
export const SYSTEM_HEARTBEAT = 'system/heartbeat'
|
||||
|
||||
export const SIGNAL_REQUEST = 'signal/request'
|
||||
export const SIGNAL_RESTART = 'signal/restart'
|
||||
export const SIGNAL_OFFER = 'signal/offer'
|
||||
export const SIGNAL_ANSWER = 'signal/answer'
|
||||
export const SIGNAL_PROVIDE = 'signal/provide'
|
||||
export const SIGNAL_CANDIDATE = 'signal/candidate'
|
||||
export const SIGNAL_VIDEO = 'signal/video'
|
||||
export const SIGNAL_AUDIO = 'signal/audio'
|
||||
export const SIGNAL_CLOSE = 'signal/close'
|
||||
|
||||
export const SESSION_CREATED = 'session/created'
|
||||
export const SESSION_DELETED = 'session/deleted'
|
||||
export const SESSION_PROFILE = 'session/profile'
|
||||
export const SESSION_STATE = 'session/state'
|
||||
export const SESSION_CURSORS = 'session/cursors'
|
||||
|
||||
export const CONTROL_HOST = 'control/host'
|
||||
export const CONTROL_RELEASE = 'control/release'
|
||||
export const CONTROL_REQUEST = 'control/request'
|
||||
// mouse
|
||||
export const CONTROL_MOVE = 'control/move'
|
||||
export const CONTROL_SCROLL = 'control/scroll'
|
||||
export const CONTROL_BUTTONPRESS = 'control/buttonpress'
|
||||
export const CONTROL_BUTTONDOWN = 'control/buttondown'
|
||||
export const CONTROL_BUTTONUP = 'control/buttonup'
|
||||
// keyboard
|
||||
export const CONTROL_KEYPRESS = 'control/keypress'
|
||||
export const CONTROL_KEYDOWN = 'control/keydown'
|
||||
export const CONTROL_KEYUP = 'control/keyup'
|
||||
// touch
|
||||
export const CONTROL_TOUCHBEGIN = 'control/touchbegin'
|
||||
export const CONTROL_TOUCHUPDATE = 'control/touchupdate'
|
||||
export const CONTROL_TOUCHEND = 'control/touchend'
|
||||
// actions
|
||||
export const CONTROL_CUT = 'control/cut'
|
||||
export const CONTROL_COPY = 'control/copy'
|
||||
export const CONTROL_PASTE = 'control/paste'
|
||||
export const CONTROL_SELECT_ALL = 'control/select_all'
|
||||
|
||||
export const SCREEN_UPDATED = 'screen/updated'
|
||||
export const SCREEN_SET = 'screen/set'
|
||||
|
||||
export const CLIPBOARD_UPDATED = 'clipboard/updated'
|
||||
export const CLIPBOARD_SET = 'clipboard/set'
|
||||
|
||||
export const KEYBOARD_MODIFIERS = 'keyboard/modifiers'
|
||||
export const KEYBOARD_MAP = 'keyboard/map'
|
||||
|
||||
export const BORADCAST_STATUS = 'broadcast/status'
|
||||
|
||||
export const SEND_UNICAST = 'send/unicast'
|
||||
export const SEND_BROADCAST = 'send/broadcast'
|
||||
|
||||
export const FILE_CHOOSER_DIALOG_OPENED = 'file_chooser_dialog/opened'
|
||||
export const FILE_CHOOSER_DIALOG_CLOSED = 'file_chooser_dialog/closed'
|
191
client/src/component/types/messages.ts
Normal file
191
client/src/component/types/messages.ts
Normal file
@ -0,0 +1,191 @@
|
||||
import type { ICEServer } from '../internal/webrtc'
|
||||
import type { Settings, ScreenSize } from './state'
|
||||
import type { PeerRequest, PeerVideo, PeerAudio } from './webrtc'
|
||||
|
||||
/////////////////////////////
|
||||
// System
|
||||
/////////////////////////////
|
||||
|
||||
export interface SystemSettingsUpdate extends Settings {
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface SystemWebRTC {
|
||||
videos: string[]
|
||||
}
|
||||
|
||||
export interface SystemInit {
|
||||
session_id: string
|
||||
control_host: ControlHost
|
||||
screen_size: ScreenSize
|
||||
sessions: Record<string, SessionData>
|
||||
settings: Settings
|
||||
touch_events: boolean
|
||||
screencast_enabled: boolean
|
||||
webrtc: SystemWebRTC
|
||||
}
|
||||
|
||||
export interface SystemAdmin {
|
||||
screen_sizes_list: ScreenSize[]
|
||||
broadcast_status: BroadcastStatus
|
||||
}
|
||||
|
||||
export type SystemLogs = SystemLog[]
|
||||
|
||||
export interface SystemLog {
|
||||
level: 'debug' | 'info' | 'warn' | 'error'
|
||||
fields: Record<string, string>
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface SystemDisconnect {
|
||||
message: string
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Signal
|
||||
/////////////////////////////
|
||||
|
||||
export type SignalRequest = PeerRequest
|
||||
|
||||
export interface SignalProvide {
|
||||
sdp: string
|
||||
iceservers: ICEServer[]
|
||||
video: PeerVideo
|
||||
audio: PeerAudio
|
||||
}
|
||||
|
||||
export type SignalCandidate = RTCIceCandidateInit
|
||||
|
||||
export interface SignalDescription {
|
||||
sdp: string
|
||||
}
|
||||
|
||||
export type SignalVideo = PeerVideo
|
||||
|
||||
export type SignalAudio = PeerAudio
|
||||
|
||||
/////////////////////////////
|
||||
// Session
|
||||
/////////////////////////////
|
||||
|
||||
export interface SessionID {
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface MemberProfile {
|
||||
id: string
|
||||
name: string
|
||||
is_admin: boolean
|
||||
can_login: boolean
|
||||
can_connect: boolean
|
||||
can_watch: boolean
|
||||
can_host: boolean
|
||||
can_share_media: boolean
|
||||
can_access_clipboard: boolean
|
||||
sends_inactive_cursor: boolean
|
||||
can_see_inactive_cursors: boolean
|
||||
}
|
||||
|
||||
export interface SessionState {
|
||||
id: string
|
||||
is_connected: boolean
|
||||
is_watching: boolean
|
||||
}
|
||||
|
||||
export interface SessionData {
|
||||
id: string
|
||||
profile: MemberProfile
|
||||
is_connected: boolean
|
||||
is_watching: boolean
|
||||
}
|
||||
|
||||
export interface SessionCursor {
|
||||
id: string
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Control
|
||||
/////////////////////////////
|
||||
|
||||
export interface ControlHost {
|
||||
id: string
|
||||
has_host: boolean
|
||||
host_id: string | undefined
|
||||
}
|
||||
|
||||
export interface ControlScroll {
|
||||
delta_x: number
|
||||
delta_y: number
|
||||
control_key: boolean
|
||||
}
|
||||
|
||||
export interface ControlPos {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface ControlButton extends Partial<ControlPos> {
|
||||
code: number
|
||||
}
|
||||
|
||||
export interface ControlKey extends Partial<ControlPos> {
|
||||
keysym: number
|
||||
}
|
||||
|
||||
export interface ControlTouch extends Partial<ControlPos> {
|
||||
touch_id: number
|
||||
pressure: number
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Screen
|
||||
/////////////////////////////
|
||||
|
||||
export interface ScreenSizeUpdate extends ScreenSize {
|
||||
id: string
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Clipboard
|
||||
/////////////////////////////
|
||||
|
||||
export interface ClipboardData {
|
||||
text: string
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Keyboard
|
||||
/////////////////////////////
|
||||
|
||||
export interface KeyboardModifiers {
|
||||
caps_lock: boolean
|
||||
num_lock: boolean
|
||||
scroll_lock: boolean
|
||||
}
|
||||
|
||||
export interface KeyboardMap {
|
||||
layout: string
|
||||
variant: string
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Broadcast
|
||||
/////////////////////////////
|
||||
|
||||
export interface BroadcastStatus {
|
||||
is_active: boolean
|
||||
url: string | undefined
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Send
|
||||
/////////////////////////////
|
||||
|
||||
export interface SendMessage {
|
||||
sender: string
|
||||
subject: string
|
||||
body: string
|
||||
}
|
5
client/src/component/types/reconnector.ts
Normal file
5
client/src/component/types/reconnector.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface ReconnectorConfig {
|
||||
max_reconnects: number
|
||||
timeout_ms: number
|
||||
backoff_ms: number
|
||||
}
|
182
client/src/component/types/state.ts
Normal file
182
client/src/component/types/state.ts
Normal file
@ -0,0 +1,182 @@
|
||||
import type * as webrtcTypes from './webrtc'
|
||||
import type * as reconnectorTypes from './reconnector'
|
||||
|
||||
export default interface State {
|
||||
authenticated: boolean
|
||||
connection: Connection
|
||||
video: Video
|
||||
control: Control
|
||||
screen: Screen
|
||||
session_id: string | null
|
||||
sessions: Record<string, Session>
|
||||
settings: Settings
|
||||
cursors: Cursors
|
||||
mobile_keyboard_open: boolean
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Connection
|
||||
/////////////////////////////
|
||||
|
||||
export interface Connection {
|
||||
url: string
|
||||
token?: string
|
||||
status: 'disconnected' | 'connecting' | 'connected'
|
||||
websocket: WebSocket
|
||||
webrtc: WebRTC
|
||||
screencast: boolean
|
||||
type: 'webrtc' | 'fallback' | 'none'
|
||||
}
|
||||
|
||||
export interface WebSocket {
|
||||
connected: boolean
|
||||
config: ReconnectorConfig
|
||||
}
|
||||
|
||||
export interface WebRTC {
|
||||
connected: boolean
|
||||
stable: boolean
|
||||
config: ReconnectorConfig
|
||||
stats: WebRTCStats | null
|
||||
video: PeerVideo
|
||||
audio: PeerAudio
|
||||
videos: string[]
|
||||
}
|
||||
|
||||
export interface ReconnectorConfig extends reconnectorTypes.ReconnectorConfig {}
|
||||
|
||||
export interface WebRTCStats extends webrtcTypes.WebRTCStats {}
|
||||
|
||||
export interface PeerVideo extends webrtcTypes.PeerVideo {}
|
||||
|
||||
export interface PeerAudio extends webrtcTypes.PeerAudio {}
|
||||
|
||||
/////////////////////////////
|
||||
// Video
|
||||
/////////////////////////////
|
||||
|
||||
export interface Video {
|
||||
playable: boolean
|
||||
playing: boolean
|
||||
volume: number
|
||||
muted: boolean
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Control
|
||||
/////////////////////////////
|
||||
|
||||
export interface Control {
|
||||
scroll: Scroll
|
||||
clipboard: Clipboard | null
|
||||
keyboard: Keyboard
|
||||
touch: Touch
|
||||
host_id: string | null
|
||||
is_host: boolean
|
||||
locked: boolean
|
||||
}
|
||||
|
||||
export interface Scroll {
|
||||
inverse: boolean
|
||||
sensitivity: number
|
||||
}
|
||||
|
||||
export interface Clipboard {
|
||||
text: string
|
||||
}
|
||||
|
||||
export interface Keyboard {
|
||||
layout: string
|
||||
variant: string
|
||||
}
|
||||
|
||||
export interface Touch {
|
||||
enabled: boolean
|
||||
supported: boolean
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Screen
|
||||
/////////////////////////////
|
||||
|
||||
export interface Screen {
|
||||
size: ScreenSize
|
||||
configurations: ScreenSize[]
|
||||
sync: ScreenSync
|
||||
}
|
||||
|
||||
export interface ScreenSize {
|
||||
width: number
|
||||
height: number
|
||||
rate: number
|
||||
}
|
||||
|
||||
export interface ScreenSync {
|
||||
enabled: boolean
|
||||
multiplier: number
|
||||
rate: number
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Session
|
||||
/////////////////////////////
|
||||
|
||||
export interface MemberProfile {
|
||||
name: string
|
||||
is_admin: boolean
|
||||
can_login: boolean
|
||||
can_connect: boolean
|
||||
can_watch: boolean
|
||||
can_host: boolean
|
||||
can_share_media: boolean
|
||||
can_access_clipboard: boolean
|
||||
sends_inactive_cursor: boolean
|
||||
can_see_inactive_cursors: boolean
|
||||
plugins?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface SessionState {
|
||||
is_connected: boolean
|
||||
connected_since?: Date
|
||||
not_connected_since?: Date
|
||||
is_watching: boolean
|
||||
watching_since?: Date
|
||||
not_watching_since?: Date
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
id: string
|
||||
profile: MemberProfile
|
||||
state: SessionState
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Settings
|
||||
/////////////////////////////
|
||||
|
||||
export interface Settings {
|
||||
private_mode: boolean
|
||||
locked_logins: boolean
|
||||
locked_controls: boolean
|
||||
control_protection: boolean
|
||||
implicit_hosting: boolean
|
||||
inactive_cursors: boolean
|
||||
merciful_reconnect: boolean
|
||||
plugins?: Record<string, any>
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Cursors
|
||||
/////////////////////////////
|
||||
|
||||
type Cursors = SessionCursors[]
|
||||
|
||||
export interface SessionCursors {
|
||||
id: string
|
||||
cursors: Cursor[]
|
||||
}
|
||||
|
||||
export interface Cursor {
|
||||
x: number
|
||||
y: number
|
||||
}
|
58
client/src/component/types/webrtc.ts
Normal file
58
client/src/component/types/webrtc.ts
Normal file
@ -0,0 +1,58 @@
|
||||
export type StreamSelectorType = 'exact' | 'nearest' | 'lower' | 'higher'
|
||||
|
||||
export interface StreamSelector {
|
||||
type: StreamSelectorType
|
||||
id?: string
|
||||
bitrate?: number
|
||||
}
|
||||
|
||||
export interface PeerRequest {
|
||||
video?: PeerVideoRequest
|
||||
audio?: PeerAudioRequest
|
||||
}
|
||||
|
||||
export interface PeerVideo {
|
||||
disabled: boolean
|
||||
id: string
|
||||
auto: boolean
|
||||
}
|
||||
|
||||
export interface PeerVideoRequest {
|
||||
disabled?: boolean
|
||||
selector?: StreamSelector
|
||||
auto?: boolean
|
||||
}
|
||||
|
||||
export interface PeerAudio {
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
export interface PeerAudioRequest {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export interface WebRTCStats {
|
||||
paused: boolean
|
||||
bitrate: number
|
||||
packetLoss: number
|
||||
fps: number
|
||||
width: number
|
||||
height: number
|
||||
muted?: boolean
|
||||
latency: number
|
||||
requestLatency: number
|
||||
responseLatency: number
|
||||
}
|
||||
|
||||
export interface CursorPosition {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface CursorImage {
|
||||
width: number
|
||||
height: number
|
||||
x: number
|
||||
y: number
|
||||
uri: string
|
||||
}
|
56
client/src/component/utils/canvas-movement.ts
Normal file
56
client/src/component/utils/canvas-movement.ts
Normal file
@ -0,0 +1,56 @@
|
||||
/* eslint-disable */
|
||||
|
||||
export interface Point {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
// movement: percent is 0-1
|
||||
export function getMovementXYatPercent(p: Point[], percent: number): Point {
|
||||
const len = p.length
|
||||
if (len == 0) {
|
||||
console.error('getMovementXYatPercent: no points specified');
|
||||
return { x:0, y:0 }
|
||||
}
|
||||
|
||||
if (len == 1) return p[0]
|
||||
if (len == 2) return getLineXYatPercent(p[0], p[1], percent)
|
||||
if (len == 3) return getQuadraticBezierXYatPercent(p[0], p[1], p[2], percent)
|
||||
if (len == 4) return getCubicBezierXYatPercent(p[0], p[1], p[2], p[3], percent)
|
||||
|
||||
// TODO: Support more than 4 points
|
||||
if (len-1 % 3 == 0) return getCubicBezierXYatPercent(p[0], p[(len-1)/3], p[((len-1)/3)*2], p[len-1], percent)
|
||||
else if (len-1 % 2 == 0) return getQuadraticBezierXYatPercent(p[0], p[(len-1)/2], p[len-1], percent)
|
||||
else return getLineXYatPercent(p[0], p[len-1], percent)
|
||||
}
|
||||
|
||||
// line: percent is 0-1
|
||||
export function getLineXYatPercent(startPt: Point, endPt: Point, percent: number) : Point {
|
||||
return {
|
||||
x: startPt.x + (endPt.x - startPt.x) * percent,
|
||||
y: startPt.y + (endPt.y - startPt.y) * percent,
|
||||
};
|
||||
}
|
||||
|
||||
// quadratic bezier: percent is 0-1
|
||||
export function getQuadraticBezierXYatPercent(startPt: Point, controlPt: Point, endPt: Point, percent: number): Point {
|
||||
return {
|
||||
x: Math.pow(1 - percent, 2) * startPt.x + 2 * (1 - percent) * percent * controlPt.x + Math.pow(percent, 2) * endPt.x,
|
||||
y: Math.pow(1 - percent, 2) * startPt.y + 2 * (1 - percent) * percent * controlPt.y + Math.pow(percent, 2) * endPt.y,
|
||||
}
|
||||
}
|
||||
|
||||
// cubic bezier percent is 0-1
|
||||
export function getCubicBezierXYatPercent(startPt: Point, controlPt1: Point, controlPt2: Point, endPt: Point, percent: number): Point {
|
||||
return {
|
||||
x: cubicN(percent, startPt.x, controlPt1.x, controlPt2.x, endPt.x),
|
||||
y: cubicN(percent, startPt.y, controlPt1.y, controlPt2.y, endPt.y),
|
||||
}
|
||||
}
|
||||
|
||||
// cubic helper formula at percent distance
|
||||
function cubicN(pct: number, a: number, b: number, c: number, d: number): number {
|
||||
var t2 = pct * pct;
|
||||
var t3 = t2 * pct;
|
||||
return a + (-a * 3 + pct * (3 * a - a * pct)) * pct + (3 * b + pct * (-6 * b + b * 3 * pct)) * pct + (c * 3 - c * 3 * pct) * t2 + d * t3;
|
||||
}
|
41
client/src/component/utils/file-upload.ts
Normal file
41
client/src/component/utils/file-upload.ts
Normal file
@ -0,0 +1,41 @@
|
||||
export async function getFilesFromDataTansfer(dataTransfer: DataTransfer): Promise<Array<File>> {
|
||||
const files: Array<File> = []
|
||||
|
||||
const traverse = (entry: any): Promise<any> => {
|
||||
return new Promise((resolve) => {
|
||||
if (entry.isFile) {
|
||||
entry.file((file: File) => {
|
||||
files.push(file)
|
||||
resolve(file)
|
||||
})
|
||||
} else if (entry.isDirectory) {
|
||||
const reader = entry.createReader()
|
||||
reader.readEntries((entries: any) => {
|
||||
const promises = entries.map(traverse)
|
||||
Promise.all(promises).then(resolve)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const promises: Array<Promise<any>> = []
|
||||
// Type 'DataTransferItemList' is not an array type or a string type. Use compiler option '--downlevelIteration' to allow iterating of iterators.
|
||||
// @ts-ignore
|
||||
for (const item of dataTransfer.items) {
|
||||
if ('webkitGetAsEntry' in item) {
|
||||
promises.push(traverse(item.webkitGetAsEntry()))
|
||||
} else if ('getAsEntry' in item) {
|
||||
// @ts-ignore
|
||||
promises.push(traverse(item.getAsEntry()))
|
||||
} else break
|
||||
}
|
||||
|
||||
if (promises.length === 0) {
|
||||
// Type 'FileList' is not an array type or a string type. Use compiler option '--downlevelIteration' to allow iterating of iterators.
|
||||
// @ts-ignore
|
||||
return [...dataTransfer.files]
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
return files
|
||||
}
|
567
client/src/component/utils/gesturehandler.js
Normal file
567
client/src/component/utils/gesturehandler.js
Normal file
@ -0,0 +1,567 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2020 The noVNC Authors
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* See README.md for usage and integration instructions.
|
||||
*
|
||||
*/
|
||||
|
||||
const GH_NOGESTURE = 0;
|
||||
const GH_ONETAP = 1;
|
||||
const GH_TWOTAP = 2;
|
||||
const GH_THREETAP = 4;
|
||||
const GH_DRAG = 8;
|
||||
const GH_LONGPRESS = 16;
|
||||
const GH_TWODRAG = 32;
|
||||
const GH_PINCH = 64;
|
||||
|
||||
const GH_INITSTATE = 127;
|
||||
|
||||
const GH_MOVE_THRESHOLD = 50;
|
||||
const GH_ANGLE_THRESHOLD = 90; // Degrees
|
||||
|
||||
// Timeout when waiting for gestures (ms)
|
||||
const GH_MULTITOUCH_TIMEOUT = 250;
|
||||
|
||||
// Maximum time between press and release for a tap (ms)
|
||||
const GH_TAP_TIMEOUT = 1000;
|
||||
|
||||
// Timeout when waiting for longpress (ms)
|
||||
const GH_LONGPRESS_TIMEOUT = 1000;
|
||||
|
||||
// Timeout when waiting to decide between PINCH and TWODRAG (ms)
|
||||
const GH_TWOTOUCH_TIMEOUT = 50;
|
||||
|
||||
export default class GestureHandler {
|
||||
constructor() {
|
||||
this._target = null;
|
||||
|
||||
this._state = GH_INITSTATE;
|
||||
|
||||
this._tracked = [];
|
||||
this._ignored = [];
|
||||
|
||||
this._waitingRelease = false;
|
||||
this._releaseStart = 0.0;
|
||||
|
||||
this._longpressTimeoutId = null;
|
||||
this._twoTouchTimeoutId = null;
|
||||
|
||||
this._boundEventHandler = this._eventHandler.bind(this);
|
||||
}
|
||||
|
||||
attach(target) {
|
||||
this.detach();
|
||||
|
||||
this._target = target;
|
||||
this._target.addEventListener('touchstart',
|
||||
this._boundEventHandler);
|
||||
this._target.addEventListener('touchmove',
|
||||
this._boundEventHandler);
|
||||
this._target.addEventListener('touchend',
|
||||
this._boundEventHandler);
|
||||
this._target.addEventListener('touchcancel',
|
||||
this._boundEventHandler);
|
||||
}
|
||||
|
||||
detach() {
|
||||
if (!this._target) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._stopLongpressTimeout();
|
||||
this._stopTwoTouchTimeout();
|
||||
|
||||
this._target.removeEventListener('touchstart',
|
||||
this._boundEventHandler);
|
||||
this._target.removeEventListener('touchmove',
|
||||
this._boundEventHandler);
|
||||
this._target.removeEventListener('touchend',
|
||||
this._boundEventHandler);
|
||||
this._target.removeEventListener('touchcancel',
|
||||
this._boundEventHandler);
|
||||
this._target = null;
|
||||
}
|
||||
|
||||
_eventHandler(e) {
|
||||
let fn;
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
switch (e.type) {
|
||||
case 'touchstart':
|
||||
fn = this._touchStart;
|
||||
break;
|
||||
case 'touchmove':
|
||||
fn = this._touchMove;
|
||||
break;
|
||||
case 'touchend':
|
||||
case 'touchcancel':
|
||||
fn = this._touchEnd;
|
||||
break;
|
||||
}
|
||||
|
||||
for (let i = 0; i < e.changedTouches.length; i++) {
|
||||
let touch = e.changedTouches[i];
|
||||
fn.call(this, touch.identifier, touch.clientX, touch.clientY);
|
||||
}
|
||||
}
|
||||
|
||||
_touchStart(id, x, y) {
|
||||
// Ignore any new touches if there is already an active gesture,
|
||||
// or we're in a cleanup state
|
||||
if (this._hasDetectedGesture() || (this._state === GH_NOGESTURE)) {
|
||||
this._ignored.push(id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Did it take too long between touches that we should no longer
|
||||
// consider this a single gesture?
|
||||
if ((this._tracked.length > 0) &&
|
||||
((Date.now() - this._tracked[0].started) > GH_MULTITOUCH_TIMEOUT)) {
|
||||
this._state = GH_NOGESTURE;
|
||||
this._ignored.push(id);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're waiting for fingers to release then we should no longer
|
||||
// recognize new touches
|
||||
if (this._waitingRelease) {
|
||||
this._state = GH_NOGESTURE;
|
||||
this._ignored.push(id);
|
||||
return;
|
||||
}
|
||||
|
||||
this._tracked.push({
|
||||
id: id,
|
||||
started: Date.now(),
|
||||
active: true,
|
||||
firstX: x,
|
||||
firstY: y,
|
||||
lastX: x,
|
||||
lastY: y,
|
||||
angle: 0
|
||||
});
|
||||
|
||||
switch (this._tracked.length) {
|
||||
case 1:
|
||||
this._startLongpressTimeout();
|
||||
break;
|
||||
|
||||
case 2:
|
||||
this._state &= ~(GH_ONETAP | GH_DRAG | GH_LONGPRESS);
|
||||
this._stopLongpressTimeout();
|
||||
break;
|
||||
|
||||
case 3:
|
||||
this._state &= ~(GH_TWOTAP | GH_TWODRAG | GH_PINCH);
|
||||
break;
|
||||
|
||||
default:
|
||||
this._state = GH_NOGESTURE;
|
||||
}
|
||||
}
|
||||
|
||||
_touchMove(id, x, y) {
|
||||
let touch = this._tracked.find(t => t.id === id);
|
||||
|
||||
// If this is an update for a touch we're not tracking, ignore it
|
||||
if (touch === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the touches last position with the event coordinates
|
||||
touch.lastX = x;
|
||||
touch.lastY = y;
|
||||
|
||||
let deltaX = x - touch.firstX;
|
||||
let deltaY = y - touch.firstY;
|
||||
|
||||
// Update angle when the touch has moved
|
||||
if ((touch.firstX !== touch.lastX) ||
|
||||
(touch.firstY !== touch.lastY)) {
|
||||
touch.angle = Math.atan2(deltaY, deltaX) * 180 / Math.PI;
|
||||
}
|
||||
|
||||
if (!this._hasDetectedGesture()) {
|
||||
// Ignore moves smaller than the minimum threshold
|
||||
if (Math.hypot(deltaX, deltaY) < GH_MOVE_THRESHOLD) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Can't be a tap or long press as we've seen movement
|
||||
this._state &= ~(GH_ONETAP | GH_TWOTAP | GH_THREETAP | GH_LONGPRESS);
|
||||
this._stopLongpressTimeout();
|
||||
|
||||
if (this._tracked.length !== 1) {
|
||||
this._state &= ~(GH_DRAG);
|
||||
}
|
||||
if (this._tracked.length !== 2) {
|
||||
this._state &= ~(GH_TWODRAG | GH_PINCH);
|
||||
}
|
||||
|
||||
// We need to figure out which of our different two touch gestures
|
||||
// this might be
|
||||
if (this._tracked.length === 2) {
|
||||
|
||||
// The other touch is the one where the id doesn't match
|
||||
let prevTouch = this._tracked.find(t => t.id !== id);
|
||||
|
||||
// How far the previous touch point has moved since start
|
||||
let prevDeltaMove = Math.hypot(prevTouch.firstX - prevTouch.lastX,
|
||||
prevTouch.firstY - prevTouch.lastY);
|
||||
|
||||
// We know that the current touch moved far enough,
|
||||
// but unless both touches moved further than their
|
||||
// threshold we don't want to disqualify any gestures
|
||||
if (prevDeltaMove > GH_MOVE_THRESHOLD) {
|
||||
|
||||
// The angle difference between the direction of the touch points
|
||||
let deltaAngle = Math.abs(touch.angle - prevTouch.angle);
|
||||
deltaAngle = Math.abs(((deltaAngle + 180) % 360) - 180);
|
||||
|
||||
// PINCH or TWODRAG can be eliminated depending on the angle
|
||||
if (deltaAngle > GH_ANGLE_THRESHOLD) {
|
||||
this._state &= ~GH_TWODRAG;
|
||||
} else {
|
||||
this._state &= ~GH_PINCH;
|
||||
}
|
||||
|
||||
if (this._isTwoTouchTimeoutRunning()) {
|
||||
this._stopTwoTouchTimeout();
|
||||
}
|
||||
} else if (!this._isTwoTouchTimeoutRunning()) {
|
||||
// We can't determine the gesture right now, let's
|
||||
// wait and see if more events are on their way
|
||||
this._startTwoTouchTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._hasDetectedGesture()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._pushEvent('gesturestart');
|
||||
}
|
||||
|
||||
this._pushEvent('gesturemove');
|
||||
}
|
||||
|
||||
_touchEnd(id, x, y) {
|
||||
// Check if this is an ignored touch
|
||||
if (this._ignored.indexOf(id) !== -1) {
|
||||
// Remove this touch from ignored
|
||||
this._ignored.splice(this._ignored.indexOf(id), 1);
|
||||
|
||||
// And reset the state if there are no more touches
|
||||
if ((this._ignored.length === 0) &&
|
||||
(this._tracked.length === 0)) {
|
||||
this._state = GH_INITSTATE;
|
||||
this._waitingRelease = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// We got a touchend before the timer triggered,
|
||||
// this cannot result in a gesture anymore.
|
||||
if (!this._hasDetectedGesture() &&
|
||||
this._isTwoTouchTimeoutRunning()) {
|
||||
this._stopTwoTouchTimeout();
|
||||
this._state = GH_NOGESTURE;
|
||||
}
|
||||
|
||||
// Some gestures don't trigger until a touch is released
|
||||
if (!this._hasDetectedGesture()) {
|
||||
// Can't be a gesture that relies on movement
|
||||
this._state &= ~(GH_DRAG | GH_TWODRAG | GH_PINCH);
|
||||
// Or something that relies on more time
|
||||
this._state &= ~GH_LONGPRESS;
|
||||
this._stopLongpressTimeout();
|
||||
|
||||
if (!this._waitingRelease) {
|
||||
this._releaseStart = Date.now();
|
||||
this._waitingRelease = true;
|
||||
|
||||
// Can't be a tap that requires more touches than we current have
|
||||
switch (this._tracked.length) {
|
||||
case 1:
|
||||
this._state &= ~(GH_TWOTAP | GH_THREETAP);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
this._state &= ~(GH_ONETAP | GH_THREETAP);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Waiting for all touches to release? (i.e. some tap)
|
||||
if (this._waitingRelease) {
|
||||
// Were all touches released at roughly the same time?
|
||||
if ((Date.now() - this._releaseStart) > GH_MULTITOUCH_TIMEOUT) {
|
||||
this._state = GH_NOGESTURE;
|
||||
}
|
||||
|
||||
// Did too long time pass between press and release?
|
||||
if (this._tracked.some(t => (Date.now() - t.started) > GH_TAP_TIMEOUT)) {
|
||||
this._state = GH_NOGESTURE;
|
||||
}
|
||||
|
||||
let touch = this._tracked.find(t => t.id === id);
|
||||
touch.active = false;
|
||||
|
||||
// Are we still waiting for more releases?
|
||||
if (this._hasDetectedGesture()) {
|
||||
this._pushEvent('gesturestart');
|
||||
} else {
|
||||
// Have we reached a dead end?
|
||||
if (this._state !== GH_NOGESTURE) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this._hasDetectedGesture()) {
|
||||
this._pushEvent('gestureend');
|
||||
}
|
||||
|
||||
// Ignore any remaining touches until they are ended
|
||||
for (let i = 0; i < this._tracked.length; i++) {
|
||||
if (this._tracked[i].active) {
|
||||
this._ignored.push(this._tracked[i].id);
|
||||
}
|
||||
}
|
||||
this._tracked = [];
|
||||
|
||||
this._state = GH_NOGESTURE;
|
||||
|
||||
// Remove this touch from ignored if it's in there
|
||||
if (this._ignored.indexOf(id) !== -1) {
|
||||
this._ignored.splice(this._ignored.indexOf(id), 1);
|
||||
}
|
||||
|
||||
// We reset the state if ignored is empty
|
||||
if ((this._ignored.length === 0)) {
|
||||
this._state = GH_INITSTATE;
|
||||
this._waitingRelease = false;
|
||||
}
|
||||
}
|
||||
|
||||
_hasDetectedGesture() {
|
||||
if (this._state === GH_NOGESTURE) {
|
||||
return false;
|
||||
}
|
||||
// Check to see if the bitmask value is a power of 2
|
||||
// (i.e. only one bit set). If it is, we have a state.
|
||||
if (this._state & (this._state - 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For taps we also need to have all touches released
|
||||
// before we've fully detected the gesture
|
||||
if (this._state & (GH_ONETAP | GH_TWOTAP | GH_THREETAP)) {
|
||||
if (this._tracked.some(t => t.active)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_startLongpressTimeout() {
|
||||
this._stopLongpressTimeout();
|
||||
this._longpressTimeoutId = setTimeout(() => this._longpressTimeout(),
|
||||
GH_LONGPRESS_TIMEOUT);
|
||||
}
|
||||
|
||||
_stopLongpressTimeout() {
|
||||
clearTimeout(this._longpressTimeoutId);
|
||||
this._longpressTimeoutId = null;
|
||||
}
|
||||
|
||||
_longpressTimeout() {
|
||||
if (this._hasDetectedGesture()) {
|
||||
throw new Error("A longpress gesture failed, conflict with a different gesture");
|
||||
}
|
||||
|
||||
this._state = GH_LONGPRESS;
|
||||
this._pushEvent('gesturestart');
|
||||
}
|
||||
|
||||
_startTwoTouchTimeout() {
|
||||
this._stopTwoTouchTimeout();
|
||||
this._twoTouchTimeoutId = setTimeout(() => this._twoTouchTimeout(),
|
||||
GH_TWOTOUCH_TIMEOUT);
|
||||
}
|
||||
|
||||
_stopTwoTouchTimeout() {
|
||||
clearTimeout(this._twoTouchTimeoutId);
|
||||
this._twoTouchTimeoutId = null;
|
||||
}
|
||||
|
||||
_isTwoTouchTimeoutRunning() {
|
||||
return this._twoTouchTimeoutId !== null;
|
||||
}
|
||||
|
||||
_twoTouchTimeout() {
|
||||
if (this._tracked.length === 0) {
|
||||
throw new Error("A pinch or two drag gesture failed, no tracked touches");
|
||||
}
|
||||
|
||||
// How far each touch point has moved since start
|
||||
let avgM = this._getAverageMovement();
|
||||
let avgMoveH = Math.abs(avgM.x);
|
||||
let avgMoveV = Math.abs(avgM.y);
|
||||
|
||||
// The difference in the distance between where
|
||||
// the touch points started and where they are now
|
||||
let avgD = this._getAverageDistance();
|
||||
let deltaTouchDistance = Math.abs(Math.hypot(avgD.first.x, avgD.first.y) -
|
||||
Math.hypot(avgD.last.x, avgD.last.y));
|
||||
|
||||
if ((avgMoveV < deltaTouchDistance) &&
|
||||
(avgMoveH < deltaTouchDistance)) {
|
||||
this._state = GH_PINCH;
|
||||
} else {
|
||||
this._state = GH_TWODRAG;
|
||||
}
|
||||
|
||||
this._pushEvent('gesturestart');
|
||||
this._pushEvent('gesturemove');
|
||||
}
|
||||
|
||||
_pushEvent(type) {
|
||||
let detail = { type: this._stateToGesture(this._state) };
|
||||
|
||||
// For most gesture events the current (average) position is the
|
||||
// most useful
|
||||
let avg = this._getPosition();
|
||||
let pos = avg.last;
|
||||
|
||||
// However we have a slight distance to detect gestures, so for the
|
||||
// first gesture event we want to use the first positions we saw
|
||||
if (type === 'gesturestart') {
|
||||
pos = avg.first;
|
||||
}
|
||||
|
||||
// For these gestures, we always want the event coordinates
|
||||
// to be where the gesture began, not the current touch location.
|
||||
switch (this._state) {
|
||||
case GH_TWODRAG:
|
||||
case GH_PINCH:
|
||||
pos = avg.first;
|
||||
break;
|
||||
}
|
||||
|
||||
detail['clientX'] = pos.x;
|
||||
detail['clientY'] = pos.y;
|
||||
|
||||
// FIXME: other coordinates?
|
||||
|
||||
// Some gestures also have a magnitude
|
||||
if (this._state === GH_PINCH) {
|
||||
let distance = this._getAverageDistance();
|
||||
if (type === 'gesturestart') {
|
||||
detail['magnitudeX'] = distance.first.x;
|
||||
detail['magnitudeY'] = distance.first.y;
|
||||
} else {
|
||||
detail['magnitudeX'] = distance.last.x;
|
||||
detail['magnitudeY'] = distance.last.y;
|
||||
}
|
||||
} else if (this._state === GH_TWODRAG) {
|
||||
if (type === 'gesturestart') {
|
||||
detail['magnitudeX'] = 0.0;
|
||||
detail['magnitudeY'] = 0.0;
|
||||
} else {
|
||||
let movement = this._getAverageMovement();
|
||||
detail['magnitudeX'] = movement.x;
|
||||
detail['magnitudeY'] = movement.y;
|
||||
}
|
||||
}
|
||||
|
||||
let gev = new CustomEvent(type, { detail: detail });
|
||||
this._target.dispatchEvent(gev);
|
||||
}
|
||||
|
||||
_stateToGesture(state) {
|
||||
switch (state) {
|
||||
case GH_ONETAP:
|
||||
return 'onetap';
|
||||
case GH_TWOTAP:
|
||||
return 'twotap';
|
||||
case GH_THREETAP:
|
||||
return 'threetap';
|
||||
case GH_DRAG:
|
||||
return 'drag';
|
||||
case GH_LONGPRESS:
|
||||
return 'longpress';
|
||||
case GH_TWODRAG:
|
||||
return 'twodrag';
|
||||
case GH_PINCH:
|
||||
return 'pinch';
|
||||
}
|
||||
|
||||
throw new Error("Unknown gesture state: " + state);
|
||||
}
|
||||
|
||||
_getPosition() {
|
||||
if (this._tracked.length === 0) {
|
||||
throw new Error("Failed to get gesture position, no tracked touches");
|
||||
}
|
||||
|
||||
let size = this._tracked.length;
|
||||
let fx = 0, fy = 0, lx = 0, ly = 0;
|
||||
|
||||
for (let i = 0; i < this._tracked.length; i++) {
|
||||
fx += this._tracked[i].firstX;
|
||||
fy += this._tracked[i].firstY;
|
||||
lx += this._tracked[i].lastX;
|
||||
ly += this._tracked[i].lastY;
|
||||
}
|
||||
|
||||
return { first: { x: fx / size,
|
||||
y: fy / size },
|
||||
last: { x: lx / size,
|
||||
y: ly / size } };
|
||||
}
|
||||
|
||||
_getAverageMovement() {
|
||||
if (this._tracked.length === 0) {
|
||||
throw new Error("Failed to get gesture movement, no tracked touches");
|
||||
}
|
||||
|
||||
let totalH, totalV;
|
||||
totalH = totalV = 0;
|
||||
let size = this._tracked.length;
|
||||
|
||||
for (let i = 0; i < this._tracked.length; i++) {
|
||||
totalH += this._tracked[i].lastX - this._tracked[i].firstX;
|
||||
totalV += this._tracked[i].lastY - this._tracked[i].firstY;
|
||||
}
|
||||
|
||||
return { x: totalH / size,
|
||||
y: totalV / size };
|
||||
}
|
||||
|
||||
_getAverageDistance() {
|
||||
if (this._tracked.length === 0) {
|
||||
throw new Error("Failed to get gesture distance, no tracked touches");
|
||||
}
|
||||
|
||||
// Distance between the first and last tracked touches
|
||||
|
||||
let first = this._tracked[0];
|
||||
let last = this._tracked[this._tracked.length - 1];
|
||||
|
||||
let fdx = Math.abs(last.firstX - first.firstX);
|
||||
let fdy = Math.abs(last.firstY - first.firstY);
|
||||
|
||||
let ldx = Math.abs(last.lastX - first.lastX);
|
||||
let ldy = Math.abs(last.lastY - first.lastY);
|
||||
|
||||
return { first: { x: fdx, y: fdy },
|
||||
last: { x: ldx, y: ldy } };
|
||||
}
|
||||
}
|
14
client/src/component/utils/gesturehandler.ts
Normal file
14
client/src/component/utils/gesturehandler.ts
Normal file
@ -0,0 +1,14 @@
|
||||
// https://github.com/novnc/noVNC/blob/ca6527c1bf7131adccfdcc5028964a1e67f9018c/core/input/gesturehandler.js#L246
|
||||
import gh from './gesturehandler.js'
|
||||
|
||||
const g = gh as GestureHandlerConstructor
|
||||
export default g
|
||||
|
||||
interface GestureHandlerConstructor {
|
||||
new (): GestureHandler
|
||||
}
|
||||
|
||||
export interface GestureHandler {
|
||||
attach(element: Element): void
|
||||
detach(): void
|
||||
}
|
37
client/src/component/utils/keyboard-remapping.ts
Normal file
37
client/src/component/utils/keyboard-remapping.ts
Normal file
@ -0,0 +1,37 @@
|
||||
export const KeyTable = {
|
||||
XK_v: 0x0076, // U+0076 LATIN SMALL LETTER V
|
||||
|
||||
XK_Control_L: 0xffe3, // Left control
|
||||
XK_Control_R: 0xffe4, // Right control
|
||||
|
||||
XK_Meta_L: 0xffe7, // Left meta
|
||||
XK_Meta_R: 0xffe8, // Right meta
|
||||
XK_Alt_L: 0xffe9, // Left alt
|
||||
XK_Alt_R: 0xffea, // Right alt
|
||||
XK_Super_L: 0xffeb, // Left super
|
||||
XK_Super_R: 0xffec, // Right super
|
||||
|
||||
XK_ISO_Level3_Shift: 0xfe03, // AltGr
|
||||
}
|
||||
|
||||
export const keySymsRemap = function (key: number) {
|
||||
const isMac = navigator && navigator.platform.match(/^mac/i)
|
||||
const isiOS = navigator && navigator.platform.match(/ipad|iphone|ipod/i)
|
||||
|
||||
// switch command with ctrl and option with altgr on mac and ios
|
||||
if (isMac || isiOS) {
|
||||
switch (key) {
|
||||
case KeyTable.XK_Meta_L: // meta is used by guacamole for CMD key
|
||||
case KeyTable.XK_Super_L: // super is used by novnc for CMD key
|
||||
return KeyTable.XK_Control_L
|
||||
case KeyTable.XK_Meta_R:
|
||||
case KeyTable.XK_Super_R:
|
||||
return KeyTable.XK_Control_R
|
||||
case KeyTable.XK_Alt_L: // alt (option key on mac) behaves like altgr
|
||||
case KeyTable.XK_Alt_R:
|
||||
return KeyTable.XK_ISO_Level3_Shift
|
||||
}
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
19
client/src/component/utils/keyboard.ts
Normal file
19
client/src/component/utils/keyboard.ts
Normal file
@ -0,0 +1,19 @@
|
||||
//
|
||||
// TODO: add support for other keyboards
|
||||
//
|
||||
import Keyboard from './keyboards/guacamole'
|
||||
|
||||
// conditional import at build time:
|
||||
// __KEYBOARD__ is replaced by the value of the env variable KEYBOARD
|
||||
|
||||
export interface KeyboardInterface {
|
||||
onkeydown?: (keysym: number) => boolean
|
||||
onkeyup?: (keysym: number) => void
|
||||
release: (keysym: number) => void
|
||||
listenTo: (element: Element | Document) => void
|
||||
removeListener: () => void
|
||||
}
|
||||
|
||||
export function NewKeyboard(element?: Element): KeyboardInterface {
|
||||
return Keyboard(element)
|
||||
}
|
20
client/src/component/utils/keyboards/guacamole.ts
Normal file
20
client/src/component/utils/keyboards/guacamole.ts
Normal file
@ -0,0 +1,20 @@
|
||||
// https://github.com/apache/guacamole-client/blob/1ca1161a68030565a37319ec6275556dfcd1a1af/guacamole-common-js/src/main/webapp/modules/Keyboard.js
|
||||
import GuacamoleKeyboard from './guacamole/keyboard'
|
||||
import type { Interface } from './guacamole/keyboard'
|
||||
|
||||
export interface GuacamoleKeyboardInterface extends Interface {
|
||||
removeListener: () => void
|
||||
}
|
||||
|
||||
export default function (element?: Element): GuacamoleKeyboardInterface {
|
||||
const keyboard = {} as GuacamoleKeyboardInterface
|
||||
|
||||
GuacamoleKeyboard.bind(keyboard, element)()
|
||||
|
||||
// add removeListener function
|
||||
keyboard.removeListener = function () {
|
||||
// Guacamole Keyboard does not provide destroy functions
|
||||
}
|
||||
|
||||
return keyboard
|
||||
}
|
68
client/src/component/utils/keyboards/guacamole/keyboard.d.ts
vendored
Normal file
68
client/src/component/utils/keyboards/guacamole/keyboard.d.ts
vendored
Normal file
@ -0,0 +1,68 @@
|
||||
declare export interface Interface {
|
||||
/**
|
||||
* Fired whenever the user presses a key with the element associated
|
||||
* with this Guacamole.Keyboard in focus.
|
||||
*
|
||||
* @event
|
||||
* @param {Number} keysym The keysym of the key being pressed.
|
||||
* @return {Boolean} true if the key event should be allowed through to the
|
||||
* browser, false otherwise.
|
||||
*/
|
||||
onkeydown?: (keysym: number) => boolean
|
||||
|
||||
/**
|
||||
* Fired whenever the user releases a key with the element associated
|
||||
* with this Guacamole.Keyboard in focus.
|
||||
*
|
||||
* @event
|
||||
* @param {Number} keysym The keysym of the key being released.
|
||||
*/
|
||||
onkeyup?: (keysym: number) => void
|
||||
|
||||
/**
|
||||
* Marks a key as pressed, firing the keydown event if registered. Key
|
||||
* repeat for the pressed key will start after a delay if that key is
|
||||
* not a modifier. The return value of this function depends on the
|
||||
* return value of the keydown event handler, if any.
|
||||
*
|
||||
* @param {Number} keysym The keysym of the key to press.
|
||||
* @return {Boolean} true if event should NOT be canceled, false otherwise.
|
||||
*/
|
||||
press: (keysym: number) => boolean
|
||||
|
||||
/**
|
||||
* Marks a key as released, firing the keyup event if registered.
|
||||
*
|
||||
* @param {Number} keysym The keysym of the key to release.
|
||||
*/
|
||||
release: (keysym: number) => void
|
||||
|
||||
/**
|
||||
* Presses and releases the keys necessary to type the given string of
|
||||
* text.
|
||||
*
|
||||
* @param {String} str
|
||||
* The string to type.
|
||||
*/
|
||||
type: (str: string) => void
|
||||
|
||||
/**
|
||||
* Resets the state of this keyboard, releasing all keys, and firing keyup
|
||||
* events for each released key.
|
||||
*/
|
||||
reset: () => void
|
||||
|
||||
/**
|
||||
* Attaches event listeners to the given Element, automatically translating
|
||||
* received key, input, and composition events into simple keydown/keyup
|
||||
* events signalled through this Guacamole.Keyboard's onkeydown and
|
||||
* onkeyup handlers.
|
||||
*
|
||||
* @param {Element|Document} element
|
||||
* The Element to attach event listeners to for the sake of handling
|
||||
* key or input events.
|
||||
*/
|
||||
listenTo: (element: Element | Document) => void
|
||||
}
|
||||
|
||||
declare export default function (element?: Element): Interface
|
1522
client/src/component/utils/keyboards/guacamole/keyboard.js
Normal file
1522
client/src/component/utils/keyboards/guacamole/keyboard.js
Normal file
File diff suppressed because it is too large
Load Diff
45
client/src/component/utils/keyboards/novnc.ts
Normal file
45
client/src/component/utils/keyboards/novnc.ts
Normal file
@ -0,0 +1,45 @@
|
||||
// https://github.com/novnc/noVNC/blob/ca6527c1bf7131adccfdcc5028964a1e67f9018c/core/input/keyboard.js
|
||||
import Keyboard from './novnc/keyboard'
|
||||
|
||||
export interface NoVncKeyboardInterface extends Keyboard {
|
||||
onkeydown?: (keysym: number) => boolean
|
||||
onkeyup?: (keysym: number) => void
|
||||
release: (keysym: number) => void
|
||||
listenTo: (element: Element | Document) => void
|
||||
removeListener: () => void
|
||||
}
|
||||
|
||||
export default function (element?: Element): NoVncKeyboardInterface {
|
||||
const keyboard = new Keyboard(element) as NoVncKeyboardInterface
|
||||
|
||||
// map on key event to onkeydown and onkeyup
|
||||
keyboard.onkeyevent = function (keysym: number | null, code: string, down: boolean) {
|
||||
if (keysym === null) return false
|
||||
if (down && this.onkeydown) return this.onkeydown(keysym)
|
||||
if (!down && this.onkeyup) this.onkeyup(keysym)
|
||||
return false
|
||||
}
|
||||
|
||||
// add release function
|
||||
keyboard.release = function (keysym: number) {
|
||||
for (const code in this._keyDownList) {
|
||||
if (this._keyDownList[code] === keysym) {
|
||||
this._sendKeyEvent(keysym, code, false)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add listenTo function
|
||||
keyboard.listenTo = function (element: Element | Document) {
|
||||
if (element) this._target = element
|
||||
this.grab()
|
||||
}
|
||||
|
||||
// add removeListener function
|
||||
keyboard.removeListener = function () {
|
||||
this.ungrab()
|
||||
}
|
||||
|
||||
return keyboard
|
||||
}
|
42
client/src/component/utils/keyboards/novnc/browser.js
Normal file
42
client/src/component/utils/keyboards/novnc/browser.js
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2019 The noVNC Authors
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* See README.md for usage and integration instructions.
|
||||
*
|
||||
* Browser feature support detection
|
||||
*/
|
||||
|
||||
export function isMac() {
|
||||
return navigator && !!(/mac/i).exec(navigator.platform);
|
||||
}
|
||||
|
||||
export function isWindows() {
|
||||
return navigator && !!(/win/i).exec(navigator.platform);
|
||||
}
|
||||
|
||||
export function isIOS() {
|
||||
return navigator &&
|
||||
(!!(/ipad/i).exec(navigator.platform) ||
|
||||
!!(/iphone/i).exec(navigator.platform) ||
|
||||
!!(/ipod/i).exec(navigator.platform));
|
||||
}
|
||||
|
||||
export function isSafari() {
|
||||
return navigator && (navigator.userAgent.indexOf('Safari') !== -1 &&
|
||||
navigator.userAgent.indexOf('Chrome') === -1);
|
||||
}
|
||||
|
||||
export function isIE() {
|
||||
return navigator && !!(/trident/i).exec(navigator.userAgent);
|
||||
}
|
||||
|
||||
export function isEdge() {
|
||||
return navigator && !!(/edge/i).exec(navigator.userAgent);
|
||||
}
|
||||
|
||||
export function isFirefox() {
|
||||
return navigator && !!(/firefox/i).exec(navigator.userAgent);
|
||||
}
|
||||
|
311
client/src/component/utils/keyboards/novnc/domkeytable.js
Normal file
311
client/src/component/utils/keyboards/novnc/domkeytable.js
Normal file
@ -0,0 +1,311 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2018 The noVNC Authors
|
||||
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
|
||||
*/
|
||||
|
||||
import KeyTable from "./keysym.js";
|
||||
|
||||
/*
|
||||
* Mapping between HTML key values and VNC/X11 keysyms for "special"
|
||||
* keys that cannot be handled via their Unicode codepoint.
|
||||
*
|
||||
* See https://www.w3.org/TR/uievents-key/ for possible values.
|
||||
*/
|
||||
|
||||
const DOMKeyTable = {};
|
||||
|
||||
function addStandard(key, standard) {
|
||||
if (standard === undefined) throw new Error("Undefined keysym for key \"" + key + "\"");
|
||||
if (key in DOMKeyTable) throw new Error("Duplicate entry for key \"" + key + "\"");
|
||||
DOMKeyTable[key] = [standard, standard, standard, standard];
|
||||
}
|
||||
|
||||
function addLeftRight(key, left, right) {
|
||||
if (left === undefined) throw new Error("Undefined keysym for key \"" + key + "\"");
|
||||
if (right === undefined) throw new Error("Undefined keysym for key \"" + key + "\"");
|
||||
if (key in DOMKeyTable) throw new Error("Duplicate entry for key \"" + key + "\"");
|
||||
DOMKeyTable[key] = [left, left, right, left];
|
||||
}
|
||||
|
||||
function addNumpad(key, standard, numpad) {
|
||||
if (standard === undefined) throw new Error("Undefined keysym for key \"" + key + "\"");
|
||||
if (numpad === undefined) throw new Error("Undefined keysym for key \"" + key + "\"");
|
||||
if (key in DOMKeyTable) throw new Error("Duplicate entry for key \"" + key + "\"");
|
||||
DOMKeyTable[key] = [standard, standard, standard, numpad];
|
||||
}
|
||||
|
||||
// 3.2. Modifier Keys
|
||||
|
||||
addLeftRight("Alt", KeyTable.XK_Alt_L, KeyTable.XK_Alt_R);
|
||||
addStandard("AltGraph", KeyTable.XK_ISO_Level3_Shift);
|
||||
addStandard("CapsLock", KeyTable.XK_Caps_Lock);
|
||||
addLeftRight("Control", KeyTable.XK_Control_L, KeyTable.XK_Control_R);
|
||||
// - Fn
|
||||
// - FnLock
|
||||
addLeftRight("Meta", KeyTable.XK_Super_L, KeyTable.XK_Super_R);
|
||||
addStandard("NumLock", KeyTable.XK_Num_Lock);
|
||||
addStandard("ScrollLock", KeyTable.XK_Scroll_Lock);
|
||||
addLeftRight("Shift", KeyTable.XK_Shift_L, KeyTable.XK_Shift_R);
|
||||
// - Symbol
|
||||
// - SymbolLock
|
||||
// - Hyper
|
||||
// - Super
|
||||
|
||||
// 3.3. Whitespace Keys
|
||||
|
||||
addNumpad("Enter", KeyTable.XK_Return, KeyTable.XK_KP_Enter);
|
||||
addStandard("Tab", KeyTable.XK_Tab);
|
||||
addNumpad(" ", KeyTable.XK_space, KeyTable.XK_KP_Space);
|
||||
|
||||
// 3.4. Navigation Keys
|
||||
|
||||
addNumpad("ArrowDown", KeyTable.XK_Down, KeyTable.XK_KP_Down);
|
||||
addNumpad("ArrowLeft", KeyTable.XK_Left, KeyTable.XK_KP_Left);
|
||||
addNumpad("ArrowRight", KeyTable.XK_Right, KeyTable.XK_KP_Right);
|
||||
addNumpad("ArrowUp", KeyTable.XK_Up, KeyTable.XK_KP_Up);
|
||||
addNumpad("End", KeyTable.XK_End, KeyTable.XK_KP_End);
|
||||
addNumpad("Home", KeyTable.XK_Home, KeyTable.XK_KP_Home);
|
||||
addNumpad("PageDown", KeyTable.XK_Next, KeyTable.XK_KP_Next);
|
||||
addNumpad("PageUp", KeyTable.XK_Prior, KeyTable.XK_KP_Prior);
|
||||
|
||||
// 3.5. Editing Keys
|
||||
|
||||
addStandard("Backspace", KeyTable.XK_BackSpace);
|
||||
// Browsers send "Clear" for the numpad 5 without NumLock because
|
||||
// Windows uses VK_Clear for that key. But Unix expects KP_Begin for
|
||||
// that scenario.
|
||||
addNumpad("Clear", KeyTable.XK_Clear, KeyTable.XK_KP_Begin);
|
||||
addStandard("Copy", KeyTable.XF86XK_Copy);
|
||||
// - CrSel
|
||||
addStandard("Cut", KeyTable.XF86XK_Cut);
|
||||
addNumpad("Delete", KeyTable.XK_Delete, KeyTable.XK_KP_Delete);
|
||||
// - EraseEof
|
||||
// - ExSel
|
||||
addNumpad("Insert", KeyTable.XK_Insert, KeyTable.XK_KP_Insert);
|
||||
addStandard("Paste", KeyTable.XF86XK_Paste);
|
||||
addStandard("Redo", KeyTable.XK_Redo);
|
||||
addStandard("Undo", KeyTable.XK_Undo);
|
||||
|
||||
// 3.6. UI Keys
|
||||
|
||||
// - Accept
|
||||
// - Again (could just be XK_Redo)
|
||||
// - Attn
|
||||
addStandard("Cancel", KeyTable.XK_Cancel);
|
||||
addStandard("ContextMenu", KeyTable.XK_Menu);
|
||||
addStandard("Escape", KeyTable.XK_Escape);
|
||||
addStandard("Execute", KeyTable.XK_Execute);
|
||||
addStandard("Find", KeyTable.XK_Find);
|
||||
addStandard("Help", KeyTable.XK_Help);
|
||||
addStandard("Pause", KeyTable.XK_Pause);
|
||||
// - Play
|
||||
// - Props
|
||||
addStandard("Select", KeyTable.XK_Select);
|
||||
addStandard("ZoomIn", KeyTable.XF86XK_ZoomIn);
|
||||
addStandard("ZoomOut", KeyTable.XF86XK_ZoomOut);
|
||||
|
||||
// 3.7. Device Keys
|
||||
|
||||
addStandard("BrightnessDown", KeyTable.XF86XK_MonBrightnessDown);
|
||||
addStandard("BrightnessUp", KeyTable.XF86XK_MonBrightnessUp);
|
||||
addStandard("Eject", KeyTable.XF86XK_Eject);
|
||||
addStandard("LogOff", KeyTable.XF86XK_LogOff);
|
||||
addStandard("Power", KeyTable.XF86XK_PowerOff);
|
||||
addStandard("PowerOff", KeyTable.XF86XK_PowerDown);
|
||||
addStandard("PrintScreen", KeyTable.XK_Print);
|
||||
addStandard("Hibernate", KeyTable.XF86XK_Hibernate);
|
||||
addStandard("Standby", KeyTable.XF86XK_Standby);
|
||||
addStandard("WakeUp", KeyTable.XF86XK_WakeUp);
|
||||
|
||||
// 3.8. IME and Composition Keys
|
||||
|
||||
addStandard("AllCandidates", KeyTable.XK_MultipleCandidate);
|
||||
addStandard("Alphanumeric", KeyTable.XK_Eisu_toggle);
|
||||
addStandard("CodeInput", KeyTable.XK_Codeinput);
|
||||
addStandard("Compose", KeyTable.XK_Multi_key);
|
||||
addStandard("Convert", KeyTable.XK_Henkan);
|
||||
// - Dead
|
||||
// - FinalMode
|
||||
addStandard("GroupFirst", KeyTable.XK_ISO_First_Group);
|
||||
addStandard("GroupLast", KeyTable.XK_ISO_Last_Group);
|
||||
addStandard("GroupNext", KeyTable.XK_ISO_Next_Group);
|
||||
addStandard("GroupPrevious", KeyTable.XK_ISO_Prev_Group);
|
||||
// - ModeChange (XK_Mode_switch is often used for AltGr)
|
||||
// - NextCandidate
|
||||
addStandard("NonConvert", KeyTable.XK_Muhenkan);
|
||||
addStandard("PreviousCandidate", KeyTable.XK_PreviousCandidate);
|
||||
// - Process
|
||||
addStandard("SingleCandidate", KeyTable.XK_SingleCandidate);
|
||||
addStandard("HangulMode", KeyTable.XK_Hangul);
|
||||
addStandard("HanjaMode", KeyTable.XK_Hangul_Hanja);
|
||||
addStandard("JunjaMode", KeyTable.XK_Hangul_Jeonja);
|
||||
addStandard("Eisu", KeyTable.XK_Eisu_toggle);
|
||||
addStandard("Hankaku", KeyTable.XK_Hankaku);
|
||||
addStandard("Hiragana", KeyTable.XK_Hiragana);
|
||||
addStandard("HiraganaKatakana", KeyTable.XK_Hiragana_Katakana);
|
||||
addStandard("KanaMode", KeyTable.XK_Kana_Shift); // could also be _Kana_Lock
|
||||
addStandard("KanjiMode", KeyTable.XK_Kanji);
|
||||
addStandard("Katakana", KeyTable.XK_Katakana);
|
||||
addStandard("Romaji", KeyTable.XK_Romaji);
|
||||
addStandard("Zenkaku", KeyTable.XK_Zenkaku);
|
||||
addStandard("ZenkakuHankaku", KeyTable.XK_Zenkaku_Hankaku);
|
||||
|
||||
// 3.9. General-Purpose Function Keys
|
||||
|
||||
addStandard("F1", KeyTable.XK_F1);
|
||||
addStandard("F2", KeyTable.XK_F2);
|
||||
addStandard("F3", KeyTable.XK_F3);
|
||||
addStandard("F4", KeyTable.XK_F4);
|
||||
addStandard("F5", KeyTable.XK_F5);
|
||||
addStandard("F6", KeyTable.XK_F6);
|
||||
addStandard("F7", KeyTable.XK_F7);
|
||||
addStandard("F8", KeyTable.XK_F8);
|
||||
addStandard("F9", KeyTable.XK_F9);
|
||||
addStandard("F10", KeyTable.XK_F10);
|
||||
addStandard("F11", KeyTable.XK_F11);
|
||||
addStandard("F12", KeyTable.XK_F12);
|
||||
addStandard("F13", KeyTable.XK_F13);
|
||||
addStandard("F14", KeyTable.XK_F14);
|
||||
addStandard("F15", KeyTable.XK_F15);
|
||||
addStandard("F16", KeyTable.XK_F16);
|
||||
addStandard("F17", KeyTable.XK_F17);
|
||||
addStandard("F18", KeyTable.XK_F18);
|
||||
addStandard("F19", KeyTable.XK_F19);
|
||||
addStandard("F20", KeyTable.XK_F20);
|
||||
addStandard("F21", KeyTable.XK_F21);
|
||||
addStandard("F22", KeyTable.XK_F22);
|
||||
addStandard("F23", KeyTable.XK_F23);
|
||||
addStandard("F24", KeyTable.XK_F24);
|
||||
addStandard("F25", KeyTable.XK_F25);
|
||||
addStandard("F26", KeyTable.XK_F26);
|
||||
addStandard("F27", KeyTable.XK_F27);
|
||||
addStandard("F28", KeyTable.XK_F28);
|
||||
addStandard("F29", KeyTable.XK_F29);
|
||||
addStandard("F30", KeyTable.XK_F30);
|
||||
addStandard("F31", KeyTable.XK_F31);
|
||||
addStandard("F32", KeyTable.XK_F32);
|
||||
addStandard("F33", KeyTable.XK_F33);
|
||||
addStandard("F34", KeyTable.XK_F34);
|
||||
addStandard("F35", KeyTable.XK_F35);
|
||||
// - Soft1...
|
||||
|
||||
// 3.10. Multimedia Keys
|
||||
|
||||
// - ChannelDown
|
||||
// - ChannelUp
|
||||
addStandard("Close", KeyTable.XF86XK_Close);
|
||||
addStandard("MailForward", KeyTable.XF86XK_MailForward);
|
||||
addStandard("MailReply", KeyTable.XF86XK_Reply);
|
||||
addStandard("MailSend", KeyTable.XF86XK_Send);
|
||||
// - MediaClose
|
||||
addStandard("MediaFastForward", KeyTable.XF86XK_AudioForward);
|
||||
addStandard("MediaPause", KeyTable.XF86XK_AudioPause);
|
||||
addStandard("MediaPlay", KeyTable.XF86XK_AudioPlay);
|
||||
// - MediaPlayPause
|
||||
addStandard("MediaRecord", KeyTable.XF86XK_AudioRecord);
|
||||
addStandard("MediaRewind", KeyTable.XF86XK_AudioRewind);
|
||||
addStandard("MediaStop", KeyTable.XF86XK_AudioStop);
|
||||
addStandard("MediaTrackNext", KeyTable.XF86XK_AudioNext);
|
||||
addStandard("MediaTrackPrevious", KeyTable.XF86XK_AudioPrev);
|
||||
addStandard("New", KeyTable.XF86XK_New);
|
||||
addStandard("Open", KeyTable.XF86XK_Open);
|
||||
addStandard("Print", KeyTable.XK_Print);
|
||||
addStandard("Save", KeyTable.XF86XK_Save);
|
||||
addStandard("SpellCheck", KeyTable.XF86XK_Spell);
|
||||
|
||||
// 3.11. Multimedia Numpad Keys
|
||||
|
||||
// - Key11
|
||||
// - Key12
|
||||
|
||||
// 3.12. Audio Keys
|
||||
|
||||
// - AudioBalanceLeft
|
||||
// - AudioBalanceRight
|
||||
// - AudioBassBoostDown
|
||||
// - AudioBassBoostToggle
|
||||
// - AudioBassBoostUp
|
||||
// - AudioFaderFront
|
||||
// - AudioFaderRear
|
||||
// - AudioSurroundModeNext
|
||||
// - AudioTrebleDown
|
||||
// - AudioTrebleUp
|
||||
addStandard("AudioVolumeDown", KeyTable.XF86XK_AudioLowerVolume);
|
||||
addStandard("AudioVolumeUp", KeyTable.XF86XK_AudioRaiseVolume);
|
||||
addStandard("AudioVolumeMute", KeyTable.XF86XK_AudioMute);
|
||||
// - MicrophoneToggle
|
||||
// - MicrophoneVolumeDown
|
||||
// - MicrophoneVolumeUp
|
||||
addStandard("MicrophoneVolumeMute", KeyTable.XF86XK_AudioMicMute);
|
||||
|
||||
// 3.13. Speech Keys
|
||||
|
||||
// - SpeechCorrectionList
|
||||
// - SpeechInputToggle
|
||||
|
||||
// 3.14. Application Keys
|
||||
|
||||
addStandard("LaunchApplication1", KeyTable.XF86XK_MyComputer);
|
||||
addStandard("LaunchApplication2", KeyTable.XF86XK_Calculator);
|
||||
addStandard("LaunchCalendar", KeyTable.XF86XK_Calendar);
|
||||
// - LaunchContacts
|
||||
addStandard("LaunchMail", KeyTable.XF86XK_Mail);
|
||||
addStandard("LaunchMediaPlayer", KeyTable.XF86XK_AudioMedia);
|
||||
addStandard("LaunchMusicPlayer", KeyTable.XF86XK_Music);
|
||||
addStandard("LaunchPhone", KeyTable.XF86XK_Phone);
|
||||
addStandard("LaunchScreenSaver", KeyTable.XF86XK_ScreenSaver);
|
||||
addStandard("LaunchSpreadsheet", KeyTable.XF86XK_Excel);
|
||||
addStandard("LaunchWebBrowser", KeyTable.XF86XK_WWW);
|
||||
addStandard("LaunchWebCam", KeyTable.XF86XK_WebCam);
|
||||
addStandard("LaunchWordProcessor", KeyTable.XF86XK_Word);
|
||||
|
||||
// 3.15. Browser Keys
|
||||
|
||||
addStandard("BrowserBack", KeyTable.XF86XK_Back);
|
||||
addStandard("BrowserFavorites", KeyTable.XF86XK_Favorites);
|
||||
addStandard("BrowserForward", KeyTable.XF86XK_Forward);
|
||||
addStandard("BrowserHome", KeyTable.XF86XK_HomePage);
|
||||
addStandard("BrowserRefresh", KeyTable.XF86XK_Refresh);
|
||||
addStandard("BrowserSearch", KeyTable.XF86XK_Search);
|
||||
addStandard("BrowserStop", KeyTable.XF86XK_Stop);
|
||||
|
||||
// 3.16. Mobile Phone Keys
|
||||
|
||||
// - A whole bunch...
|
||||
|
||||
// 3.17. TV Keys
|
||||
|
||||
// - A whole bunch...
|
||||
|
||||
// 3.18. Media Controller Keys
|
||||
|
||||
// - A whole bunch...
|
||||
addStandard("Dimmer", KeyTable.XF86XK_BrightnessAdjust);
|
||||
addStandard("MediaAudioTrack", KeyTable.XF86XK_AudioCycleTrack);
|
||||
addStandard("RandomToggle", KeyTable.XF86XK_AudioRandomPlay);
|
||||
addStandard("SplitScreenToggle", KeyTable.XF86XK_SplitScreen);
|
||||
addStandard("Subtitle", KeyTable.XF86XK_Subtitle);
|
||||
addStandard("VideoModeNext", KeyTable.XF86XK_Next_VMode);
|
||||
|
||||
// Extra: Numpad
|
||||
|
||||
addNumpad("=", KeyTable.XK_equal, KeyTable.XK_KP_Equal);
|
||||
addNumpad("+", KeyTable.XK_plus, KeyTable.XK_KP_Add);
|
||||
addNumpad("-", KeyTable.XK_minus, KeyTable.XK_KP_Subtract);
|
||||
addNumpad("*", KeyTable.XK_asterisk, KeyTable.XK_KP_Multiply);
|
||||
addNumpad("/", KeyTable.XK_slash, KeyTable.XK_KP_Divide);
|
||||
addNumpad(".", KeyTable.XK_period, KeyTable.XK_KP_Decimal);
|
||||
addNumpad(",", KeyTable.XK_comma, KeyTable.XK_KP_Separator);
|
||||
addNumpad("0", KeyTable.XK_0, KeyTable.XK_KP_0);
|
||||
addNumpad("1", KeyTable.XK_1, KeyTable.XK_KP_1);
|
||||
addNumpad("2", KeyTable.XK_2, KeyTable.XK_KP_2);
|
||||
addNumpad("3", KeyTable.XK_3, KeyTable.XK_KP_3);
|
||||
addNumpad("4", KeyTable.XK_4, KeyTable.XK_KP_4);
|
||||
addNumpad("5", KeyTable.XK_5, KeyTable.XK_KP_5);
|
||||
addNumpad("6", KeyTable.XK_6, KeyTable.XK_KP_6);
|
||||
addNumpad("7", KeyTable.XK_7, KeyTable.XK_KP_7);
|
||||
addNumpad("8", KeyTable.XK_8, KeyTable.XK_KP_8);
|
||||
addNumpad("9", KeyTable.XK_9, KeyTable.XK_KP_9);
|
||||
|
||||
export default DOMKeyTable;
|
129
client/src/component/utils/keyboards/novnc/fixedkeys.js
Normal file
129
client/src/component/utils/keyboards/novnc/fixedkeys.js
Normal file
@ -0,0 +1,129 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2018 The noVNC Authors
|
||||
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
|
||||
*/
|
||||
|
||||
/*
|
||||
* Fallback mapping between HTML key codes (physical keys) and
|
||||
* HTML key values. This only works for keys that don't vary
|
||||
* between layouts. We also omit those who manage fine by mapping the
|
||||
* Unicode representation.
|
||||
*
|
||||
* See https://www.w3.org/TR/uievents-code/ for possible codes.
|
||||
* See https://www.w3.org/TR/uievents-key/ for possible values.
|
||||
*/
|
||||
|
||||
/* eslint-disable key-spacing */
|
||||
|
||||
export default {
|
||||
|
||||
// 3.1.1.1. Writing System Keys
|
||||
|
||||
'Backspace': 'Backspace',
|
||||
|
||||
// 3.1.1.2. Functional Keys
|
||||
|
||||
'AltLeft': 'Alt',
|
||||
'AltRight': 'Alt', // This could also be 'AltGraph'
|
||||
'CapsLock': 'CapsLock',
|
||||
'ContextMenu': 'ContextMenu',
|
||||
'ControlLeft': 'Control',
|
||||
'ControlRight': 'Control',
|
||||
'Enter': 'Enter',
|
||||
'MetaLeft': 'Meta',
|
||||
'MetaRight': 'Meta',
|
||||
'ShiftLeft': 'Shift',
|
||||
'ShiftRight': 'Shift',
|
||||
'Tab': 'Tab',
|
||||
// FIXME: Japanese/Korean keys
|
||||
|
||||
// 3.1.2. Control Pad Section
|
||||
|
||||
'Delete': 'Delete',
|
||||
'End': 'End',
|
||||
'Help': 'Help',
|
||||
'Home': 'Home',
|
||||
'Insert': 'Insert',
|
||||
'PageDown': 'PageDown',
|
||||
'PageUp': 'PageUp',
|
||||
|
||||
// 3.1.3. Arrow Pad Section
|
||||
|
||||
'ArrowDown': 'ArrowDown',
|
||||
'ArrowLeft': 'ArrowLeft',
|
||||
'ArrowRight': 'ArrowRight',
|
||||
'ArrowUp': 'ArrowUp',
|
||||
|
||||
// 3.1.4. Numpad Section
|
||||
|
||||
'NumLock': 'NumLock',
|
||||
'NumpadBackspace': 'Backspace',
|
||||
'NumpadClear': 'Clear',
|
||||
|
||||
// 3.1.5. Function Section
|
||||
|
||||
'Escape': 'Escape',
|
||||
'F1': 'F1',
|
||||
'F2': 'F2',
|
||||
'F3': 'F3',
|
||||
'F4': 'F4',
|
||||
'F5': 'F5',
|
||||
'F6': 'F6',
|
||||
'F7': 'F7',
|
||||
'F8': 'F8',
|
||||
'F9': 'F9',
|
||||
'F10': 'F10',
|
||||
'F11': 'F11',
|
||||
'F12': 'F12',
|
||||
'F13': 'F13',
|
||||
'F14': 'F14',
|
||||
'F15': 'F15',
|
||||
'F16': 'F16',
|
||||
'F17': 'F17',
|
||||
'F18': 'F18',
|
||||
'F19': 'F19',
|
||||
'F20': 'F20',
|
||||
'F21': 'F21',
|
||||
'F22': 'F22',
|
||||
'F23': 'F23',
|
||||
'F24': 'F24',
|
||||
'F25': 'F25',
|
||||
'F26': 'F26',
|
||||
'F27': 'F27',
|
||||
'F28': 'F28',
|
||||
'F29': 'F29',
|
||||
'F30': 'F30',
|
||||
'F31': 'F31',
|
||||
'F32': 'F32',
|
||||
'F33': 'F33',
|
||||
'F34': 'F34',
|
||||
'F35': 'F35',
|
||||
'PrintScreen': 'PrintScreen',
|
||||
'ScrollLock': 'ScrollLock',
|
||||
'Pause': 'Pause',
|
||||
|
||||
// 3.1.6. Media Keys
|
||||
|
||||
'BrowserBack': 'BrowserBack',
|
||||
'BrowserFavorites': 'BrowserFavorites',
|
||||
'BrowserForward': 'BrowserForward',
|
||||
'BrowserHome': 'BrowserHome',
|
||||
'BrowserRefresh': 'BrowserRefresh',
|
||||
'BrowserSearch': 'BrowserSearch',
|
||||
'BrowserStop': 'BrowserStop',
|
||||
'Eject': 'Eject',
|
||||
'LaunchApp1': 'LaunchMyComputer',
|
||||
'LaunchApp2': 'LaunchCalendar',
|
||||
'LaunchMail': 'LaunchMail',
|
||||
'MediaPlayPause': 'MediaPlay',
|
||||
'MediaStop': 'MediaStop',
|
||||
'MediaTrackNext': 'MediaTrackNext',
|
||||
'MediaTrackPrevious': 'MediaTrackPrevious',
|
||||
'Power': 'Power',
|
||||
'Sleep': 'Sleep',
|
||||
'AudioVolumeDown': 'AudioVolumeDown',
|
||||
'AudioVolumeMute': 'AudioVolumeMute',
|
||||
'AudioVolumeUp': 'AudioVolumeUp',
|
||||
'WakeUp': 'WakeUp',
|
||||
};
|
23
client/src/component/utils/keyboards/novnc/keyboard.d.ts
vendored
Normal file
23
client/src/component/utils/keyboards/novnc/keyboard.d.ts
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
declare export default class Keyboard {
|
||||
constructor (element?: Element)
|
||||
|
||||
_target: Element | Document | null
|
||||
_keyDownList: { [key: string]: number }
|
||||
_altGrArmed: boolean
|
||||
_eventHandlers: {
|
||||
keyup: (event: KeyboardEvent) => void
|
||||
keydown: (event: KeyboardEvent) => void
|
||||
blur: () => void
|
||||
}
|
||||
|
||||
_sendKeyEvent(keysym: number, code: string, down: boolean): void
|
||||
_getKeyCode(e: KeyboardEvent): string
|
||||
_handleKeyDown(e: KeyboardEvent): void
|
||||
_handleKeyUp(e: KeyboardEvent): void
|
||||
_handleAltGrTimeout(): void
|
||||
_allKeysUp(): void
|
||||
|
||||
onkeyevent: (keysym: number | null, code: string, down: boolean) => boolean
|
||||
grab: () => void
|
||||
ungrab: () => void
|
||||
}
|
293
client/src/component/utils/keyboards/novnc/keyboard.js
Normal file
293
client/src/component/utils/keyboards/novnc/keyboard.js
Normal file
@ -0,0 +1,293 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2019 The noVNC Authors
|
||||
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
|
||||
*/
|
||||
|
||||
import * as KeyboardUtil from "./util.js";
|
||||
import KeyTable from "./keysym.js";
|
||||
import * as browser from "./browser.js";
|
||||
|
||||
//
|
||||
// Keyboard event handler
|
||||
//
|
||||
|
||||
function stopEvent(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
export default class Keyboard {
|
||||
constructor(target) {
|
||||
this._target = target || null;
|
||||
|
||||
this._keyDownList = {}; // List of depressed keys
|
||||
// (even if they are happy)
|
||||
this._altGrArmed = false; // Windows AltGr detection
|
||||
|
||||
// keep these here so we can refer to them later
|
||||
this._eventHandlers = {
|
||||
'keyup': this._handleKeyUp.bind(this),
|
||||
'keydown': this._handleKeyDown.bind(this),
|
||||
'blur': this._allKeysUp.bind(this),
|
||||
};
|
||||
|
||||
// ===== EVENT HANDLERS =====
|
||||
|
||||
this.onkeyevent = () => {}; // Handler for key press/release
|
||||
}
|
||||
|
||||
// ===== PRIVATE METHODS =====
|
||||
|
||||
_sendKeyEvent(keysym, code, down) {
|
||||
if (down) {
|
||||
this._keyDownList[code] = keysym;
|
||||
} else {
|
||||
// Do we really think this key is down?
|
||||
if (!(code in this._keyDownList)) {
|
||||
return;
|
||||
}
|
||||
delete this._keyDownList[code];
|
||||
}
|
||||
|
||||
//console.debug("onkeyevent " + (down ? "down" : "up") +
|
||||
// ", keysym: " + keysym, ", code: " + code);
|
||||
//20220924: NEKO: Return a value.
|
||||
return this.onkeyevent(keysym, code, down);
|
||||
}
|
||||
|
||||
_getKeyCode(e) {
|
||||
const code = KeyboardUtil.getKeycode(e);
|
||||
if (code !== 'Unidentified') {
|
||||
return code;
|
||||
}
|
||||
|
||||
// Unstable, but we don't have anything else to go on
|
||||
if (e.keyCode) {
|
||||
// 229 is used for composition events
|
||||
if (e.keyCode !== 229) {
|
||||
return 'Platform' + e.keyCode;
|
||||
}
|
||||
}
|
||||
|
||||
// A precursor to the final DOM3 standard. Unfortunately it
|
||||
// is not layout independent, so it is as bad as using keyCode
|
||||
if (e.keyIdentifier) {
|
||||
// Non-character key?
|
||||
if (e.keyIdentifier.substr(0, 2) !== 'U+') {
|
||||
return e.keyIdentifier;
|
||||
}
|
||||
|
||||
const codepoint = parseInt(e.keyIdentifier.substr(2), 16);
|
||||
const char = String.fromCharCode(codepoint).toUpperCase();
|
||||
|
||||
return 'Platform' + char.charCodeAt();
|
||||
}
|
||||
|
||||
return 'Unidentified';
|
||||
}
|
||||
|
||||
_handleKeyDown(e) {
|
||||
const code = this._getKeyCode(e);
|
||||
let keysym = KeyboardUtil.getKeysym(e);
|
||||
|
||||
// Windows doesn't have a proper AltGr, but handles it using
|
||||
// fake Ctrl+Alt. However the remote end might not be Windows,
|
||||
// so we need to merge those in to a single AltGr event. We
|
||||
// detect this case by seeing the two key events directly after
|
||||
// each other with a very short time between them (<50ms).
|
||||
if (this._altGrArmed) {
|
||||
this._altGrArmed = false;
|
||||
clearTimeout(this._altGrTimeout);
|
||||
|
||||
if ((code === "AltRight") &&
|
||||
((e.timeStamp - this._altGrCtrlTime) < 50)) {
|
||||
// FIXME: We fail to detect this if either Ctrl key is
|
||||
// first manually pressed as Windows then no
|
||||
// longer sends the fake Ctrl down event. It
|
||||
// does however happily send real Ctrl events
|
||||
// even when AltGr is already down. Some
|
||||
// browsers detect this for us though and set the
|
||||
// key to "AltGraph".
|
||||
keysym = KeyTable.XK_ISO_Level3_Shift;
|
||||
} else {
|
||||
this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
|
||||
}
|
||||
}
|
||||
|
||||
// We cannot handle keys we cannot track, but we also need
|
||||
// to deal with virtual keyboards which omit key info
|
||||
if (code === 'Unidentified') {
|
||||
if (keysym) {
|
||||
// If it's a virtual keyboard then it should be
|
||||
// sufficient to just send press and release right
|
||||
// after each other
|
||||
this._sendKeyEvent(keysym, code, true);
|
||||
this._sendKeyEvent(keysym, code, false);
|
||||
}
|
||||
|
||||
stopEvent(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (browser.isMac() || browser.isIOS()) {
|
||||
// If a key is pressed while meta is held down, the keyup will
|
||||
// never be sent in Chrome (bug #108404) and possibly others
|
||||
if (e.metaKey && keysym !== KeyTable.XK_Super_L && keysym !== KeyTable.XK_Super_R) {
|
||||
//20220927: NEKO: Stop propagation only if wanted.
|
||||
let propagation = this._sendKeyEvent(keysym, code, true);
|
||||
this._sendKeyEvent(keysym, code, false);
|
||||
//20220927: NEKO: Stop propagation only if wanted.
|
||||
if (!propagation) stopEvent(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Alt behaves more like AltGraph on macOS, so shuffle the
|
||||
// keys around a bit to make things more sane for the remote
|
||||
// server. This method is used by RealVNC and TigerVNC (and
|
||||
// possibly others).
|
||||
switch (keysym) {
|
||||
//20220927: NEKO: Do not hijack CMD keys.
|
||||
//case KeyTable.XK_Super_L:
|
||||
// keysym = KeyTable.XK_Alt_L;
|
||||
// break;
|
||||
//case KeyTable.XK_Super_R:
|
||||
// keysym = KeyTable.XK_Super_L;
|
||||
// break;
|
||||
case KeyTable.XK_Alt_L:
|
||||
keysym = KeyTable.XK_Mode_switch;
|
||||
break;
|
||||
case KeyTable.XK_Alt_R:
|
||||
keysym = KeyTable.XK_ISO_Level3_Shift;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Is this key already pressed? If so, then we must use the
|
||||
// same keysym or we'll confuse the server
|
||||
if (code in this._keyDownList) {
|
||||
keysym = this._keyDownList[code];
|
||||
}
|
||||
|
||||
// macOS doesn't send proper key events for modifiers, only
|
||||
// state change events. That gets extra confusing for CapsLock
|
||||
// which toggles on each press, but not on release. So pretend
|
||||
// it was a quick press and release of the button.
|
||||
if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) {
|
||||
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
|
||||
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
|
||||
stopEvent(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Windows doesn't send proper key releases for a bunch of
|
||||
// Japanese IM keys so we have to fake the release right away
|
||||
const jpBadKeys = [ KeyTable.XK_Zenkaku_Hankaku,
|
||||
KeyTable.XK_Eisu_toggle,
|
||||
KeyTable.XK_Katakana,
|
||||
KeyTable.XK_Hiragana,
|
||||
KeyTable.XK_Romaji ];
|
||||
if (browser.isWindows() && jpBadKeys.includes(keysym)) {
|
||||
this._sendKeyEvent(keysym, code, true);
|
||||
this._sendKeyEvent(keysym, code, false);
|
||||
stopEvent(e);
|
||||
return;
|
||||
}
|
||||
|
||||
//20220924: NEKO: Do not stop propagation.
|
||||
//stopEvent(e);
|
||||
|
||||
// Possible start of AltGr sequence? (see above)
|
||||
if ((code === "ControlLeft") && browser.isWindows() &&
|
||||
!("ControlLeft" in this._keyDownList)) {
|
||||
this._altGrArmed = true;
|
||||
this._altGrTimeout = setTimeout(this._handleAltGrTimeout.bind(this), 100);
|
||||
this._altGrCtrlTime = e.timeStamp;
|
||||
return;
|
||||
}
|
||||
|
||||
//20220924: NEKO: Stop propagation only if wanted.
|
||||
if(!this._sendKeyEvent(keysym, code, true)) {
|
||||
stopEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
_handleKeyUp(e) {
|
||||
stopEvent(e);
|
||||
|
||||
const code = this._getKeyCode(e);
|
||||
|
||||
// We can't get a release in the middle of an AltGr sequence, so
|
||||
// abort that detection
|
||||
if (this._altGrArmed) {
|
||||
this._altGrArmed = false;
|
||||
clearTimeout(this._altGrTimeout);
|
||||
this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
|
||||
}
|
||||
|
||||
// See comment in _handleKeyDown()
|
||||
if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) {
|
||||
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
|
||||
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
|
||||
return;
|
||||
}
|
||||
|
||||
this._sendKeyEvent(this._keyDownList[code], code, false);
|
||||
|
||||
// Windows has a rather nasty bug where it won't send key
|
||||
// release events for a Shift button if the other Shift is still
|
||||
// pressed
|
||||
if (browser.isWindows() && ((code === 'ShiftLeft') ||
|
||||
(code === 'ShiftRight'))) {
|
||||
if ('ShiftRight' in this._keyDownList) {
|
||||
this._sendKeyEvent(this._keyDownList['ShiftRight'],
|
||||
'ShiftRight', false);
|
||||
}
|
||||
if ('ShiftLeft' in this._keyDownList) {
|
||||
this._sendKeyEvent(this._keyDownList['ShiftLeft'],
|
||||
'ShiftLeft', false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_handleAltGrTimeout() {
|
||||
this._altGrArmed = false;
|
||||
clearTimeout(this._altGrTimeout);
|
||||
this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
|
||||
}
|
||||
|
||||
_allKeysUp() {
|
||||
//console.debug(">> Keyboard.allKeysUp");
|
||||
for (let code in this._keyDownList) {
|
||||
this._sendKeyEvent(this._keyDownList[code], code, false);
|
||||
}
|
||||
//console.debug("<< Keyboard.allKeysUp");
|
||||
}
|
||||
|
||||
// ===== PUBLIC METHODS =====
|
||||
|
||||
grab() {
|
||||
//console.debug(">> Keyboard.grab");
|
||||
|
||||
this._target.addEventListener('keydown', this._eventHandlers.keydown);
|
||||
this._target.addEventListener('keyup', this._eventHandlers.keyup);
|
||||
|
||||
// Release (key up) if window loses focus
|
||||
window.addEventListener('blur', this._eventHandlers.blur);
|
||||
|
||||
//console.debug("<< Keyboard.grab");
|
||||
}
|
||||
|
||||
ungrab() {
|
||||
//console.debug(">> Keyboard.ungrab");
|
||||
|
||||
this._target.removeEventListener('keydown', this._eventHandlers.keydown);
|
||||
this._target.removeEventListener('keyup', this._eventHandlers.keyup);
|
||||
window.removeEventListener('blur', this._eventHandlers.blur);
|
||||
|
||||
// Release (key up) all keys that are in a down state
|
||||
this._allKeysUp();
|
||||
|
||||
//console.debug(">> Keyboard.ungrab");
|
||||
}
|
||||
}
|
616
client/src/component/utils/keyboards/novnc/keysym.js
Normal file
616
client/src/component/utils/keyboards/novnc/keysym.js
Normal file
@ -0,0 +1,616 @@
|
||||
/* eslint-disable key-spacing */
|
||||
|
||||
export default {
|
||||
XK_VoidSymbol: 0xffffff, /* Void symbol */
|
||||
|
||||
XK_BackSpace: 0xff08, /* Back space, back char */
|
||||
XK_Tab: 0xff09,
|
||||
XK_Linefeed: 0xff0a, /* Linefeed, LF */
|
||||
XK_Clear: 0xff0b,
|
||||
XK_Return: 0xff0d, /* Return, enter */
|
||||
XK_Pause: 0xff13, /* Pause, hold */
|
||||
XK_Scroll_Lock: 0xff14,
|
||||
XK_Sys_Req: 0xff15,
|
||||
XK_Escape: 0xff1b,
|
||||
XK_Delete: 0xffff, /* Delete, rubout */
|
||||
|
||||
/* International & multi-key character composition */
|
||||
|
||||
XK_Multi_key: 0xff20, /* Multi-key character compose */
|
||||
XK_Codeinput: 0xff37,
|
||||
XK_SingleCandidate: 0xff3c,
|
||||
XK_MultipleCandidate: 0xff3d,
|
||||
XK_PreviousCandidate: 0xff3e,
|
||||
|
||||
/* Japanese keyboard support */
|
||||
|
||||
XK_Kanji: 0xff21, /* Kanji, Kanji convert */
|
||||
XK_Muhenkan: 0xff22, /* Cancel Conversion */
|
||||
XK_Henkan_Mode: 0xff23, /* Start/Stop Conversion */
|
||||
XK_Henkan: 0xff23, /* Alias for Henkan_Mode */
|
||||
XK_Romaji: 0xff24, /* to Romaji */
|
||||
XK_Hiragana: 0xff25, /* to Hiragana */
|
||||
XK_Katakana: 0xff26, /* to Katakana */
|
||||
XK_Hiragana_Katakana: 0xff27, /* Hiragana/Katakana toggle */
|
||||
XK_Zenkaku: 0xff28, /* to Zenkaku */
|
||||
XK_Hankaku: 0xff29, /* to Hankaku */
|
||||
XK_Zenkaku_Hankaku: 0xff2a, /* Zenkaku/Hankaku toggle */
|
||||
XK_Touroku: 0xff2b, /* Add to Dictionary */
|
||||
XK_Massyo: 0xff2c, /* Delete from Dictionary */
|
||||
XK_Kana_Lock: 0xff2d, /* Kana Lock */
|
||||
XK_Kana_Shift: 0xff2e, /* Kana Shift */
|
||||
XK_Eisu_Shift: 0xff2f, /* Alphanumeric Shift */
|
||||
XK_Eisu_toggle: 0xff30, /* Alphanumeric toggle */
|
||||
XK_Kanji_Bangou: 0xff37, /* Codeinput */
|
||||
XK_Zen_Koho: 0xff3d, /* Multiple/All Candidate(s) */
|
||||
XK_Mae_Koho: 0xff3e, /* Previous Candidate */
|
||||
|
||||
/* Cursor control & motion */
|
||||
|
||||
XK_Home: 0xff50,
|
||||
XK_Left: 0xff51, /* Move left, left arrow */
|
||||
XK_Up: 0xff52, /* Move up, up arrow */
|
||||
XK_Right: 0xff53, /* Move right, right arrow */
|
||||
XK_Down: 0xff54, /* Move down, down arrow */
|
||||
XK_Prior: 0xff55, /* Prior, previous */
|
||||
XK_Page_Up: 0xff55,
|
||||
XK_Next: 0xff56, /* Next */
|
||||
XK_Page_Down: 0xff56,
|
||||
XK_End: 0xff57, /* EOL */
|
||||
XK_Begin: 0xff58, /* BOL */
|
||||
|
||||
|
||||
/* Misc functions */
|
||||
|
||||
XK_Select: 0xff60, /* Select, mark */
|
||||
XK_Print: 0xff61,
|
||||
XK_Execute: 0xff62, /* Execute, run, do */
|
||||
XK_Insert: 0xff63, /* Insert, insert here */
|
||||
XK_Undo: 0xff65,
|
||||
XK_Redo: 0xff66, /* Redo, again */
|
||||
XK_Menu: 0xff67,
|
||||
XK_Find: 0xff68, /* Find, search */
|
||||
XK_Cancel: 0xff69, /* Cancel, stop, abort, exit */
|
||||
XK_Help: 0xff6a, /* Help */
|
||||
XK_Break: 0xff6b,
|
||||
XK_Mode_switch: 0xff7e, /* Character set switch */
|
||||
XK_script_switch: 0xff7e, /* Alias for mode_switch */
|
||||
XK_Num_Lock: 0xff7f,
|
||||
|
||||
/* Keypad functions, keypad numbers cleverly chosen to map to ASCII */
|
||||
|
||||
XK_KP_Space: 0xff80, /* Space */
|
||||
XK_KP_Tab: 0xff89,
|
||||
XK_KP_Enter: 0xff8d, /* Enter */
|
||||
XK_KP_F1: 0xff91, /* PF1, KP_A, ... */
|
||||
XK_KP_F2: 0xff92,
|
||||
XK_KP_F3: 0xff93,
|
||||
XK_KP_F4: 0xff94,
|
||||
XK_KP_Home: 0xff95,
|
||||
XK_KP_Left: 0xff96,
|
||||
XK_KP_Up: 0xff97,
|
||||
XK_KP_Right: 0xff98,
|
||||
XK_KP_Down: 0xff99,
|
||||
XK_KP_Prior: 0xff9a,
|
||||
XK_KP_Page_Up: 0xff9a,
|
||||
XK_KP_Next: 0xff9b,
|
||||
XK_KP_Page_Down: 0xff9b,
|
||||
XK_KP_End: 0xff9c,
|
||||
XK_KP_Begin: 0xff9d,
|
||||
XK_KP_Insert: 0xff9e,
|
||||
XK_KP_Delete: 0xff9f,
|
||||
XK_KP_Equal: 0xffbd, /* Equals */
|
||||
XK_KP_Multiply: 0xffaa,
|
||||
XK_KP_Add: 0xffab,
|
||||
XK_KP_Separator: 0xffac, /* Separator, often comma */
|
||||
XK_KP_Subtract: 0xffad,
|
||||
XK_KP_Decimal: 0xffae,
|
||||
XK_KP_Divide: 0xffaf,
|
||||
|
||||
XK_KP_0: 0xffb0,
|
||||
XK_KP_1: 0xffb1,
|
||||
XK_KP_2: 0xffb2,
|
||||
XK_KP_3: 0xffb3,
|
||||
XK_KP_4: 0xffb4,
|
||||
XK_KP_5: 0xffb5,
|
||||
XK_KP_6: 0xffb6,
|
||||
XK_KP_7: 0xffb7,
|
||||
XK_KP_8: 0xffb8,
|
||||
XK_KP_9: 0xffb9,
|
||||
|
||||
/*
|
||||
* Auxiliary functions; note the duplicate definitions for left and right
|
||||
* function keys; Sun keyboards and a few other manufacturers have such
|
||||
* function key groups on the left and/or right sides of the keyboard.
|
||||
* We've not found a keyboard with more than 35 function keys total.
|
||||
*/
|
||||
|
||||
XK_F1: 0xffbe,
|
||||
XK_F2: 0xffbf,
|
||||
XK_F3: 0xffc0,
|
||||
XK_F4: 0xffc1,
|
||||
XK_F5: 0xffc2,
|
||||
XK_F6: 0xffc3,
|
||||
XK_F7: 0xffc4,
|
||||
XK_F8: 0xffc5,
|
||||
XK_F9: 0xffc6,
|
||||
XK_F10: 0xffc7,
|
||||
XK_F11: 0xffc8,
|
||||
XK_L1: 0xffc8,
|
||||
XK_F12: 0xffc9,
|
||||
XK_L2: 0xffc9,
|
||||
XK_F13: 0xffca,
|
||||
XK_L3: 0xffca,
|
||||
XK_F14: 0xffcb,
|
||||
XK_L4: 0xffcb,
|
||||
XK_F15: 0xffcc,
|
||||
XK_L5: 0xffcc,
|
||||
XK_F16: 0xffcd,
|
||||
XK_L6: 0xffcd,
|
||||
XK_F17: 0xffce,
|
||||
XK_L7: 0xffce,
|
||||
XK_F18: 0xffcf,
|
||||
XK_L8: 0xffcf,
|
||||
XK_F19: 0xffd0,
|
||||
XK_L9: 0xffd0,
|
||||
XK_F20: 0xffd1,
|
||||
XK_L10: 0xffd1,
|
||||
XK_F21: 0xffd2,
|
||||
XK_R1: 0xffd2,
|
||||
XK_F22: 0xffd3,
|
||||
XK_R2: 0xffd3,
|
||||
XK_F23: 0xffd4,
|
||||
XK_R3: 0xffd4,
|
||||
XK_F24: 0xffd5,
|
||||
XK_R4: 0xffd5,
|
||||
XK_F25: 0xffd6,
|
||||
XK_R5: 0xffd6,
|
||||
XK_F26: 0xffd7,
|
||||
XK_R6: 0xffd7,
|
||||
XK_F27: 0xffd8,
|
||||
XK_R7: 0xffd8,
|
||||
XK_F28: 0xffd9,
|
||||
XK_R8: 0xffd9,
|
||||
XK_F29: 0xffda,
|
||||
XK_R9: 0xffda,
|
||||
XK_F30: 0xffdb,
|
||||
XK_R10: 0xffdb,
|
||||
XK_F31: 0xffdc,
|
||||
XK_R11: 0xffdc,
|
||||
XK_F32: 0xffdd,
|
||||
XK_R12: 0xffdd,
|
||||
XK_F33: 0xffde,
|
||||
XK_R13: 0xffde,
|
||||
XK_F34: 0xffdf,
|
||||
XK_R14: 0xffdf,
|
||||
XK_F35: 0xffe0,
|
||||
XK_R15: 0xffe0,
|
||||
|
||||
/* Modifiers */
|
||||
|
||||
XK_Shift_L: 0xffe1, /* Left shift */
|
||||
XK_Shift_R: 0xffe2, /* Right shift */
|
||||
XK_Control_L: 0xffe3, /* Left control */
|
||||
XK_Control_R: 0xffe4, /* Right control */
|
||||
XK_Caps_Lock: 0xffe5, /* Caps lock */
|
||||
XK_Shift_Lock: 0xffe6, /* Shift lock */
|
||||
|
||||
XK_Meta_L: 0xffe7, /* Left meta */
|
||||
XK_Meta_R: 0xffe8, /* Right meta */
|
||||
XK_Alt_L: 0xffe9, /* Left alt */
|
||||
XK_Alt_R: 0xffea, /* Right alt */
|
||||
XK_Super_L: 0xffeb, /* Left super */
|
||||
XK_Super_R: 0xffec, /* Right super */
|
||||
XK_Hyper_L: 0xffed, /* Left hyper */
|
||||
XK_Hyper_R: 0xffee, /* Right hyper */
|
||||
|
||||
/*
|
||||
* Keyboard (XKB) Extension function and modifier keys
|
||||
* (from Appendix C of "The X Keyboard Extension: Protocol Specification")
|
||||
* Byte 3 = 0xfe
|
||||
*/
|
||||
|
||||
XK_ISO_Level3_Shift: 0xfe03, /* AltGr */
|
||||
XK_ISO_Next_Group: 0xfe08,
|
||||
XK_ISO_Prev_Group: 0xfe0a,
|
||||
XK_ISO_First_Group: 0xfe0c,
|
||||
XK_ISO_Last_Group: 0xfe0e,
|
||||
|
||||
/*
|
||||
* Latin 1
|
||||
* (ISO/IEC 8859-1: Unicode U+0020..U+00FF)
|
||||
* Byte 3: 0
|
||||
*/
|
||||
|
||||
XK_space: 0x0020, /* U+0020 SPACE */
|
||||
XK_exclam: 0x0021, /* U+0021 EXCLAMATION MARK */
|
||||
XK_quotedbl: 0x0022, /* U+0022 QUOTATION MARK */
|
||||
XK_numbersign: 0x0023, /* U+0023 NUMBER SIGN */
|
||||
XK_dollar: 0x0024, /* U+0024 DOLLAR SIGN */
|
||||
XK_percent: 0x0025, /* U+0025 PERCENT SIGN */
|
||||
XK_ampersand: 0x0026, /* U+0026 AMPERSAND */
|
||||
XK_apostrophe: 0x0027, /* U+0027 APOSTROPHE */
|
||||
XK_quoteright: 0x0027, /* deprecated */
|
||||
XK_parenleft: 0x0028, /* U+0028 LEFT PARENTHESIS */
|
||||
XK_parenright: 0x0029, /* U+0029 RIGHT PARENTHESIS */
|
||||
XK_asterisk: 0x002a, /* U+002A ASTERISK */
|
||||
XK_plus: 0x002b, /* U+002B PLUS SIGN */
|
||||
XK_comma: 0x002c, /* U+002C COMMA */
|
||||
XK_minus: 0x002d, /* U+002D HYPHEN-MINUS */
|
||||
XK_period: 0x002e, /* U+002E FULL STOP */
|
||||
XK_slash: 0x002f, /* U+002F SOLIDUS */
|
||||
XK_0: 0x0030, /* U+0030 DIGIT ZERO */
|
||||
XK_1: 0x0031, /* U+0031 DIGIT ONE */
|
||||
XK_2: 0x0032, /* U+0032 DIGIT TWO */
|
||||
XK_3: 0x0033, /* U+0033 DIGIT THREE */
|
||||
XK_4: 0x0034, /* U+0034 DIGIT FOUR */
|
||||
XK_5: 0x0035, /* U+0035 DIGIT FIVE */
|
||||
XK_6: 0x0036, /* U+0036 DIGIT SIX */
|
||||
XK_7: 0x0037, /* U+0037 DIGIT SEVEN */
|
||||
XK_8: 0x0038, /* U+0038 DIGIT EIGHT */
|
||||
XK_9: 0x0039, /* U+0039 DIGIT NINE */
|
||||
XK_colon: 0x003a, /* U+003A COLON */
|
||||
XK_semicolon: 0x003b, /* U+003B SEMICOLON */
|
||||
XK_less: 0x003c, /* U+003C LESS-THAN SIGN */
|
||||
XK_equal: 0x003d, /* U+003D EQUALS SIGN */
|
||||
XK_greater: 0x003e, /* U+003E GREATER-THAN SIGN */
|
||||
XK_question: 0x003f, /* U+003F QUESTION MARK */
|
||||
XK_at: 0x0040, /* U+0040 COMMERCIAL AT */
|
||||
XK_A: 0x0041, /* U+0041 LATIN CAPITAL LETTER A */
|
||||
XK_B: 0x0042, /* U+0042 LATIN CAPITAL LETTER B */
|
||||
XK_C: 0x0043, /* U+0043 LATIN CAPITAL LETTER C */
|
||||
XK_D: 0x0044, /* U+0044 LATIN CAPITAL LETTER D */
|
||||
XK_E: 0x0045, /* U+0045 LATIN CAPITAL LETTER E */
|
||||
XK_F: 0x0046, /* U+0046 LATIN CAPITAL LETTER F */
|
||||
XK_G: 0x0047, /* U+0047 LATIN CAPITAL LETTER G */
|
||||
XK_H: 0x0048, /* U+0048 LATIN CAPITAL LETTER H */
|
||||
XK_I: 0x0049, /* U+0049 LATIN CAPITAL LETTER I */
|
||||
XK_J: 0x004a, /* U+004A LATIN CAPITAL LETTER J */
|
||||
XK_K: 0x004b, /* U+004B LATIN CAPITAL LETTER K */
|
||||
XK_L: 0x004c, /* U+004C LATIN CAPITAL LETTER L */
|
||||
XK_M: 0x004d, /* U+004D LATIN CAPITAL LETTER M */
|
||||
XK_N: 0x004e, /* U+004E LATIN CAPITAL LETTER N */
|
||||
XK_O: 0x004f, /* U+004F LATIN CAPITAL LETTER O */
|
||||
XK_P: 0x0050, /* U+0050 LATIN CAPITAL LETTER P */
|
||||
XK_Q: 0x0051, /* U+0051 LATIN CAPITAL LETTER Q */
|
||||
XK_R: 0x0052, /* U+0052 LATIN CAPITAL LETTER R */
|
||||
XK_S: 0x0053, /* U+0053 LATIN CAPITAL LETTER S */
|
||||
XK_T: 0x0054, /* U+0054 LATIN CAPITAL LETTER T */
|
||||
XK_U: 0x0055, /* U+0055 LATIN CAPITAL LETTER U */
|
||||
XK_V: 0x0056, /* U+0056 LATIN CAPITAL LETTER V */
|
||||
XK_W: 0x0057, /* U+0057 LATIN CAPITAL LETTER W */
|
||||
XK_X: 0x0058, /* U+0058 LATIN CAPITAL LETTER X */
|
||||
XK_Y: 0x0059, /* U+0059 LATIN CAPITAL LETTER Y */
|
||||
XK_Z: 0x005a, /* U+005A LATIN CAPITAL LETTER Z */
|
||||
XK_bracketleft: 0x005b, /* U+005B LEFT SQUARE BRACKET */
|
||||
XK_backslash: 0x005c, /* U+005C REVERSE SOLIDUS */
|
||||
XK_bracketright: 0x005d, /* U+005D RIGHT SQUARE BRACKET */
|
||||
XK_asciicircum: 0x005e, /* U+005E CIRCUMFLEX ACCENT */
|
||||
XK_underscore: 0x005f, /* U+005F LOW LINE */
|
||||
XK_grave: 0x0060, /* U+0060 GRAVE ACCENT */
|
||||
XK_quoteleft: 0x0060, /* deprecated */
|
||||
XK_a: 0x0061, /* U+0061 LATIN SMALL LETTER A */
|
||||
XK_b: 0x0062, /* U+0062 LATIN SMALL LETTER B */
|
||||
XK_c: 0x0063, /* U+0063 LATIN SMALL LETTER C */
|
||||
XK_d: 0x0064, /* U+0064 LATIN SMALL LETTER D */
|
||||
XK_e: 0x0065, /* U+0065 LATIN SMALL LETTER E */
|
||||
XK_f: 0x0066, /* U+0066 LATIN SMALL LETTER F */
|
||||
XK_g: 0x0067, /* U+0067 LATIN SMALL LETTER G */
|
||||
XK_h: 0x0068, /* U+0068 LATIN SMALL LETTER H */
|
||||
XK_i: 0x0069, /* U+0069 LATIN SMALL LETTER I */
|
||||
XK_j: 0x006a, /* U+006A LATIN SMALL LETTER J */
|
||||
XK_k: 0x006b, /* U+006B LATIN SMALL LETTER K */
|
||||
XK_l: 0x006c, /* U+006C LATIN SMALL LETTER L */
|
||||
XK_m: 0x006d, /* U+006D LATIN SMALL LETTER M */
|
||||
XK_n: 0x006e, /* U+006E LATIN SMALL LETTER N */
|
||||
XK_o: 0x006f, /* U+006F LATIN SMALL LETTER O */
|
||||
XK_p: 0x0070, /* U+0070 LATIN SMALL LETTER P */
|
||||
XK_q: 0x0071, /* U+0071 LATIN SMALL LETTER Q */
|
||||
XK_r: 0x0072, /* U+0072 LATIN SMALL LETTER R */
|
||||
XK_s: 0x0073, /* U+0073 LATIN SMALL LETTER S */
|
||||
XK_t: 0x0074, /* U+0074 LATIN SMALL LETTER T */
|
||||
XK_u: 0x0075, /* U+0075 LATIN SMALL LETTER U */
|
||||
XK_v: 0x0076, /* U+0076 LATIN SMALL LETTER V */
|
||||
XK_w: 0x0077, /* U+0077 LATIN SMALL LETTER W */
|
||||
XK_x: 0x0078, /* U+0078 LATIN SMALL LETTER X */
|
||||
XK_y: 0x0079, /* U+0079 LATIN SMALL LETTER Y */
|
||||
XK_z: 0x007a, /* U+007A LATIN SMALL LETTER Z */
|
||||
XK_braceleft: 0x007b, /* U+007B LEFT CURLY BRACKET */
|
||||
XK_bar: 0x007c, /* U+007C VERTICAL LINE */
|
||||
XK_braceright: 0x007d, /* U+007D RIGHT CURLY BRACKET */
|
||||
XK_asciitilde: 0x007e, /* U+007E TILDE */
|
||||
|
||||
XK_nobreakspace: 0x00a0, /* U+00A0 NO-BREAK SPACE */
|
||||
XK_exclamdown: 0x00a1, /* U+00A1 INVERTED EXCLAMATION MARK */
|
||||
XK_cent: 0x00a2, /* U+00A2 CENT SIGN */
|
||||
XK_sterling: 0x00a3, /* U+00A3 POUND SIGN */
|
||||
XK_currency: 0x00a4, /* U+00A4 CURRENCY SIGN */
|
||||
XK_yen: 0x00a5, /* U+00A5 YEN SIGN */
|
||||
XK_brokenbar: 0x00a6, /* U+00A6 BROKEN BAR */
|
||||
XK_section: 0x00a7, /* U+00A7 SECTION SIGN */
|
||||
XK_diaeresis: 0x00a8, /* U+00A8 DIAERESIS */
|
||||
XK_copyright: 0x00a9, /* U+00A9 COPYRIGHT SIGN */
|
||||
XK_ordfeminine: 0x00aa, /* U+00AA FEMININE ORDINAL INDICATOR */
|
||||
XK_guillemotleft: 0x00ab, /* U+00AB LEFT-POINTING DOUBLE ANGLE QUOTATION MARK */
|
||||
XK_notsign: 0x00ac, /* U+00AC NOT SIGN */
|
||||
XK_hyphen: 0x00ad, /* U+00AD SOFT HYPHEN */
|
||||
XK_registered: 0x00ae, /* U+00AE REGISTERED SIGN */
|
||||
XK_macron: 0x00af, /* U+00AF MACRON */
|
||||
XK_degree: 0x00b0, /* U+00B0 DEGREE SIGN */
|
||||
XK_plusminus: 0x00b1, /* U+00B1 PLUS-MINUS SIGN */
|
||||
XK_twosuperior: 0x00b2, /* U+00B2 SUPERSCRIPT TWO */
|
||||
XK_threesuperior: 0x00b3, /* U+00B3 SUPERSCRIPT THREE */
|
||||
XK_acute: 0x00b4, /* U+00B4 ACUTE ACCENT */
|
||||
XK_mu: 0x00b5, /* U+00B5 MICRO SIGN */
|
||||
XK_paragraph: 0x00b6, /* U+00B6 PILCROW SIGN */
|
||||
XK_periodcentered: 0x00b7, /* U+00B7 MIDDLE DOT */
|
||||
XK_cedilla: 0x00b8, /* U+00B8 CEDILLA */
|
||||
XK_onesuperior: 0x00b9, /* U+00B9 SUPERSCRIPT ONE */
|
||||
XK_masculine: 0x00ba, /* U+00BA MASCULINE ORDINAL INDICATOR */
|
||||
XK_guillemotright: 0x00bb, /* U+00BB RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK */
|
||||
XK_onequarter: 0x00bc, /* U+00BC VULGAR FRACTION ONE QUARTER */
|
||||
XK_onehalf: 0x00bd, /* U+00BD VULGAR FRACTION ONE HALF */
|
||||
XK_threequarters: 0x00be, /* U+00BE VULGAR FRACTION THREE QUARTERS */
|
||||
XK_questiondown: 0x00bf, /* U+00BF INVERTED QUESTION MARK */
|
||||
XK_Agrave: 0x00c0, /* U+00C0 LATIN CAPITAL LETTER A WITH GRAVE */
|
||||
XK_Aacute: 0x00c1, /* U+00C1 LATIN CAPITAL LETTER A WITH ACUTE */
|
||||
XK_Acircumflex: 0x00c2, /* U+00C2 LATIN CAPITAL LETTER A WITH CIRCUMFLEX */
|
||||
XK_Atilde: 0x00c3, /* U+00C3 LATIN CAPITAL LETTER A WITH TILDE */
|
||||
XK_Adiaeresis: 0x00c4, /* U+00C4 LATIN CAPITAL LETTER A WITH DIAERESIS */
|
||||
XK_Aring: 0x00c5, /* U+00C5 LATIN CAPITAL LETTER A WITH RING ABOVE */
|
||||
XK_AE: 0x00c6, /* U+00C6 LATIN CAPITAL LETTER AE */
|
||||
XK_Ccedilla: 0x00c7, /* U+00C7 LATIN CAPITAL LETTER C WITH CEDILLA */
|
||||
XK_Egrave: 0x00c8, /* U+00C8 LATIN CAPITAL LETTER E WITH GRAVE */
|
||||
XK_Eacute: 0x00c9, /* U+00C9 LATIN CAPITAL LETTER E WITH ACUTE */
|
||||
XK_Ecircumflex: 0x00ca, /* U+00CA LATIN CAPITAL LETTER E WITH CIRCUMFLEX */
|
||||
XK_Ediaeresis: 0x00cb, /* U+00CB LATIN CAPITAL LETTER E WITH DIAERESIS */
|
||||
XK_Igrave: 0x00cc, /* U+00CC LATIN CAPITAL LETTER I WITH GRAVE */
|
||||
XK_Iacute: 0x00cd, /* U+00CD LATIN CAPITAL LETTER I WITH ACUTE */
|
||||
XK_Icircumflex: 0x00ce, /* U+00CE LATIN CAPITAL LETTER I WITH CIRCUMFLEX */
|
||||
XK_Idiaeresis: 0x00cf, /* U+00CF LATIN CAPITAL LETTER I WITH DIAERESIS */
|
||||
XK_ETH: 0x00d0, /* U+00D0 LATIN CAPITAL LETTER ETH */
|
||||
XK_Eth: 0x00d0, /* deprecated */
|
||||
XK_Ntilde: 0x00d1, /* U+00D1 LATIN CAPITAL LETTER N WITH TILDE */
|
||||
XK_Ograve: 0x00d2, /* U+00D2 LATIN CAPITAL LETTER O WITH GRAVE */
|
||||
XK_Oacute: 0x00d3, /* U+00D3 LATIN CAPITAL LETTER O WITH ACUTE */
|
||||
XK_Ocircumflex: 0x00d4, /* U+00D4 LATIN CAPITAL LETTER O WITH CIRCUMFLEX */
|
||||
XK_Otilde: 0x00d5, /* U+00D5 LATIN CAPITAL LETTER O WITH TILDE */
|
||||
XK_Odiaeresis: 0x00d6, /* U+00D6 LATIN CAPITAL LETTER O WITH DIAERESIS */
|
||||
XK_multiply: 0x00d7, /* U+00D7 MULTIPLICATION SIGN */
|
||||
XK_Oslash: 0x00d8, /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */
|
||||
XK_Ooblique: 0x00d8, /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */
|
||||
XK_Ugrave: 0x00d9, /* U+00D9 LATIN CAPITAL LETTER U WITH GRAVE */
|
||||
XK_Uacute: 0x00da, /* U+00DA LATIN CAPITAL LETTER U WITH ACUTE */
|
||||
XK_Ucircumflex: 0x00db, /* U+00DB LATIN CAPITAL LETTER U WITH CIRCUMFLEX */
|
||||
XK_Udiaeresis: 0x00dc, /* U+00DC LATIN CAPITAL LETTER U WITH DIAERESIS */
|
||||
XK_Yacute: 0x00dd, /* U+00DD LATIN CAPITAL LETTER Y WITH ACUTE */
|
||||
XK_THORN: 0x00de, /* U+00DE LATIN CAPITAL LETTER THORN */
|
||||
XK_Thorn: 0x00de, /* deprecated */
|
||||
XK_ssharp: 0x00df, /* U+00DF LATIN SMALL LETTER SHARP S */
|
||||
XK_agrave: 0x00e0, /* U+00E0 LATIN SMALL LETTER A WITH GRAVE */
|
||||
XK_aacute: 0x00e1, /* U+00E1 LATIN SMALL LETTER A WITH ACUTE */
|
||||
XK_acircumflex: 0x00e2, /* U+00E2 LATIN SMALL LETTER A WITH CIRCUMFLEX */
|
||||
XK_atilde: 0x00e3, /* U+00E3 LATIN SMALL LETTER A WITH TILDE */
|
||||
XK_adiaeresis: 0x00e4, /* U+00E4 LATIN SMALL LETTER A WITH DIAERESIS */
|
||||
XK_aring: 0x00e5, /* U+00E5 LATIN SMALL LETTER A WITH RING ABOVE */
|
||||
XK_ae: 0x00e6, /* U+00E6 LATIN SMALL LETTER AE */
|
||||
XK_ccedilla: 0x00e7, /* U+00E7 LATIN SMALL LETTER C WITH CEDILLA */
|
||||
XK_egrave: 0x00e8, /* U+00E8 LATIN SMALL LETTER E WITH GRAVE */
|
||||
XK_eacute: 0x00e9, /* U+00E9 LATIN SMALL LETTER E WITH ACUTE */
|
||||
XK_ecircumflex: 0x00ea, /* U+00EA LATIN SMALL LETTER E WITH CIRCUMFLEX */
|
||||
XK_ediaeresis: 0x00eb, /* U+00EB LATIN SMALL LETTER E WITH DIAERESIS */
|
||||
XK_igrave: 0x00ec, /* U+00EC LATIN SMALL LETTER I WITH GRAVE */
|
||||
XK_iacute: 0x00ed, /* U+00ED LATIN SMALL LETTER I WITH ACUTE */
|
||||
XK_icircumflex: 0x00ee, /* U+00EE LATIN SMALL LETTER I WITH CIRCUMFLEX */
|
||||
XK_idiaeresis: 0x00ef, /* U+00EF LATIN SMALL LETTER I WITH DIAERESIS */
|
||||
XK_eth: 0x00f0, /* U+00F0 LATIN SMALL LETTER ETH */
|
||||
XK_ntilde: 0x00f1, /* U+00F1 LATIN SMALL LETTER N WITH TILDE */
|
||||
XK_ograve: 0x00f2, /* U+00F2 LATIN SMALL LETTER O WITH GRAVE */
|
||||
XK_oacute: 0x00f3, /* U+00F3 LATIN SMALL LETTER O WITH ACUTE */
|
||||
XK_ocircumflex: 0x00f4, /* U+00F4 LATIN SMALL LETTER O WITH CIRCUMFLEX */
|
||||
XK_otilde: 0x00f5, /* U+00F5 LATIN SMALL LETTER O WITH TILDE */
|
||||
XK_odiaeresis: 0x00f6, /* U+00F6 LATIN SMALL LETTER O WITH DIAERESIS */
|
||||
XK_division: 0x00f7, /* U+00F7 DIVISION SIGN */
|
||||
XK_oslash: 0x00f8, /* U+00F8 LATIN SMALL LETTER O WITH STROKE */
|
||||
XK_ooblique: 0x00f8, /* U+00F8 LATIN SMALL LETTER O WITH STROKE */
|
||||
XK_ugrave: 0x00f9, /* U+00F9 LATIN SMALL LETTER U WITH GRAVE */
|
||||
XK_uacute: 0x00fa, /* U+00FA LATIN SMALL LETTER U WITH ACUTE */
|
||||
XK_ucircumflex: 0x00fb, /* U+00FB LATIN SMALL LETTER U WITH CIRCUMFLEX */
|
||||
XK_udiaeresis: 0x00fc, /* U+00FC LATIN SMALL LETTER U WITH DIAERESIS */
|
||||
XK_yacute: 0x00fd, /* U+00FD LATIN SMALL LETTER Y WITH ACUTE */
|
||||
XK_thorn: 0x00fe, /* U+00FE LATIN SMALL LETTER THORN */
|
||||
XK_ydiaeresis: 0x00ff, /* U+00FF LATIN SMALL LETTER Y WITH DIAERESIS */
|
||||
|
||||
/*
|
||||
* Korean
|
||||
* Byte 3 = 0x0e
|
||||
*/
|
||||
|
||||
XK_Hangul: 0xff31, /* Hangul start/stop(toggle) */
|
||||
XK_Hangul_Hanja: 0xff34, /* Start Hangul->Hanja Conversion */
|
||||
XK_Hangul_Jeonja: 0xff38, /* Jeonja mode */
|
||||
|
||||
/*
|
||||
* XFree86 vendor specific keysyms.
|
||||
*
|
||||
* The XFree86 keysym range is 0x10080001 - 0x1008FFFF.
|
||||
*/
|
||||
|
||||
XF86XK_ModeLock: 0x1008FF01,
|
||||
XF86XK_MonBrightnessUp: 0x1008FF02,
|
||||
XF86XK_MonBrightnessDown: 0x1008FF03,
|
||||
XF86XK_KbdLightOnOff: 0x1008FF04,
|
||||
XF86XK_KbdBrightnessUp: 0x1008FF05,
|
||||
XF86XK_KbdBrightnessDown: 0x1008FF06,
|
||||
XF86XK_Standby: 0x1008FF10,
|
||||
XF86XK_AudioLowerVolume: 0x1008FF11,
|
||||
XF86XK_AudioMute: 0x1008FF12,
|
||||
XF86XK_AudioRaiseVolume: 0x1008FF13,
|
||||
XF86XK_AudioPlay: 0x1008FF14,
|
||||
XF86XK_AudioStop: 0x1008FF15,
|
||||
XF86XK_AudioPrev: 0x1008FF16,
|
||||
XF86XK_AudioNext: 0x1008FF17,
|
||||
XF86XK_HomePage: 0x1008FF18,
|
||||
XF86XK_Mail: 0x1008FF19,
|
||||
XF86XK_Start: 0x1008FF1A,
|
||||
XF86XK_Search: 0x1008FF1B,
|
||||
XF86XK_AudioRecord: 0x1008FF1C,
|
||||
XF86XK_Calculator: 0x1008FF1D,
|
||||
XF86XK_Memo: 0x1008FF1E,
|
||||
XF86XK_ToDoList: 0x1008FF1F,
|
||||
XF86XK_Calendar: 0x1008FF20,
|
||||
XF86XK_PowerDown: 0x1008FF21,
|
||||
XF86XK_ContrastAdjust: 0x1008FF22,
|
||||
XF86XK_RockerUp: 0x1008FF23,
|
||||
XF86XK_RockerDown: 0x1008FF24,
|
||||
XF86XK_RockerEnter: 0x1008FF25,
|
||||
XF86XK_Back: 0x1008FF26,
|
||||
XF86XK_Forward: 0x1008FF27,
|
||||
XF86XK_Stop: 0x1008FF28,
|
||||
XF86XK_Refresh: 0x1008FF29,
|
||||
XF86XK_PowerOff: 0x1008FF2A,
|
||||
XF86XK_WakeUp: 0x1008FF2B,
|
||||
XF86XK_Eject: 0x1008FF2C,
|
||||
XF86XK_ScreenSaver: 0x1008FF2D,
|
||||
XF86XK_WWW: 0x1008FF2E,
|
||||
XF86XK_Sleep: 0x1008FF2F,
|
||||
XF86XK_Favorites: 0x1008FF30,
|
||||
XF86XK_AudioPause: 0x1008FF31,
|
||||
XF86XK_AudioMedia: 0x1008FF32,
|
||||
XF86XK_MyComputer: 0x1008FF33,
|
||||
XF86XK_VendorHome: 0x1008FF34,
|
||||
XF86XK_LightBulb: 0x1008FF35,
|
||||
XF86XK_Shop: 0x1008FF36,
|
||||
XF86XK_History: 0x1008FF37,
|
||||
XF86XK_OpenURL: 0x1008FF38,
|
||||
XF86XK_AddFavorite: 0x1008FF39,
|
||||
XF86XK_HotLinks: 0x1008FF3A,
|
||||
XF86XK_BrightnessAdjust: 0x1008FF3B,
|
||||
XF86XK_Finance: 0x1008FF3C,
|
||||
XF86XK_Community: 0x1008FF3D,
|
||||
XF86XK_AudioRewind: 0x1008FF3E,
|
||||
XF86XK_BackForward: 0x1008FF3F,
|
||||
XF86XK_Launch0: 0x1008FF40,
|
||||
XF86XK_Launch1: 0x1008FF41,
|
||||
XF86XK_Launch2: 0x1008FF42,
|
||||
XF86XK_Launch3: 0x1008FF43,
|
||||
XF86XK_Launch4: 0x1008FF44,
|
||||
XF86XK_Launch5: 0x1008FF45,
|
||||
XF86XK_Launch6: 0x1008FF46,
|
||||
XF86XK_Launch7: 0x1008FF47,
|
||||
XF86XK_Launch8: 0x1008FF48,
|
||||
XF86XK_Launch9: 0x1008FF49,
|
||||
XF86XK_LaunchA: 0x1008FF4A,
|
||||
XF86XK_LaunchB: 0x1008FF4B,
|
||||
XF86XK_LaunchC: 0x1008FF4C,
|
||||
XF86XK_LaunchD: 0x1008FF4D,
|
||||
XF86XK_LaunchE: 0x1008FF4E,
|
||||
XF86XK_LaunchF: 0x1008FF4F,
|
||||
XF86XK_ApplicationLeft: 0x1008FF50,
|
||||
XF86XK_ApplicationRight: 0x1008FF51,
|
||||
XF86XK_Book: 0x1008FF52,
|
||||
XF86XK_CD: 0x1008FF53,
|
||||
XF86XK_Calculater: 0x1008FF54,
|
||||
XF86XK_Clear: 0x1008FF55,
|
||||
XF86XK_Close: 0x1008FF56,
|
||||
XF86XK_Copy: 0x1008FF57,
|
||||
XF86XK_Cut: 0x1008FF58,
|
||||
XF86XK_Display: 0x1008FF59,
|
||||
XF86XK_DOS: 0x1008FF5A,
|
||||
XF86XK_Documents: 0x1008FF5B,
|
||||
XF86XK_Excel: 0x1008FF5C,
|
||||
XF86XK_Explorer: 0x1008FF5D,
|
||||
XF86XK_Game: 0x1008FF5E,
|
||||
XF86XK_Go: 0x1008FF5F,
|
||||
XF86XK_iTouch: 0x1008FF60,
|
||||
XF86XK_LogOff: 0x1008FF61,
|
||||
XF86XK_Market: 0x1008FF62,
|
||||
XF86XK_Meeting: 0x1008FF63,
|
||||
XF86XK_MenuKB: 0x1008FF65,
|
||||
XF86XK_MenuPB: 0x1008FF66,
|
||||
XF86XK_MySites: 0x1008FF67,
|
||||
XF86XK_New: 0x1008FF68,
|
||||
XF86XK_News: 0x1008FF69,
|
||||
XF86XK_OfficeHome: 0x1008FF6A,
|
||||
XF86XK_Open: 0x1008FF6B,
|
||||
XF86XK_Option: 0x1008FF6C,
|
||||
XF86XK_Paste: 0x1008FF6D,
|
||||
XF86XK_Phone: 0x1008FF6E,
|
||||
XF86XK_Q: 0x1008FF70,
|
||||
XF86XK_Reply: 0x1008FF72,
|
||||
XF86XK_Reload: 0x1008FF73,
|
||||
XF86XK_RotateWindows: 0x1008FF74,
|
||||
XF86XK_RotationPB: 0x1008FF75,
|
||||
XF86XK_RotationKB: 0x1008FF76,
|
||||
XF86XK_Save: 0x1008FF77,
|
||||
XF86XK_ScrollUp: 0x1008FF78,
|
||||
XF86XK_ScrollDown: 0x1008FF79,
|
||||
XF86XK_ScrollClick: 0x1008FF7A,
|
||||
XF86XK_Send: 0x1008FF7B,
|
||||
XF86XK_Spell: 0x1008FF7C,
|
||||
XF86XK_SplitScreen: 0x1008FF7D,
|
||||
XF86XK_Support: 0x1008FF7E,
|
||||
XF86XK_TaskPane: 0x1008FF7F,
|
||||
XF86XK_Terminal: 0x1008FF80,
|
||||
XF86XK_Tools: 0x1008FF81,
|
||||
XF86XK_Travel: 0x1008FF82,
|
||||
XF86XK_UserPB: 0x1008FF84,
|
||||
XF86XK_User1KB: 0x1008FF85,
|
||||
XF86XK_User2KB: 0x1008FF86,
|
||||
XF86XK_Video: 0x1008FF87,
|
||||
XF86XK_WheelButton: 0x1008FF88,
|
||||
XF86XK_Word: 0x1008FF89,
|
||||
XF86XK_Xfer: 0x1008FF8A,
|
||||
XF86XK_ZoomIn: 0x1008FF8B,
|
||||
XF86XK_ZoomOut: 0x1008FF8C,
|
||||
XF86XK_Away: 0x1008FF8D,
|
||||
XF86XK_Messenger: 0x1008FF8E,
|
||||
XF86XK_WebCam: 0x1008FF8F,
|
||||
XF86XK_MailForward: 0x1008FF90,
|
||||
XF86XK_Pictures: 0x1008FF91,
|
||||
XF86XK_Music: 0x1008FF92,
|
||||
XF86XK_Battery: 0x1008FF93,
|
||||
XF86XK_Bluetooth: 0x1008FF94,
|
||||
XF86XK_WLAN: 0x1008FF95,
|
||||
XF86XK_UWB: 0x1008FF96,
|
||||
XF86XK_AudioForward: 0x1008FF97,
|
||||
XF86XK_AudioRepeat: 0x1008FF98,
|
||||
XF86XK_AudioRandomPlay: 0x1008FF99,
|
||||
XF86XK_Subtitle: 0x1008FF9A,
|
||||
XF86XK_AudioCycleTrack: 0x1008FF9B,
|
||||
XF86XK_CycleAngle: 0x1008FF9C,
|
||||
XF86XK_FrameBack: 0x1008FF9D,
|
||||
XF86XK_FrameForward: 0x1008FF9E,
|
||||
XF86XK_Time: 0x1008FF9F,
|
||||
XF86XK_Select: 0x1008FFA0,
|
||||
XF86XK_View: 0x1008FFA1,
|
||||
XF86XK_TopMenu: 0x1008FFA2,
|
||||
XF86XK_Red: 0x1008FFA3,
|
||||
XF86XK_Green: 0x1008FFA4,
|
||||
XF86XK_Yellow: 0x1008FFA5,
|
||||
XF86XK_Blue: 0x1008FFA6,
|
||||
XF86XK_Suspend: 0x1008FFA7,
|
||||
XF86XK_Hibernate: 0x1008FFA8,
|
||||
XF86XK_TouchpadToggle: 0x1008FFA9,
|
||||
XF86XK_TouchpadOn: 0x1008FFB0,
|
||||
XF86XK_TouchpadOff: 0x1008FFB1,
|
||||
XF86XK_AudioMicMute: 0x1008FFB2,
|
||||
XF86XK_Switch_VT_1: 0x1008FE01,
|
||||
XF86XK_Switch_VT_2: 0x1008FE02,
|
||||
XF86XK_Switch_VT_3: 0x1008FE03,
|
||||
XF86XK_Switch_VT_4: 0x1008FE04,
|
||||
XF86XK_Switch_VT_5: 0x1008FE05,
|
||||
XF86XK_Switch_VT_6: 0x1008FE06,
|
||||
XF86XK_Switch_VT_7: 0x1008FE07,
|
||||
XF86XK_Switch_VT_8: 0x1008FE08,
|
||||
XF86XK_Switch_VT_9: 0x1008FE09,
|
||||
XF86XK_Switch_VT_10: 0x1008FE0A,
|
||||
XF86XK_Switch_VT_11: 0x1008FE0B,
|
||||
XF86XK_Switch_VT_12: 0x1008FE0C,
|
||||
XF86XK_Ungrab: 0x1008FE20,
|
||||
XF86XK_ClearGrab: 0x1008FE21,
|
||||
XF86XK_Next_VMode: 0x1008FE22,
|
||||
XF86XK_Prev_VMode: 0x1008FE23,
|
||||
XF86XK_LogWindowTree: 0x1008FE24,
|
||||
XF86XK_LogGrabInfo: 0x1008FE25,
|
||||
};
|
688
client/src/component/utils/keyboards/novnc/keysymdef.js
Normal file
688
client/src/component/utils/keyboards/novnc/keysymdef.js
Normal file
@ -0,0 +1,688 @@
|
||||
/*
|
||||
* Mapping from Unicode codepoints to X11/RFB keysyms
|
||||
*
|
||||
* This file was automatically generated from keysymdef.h
|
||||
* DO NOT EDIT!
|
||||
*/
|
||||
|
||||
/* Functions at the bottom */
|
||||
|
||||
const codepoints = {
|
||||
0x0100: 0x03c0, // XK_Amacron
|
||||
0x0101: 0x03e0, // XK_amacron
|
||||
0x0102: 0x01c3, // XK_Abreve
|
||||
0x0103: 0x01e3, // XK_abreve
|
||||
0x0104: 0x01a1, // XK_Aogonek
|
||||
0x0105: 0x01b1, // XK_aogonek
|
||||
0x0106: 0x01c6, // XK_Cacute
|
||||
0x0107: 0x01e6, // XK_cacute
|
||||
0x0108: 0x02c6, // XK_Ccircumflex
|
||||
0x0109: 0x02e6, // XK_ccircumflex
|
||||
0x010a: 0x02c5, // XK_Cabovedot
|
||||
0x010b: 0x02e5, // XK_cabovedot
|
||||
0x010c: 0x01c8, // XK_Ccaron
|
||||
0x010d: 0x01e8, // XK_ccaron
|
||||
0x010e: 0x01cf, // XK_Dcaron
|
||||
0x010f: 0x01ef, // XK_dcaron
|
||||
0x0110: 0x01d0, // XK_Dstroke
|
||||
0x0111: 0x01f0, // XK_dstroke
|
||||
0x0112: 0x03aa, // XK_Emacron
|
||||
0x0113: 0x03ba, // XK_emacron
|
||||
0x0116: 0x03cc, // XK_Eabovedot
|
||||
0x0117: 0x03ec, // XK_eabovedot
|
||||
0x0118: 0x01ca, // XK_Eogonek
|
||||
0x0119: 0x01ea, // XK_eogonek
|
||||
0x011a: 0x01cc, // XK_Ecaron
|
||||
0x011b: 0x01ec, // XK_ecaron
|
||||
0x011c: 0x02d8, // XK_Gcircumflex
|
||||
0x011d: 0x02f8, // XK_gcircumflex
|
||||
0x011e: 0x02ab, // XK_Gbreve
|
||||
0x011f: 0x02bb, // XK_gbreve
|
||||
0x0120: 0x02d5, // XK_Gabovedot
|
||||
0x0121: 0x02f5, // XK_gabovedot
|
||||
0x0122: 0x03ab, // XK_Gcedilla
|
||||
0x0123: 0x03bb, // XK_gcedilla
|
||||
0x0124: 0x02a6, // XK_Hcircumflex
|
||||
0x0125: 0x02b6, // XK_hcircumflex
|
||||
0x0126: 0x02a1, // XK_Hstroke
|
||||
0x0127: 0x02b1, // XK_hstroke
|
||||
0x0128: 0x03a5, // XK_Itilde
|
||||
0x0129: 0x03b5, // XK_itilde
|
||||
0x012a: 0x03cf, // XK_Imacron
|
||||
0x012b: 0x03ef, // XK_imacron
|
||||
0x012e: 0x03c7, // XK_Iogonek
|
||||
0x012f: 0x03e7, // XK_iogonek
|
||||
0x0130: 0x02a9, // XK_Iabovedot
|
||||
0x0131: 0x02b9, // XK_idotless
|
||||
0x0134: 0x02ac, // XK_Jcircumflex
|
||||
0x0135: 0x02bc, // XK_jcircumflex
|
||||
0x0136: 0x03d3, // XK_Kcedilla
|
||||
0x0137: 0x03f3, // XK_kcedilla
|
||||
0x0138: 0x03a2, // XK_kra
|
||||
0x0139: 0x01c5, // XK_Lacute
|
||||
0x013a: 0x01e5, // XK_lacute
|
||||
0x013b: 0x03a6, // XK_Lcedilla
|
||||
0x013c: 0x03b6, // XK_lcedilla
|
||||
0x013d: 0x01a5, // XK_Lcaron
|
||||
0x013e: 0x01b5, // XK_lcaron
|
||||
0x0141: 0x01a3, // XK_Lstroke
|
||||
0x0142: 0x01b3, // XK_lstroke
|
||||
0x0143: 0x01d1, // XK_Nacute
|
||||
0x0144: 0x01f1, // XK_nacute
|
||||
0x0145: 0x03d1, // XK_Ncedilla
|
||||
0x0146: 0x03f1, // XK_ncedilla
|
||||
0x0147: 0x01d2, // XK_Ncaron
|
||||
0x0148: 0x01f2, // XK_ncaron
|
||||
0x014a: 0x03bd, // XK_ENG
|
||||
0x014b: 0x03bf, // XK_eng
|
||||
0x014c: 0x03d2, // XK_Omacron
|
||||
0x014d: 0x03f2, // XK_omacron
|
||||
0x0150: 0x01d5, // XK_Odoubleacute
|
||||
0x0151: 0x01f5, // XK_odoubleacute
|
||||
0x0152: 0x13bc, // XK_OE
|
||||
0x0153: 0x13bd, // XK_oe
|
||||
0x0154: 0x01c0, // XK_Racute
|
||||
0x0155: 0x01e0, // XK_racute
|
||||
0x0156: 0x03a3, // XK_Rcedilla
|
||||
0x0157: 0x03b3, // XK_rcedilla
|
||||
0x0158: 0x01d8, // XK_Rcaron
|
||||
0x0159: 0x01f8, // XK_rcaron
|
||||
0x015a: 0x01a6, // XK_Sacute
|
||||
0x015b: 0x01b6, // XK_sacute
|
||||
0x015c: 0x02de, // XK_Scircumflex
|
||||
0x015d: 0x02fe, // XK_scircumflex
|
||||
0x015e: 0x01aa, // XK_Scedilla
|
||||
0x015f: 0x01ba, // XK_scedilla
|
||||
0x0160: 0x01a9, // XK_Scaron
|
||||
0x0161: 0x01b9, // XK_scaron
|
||||
0x0162: 0x01de, // XK_Tcedilla
|
||||
0x0163: 0x01fe, // XK_tcedilla
|
||||
0x0164: 0x01ab, // XK_Tcaron
|
||||
0x0165: 0x01bb, // XK_tcaron
|
||||
0x0166: 0x03ac, // XK_Tslash
|
||||
0x0167: 0x03bc, // XK_tslash
|
||||
0x0168: 0x03dd, // XK_Utilde
|
||||
0x0169: 0x03fd, // XK_utilde
|
||||
0x016a: 0x03de, // XK_Umacron
|
||||
0x016b: 0x03fe, // XK_umacron
|
||||
0x016c: 0x02dd, // XK_Ubreve
|
||||
0x016d: 0x02fd, // XK_ubreve
|
||||
0x016e: 0x01d9, // XK_Uring
|
||||
0x016f: 0x01f9, // XK_uring
|
||||
0x0170: 0x01db, // XK_Udoubleacute
|
||||
0x0171: 0x01fb, // XK_udoubleacute
|
||||
0x0172: 0x03d9, // XK_Uogonek
|
||||
0x0173: 0x03f9, // XK_uogonek
|
||||
0x0178: 0x13be, // XK_Ydiaeresis
|
||||
0x0179: 0x01ac, // XK_Zacute
|
||||
0x017a: 0x01bc, // XK_zacute
|
||||
0x017b: 0x01af, // XK_Zabovedot
|
||||
0x017c: 0x01bf, // XK_zabovedot
|
||||
0x017d: 0x01ae, // XK_Zcaron
|
||||
0x017e: 0x01be, // XK_zcaron
|
||||
0x0192: 0x08f6, // XK_function
|
||||
0x01d2: 0x10001d1, // XK_Ocaron
|
||||
0x02c7: 0x01b7, // XK_caron
|
||||
0x02d8: 0x01a2, // XK_breve
|
||||
0x02d9: 0x01ff, // XK_abovedot
|
||||
0x02db: 0x01b2, // XK_ogonek
|
||||
0x02dd: 0x01bd, // XK_doubleacute
|
||||
0x0385: 0x07ae, // XK_Greek_accentdieresis
|
||||
0x0386: 0x07a1, // XK_Greek_ALPHAaccent
|
||||
0x0388: 0x07a2, // XK_Greek_EPSILONaccent
|
||||
0x0389: 0x07a3, // XK_Greek_ETAaccent
|
||||
0x038a: 0x07a4, // XK_Greek_IOTAaccent
|
||||
0x038c: 0x07a7, // XK_Greek_OMICRONaccent
|
||||
0x038e: 0x07a8, // XK_Greek_UPSILONaccent
|
||||
0x038f: 0x07ab, // XK_Greek_OMEGAaccent
|
||||
0x0390: 0x07b6, // XK_Greek_iotaaccentdieresis
|
||||
0x0391: 0x07c1, // XK_Greek_ALPHA
|
||||
0x0392: 0x07c2, // XK_Greek_BETA
|
||||
0x0393: 0x07c3, // XK_Greek_GAMMA
|
||||
0x0394: 0x07c4, // XK_Greek_DELTA
|
||||
0x0395: 0x07c5, // XK_Greek_EPSILON
|
||||
0x0396: 0x07c6, // XK_Greek_ZETA
|
||||
0x0397: 0x07c7, // XK_Greek_ETA
|
||||
0x0398: 0x07c8, // XK_Greek_THETA
|
||||
0x0399: 0x07c9, // XK_Greek_IOTA
|
||||
0x039a: 0x07ca, // XK_Greek_KAPPA
|
||||
0x039b: 0x07cb, // XK_Greek_LAMDA
|
||||
0x039c: 0x07cc, // XK_Greek_MU
|
||||
0x039d: 0x07cd, // XK_Greek_NU
|
||||
0x039e: 0x07ce, // XK_Greek_XI
|
||||
0x039f: 0x07cf, // XK_Greek_OMICRON
|
||||
0x03a0: 0x07d0, // XK_Greek_PI
|
||||
0x03a1: 0x07d1, // XK_Greek_RHO
|
||||
0x03a3: 0x07d2, // XK_Greek_SIGMA
|
||||
0x03a4: 0x07d4, // XK_Greek_TAU
|
||||
0x03a5: 0x07d5, // XK_Greek_UPSILON
|
||||
0x03a6: 0x07d6, // XK_Greek_PHI
|
||||
0x03a7: 0x07d7, // XK_Greek_CHI
|
||||
0x03a8: 0x07d8, // XK_Greek_PSI
|
||||
0x03a9: 0x07d9, // XK_Greek_OMEGA
|
||||
0x03aa: 0x07a5, // XK_Greek_IOTAdieresis
|
||||
0x03ab: 0x07a9, // XK_Greek_UPSILONdieresis
|
||||
0x03ac: 0x07b1, // XK_Greek_alphaaccent
|
||||
0x03ad: 0x07b2, // XK_Greek_epsilonaccent
|
||||
0x03ae: 0x07b3, // XK_Greek_etaaccent
|
||||
0x03af: 0x07b4, // XK_Greek_iotaaccent
|
||||
0x03b0: 0x07ba, // XK_Greek_upsilonaccentdieresis
|
||||
0x03b1: 0x07e1, // XK_Greek_alpha
|
||||
0x03b2: 0x07e2, // XK_Greek_beta
|
||||
0x03b3: 0x07e3, // XK_Greek_gamma
|
||||
0x03b4: 0x07e4, // XK_Greek_delta
|
||||
0x03b5: 0x07e5, // XK_Greek_epsilon
|
||||
0x03b6: 0x07e6, // XK_Greek_zeta
|
||||
0x03b7: 0x07e7, // XK_Greek_eta
|
||||
0x03b8: 0x07e8, // XK_Greek_theta
|
||||
0x03b9: 0x07e9, // XK_Greek_iota
|
||||
0x03ba: 0x07ea, // XK_Greek_kappa
|
||||
0x03bb: 0x07eb, // XK_Greek_lamda
|
||||
0x03bc: 0x07ec, // XK_Greek_mu
|
||||
0x03bd: 0x07ed, // XK_Greek_nu
|
||||
0x03be: 0x07ee, // XK_Greek_xi
|
||||
0x03bf: 0x07ef, // XK_Greek_omicron
|
||||
0x03c0: 0x07f0, // XK_Greek_pi
|
||||
0x03c1: 0x07f1, // XK_Greek_rho
|
||||
0x03c2: 0x07f3, // XK_Greek_finalsmallsigma
|
||||
0x03c3: 0x07f2, // XK_Greek_sigma
|
||||
0x03c4: 0x07f4, // XK_Greek_tau
|
||||
0x03c5: 0x07f5, // XK_Greek_upsilon
|
||||
0x03c6: 0x07f6, // XK_Greek_phi
|
||||
0x03c7: 0x07f7, // XK_Greek_chi
|
||||
0x03c8: 0x07f8, // XK_Greek_psi
|
||||
0x03c9: 0x07f9, // XK_Greek_omega
|
||||
0x03ca: 0x07b5, // XK_Greek_iotadieresis
|
||||
0x03cb: 0x07b9, // XK_Greek_upsilondieresis
|
||||
0x03cc: 0x07b7, // XK_Greek_omicronaccent
|
||||
0x03cd: 0x07b8, // XK_Greek_upsilonaccent
|
||||
0x03ce: 0x07bb, // XK_Greek_omegaaccent
|
||||
0x0401: 0x06b3, // XK_Cyrillic_IO
|
||||
0x0402: 0x06b1, // XK_Serbian_DJE
|
||||
0x0403: 0x06b2, // XK_Macedonia_GJE
|
||||
0x0404: 0x06b4, // XK_Ukrainian_IE
|
||||
0x0405: 0x06b5, // XK_Macedonia_DSE
|
||||
0x0406: 0x06b6, // XK_Ukrainian_I
|
||||
0x0407: 0x06b7, // XK_Ukrainian_YI
|
||||
0x0408: 0x06b8, // XK_Cyrillic_JE
|
||||
0x0409: 0x06b9, // XK_Cyrillic_LJE
|
||||
0x040a: 0x06ba, // XK_Cyrillic_NJE
|
||||
0x040b: 0x06bb, // XK_Serbian_TSHE
|
||||
0x040c: 0x06bc, // XK_Macedonia_KJE
|
||||
0x040e: 0x06be, // XK_Byelorussian_SHORTU
|
||||
0x040f: 0x06bf, // XK_Cyrillic_DZHE
|
||||
0x0410: 0x06e1, // XK_Cyrillic_A
|
||||
0x0411: 0x06e2, // XK_Cyrillic_BE
|
||||
0x0412: 0x06f7, // XK_Cyrillic_VE
|
||||
0x0413: 0x06e7, // XK_Cyrillic_GHE
|
||||
0x0414: 0x06e4, // XK_Cyrillic_DE
|
||||
0x0415: 0x06e5, // XK_Cyrillic_IE
|
||||
0x0416: 0x06f6, // XK_Cyrillic_ZHE
|
||||
0x0417: 0x06fa, // XK_Cyrillic_ZE
|
||||
0x0418: 0x06e9, // XK_Cyrillic_I
|
||||
0x0419: 0x06ea, // XK_Cyrillic_SHORTI
|
||||
0x041a: 0x06eb, // XK_Cyrillic_KA
|
||||
0x041b: 0x06ec, // XK_Cyrillic_EL
|
||||
0x041c: 0x06ed, // XK_Cyrillic_EM
|
||||
0x041d: 0x06ee, // XK_Cyrillic_EN
|
||||
0x041e: 0x06ef, // XK_Cyrillic_O
|
||||
0x041f: 0x06f0, // XK_Cyrillic_PE
|
||||
0x0420: 0x06f2, // XK_Cyrillic_ER
|
||||
0x0421: 0x06f3, // XK_Cyrillic_ES
|
||||
0x0422: 0x06f4, // XK_Cyrillic_TE
|
||||
0x0423: 0x06f5, // XK_Cyrillic_U
|
||||
0x0424: 0x06e6, // XK_Cyrillic_EF
|
||||
0x0425: 0x06e8, // XK_Cyrillic_HA
|
||||
0x0426: 0x06e3, // XK_Cyrillic_TSE
|
||||
0x0427: 0x06fe, // XK_Cyrillic_CHE
|
||||
0x0428: 0x06fb, // XK_Cyrillic_SHA
|
||||
0x0429: 0x06fd, // XK_Cyrillic_SHCHA
|
||||
0x042a: 0x06ff, // XK_Cyrillic_HARDSIGN
|
||||
0x042b: 0x06f9, // XK_Cyrillic_YERU
|
||||
0x042c: 0x06f8, // XK_Cyrillic_SOFTSIGN
|
||||
0x042d: 0x06fc, // XK_Cyrillic_E
|
||||
0x042e: 0x06e0, // XK_Cyrillic_YU
|
||||
0x042f: 0x06f1, // XK_Cyrillic_YA
|
||||
0x0430: 0x06c1, // XK_Cyrillic_a
|
||||
0x0431: 0x06c2, // XK_Cyrillic_be
|
||||
0x0432: 0x06d7, // XK_Cyrillic_ve
|
||||
0x0433: 0x06c7, // XK_Cyrillic_ghe
|
||||
0x0434: 0x06c4, // XK_Cyrillic_de
|
||||
0x0435: 0x06c5, // XK_Cyrillic_ie
|
||||
0x0436: 0x06d6, // XK_Cyrillic_zhe
|
||||
0x0437: 0x06da, // XK_Cyrillic_ze
|
||||
0x0438: 0x06c9, // XK_Cyrillic_i
|
||||
0x0439: 0x06ca, // XK_Cyrillic_shorti
|
||||
0x043a: 0x06cb, // XK_Cyrillic_ka
|
||||
0x043b: 0x06cc, // XK_Cyrillic_el
|
||||
0x043c: 0x06cd, // XK_Cyrillic_em
|
||||
0x043d: 0x06ce, // XK_Cyrillic_en
|
||||
0x043e: 0x06cf, // XK_Cyrillic_o
|
||||
0x043f: 0x06d0, // XK_Cyrillic_pe
|
||||
0x0440: 0x06d2, // XK_Cyrillic_er
|
||||
0x0441: 0x06d3, // XK_Cyrillic_es
|
||||
0x0442: 0x06d4, // XK_Cyrillic_te
|
||||
0x0443: 0x06d5, // XK_Cyrillic_u
|
||||
0x0444: 0x06c6, // XK_Cyrillic_ef
|
||||
0x0445: 0x06c8, // XK_Cyrillic_ha
|
||||
0x0446: 0x06c3, // XK_Cyrillic_tse
|
||||
0x0447: 0x06de, // XK_Cyrillic_che
|
||||
0x0448: 0x06db, // XK_Cyrillic_sha
|
||||
0x0449: 0x06dd, // XK_Cyrillic_shcha
|
||||
0x044a: 0x06df, // XK_Cyrillic_hardsign
|
||||
0x044b: 0x06d9, // XK_Cyrillic_yeru
|
||||
0x044c: 0x06d8, // XK_Cyrillic_softsign
|
||||
0x044d: 0x06dc, // XK_Cyrillic_e
|
||||
0x044e: 0x06c0, // XK_Cyrillic_yu
|
||||
0x044f: 0x06d1, // XK_Cyrillic_ya
|
||||
0x0451: 0x06a3, // XK_Cyrillic_io
|
||||
0x0452: 0x06a1, // XK_Serbian_dje
|
||||
0x0453: 0x06a2, // XK_Macedonia_gje
|
||||
0x0454: 0x06a4, // XK_Ukrainian_ie
|
||||
0x0455: 0x06a5, // XK_Macedonia_dse
|
||||
0x0456: 0x06a6, // XK_Ukrainian_i
|
||||
0x0457: 0x06a7, // XK_Ukrainian_yi
|
||||
0x0458: 0x06a8, // XK_Cyrillic_je
|
||||
0x0459: 0x06a9, // XK_Cyrillic_lje
|
||||
0x045a: 0x06aa, // XK_Cyrillic_nje
|
||||
0x045b: 0x06ab, // XK_Serbian_tshe
|
||||
0x045c: 0x06ac, // XK_Macedonia_kje
|
||||
0x045e: 0x06ae, // XK_Byelorussian_shortu
|
||||
0x045f: 0x06af, // XK_Cyrillic_dzhe
|
||||
0x0490: 0x06bd, // XK_Ukrainian_GHE_WITH_UPTURN
|
||||
0x0491: 0x06ad, // XK_Ukrainian_ghe_with_upturn
|
||||
0x05d0: 0x0ce0, // XK_hebrew_aleph
|
||||
0x05d1: 0x0ce1, // XK_hebrew_bet
|
||||
0x05d2: 0x0ce2, // XK_hebrew_gimel
|
||||
0x05d3: 0x0ce3, // XK_hebrew_dalet
|
||||
0x05d4: 0x0ce4, // XK_hebrew_he
|
||||
0x05d5: 0x0ce5, // XK_hebrew_waw
|
||||
0x05d6: 0x0ce6, // XK_hebrew_zain
|
||||
0x05d7: 0x0ce7, // XK_hebrew_chet
|
||||
0x05d8: 0x0ce8, // XK_hebrew_tet
|
||||
0x05d9: 0x0ce9, // XK_hebrew_yod
|
||||
0x05da: 0x0cea, // XK_hebrew_finalkaph
|
||||
0x05db: 0x0ceb, // XK_hebrew_kaph
|
||||
0x05dc: 0x0cec, // XK_hebrew_lamed
|
||||
0x05dd: 0x0ced, // XK_hebrew_finalmem
|
||||
0x05de: 0x0cee, // XK_hebrew_mem
|
||||
0x05df: 0x0cef, // XK_hebrew_finalnun
|
||||
0x05e0: 0x0cf0, // XK_hebrew_nun
|
||||
0x05e1: 0x0cf1, // XK_hebrew_samech
|
||||
0x05e2: 0x0cf2, // XK_hebrew_ayin
|
||||
0x05e3: 0x0cf3, // XK_hebrew_finalpe
|
||||
0x05e4: 0x0cf4, // XK_hebrew_pe
|
||||
0x05e5: 0x0cf5, // XK_hebrew_finalzade
|
||||
0x05e6: 0x0cf6, // XK_hebrew_zade
|
||||
0x05e7: 0x0cf7, // XK_hebrew_qoph
|
||||
0x05e8: 0x0cf8, // XK_hebrew_resh
|
||||
0x05e9: 0x0cf9, // XK_hebrew_shin
|
||||
0x05ea: 0x0cfa, // XK_hebrew_taw
|
||||
0x060c: 0x05ac, // XK_Arabic_comma
|
||||
0x061b: 0x05bb, // XK_Arabic_semicolon
|
||||
0x061f: 0x05bf, // XK_Arabic_question_mark
|
||||
0x0621: 0x05c1, // XK_Arabic_hamza
|
||||
0x0622: 0x05c2, // XK_Arabic_maddaonalef
|
||||
0x0623: 0x05c3, // XK_Arabic_hamzaonalef
|
||||
0x0624: 0x05c4, // XK_Arabic_hamzaonwaw
|
||||
0x0625: 0x05c5, // XK_Arabic_hamzaunderalef
|
||||
0x0626: 0x05c6, // XK_Arabic_hamzaonyeh
|
||||
0x0627: 0x05c7, // XK_Arabic_alef
|
||||
0x0628: 0x05c8, // XK_Arabic_beh
|
||||
0x0629: 0x05c9, // XK_Arabic_tehmarbuta
|
||||
0x062a: 0x05ca, // XK_Arabic_teh
|
||||
0x062b: 0x05cb, // XK_Arabic_theh
|
||||
0x062c: 0x05cc, // XK_Arabic_jeem
|
||||
0x062d: 0x05cd, // XK_Arabic_hah
|
||||
0x062e: 0x05ce, // XK_Arabic_khah
|
||||
0x062f: 0x05cf, // XK_Arabic_dal
|
||||
0x0630: 0x05d0, // XK_Arabic_thal
|
||||
0x0631: 0x05d1, // XK_Arabic_ra
|
||||
0x0632: 0x05d2, // XK_Arabic_zain
|
||||
0x0633: 0x05d3, // XK_Arabic_seen
|
||||
0x0634: 0x05d4, // XK_Arabic_sheen
|
||||
0x0635: 0x05d5, // XK_Arabic_sad
|
||||
0x0636: 0x05d6, // XK_Arabic_dad
|
||||
0x0637: 0x05d7, // XK_Arabic_tah
|
||||
0x0638: 0x05d8, // XK_Arabic_zah
|
||||
0x0639: 0x05d9, // XK_Arabic_ain
|
||||
0x063a: 0x05da, // XK_Arabic_ghain
|
||||
0x0640: 0x05e0, // XK_Arabic_tatweel
|
||||
0x0641: 0x05e1, // XK_Arabic_feh
|
||||
0x0642: 0x05e2, // XK_Arabic_qaf
|
||||
0x0643: 0x05e3, // XK_Arabic_kaf
|
||||
0x0644: 0x05e4, // XK_Arabic_lam
|
||||
0x0645: 0x05e5, // XK_Arabic_meem
|
||||
0x0646: 0x05e6, // XK_Arabic_noon
|
||||
0x0647: 0x05e7, // XK_Arabic_ha
|
||||
0x0648: 0x05e8, // XK_Arabic_waw
|
||||
0x0649: 0x05e9, // XK_Arabic_alefmaksura
|
||||
0x064a: 0x05ea, // XK_Arabic_yeh
|
||||
0x064b: 0x05eb, // XK_Arabic_fathatan
|
||||
0x064c: 0x05ec, // XK_Arabic_dammatan
|
||||
0x064d: 0x05ed, // XK_Arabic_kasratan
|
||||
0x064e: 0x05ee, // XK_Arabic_fatha
|
||||
0x064f: 0x05ef, // XK_Arabic_damma
|
||||
0x0650: 0x05f0, // XK_Arabic_kasra
|
||||
0x0651: 0x05f1, // XK_Arabic_shadda
|
||||
0x0652: 0x05f2, // XK_Arabic_sukun
|
||||
0x0e01: 0x0da1, // XK_Thai_kokai
|
||||
0x0e02: 0x0da2, // XK_Thai_khokhai
|
||||
0x0e03: 0x0da3, // XK_Thai_khokhuat
|
||||
0x0e04: 0x0da4, // XK_Thai_khokhwai
|
||||
0x0e05: 0x0da5, // XK_Thai_khokhon
|
||||
0x0e06: 0x0da6, // XK_Thai_khorakhang
|
||||
0x0e07: 0x0da7, // XK_Thai_ngongu
|
||||
0x0e08: 0x0da8, // XK_Thai_chochan
|
||||
0x0e09: 0x0da9, // XK_Thai_choching
|
||||
0x0e0a: 0x0daa, // XK_Thai_chochang
|
||||
0x0e0b: 0x0dab, // XK_Thai_soso
|
||||
0x0e0c: 0x0dac, // XK_Thai_chochoe
|
||||
0x0e0d: 0x0dad, // XK_Thai_yoying
|
||||
0x0e0e: 0x0dae, // XK_Thai_dochada
|
||||
0x0e0f: 0x0daf, // XK_Thai_topatak
|
||||
0x0e10: 0x0db0, // XK_Thai_thothan
|
||||
0x0e11: 0x0db1, // XK_Thai_thonangmontho
|
||||
0x0e12: 0x0db2, // XK_Thai_thophuthao
|
||||
0x0e13: 0x0db3, // XK_Thai_nonen
|
||||
0x0e14: 0x0db4, // XK_Thai_dodek
|
||||
0x0e15: 0x0db5, // XK_Thai_totao
|
||||
0x0e16: 0x0db6, // XK_Thai_thothung
|
||||
0x0e17: 0x0db7, // XK_Thai_thothahan
|
||||
0x0e18: 0x0db8, // XK_Thai_thothong
|
||||
0x0e19: 0x0db9, // XK_Thai_nonu
|
||||
0x0e1a: 0x0dba, // XK_Thai_bobaimai
|
||||
0x0e1b: 0x0dbb, // XK_Thai_popla
|
||||
0x0e1c: 0x0dbc, // XK_Thai_phophung
|
||||
0x0e1d: 0x0dbd, // XK_Thai_fofa
|
||||
0x0e1e: 0x0dbe, // XK_Thai_phophan
|
||||
0x0e1f: 0x0dbf, // XK_Thai_fofan
|
||||
0x0e20: 0x0dc0, // XK_Thai_phosamphao
|
||||
0x0e21: 0x0dc1, // XK_Thai_moma
|
||||
0x0e22: 0x0dc2, // XK_Thai_yoyak
|
||||
0x0e23: 0x0dc3, // XK_Thai_rorua
|
||||
0x0e24: 0x0dc4, // XK_Thai_ru
|
||||
0x0e25: 0x0dc5, // XK_Thai_loling
|
||||
0x0e26: 0x0dc6, // XK_Thai_lu
|
||||
0x0e27: 0x0dc7, // XK_Thai_wowaen
|
||||
0x0e28: 0x0dc8, // XK_Thai_sosala
|
||||
0x0e29: 0x0dc9, // XK_Thai_sorusi
|
||||
0x0e2a: 0x0dca, // XK_Thai_sosua
|
||||
0x0e2b: 0x0dcb, // XK_Thai_hohip
|
||||
0x0e2c: 0x0dcc, // XK_Thai_lochula
|
||||
0x0e2d: 0x0dcd, // XK_Thai_oang
|
||||
0x0e2e: 0x0dce, // XK_Thai_honokhuk
|
||||
0x0e2f: 0x0dcf, // XK_Thai_paiyannoi
|
||||
0x0e30: 0x0dd0, // XK_Thai_saraa
|
||||
0x0e31: 0x0dd1, // XK_Thai_maihanakat
|
||||
0x0e32: 0x0dd2, // XK_Thai_saraaa
|
||||
0x0e33: 0x0dd3, // XK_Thai_saraam
|
||||
0x0e34: 0x0dd4, // XK_Thai_sarai
|
||||
0x0e35: 0x0dd5, // XK_Thai_saraii
|
||||
0x0e36: 0x0dd6, // XK_Thai_saraue
|
||||
0x0e37: 0x0dd7, // XK_Thai_sarauee
|
||||
0x0e38: 0x0dd8, // XK_Thai_sarau
|
||||
0x0e39: 0x0dd9, // XK_Thai_sarauu
|
||||
0x0e3a: 0x0dda, // XK_Thai_phinthu
|
||||
0x0e3f: 0x0ddf, // XK_Thai_baht
|
||||
0x0e40: 0x0de0, // XK_Thai_sarae
|
||||
0x0e41: 0x0de1, // XK_Thai_saraae
|
||||
0x0e42: 0x0de2, // XK_Thai_sarao
|
||||
0x0e43: 0x0de3, // XK_Thai_saraaimaimuan
|
||||
0x0e44: 0x0de4, // XK_Thai_saraaimaimalai
|
||||
0x0e45: 0x0de5, // XK_Thai_lakkhangyao
|
||||
0x0e46: 0x0de6, // XK_Thai_maiyamok
|
||||
0x0e47: 0x0de7, // XK_Thai_maitaikhu
|
||||
0x0e48: 0x0de8, // XK_Thai_maiek
|
||||
0x0e49: 0x0de9, // XK_Thai_maitho
|
||||
0x0e4a: 0x0dea, // XK_Thai_maitri
|
||||
0x0e4b: 0x0deb, // XK_Thai_maichattawa
|
||||
0x0e4c: 0x0dec, // XK_Thai_thanthakhat
|
||||
0x0e4d: 0x0ded, // XK_Thai_nikhahit
|
||||
0x0e50: 0x0df0, // XK_Thai_leksun
|
||||
0x0e51: 0x0df1, // XK_Thai_leknung
|
||||
0x0e52: 0x0df2, // XK_Thai_leksong
|
||||
0x0e53: 0x0df3, // XK_Thai_leksam
|
||||
0x0e54: 0x0df4, // XK_Thai_leksi
|
||||
0x0e55: 0x0df5, // XK_Thai_lekha
|
||||
0x0e56: 0x0df6, // XK_Thai_lekhok
|
||||
0x0e57: 0x0df7, // XK_Thai_lekchet
|
||||
0x0e58: 0x0df8, // XK_Thai_lekpaet
|
||||
0x0e59: 0x0df9, // XK_Thai_lekkao
|
||||
0x2002: 0x0aa2, // XK_enspace
|
||||
0x2003: 0x0aa1, // XK_emspace
|
||||
0x2004: 0x0aa3, // XK_em3space
|
||||
0x2005: 0x0aa4, // XK_em4space
|
||||
0x2007: 0x0aa5, // XK_digitspace
|
||||
0x2008: 0x0aa6, // XK_punctspace
|
||||
0x2009: 0x0aa7, // XK_thinspace
|
||||
0x200a: 0x0aa8, // XK_hairspace
|
||||
0x2012: 0x0abb, // XK_figdash
|
||||
0x2013: 0x0aaa, // XK_endash
|
||||
0x2014: 0x0aa9, // XK_emdash
|
||||
0x2015: 0x07af, // XK_Greek_horizbar
|
||||
0x2017: 0x0cdf, // XK_hebrew_doublelowline
|
||||
0x2018: 0x0ad0, // XK_leftsinglequotemark
|
||||
0x2019: 0x0ad1, // XK_rightsinglequotemark
|
||||
0x201a: 0x0afd, // XK_singlelowquotemark
|
||||
0x201c: 0x0ad2, // XK_leftdoublequotemark
|
||||
0x201d: 0x0ad3, // XK_rightdoublequotemark
|
||||
0x201e: 0x0afe, // XK_doublelowquotemark
|
||||
0x2020: 0x0af1, // XK_dagger
|
||||
0x2021: 0x0af2, // XK_doubledagger
|
||||
0x2022: 0x0ae6, // XK_enfilledcircbullet
|
||||
0x2025: 0x0aaf, // XK_doubbaselinedot
|
||||
0x2026: 0x0aae, // XK_ellipsis
|
||||
0x2030: 0x0ad5, // XK_permille
|
||||
0x2032: 0x0ad6, // XK_minutes
|
||||
0x2033: 0x0ad7, // XK_seconds
|
||||
0x2038: 0x0afc, // XK_caret
|
||||
0x203e: 0x047e, // XK_overline
|
||||
0x20a9: 0x0eff, // XK_Korean_Won
|
||||
0x20ac: 0x20ac, // XK_EuroSign
|
||||
0x2105: 0x0ab8, // XK_careof
|
||||
0x2116: 0x06b0, // XK_numerosign
|
||||
0x2117: 0x0afb, // XK_phonographcopyright
|
||||
0x211e: 0x0ad4, // XK_prescription
|
||||
0x2122: 0x0ac9, // XK_trademark
|
||||
0x2153: 0x0ab0, // XK_onethird
|
||||
0x2154: 0x0ab1, // XK_twothirds
|
||||
0x2155: 0x0ab2, // XK_onefifth
|
||||
0x2156: 0x0ab3, // XK_twofifths
|
||||
0x2157: 0x0ab4, // XK_threefifths
|
||||
0x2158: 0x0ab5, // XK_fourfifths
|
||||
0x2159: 0x0ab6, // XK_onesixth
|
||||
0x215a: 0x0ab7, // XK_fivesixths
|
||||
0x215b: 0x0ac3, // XK_oneeighth
|
||||
0x215c: 0x0ac4, // XK_threeeighths
|
||||
0x215d: 0x0ac5, // XK_fiveeighths
|
||||
0x215e: 0x0ac6, // XK_seveneighths
|
||||
0x2190: 0x08fb, // XK_leftarrow
|
||||
0x2191: 0x08fc, // XK_uparrow
|
||||
0x2192: 0x08fd, // XK_rightarrow
|
||||
0x2193: 0x08fe, // XK_downarrow
|
||||
0x21d2: 0x08ce, // XK_implies
|
||||
0x21d4: 0x08cd, // XK_ifonlyif
|
||||
0x2202: 0x08ef, // XK_partialderivative
|
||||
0x2207: 0x08c5, // XK_nabla
|
||||
0x2218: 0x0bca, // XK_jot
|
||||
0x221a: 0x08d6, // XK_radical
|
||||
0x221d: 0x08c1, // XK_variation
|
||||
0x221e: 0x08c2, // XK_infinity
|
||||
0x2227: 0x08de, // XK_logicaland
|
||||
0x2228: 0x08df, // XK_logicalor
|
||||
0x2229: 0x08dc, // XK_intersection
|
||||
0x222a: 0x08dd, // XK_union
|
||||
0x222b: 0x08bf, // XK_integral
|
||||
0x2234: 0x08c0, // XK_therefore
|
||||
0x223c: 0x08c8, // XK_approximate
|
||||
0x2243: 0x08c9, // XK_similarequal
|
||||
0x2245: 0x1002248, // XK_approxeq
|
||||
0x2260: 0x08bd, // XK_notequal
|
||||
0x2261: 0x08cf, // XK_identical
|
||||
0x2264: 0x08bc, // XK_lessthanequal
|
||||
0x2265: 0x08be, // XK_greaterthanequal
|
||||
0x2282: 0x08da, // XK_includedin
|
||||
0x2283: 0x08db, // XK_includes
|
||||
0x22a2: 0x0bfc, // XK_righttack
|
||||
0x22a3: 0x0bdc, // XK_lefttack
|
||||
0x22a4: 0x0bc2, // XK_downtack
|
||||
0x22a5: 0x0bce, // XK_uptack
|
||||
0x2308: 0x0bd3, // XK_upstile
|
||||
0x230a: 0x0bc4, // XK_downstile
|
||||
0x2315: 0x0afa, // XK_telephonerecorder
|
||||
0x2320: 0x08a4, // XK_topintegral
|
||||
0x2321: 0x08a5, // XK_botintegral
|
||||
0x2395: 0x0bcc, // XK_quad
|
||||
0x239b: 0x08ab, // XK_topleftparens
|
||||
0x239d: 0x08ac, // XK_botleftparens
|
||||
0x239e: 0x08ad, // XK_toprightparens
|
||||
0x23a0: 0x08ae, // XK_botrightparens
|
||||
0x23a1: 0x08a7, // XK_topleftsqbracket
|
||||
0x23a3: 0x08a8, // XK_botleftsqbracket
|
||||
0x23a4: 0x08a9, // XK_toprightsqbracket
|
||||
0x23a6: 0x08aa, // XK_botrightsqbracket
|
||||
0x23a8: 0x08af, // XK_leftmiddlecurlybrace
|
||||
0x23ac: 0x08b0, // XK_rightmiddlecurlybrace
|
||||
0x23b7: 0x08a1, // XK_leftradical
|
||||
0x23ba: 0x09ef, // XK_horizlinescan1
|
||||
0x23bb: 0x09f0, // XK_horizlinescan3
|
||||
0x23bc: 0x09f2, // XK_horizlinescan7
|
||||
0x23bd: 0x09f3, // XK_horizlinescan9
|
||||
0x2409: 0x09e2, // XK_ht
|
||||
0x240a: 0x09e5, // XK_lf
|
||||
0x240b: 0x09e9, // XK_vt
|
||||
0x240c: 0x09e3, // XK_ff
|
||||
0x240d: 0x09e4, // XK_cr
|
||||
0x2423: 0x0aac, // XK_signifblank
|
||||
0x2424: 0x09e8, // XK_nl
|
||||
0x2500: 0x08a3, // XK_horizconnector
|
||||
0x2502: 0x08a6, // XK_vertconnector
|
||||
0x250c: 0x08a2, // XK_topleftradical
|
||||
0x2510: 0x09eb, // XK_uprightcorner
|
||||
0x2514: 0x09ed, // XK_lowleftcorner
|
||||
0x2518: 0x09ea, // XK_lowrightcorner
|
||||
0x251c: 0x09f4, // XK_leftt
|
||||
0x2524: 0x09f5, // XK_rightt
|
||||
0x252c: 0x09f7, // XK_topt
|
||||
0x2534: 0x09f6, // XK_bott
|
||||
0x253c: 0x09ee, // XK_crossinglines
|
||||
0x2592: 0x09e1, // XK_checkerboard
|
||||
0x25aa: 0x0ae7, // XK_enfilledsqbullet
|
||||
0x25ab: 0x0ae1, // XK_enopensquarebullet
|
||||
0x25ac: 0x0adb, // XK_filledrectbullet
|
||||
0x25ad: 0x0ae2, // XK_openrectbullet
|
||||
0x25ae: 0x0adf, // XK_emfilledrect
|
||||
0x25af: 0x0acf, // XK_emopenrectangle
|
||||
0x25b2: 0x0ae8, // XK_filledtribulletup
|
||||
0x25b3: 0x0ae3, // XK_opentribulletup
|
||||
0x25b6: 0x0add, // XK_filledrighttribullet
|
||||
0x25b7: 0x0acd, // XK_rightopentriangle
|
||||
0x25bc: 0x0ae9, // XK_filledtribulletdown
|
||||
0x25bd: 0x0ae4, // XK_opentribulletdown
|
||||
0x25c0: 0x0adc, // XK_filledlefttribullet
|
||||
0x25c1: 0x0acc, // XK_leftopentriangle
|
||||
0x25c6: 0x09e0, // XK_soliddiamond
|
||||
0x25cb: 0x0ace, // XK_emopencircle
|
||||
0x25cf: 0x0ade, // XK_emfilledcircle
|
||||
0x25e6: 0x0ae0, // XK_enopencircbullet
|
||||
0x2606: 0x0ae5, // XK_openstar
|
||||
0x260e: 0x0af9, // XK_telephone
|
||||
0x2613: 0x0aca, // XK_signaturemark
|
||||
0x261c: 0x0aea, // XK_leftpointer
|
||||
0x261e: 0x0aeb, // XK_rightpointer
|
||||
0x2640: 0x0af8, // XK_femalesymbol
|
||||
0x2642: 0x0af7, // XK_malesymbol
|
||||
0x2663: 0x0aec, // XK_club
|
||||
0x2665: 0x0aee, // XK_heart
|
||||
0x2666: 0x0aed, // XK_diamond
|
||||
0x266d: 0x0af6, // XK_musicalflat
|
||||
0x266f: 0x0af5, // XK_musicalsharp
|
||||
0x2713: 0x0af3, // XK_checkmark
|
||||
0x2717: 0x0af4, // XK_ballotcross
|
||||
0x271d: 0x0ad9, // XK_latincross
|
||||
0x2720: 0x0af0, // XK_maltesecross
|
||||
0x27e8: 0x0abc, // XK_leftanglebracket
|
||||
0x27e9: 0x0abe, // XK_rightanglebracket
|
||||
0x3001: 0x04a4, // XK_kana_comma
|
||||
0x3002: 0x04a1, // XK_kana_fullstop
|
||||
0x300c: 0x04a2, // XK_kana_openingbracket
|
||||
0x300d: 0x04a3, // XK_kana_closingbracket
|
||||
0x309b: 0x04de, // XK_voicedsound
|
||||
0x309c: 0x04df, // XK_semivoicedsound
|
||||
0x30a1: 0x04a7, // XK_kana_a
|
||||
0x30a2: 0x04b1, // XK_kana_A
|
||||
0x30a3: 0x04a8, // XK_kana_i
|
||||
0x30a4: 0x04b2, // XK_kana_I
|
||||
0x30a5: 0x04a9, // XK_kana_u
|
||||
0x30a6: 0x04b3, // XK_kana_U
|
||||
0x30a7: 0x04aa, // XK_kana_e
|
||||
0x30a8: 0x04b4, // XK_kana_E
|
||||
0x30a9: 0x04ab, // XK_kana_o
|
||||
0x30aa: 0x04b5, // XK_kana_O
|
||||
0x30ab: 0x04b6, // XK_kana_KA
|
||||
0x30ad: 0x04b7, // XK_kana_KI
|
||||
0x30af: 0x04b8, // XK_kana_KU
|
||||
0x30b1: 0x04b9, // XK_kana_KE
|
||||
0x30b3: 0x04ba, // XK_kana_KO
|
||||
0x30b5: 0x04bb, // XK_kana_SA
|
||||
0x30b7: 0x04bc, // XK_kana_SHI
|
||||
0x30b9: 0x04bd, // XK_kana_SU
|
||||
0x30bb: 0x04be, // XK_kana_SE
|
||||
0x30bd: 0x04bf, // XK_kana_SO
|
||||
0x30bf: 0x04c0, // XK_kana_TA
|
||||
0x30c1: 0x04c1, // XK_kana_CHI
|
||||
0x30c3: 0x04af, // XK_kana_tsu
|
||||
0x30c4: 0x04c2, // XK_kana_TSU
|
||||
0x30c6: 0x04c3, // XK_kana_TE
|
||||
0x30c8: 0x04c4, // XK_kana_TO
|
||||
0x30ca: 0x04c5, // XK_kana_NA
|
||||
0x30cb: 0x04c6, // XK_kana_NI
|
||||
0x30cc: 0x04c7, // XK_kana_NU
|
||||
0x30cd: 0x04c8, // XK_kana_NE
|
||||
0x30ce: 0x04c9, // XK_kana_NO
|
||||
0x30cf: 0x04ca, // XK_kana_HA
|
||||
0x30d2: 0x04cb, // XK_kana_HI
|
||||
0x30d5: 0x04cc, // XK_kana_FU
|
||||
0x30d8: 0x04cd, // XK_kana_HE
|
||||
0x30db: 0x04ce, // XK_kana_HO
|
||||
0x30de: 0x04cf, // XK_kana_MA
|
||||
0x30df: 0x04d0, // XK_kana_MI
|
||||
0x30e0: 0x04d1, // XK_kana_MU
|
||||
0x30e1: 0x04d2, // XK_kana_ME
|
||||
0x30e2: 0x04d3, // XK_kana_MO
|
||||
0x30e3: 0x04ac, // XK_kana_ya
|
||||
0x30e4: 0x04d4, // XK_kana_YA
|
||||
0x30e5: 0x04ad, // XK_kana_yu
|
||||
0x30e6: 0x04d5, // XK_kana_YU
|
||||
0x30e7: 0x04ae, // XK_kana_yo
|
||||
0x30e8: 0x04d6, // XK_kana_YO
|
||||
0x30e9: 0x04d7, // XK_kana_RA
|
||||
0x30ea: 0x04d8, // XK_kana_RI
|
||||
0x30eb: 0x04d9, // XK_kana_RU
|
||||
0x30ec: 0x04da, // XK_kana_RE
|
||||
0x30ed: 0x04db, // XK_kana_RO
|
||||
0x30ef: 0x04dc, // XK_kana_WA
|
||||
0x30f2: 0x04a6, // XK_kana_WO
|
||||
0x30f3: 0x04dd, // XK_kana_N
|
||||
0x30fb: 0x04a5, // XK_kana_conjunctive
|
||||
0x30fc: 0x04b0, // XK_prolongedsound
|
||||
};
|
||||
|
||||
export default {
|
||||
lookup(u) {
|
||||
// Latin-1 is one-to-one mapping
|
||||
if ((u >= 0x20) && (u <= 0xff)) {
|
||||
return u;
|
||||
}
|
||||
|
||||
// Lookup table (fairly random)
|
||||
const keysym = codepoints[u];
|
||||
if (keysym !== undefined) {
|
||||
return keysym;
|
||||
}
|
||||
|
||||
// General mapping as final fallback
|
||||
return 0x01000000 | u;
|
||||
},
|
||||
};
|
191
client/src/component/utils/keyboards/novnc/util.js
Normal file
191
client/src/component/utils/keyboards/novnc/util.js
Normal file
@ -0,0 +1,191 @@
|
||||
import KeyTable from "./keysym.js";
|
||||
import keysyms from "./keysymdef.js";
|
||||
import vkeys from "./vkeys.js";
|
||||
import fixedkeys from "./fixedkeys.js";
|
||||
import DOMKeyTable from "./domkeytable.js";
|
||||
import * as browser from "./browser.js";
|
||||
|
||||
// Get 'KeyboardEvent.code', handling legacy browsers
|
||||
export function getKeycode(evt) {
|
||||
// Are we getting proper key identifiers?
|
||||
// (unfortunately Firefox and Chrome are crappy here and gives
|
||||
// us an empty string on some platforms, rather than leaving it
|
||||
// undefined)
|
||||
if (evt.code) {
|
||||
// Mozilla isn't fully in sync with the spec yet
|
||||
switch (evt.code) {
|
||||
case 'OSLeft': return 'MetaLeft';
|
||||
case 'OSRight': return 'MetaRight';
|
||||
}
|
||||
|
||||
return evt.code;
|
||||
}
|
||||
|
||||
// The de-facto standard is to use Windows Virtual-Key codes
|
||||
// in the 'keyCode' field for non-printable characters
|
||||
if (evt.keyCode in vkeys) {
|
||||
let code = vkeys[evt.keyCode];
|
||||
|
||||
// macOS has messed up this code for some reason
|
||||
if (browser.isMac() && (code === 'ContextMenu')) {
|
||||
code = 'MetaRight';
|
||||
}
|
||||
|
||||
// The keyCode doesn't distinguish between left and right
|
||||
// for the standard modifiers
|
||||
if (evt.location === 2) {
|
||||
switch (code) {
|
||||
case 'ShiftLeft': return 'ShiftRight';
|
||||
case 'ControlLeft': return 'ControlRight';
|
||||
case 'AltLeft': return 'AltRight';
|
||||
}
|
||||
}
|
||||
|
||||
// Nor a bunch of the numpad keys
|
||||
if (evt.location === 3) {
|
||||
switch (code) {
|
||||
case 'Delete': return 'NumpadDecimal';
|
||||
case 'Insert': return 'Numpad0';
|
||||
case 'End': return 'Numpad1';
|
||||
case 'ArrowDown': return 'Numpad2';
|
||||
case 'PageDown': return 'Numpad3';
|
||||
case 'ArrowLeft': return 'Numpad4';
|
||||
case 'ArrowRight': return 'Numpad6';
|
||||
case 'Home': return 'Numpad7';
|
||||
case 'ArrowUp': return 'Numpad8';
|
||||
case 'PageUp': return 'Numpad9';
|
||||
case 'Enter': return 'NumpadEnter';
|
||||
}
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
return 'Unidentified';
|
||||
}
|
||||
|
||||
// Get 'KeyboardEvent.key', handling legacy browsers
|
||||
export function getKey(evt) {
|
||||
// Are we getting a proper key value?
|
||||
if (evt.key !== undefined) {
|
||||
// Mozilla isn't fully in sync with the spec yet
|
||||
switch (evt.key) {
|
||||
case 'OS': return 'Meta';
|
||||
case 'LaunchMyComputer': return 'LaunchApplication1';
|
||||
case 'LaunchCalculator': return 'LaunchApplication2';
|
||||
}
|
||||
|
||||
// iOS leaks some OS names
|
||||
switch (evt.key) {
|
||||
case 'UIKeyInputUpArrow': return 'ArrowUp';
|
||||
case 'UIKeyInputDownArrow': return 'ArrowDown';
|
||||
case 'UIKeyInputLeftArrow': return 'ArrowLeft';
|
||||
case 'UIKeyInputRightArrow': return 'ArrowRight';
|
||||
case 'UIKeyInputEscape': return 'Escape';
|
||||
}
|
||||
|
||||
// Broken behaviour in Chrome
|
||||
if ((evt.key === '\x00') && (evt.code === 'NumpadDecimal')) {
|
||||
return 'Delete';
|
||||
}
|
||||
|
||||
return evt.key;
|
||||
}
|
||||
|
||||
// Try to deduce it based on the physical key
|
||||
const code = getKeycode(evt);
|
||||
if (code in fixedkeys) {
|
||||
return fixedkeys[code];
|
||||
}
|
||||
|
||||
// If that failed, then see if we have a printable character
|
||||
if (evt.charCode) {
|
||||
return String.fromCharCode(evt.charCode);
|
||||
}
|
||||
|
||||
// At this point we have nothing left to go on
|
||||
return 'Unidentified';
|
||||
}
|
||||
|
||||
// Get the most reliable keysym value we can get from a key event
|
||||
export function getKeysym(evt) {
|
||||
const key = getKey(evt);
|
||||
|
||||
if (key === 'Unidentified') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// First look up special keys
|
||||
if (key in DOMKeyTable) {
|
||||
let location = evt.location;
|
||||
|
||||
// Safari screws up location for the right cmd key
|
||||
if ((key === 'Meta') && (location === 0)) {
|
||||
location = 2;
|
||||
}
|
||||
|
||||
// And for Clear
|
||||
if ((key === 'Clear') && (location === 3)) {
|
||||
let code = getKeycode(evt);
|
||||
if (code === 'NumLock') {
|
||||
location = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if ((location === undefined) || (location > 3)) {
|
||||
location = 0;
|
||||
}
|
||||
|
||||
// The original Meta key now gets confused with the Windows key
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=1020141
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1232918
|
||||
if (key === 'Meta') {
|
||||
let code = getKeycode(evt);
|
||||
if (code === 'AltLeft') {
|
||||
return KeyTable.XK_Meta_L;
|
||||
} else if (code === 'AltRight') {
|
||||
return KeyTable.XK_Meta_R;
|
||||
}
|
||||
}
|
||||
|
||||
// macOS has Clear instead of NumLock, but the remote system is
|
||||
// probably not macOS, so lying here is probably best...
|
||||
if (key === 'Clear') {
|
||||
let code = getKeycode(evt);
|
||||
if (code === 'NumLock') {
|
||||
return KeyTable.XK_Num_Lock;
|
||||
}
|
||||
}
|
||||
|
||||
// Windows sends alternating symbols for some keys when using a
|
||||
// Japanese layout. We have no way of synchronising with the IM
|
||||
// running on the remote system, so we send some combined keysym
|
||||
// instead and hope for the best.
|
||||
if (browser.isWindows()) {
|
||||
switch (key) {
|
||||
case 'Zenkaku':
|
||||
case 'Hankaku':
|
||||
return KeyTable.XK_Zenkaku_Hankaku;
|
||||
case 'Romaji':
|
||||
case 'KanaMode':
|
||||
return KeyTable.XK_Romaji;
|
||||
}
|
||||
}
|
||||
|
||||
return DOMKeyTable[key][location];
|
||||
}
|
||||
|
||||
// Now we need to look at the Unicode symbol instead
|
||||
|
||||
// Special key? (FIXME: Should have been caught earlier)
|
||||
if (key.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const codepoint = key.charCodeAt();
|
||||
if (codepoint) {
|
||||
return keysyms.lookup(codepoint);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
116
client/src/component/utils/keyboards/novnc/vkeys.js
Normal file
116
client/src/component/utils/keyboards/novnc/vkeys.js
Normal file
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2018 The noVNC Authors
|
||||
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
|
||||
*/
|
||||
|
||||
/*
|
||||
* Mapping between Microsoft® Windows® Virtual-Key codes and
|
||||
* HTML key codes.
|
||||
*/
|
||||
|
||||
export default {
|
||||
0x08: 'Backspace',
|
||||
0x09: 'Tab',
|
||||
0x0a: 'NumpadClear',
|
||||
0x0d: 'Enter',
|
||||
0x10: 'ShiftLeft',
|
||||
0x11: 'ControlLeft',
|
||||
0x12: 'AltLeft',
|
||||
0x13: 'Pause',
|
||||
0x14: 'CapsLock',
|
||||
0x15: 'Lang1',
|
||||
0x19: 'Lang2',
|
||||
0x1b: 'Escape',
|
||||
0x1c: 'Convert',
|
||||
0x1d: 'NonConvert',
|
||||
0x20: 'Space',
|
||||
0x21: 'PageUp',
|
||||
0x22: 'PageDown',
|
||||
0x23: 'End',
|
||||
0x24: 'Home',
|
||||
0x25: 'ArrowLeft',
|
||||
0x26: 'ArrowUp',
|
||||
0x27: 'ArrowRight',
|
||||
0x28: 'ArrowDown',
|
||||
0x29: 'Select',
|
||||
0x2c: 'PrintScreen',
|
||||
0x2d: 'Insert',
|
||||
0x2e: 'Delete',
|
||||
0x2f: 'Help',
|
||||
0x30: 'Digit0',
|
||||
0x31: 'Digit1',
|
||||
0x32: 'Digit2',
|
||||
0x33: 'Digit3',
|
||||
0x34: 'Digit4',
|
||||
0x35: 'Digit5',
|
||||
0x36: 'Digit6',
|
||||
0x37: 'Digit7',
|
||||
0x38: 'Digit8',
|
||||
0x39: 'Digit9',
|
||||
0x5b: 'MetaLeft',
|
||||
0x5c: 'MetaRight',
|
||||
0x5d: 'ContextMenu',
|
||||
0x5f: 'Sleep',
|
||||
0x60: 'Numpad0',
|
||||
0x61: 'Numpad1',
|
||||
0x62: 'Numpad2',
|
||||
0x63: 'Numpad3',
|
||||
0x64: 'Numpad4',
|
||||
0x65: 'Numpad5',
|
||||
0x66: 'Numpad6',
|
||||
0x67: 'Numpad7',
|
||||
0x68: 'Numpad8',
|
||||
0x69: 'Numpad9',
|
||||
0x6a: 'NumpadMultiply',
|
||||
0x6b: 'NumpadAdd',
|
||||
0x6c: 'NumpadDecimal',
|
||||
0x6d: 'NumpadSubtract',
|
||||
0x6e: 'NumpadDecimal', // Duplicate, because buggy on Windows
|
||||
0x6f: 'NumpadDivide',
|
||||
0x70: 'F1',
|
||||
0x71: 'F2',
|
||||
0x72: 'F3',
|
||||
0x73: 'F4',
|
||||
0x74: 'F5',
|
||||
0x75: 'F6',
|
||||
0x76: 'F7',
|
||||
0x77: 'F8',
|
||||
0x78: 'F9',
|
||||
0x79: 'F10',
|
||||
0x7a: 'F11',
|
||||
0x7b: 'F12',
|
||||
0x7c: 'F13',
|
||||
0x7d: 'F14',
|
||||
0x7e: 'F15',
|
||||
0x7f: 'F16',
|
||||
0x80: 'F17',
|
||||
0x81: 'F18',
|
||||
0x82: 'F19',
|
||||
0x83: 'F20',
|
||||
0x84: 'F21',
|
||||
0x85: 'F22',
|
||||
0x86: 'F23',
|
||||
0x87: 'F24',
|
||||
0x90: 'NumLock',
|
||||
0x91: 'ScrollLock',
|
||||
0xa6: 'BrowserBack',
|
||||
0xa7: 'BrowserForward',
|
||||
0xa8: 'BrowserRefresh',
|
||||
0xa9: 'BrowserStop',
|
||||
0xaa: 'BrowserSearch',
|
||||
0xab: 'BrowserFavorites',
|
||||
0xac: 'BrowserHome',
|
||||
0xad: 'AudioVolumeMute',
|
||||
0xae: 'AudioVolumeDown',
|
||||
0xaf: 'AudioVolumeUp',
|
||||
0xb0: 'MediaTrackNext',
|
||||
0xb1: 'MediaTrackPrevious',
|
||||
0xb2: 'MediaStop',
|
||||
0xb3: 'MediaPlayPause',
|
||||
0xb4: 'LaunchMail',
|
||||
0xb5: 'MediaSelect',
|
||||
0xb6: 'LaunchApp1',
|
||||
0xb7: 'LaunchApp2',
|
||||
0xe1: 'AltRight', // Only when it is AltGraph
|
||||
};
|
88
client/src/component/utils/logger.ts
Normal file
88
client/src/component/utils/logger.ts
Normal file
@ -0,0 +1,88 @@
|
||||
export class Logger {
|
||||
// eslint-disable-next-line
|
||||
constructor(
|
||||
protected readonly _scope: string = 'main',
|
||||
private readonly _color: boolean = false // !!process.env.VUE_APP_LOG_COLOR, // TODO: add support for color
|
||||
) {}
|
||||
|
||||
protected _console(level: string, m: string, fields?: Record<string, any>) {
|
||||
const scope = this._scope
|
||||
|
||||
let t = ''
|
||||
const args = []
|
||||
for (const name in fields) {
|
||||
if (fields[name] instanceof Error) {
|
||||
if (this._color) {
|
||||
t += ' %c%s="%s"%c'
|
||||
args.push('color:#d84949;', name, (fields[name] as Error).message, '')
|
||||
} else {
|
||||
t += ' %s="%s"'
|
||||
args.push(name, (fields[name] as Error).message)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (typeof fields[name] === 'string' || fields[name] instanceof String) {
|
||||
t += this._color ? ' %c%s=%c"%s"' : ' %s="%s"'
|
||||
} else {
|
||||
t += this._color ? ' %c%s=%c%o' : ' %s=%o'
|
||||
}
|
||||
|
||||
if (this._color) {
|
||||
args.push('color:#498ad8;', name, '', fields[name])
|
||||
} else {
|
||||
args.push(name, fields[name])
|
||||
}
|
||||
}
|
||||
|
||||
if (this._color) {
|
||||
switch (level) {
|
||||
case 'error':
|
||||
console.error('[%cNEKO%c] [%s] %cERR%c %s' + t, 'color:#498ad8;', '', scope, 'color:#d84949;', '', m, ...args)
|
||||
break
|
||||
case 'warn':
|
||||
console.warn('[%cNEKO%c] [%s] %cWRN%c %s' + t, 'color:#498ad8;', '', scope, 'color:#eae364;', '', m, ...args)
|
||||
break
|
||||
case 'info':
|
||||
console.info('[%cNEKO%c] [%s] %cINF%c %s' + t, 'color:#498ad8;', '', scope, 'color:#4ac94c;', '', m, ...args)
|
||||
break
|
||||
default:
|
||||
case 'debug':
|
||||
console.debug('[%cNEKO%c] [%s] %cDBG%c %s' + t, 'color:#498ad8;', '', scope, 'color:#eae364;', '', m, ...args)
|
||||
break
|
||||
}
|
||||
} else {
|
||||
switch (level) {
|
||||
case 'error':
|
||||
console.error('[NEKO] [%s] ERR %s' + t, scope, m, ...args)
|
||||
break
|
||||
case 'warn':
|
||||
console.warn('[NEKO] [%s] WRN %s' + t, scope, m, ...args)
|
||||
break
|
||||
case 'info':
|
||||
console.info('[NEKO] [%s] INF %s' + t, scope, m, ...args)
|
||||
break
|
||||
default:
|
||||
case 'debug':
|
||||
console.debug('[NEKO] [%s] DBG %s' + t, scope, m, ...args)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public error(message: string, fields?: Record<string, any>) {
|
||||
this._console('error', message, fields)
|
||||
}
|
||||
|
||||
public warn(message: string, fields?: Record<string, any>) {
|
||||
this._console('warn', message, fields)
|
||||
}
|
||||
|
||||
public info(message: string, fields?: Record<string, any>) {
|
||||
this._console('info', message, fields)
|
||||
}
|
||||
|
||||
public debug(message: string, fields?: Record<string, any>) {
|
||||
this._console('debug', message, fields)
|
||||
}
|
||||
}
|
33
client/src/component/utils/video-snap.ts
Normal file
33
client/src/component/utils/video-snap.ts
Normal file
@ -0,0 +1,33 @@
|
||||
// Takes a snapshot of the video and returns as object url
|
||||
export function videoSnap(video: HTMLVideoElement): Promise<string> {
|
||||
return new Promise((res, rej) => {
|
||||
const width = video.videoWidth
|
||||
const height = video.videoHeight
|
||||
|
||||
const canvas = document.createElement('canvas') as HTMLCanvasElement
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (ctx === null) {
|
||||
rej('canvas context is null')
|
||||
return
|
||||
}
|
||||
|
||||
// Define the size of the rectangle that will be filled (basically the entire element)
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
|
||||
// Grab the image from the video
|
||||
ctx.drawImage(video, 0, 0, width, height)
|
||||
|
||||
canvas.toBlob(function (blob) {
|
||||
if (blob === null) {
|
||||
rej('canvas blob is null')
|
||||
return
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(blob)
|
||||
res(url)
|
||||
})
|
||||
})
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user