Merge remote-tracking branch 'origin/demodesk-client-v3' into v3

This commit is contained in:
Miroslav Šedivý 2024-06-23 17:57:19 +02:00
commit ac76894800
161 changed files with 26015 additions and 17 deletions

95
.github/workflows/client_tags.yml vendored Normal file
View 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 }}

View File

@ -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

View File

@ -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 }}

View File

@ -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
View File

@ -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
View File

@ -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"
},
}

View File

@ -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
View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,4 @@
{
"extends": "@tsconfig/node20/tsconfig.json",
"include": ["./**/*"]
}

8
client/e2e/vue.spec.ts Normal file
View 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
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

12
client/index.html Normal file
View 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

File diff suppressed because it is too large Load Diff

61
client/package.json Normal file
View 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
View 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
View File

@ -0,0 +1,4 @@
wwwroot/*.js
node_modules
typings
dist

View File

@ -0,0 +1 @@
# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm

View 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

View 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

View File

@ -0,0 +1 @@
7.6.0-SNAPSHOT

View 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';

View 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));
}
}

View 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));
}
}

File diff suppressed because it is too large Load Diff

View 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));
}
}

View 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 = {
}

View 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);
};
}

View 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');
}
}

View 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";

View 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];

View 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];

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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';

View 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;
}

View 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;
}

View 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>;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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; };
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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; };
}

View 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>

View 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

View 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]
}
}

View 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)
}
}

View 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)
}
}

View 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')
}
}

View 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()
}
}

View 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)
}
}

View 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)
}
}

View 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
}

View 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)
}
}
}

View 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')
}
}

View 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>

View 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>

View 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>

View 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
}

View 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'

View 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
}

View File

@ -0,0 +1,5 @@
export interface ReconnectorConfig {
max_reconnects: number
timeout_ms: number
backoff_ms: number
}

View 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
}

View 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
}

View 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;
}

View 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
}

View 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 } };
}
}

View 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
}

View 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
}

View 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)
}

View 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
}

View 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

File diff suppressed because it is too large Load Diff

View 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
}

View 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);
}

View 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;

View 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',
};

View 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
}

View 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");
}
}

View 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,
};

View 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;
},
};

View 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;
}

View 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
};

View 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)
}
}

View 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