From 3e83277f596bb3928a979384de400a34e9713044 Mon Sep 17 00:00:00 2001
From: Christian Beier <info@christianbeier.net>
Date: Thu, 15 Aug 2024 16:23:28 +0200
Subject: [PATCH] assets: add noVNC v1.3.0 minus docs and tests

Using v1.3.0 since this is the last version that does not require HTTPS.

re #6
---
 app/src/main/assets/novnc/.eslintignore       |    1 +
 app/src/main/assets/novnc/.eslintrc           |   50 +
 .../.github/ISSUE_TEMPLATE/bug_report.md      |   34 +
 .../novnc/.github/ISSUE_TEMPLATE/config.yml   |    5 +
 .../.github/ISSUE_TEMPLATE/feature_request.md |   17 +
 .../assets/novnc/.github/workflows/deploy.yml |   56 +
 .../assets/novnc/.github/workflows/lint.yml   |   19 +
 .../assets/novnc/.github/workflows/test.yml   |   28 +
 app/src/main/assets/novnc/.gitignore          |   12 +
 app/src/main/assets/novnc/.gitmodules         |    0
 app/src/main/assets/novnc/AUTHORS             |   13 +
 app/src/main/assets/novnc/LICENSE.txt         |   62 +
 app/src/main/assets/novnc/README.md           |  214 ++
 .../main/assets/novnc/app/error-handler.js    |   66 +
 app/src/main/assets/novnc/app/images/alt.svg  |   92 +
 .../assets/novnc/app/images/clipboard.svg     |  106 +
 .../main/assets/novnc/app/images/connect.svg  |   96 +
 app/src/main/assets/novnc/app/images/ctrl.svg |   96 +
 .../assets/novnc/app/images/ctrlaltdel.svg    |  100 +
 .../assets/novnc/app/images/disconnect.svg    |   94 +
 app/src/main/assets/novnc/app/images/drag.svg |   76 +
 .../main/assets/novnc/app/images/error.svg    |   81 +
 app/src/main/assets/novnc/app/images/esc.svg  |   92 +
 .../main/assets/novnc/app/images/expander.svg |   69 +
 .../assets/novnc/app/images/fullscreen.svg    |   93 +
 .../main/assets/novnc/app/images/handle.svg   |   82 +
 .../assets/novnc/app/images/handle_bg.svg     |  172 +
 .../assets/novnc/app/images/icons/Makefile    |   42 +
 .../novnc/app/images/icons/novnc-120x120.png  |  Bin 0 -> 4028 bytes
 .../novnc/app/images/icons/novnc-144x144.png  |  Bin 0 -> 4582 bytes
 .../novnc/app/images/icons/novnc-152x152.png  |  Bin 0 -> 5216 bytes
 .../novnc/app/images/icons/novnc-16x16.png    |  Bin 0 -> 675 bytes
 .../novnc/app/images/icons/novnc-192x192.png  |  Bin 0 -> 5787 bytes
 .../novnc/app/images/icons/novnc-24x24.png    |  Bin 0 -> 1000 bytes
 .../novnc/app/images/icons/novnc-32x32.png    |  Bin 0 -> 1064 bytes
 .../novnc/app/images/icons/novnc-48x48.png    |  Bin 0 -> 1397 bytes
 .../novnc/app/images/icons/novnc-60x60.png    |  Bin 0 -> 1932 bytes
 .../novnc/app/images/icons/novnc-64x64.png    |  Bin 0 -> 1946 bytes
 .../novnc/app/images/icons/novnc-72x72.png    |  Bin 0 -> 2699 bytes
 .../novnc/app/images/icons/novnc-76x76.png    |  Bin 0 -> 2874 bytes
 .../novnc/app/images/icons/novnc-96x96.png    |  Bin 0 -> 2351 bytes
 .../novnc/app/images/icons/novnc-icon-sm.svg  |  163 +
 .../novnc/app/images/icons/novnc-icon.svg     |  163 +
 app/src/main/assets/novnc/app/images/info.svg |   81 +
 .../main/assets/novnc/app/images/keyboard.svg |   88 +
 .../main/assets/novnc/app/images/power.svg    |   87 +
 .../main/assets/novnc/app/images/settings.svg |   76 +
 app/src/main/assets/novnc/app/images/tab.svg  |   86 +
 .../novnc/app/images/toggleextrakeys.svg      |   90 +
 .../main/assets/novnc/app/images/warning.svg  |   81 +
 .../main/assets/novnc/app/images/windows.svg  |   65 +
 app/src/main/assets/novnc/app/locale/README   |    1 +
 app/src/main/assets/novnc/app/locale/cs.json  |   71 +
 app/src/main/assets/novnc/app/locale/de.json  |   69 +
 app/src/main/assets/novnc/app/locale/el.json  |   69 +
 app/src/main/assets/novnc/app/locale/es.json  |   68 +
 app/src/main/assets/novnc/app/locale/fr.json  |   72 +
 app/src/main/assets/novnc/app/locale/ja.json  |   72 +
 app/src/main/assets/novnc/app/locale/ko.json  |   70 +
 app/src/main/assets/novnc/app/locale/nl.json  |   73 +
 app/src/main/assets/novnc/app/locale/pl.json  |   69 +
 .../main/assets/novnc/app/locale/pt_BR.json   |   72 +
 app/src/main/assets/novnc/app/locale/ru.json  |   72 +
 app/src/main/assets/novnc/app/locale/sv.json  |   72 +
 app/src/main/assets/novnc/app/locale/tr.json  |   69 +
 .../main/assets/novnc/app/locale/zh_CN.json   |   69 +
 .../main/assets/novnc/app/locale/zh_TW.json   |   69 +
 app/src/main/assets/novnc/app/localization.js |  172 +
 app/src/main/assets/novnc/app/sounds/CREDITS  |    4 +
 app/src/main/assets/novnc/app/sounds/bell.mp3 |  Bin 0 -> 4531 bytes
 app/src/main/assets/novnc/app/sounds/bell.oga |  Bin 0 -> 8495 bytes
 .../assets/novnc/app/styles/Orbitron700.ttf   |  Bin 0 -> 38580 bytes
 .../assets/novnc/app/styles/Orbitron700.woff  |  Bin 0 -> 17472 bytes
 app/src/main/assets/novnc/app/styles/base.css |  970 ++++++
 app/src/main/assets/novnc/app/ui.js           | 1715 ++++++++++
 app/src/main/assets/novnc/app/webutil.js      |  186 +
 app/src/main/assets/novnc/core/base64.js      |  104 +
 .../assets/novnc/core/decoders/copyrect.js    |   27 +
 .../assets/novnc/core/decoders/hextile.js     |  191 ++
 .../main/assets/novnc/core/decoders/raw.js    |   66 +
 .../main/assets/novnc/core/decoders/rre.js    |   44 +
 .../main/assets/novnc/core/decoders/tight.js  |  331 ++
 .../assets/novnc/core/decoders/tightpng.js    |   27 +
 app/src/main/assets/novnc/core/deflator.js    |   85 +
 app/src/main/assets/novnc/core/des.js         |  266 ++
 app/src/main/assets/novnc/core/display.js     |  513 +++
 app/src/main/assets/novnc/core/encodings.js   |   44 +
 app/src/main/assets/novnc/core/inflator.js    |   66 +
 .../assets/novnc/core/input/domkeytable.js    |  311 ++
 .../main/assets/novnc/core/input/fixedkeys.js |  129 +
 .../assets/novnc/core/input/gesturehandler.js |  567 ++++
 .../main/assets/novnc/core/input/keyboard.js  |  273 ++
 .../main/assets/novnc/core/input/keysym.js    |  616 ++++
 .../main/assets/novnc/core/input/keysymdef.js |  688 ++++
 app/src/main/assets/novnc/core/input/util.js  |  191 ++
 app/src/main/assets/novnc/core/input/vkeys.js |  116 +
 .../assets/novnc/core/input/xtscancodes.js    |  173 +
 app/src/main/assets/novnc/core/rfb.js         | 2988 +++++++++++++++++
 .../main/assets/novnc/core/util/browser.js    |  103 +
 app/src/main/assets/novnc/core/util/cursor.js |  243 ++
 .../main/assets/novnc/core/util/element.js    |   32 +
 app/src/main/assets/novnc/core/util/events.js |  138 +
 .../assets/novnc/core/util/eventtarget.js     |   35 +
 app/src/main/assets/novnc/core/util/int.js    |   15 +
 .../main/assets/novnc/core/util/logging.js    |   56 +
 .../main/assets/novnc/core/util/strings.js    |   28 +
 app/src/main/assets/novnc/core/websock.js     |  353 ++
 app/src/main/assets/novnc/karma.conf.js       |   85 +
 app/src/main/assets/novnc/package.json        |   82 +
 app/src/main/assets/novnc/po/.eslintrc        |    5 +
 app/src/main/assets/novnc/po/Makefile         |   36 +
 app/src/main/assets/novnc/po/cs.po            |  294 ++
 app/src/main/assets/novnc/po/de.po            |  303 ++
 app/src/main/assets/novnc/po/el.po            |  323 ++
 app/src/main/assets/novnc/po/es.po            |  284 ++
 app/src/main/assets/novnc/po/fr.po            |  299 ++
 app/src/main/assets/novnc/po/ja.po            |  324 ++
 app/src/main/assets/novnc/po/ko.po            |  290 ++
 app/src/main/assets/novnc/po/nl.po            |  322 ++
 app/src/main/assets/novnc/po/noVNC.pot        |  298 ++
 app/src/main/assets/novnc/po/pl.po            |  325 ++
 app/src/main/assets/novnc/po/po2js            |   43 +
 app/src/main/assets/novnc/po/pt_BR.po         |  299 ++
 app/src/main/assets/novnc/po/ru.po            |  302 ++
 app/src/main/assets/novnc/po/sv.po            |  300 ++
 app/src/main/assets/novnc/po/tr.po            |  288 ++
 app/src/main/assets/novnc/po/xgettext-html    |  115 +
 app/src/main/assets/novnc/po/zh_CN.po         |  284 ++
 app/src/main/assets/novnc/po/zh_TW.po         |  285 ++
 .../main/assets/novnc/snap/hooks/configure    |    3 +
 .../assets/novnc/snap/local/svc_wrapper.sh    |   29 +
 app/src/main/assets/novnc/snap/snapcraft.yaml |   55 +
 app/src/main/assets/novnc/utils/.eslintrc     |    8 +
 app/src/main/assets/novnc/utils/README.md     |   14 +
 .../main/assets/novnc/utils/b64-to-binary.pl  |   17 +
 .../main/assets/novnc/utils/genkeysymdef.js   |  127 +
 app/src/main/assets/novnc/utils/novnc_proxy   |  198 ++
 app/src/main/assets/novnc/utils/u2x11         |   28 +
 .../main/assets/novnc/utils/use_require.js    |  140 +
 app/src/main/assets/novnc/utils/validate      |   45 +
 app/src/main/assets/novnc/vendor/pako/LICENSE |   21 +
 .../main/assets/novnc/vendor/pako/README.md   |    6 +
 .../novnc/vendor/pako/lib/utils/common.js     |   45 +
 .../novnc/vendor/pako/lib/zlib/adler32.js     |   27 +
 .../novnc/vendor/pako/lib/zlib/constants.js   |   47 +
 .../novnc/vendor/pako/lib/zlib/crc32.js       |   36 +
 .../novnc/vendor/pako/lib/zlib/deflate.js     | 1846 ++++++++++
 .../novnc/vendor/pako/lib/zlib/gzheader.js    |   35 +
 .../novnc/vendor/pako/lib/zlib/inffast.js     |  324 ++
 .../novnc/vendor/pako/lib/zlib/inflate.js     | 1527 +++++++++
 .../novnc/vendor/pako/lib/zlib/inftrees.js    |  322 ++
 .../novnc/vendor/pako/lib/zlib/messages.js    |   11 +
 .../novnc/vendor/pako/lib/zlib/trees.js       | 1195 +++++++
 .../novnc/vendor/pako/lib/zlib/zstream.js     |   24 +
 app/src/main/assets/novnc/vnc.html            |  320 ++
 app/src/main/assets/novnc/vnc_lite.html       |  189 ++
 156 files changed, 27596 insertions(+)
 create mode 100644 app/src/main/assets/novnc/.eslintignore
 create mode 100644 app/src/main/assets/novnc/.eslintrc
 create mode 100644 app/src/main/assets/novnc/.github/ISSUE_TEMPLATE/bug_report.md
 create mode 100644 app/src/main/assets/novnc/.github/ISSUE_TEMPLATE/config.yml
 create mode 100644 app/src/main/assets/novnc/.github/ISSUE_TEMPLATE/feature_request.md
 create mode 100644 app/src/main/assets/novnc/.github/workflows/deploy.yml
 create mode 100644 app/src/main/assets/novnc/.github/workflows/lint.yml
 create mode 100644 app/src/main/assets/novnc/.github/workflows/test.yml
 create mode 100644 app/src/main/assets/novnc/.gitignore
 create mode 100644 app/src/main/assets/novnc/.gitmodules
 create mode 100644 app/src/main/assets/novnc/AUTHORS
 create mode 100644 app/src/main/assets/novnc/LICENSE.txt
 create mode 100644 app/src/main/assets/novnc/README.md
 create mode 100644 app/src/main/assets/novnc/app/error-handler.js
 create mode 100644 app/src/main/assets/novnc/app/images/alt.svg
 create mode 100644 app/src/main/assets/novnc/app/images/clipboard.svg
 create mode 100644 app/src/main/assets/novnc/app/images/connect.svg
 create mode 100644 app/src/main/assets/novnc/app/images/ctrl.svg
 create mode 100644 app/src/main/assets/novnc/app/images/ctrlaltdel.svg
 create mode 100644 app/src/main/assets/novnc/app/images/disconnect.svg
 create mode 100644 app/src/main/assets/novnc/app/images/drag.svg
 create mode 100644 app/src/main/assets/novnc/app/images/error.svg
 create mode 100644 app/src/main/assets/novnc/app/images/esc.svg
 create mode 100644 app/src/main/assets/novnc/app/images/expander.svg
 create mode 100644 app/src/main/assets/novnc/app/images/fullscreen.svg
 create mode 100644 app/src/main/assets/novnc/app/images/handle.svg
 create mode 100644 app/src/main/assets/novnc/app/images/handle_bg.svg
 create mode 100644 app/src/main/assets/novnc/app/images/icons/Makefile
 create mode 100644 app/src/main/assets/novnc/app/images/icons/novnc-120x120.png
 create mode 100644 app/src/main/assets/novnc/app/images/icons/novnc-144x144.png
 create mode 100644 app/src/main/assets/novnc/app/images/icons/novnc-152x152.png
 create mode 100644 app/src/main/assets/novnc/app/images/icons/novnc-16x16.png
 create mode 100644 app/src/main/assets/novnc/app/images/icons/novnc-192x192.png
 create mode 100644 app/src/main/assets/novnc/app/images/icons/novnc-24x24.png
 create mode 100644 app/src/main/assets/novnc/app/images/icons/novnc-32x32.png
 create mode 100644 app/src/main/assets/novnc/app/images/icons/novnc-48x48.png
 create mode 100644 app/src/main/assets/novnc/app/images/icons/novnc-60x60.png
 create mode 100644 app/src/main/assets/novnc/app/images/icons/novnc-64x64.png
 create mode 100644 app/src/main/assets/novnc/app/images/icons/novnc-72x72.png
 create mode 100644 app/src/main/assets/novnc/app/images/icons/novnc-76x76.png
 create mode 100644 app/src/main/assets/novnc/app/images/icons/novnc-96x96.png
 create mode 100644 app/src/main/assets/novnc/app/images/icons/novnc-icon-sm.svg
 create mode 100644 app/src/main/assets/novnc/app/images/icons/novnc-icon.svg
 create mode 100644 app/src/main/assets/novnc/app/images/info.svg
 create mode 100644 app/src/main/assets/novnc/app/images/keyboard.svg
 create mode 100644 app/src/main/assets/novnc/app/images/power.svg
 create mode 100644 app/src/main/assets/novnc/app/images/settings.svg
 create mode 100644 app/src/main/assets/novnc/app/images/tab.svg
 create mode 100644 app/src/main/assets/novnc/app/images/toggleextrakeys.svg
 create mode 100644 app/src/main/assets/novnc/app/images/warning.svg
 create mode 100644 app/src/main/assets/novnc/app/images/windows.svg
 create mode 100644 app/src/main/assets/novnc/app/locale/README
 create mode 100644 app/src/main/assets/novnc/app/locale/cs.json
 create mode 100644 app/src/main/assets/novnc/app/locale/de.json
 create mode 100644 app/src/main/assets/novnc/app/locale/el.json
 create mode 100644 app/src/main/assets/novnc/app/locale/es.json
 create mode 100644 app/src/main/assets/novnc/app/locale/fr.json
 create mode 100644 app/src/main/assets/novnc/app/locale/ja.json
 create mode 100644 app/src/main/assets/novnc/app/locale/ko.json
 create mode 100644 app/src/main/assets/novnc/app/locale/nl.json
 create mode 100644 app/src/main/assets/novnc/app/locale/pl.json
 create mode 100644 app/src/main/assets/novnc/app/locale/pt_BR.json
 create mode 100644 app/src/main/assets/novnc/app/locale/ru.json
 create mode 100644 app/src/main/assets/novnc/app/locale/sv.json
 create mode 100644 app/src/main/assets/novnc/app/locale/tr.json
 create mode 100644 app/src/main/assets/novnc/app/locale/zh_CN.json
 create mode 100644 app/src/main/assets/novnc/app/locale/zh_TW.json
 create mode 100644 app/src/main/assets/novnc/app/localization.js
 create mode 100644 app/src/main/assets/novnc/app/sounds/CREDITS
 create mode 100644 app/src/main/assets/novnc/app/sounds/bell.mp3
 create mode 100644 app/src/main/assets/novnc/app/sounds/bell.oga
 create mode 100644 app/src/main/assets/novnc/app/styles/Orbitron700.ttf
 create mode 100644 app/src/main/assets/novnc/app/styles/Orbitron700.woff
 create mode 100644 app/src/main/assets/novnc/app/styles/base.css
 create mode 100644 app/src/main/assets/novnc/app/ui.js
 create mode 100644 app/src/main/assets/novnc/app/webutil.js
 create mode 100644 app/src/main/assets/novnc/core/base64.js
 create mode 100644 app/src/main/assets/novnc/core/decoders/copyrect.js
 create mode 100644 app/src/main/assets/novnc/core/decoders/hextile.js
 create mode 100644 app/src/main/assets/novnc/core/decoders/raw.js
 create mode 100644 app/src/main/assets/novnc/core/decoders/rre.js
 create mode 100644 app/src/main/assets/novnc/core/decoders/tight.js
 create mode 100644 app/src/main/assets/novnc/core/decoders/tightpng.js
 create mode 100644 app/src/main/assets/novnc/core/deflator.js
 create mode 100644 app/src/main/assets/novnc/core/des.js
 create mode 100644 app/src/main/assets/novnc/core/display.js
 create mode 100644 app/src/main/assets/novnc/core/encodings.js
 create mode 100644 app/src/main/assets/novnc/core/inflator.js
 create mode 100644 app/src/main/assets/novnc/core/input/domkeytable.js
 create mode 100644 app/src/main/assets/novnc/core/input/fixedkeys.js
 create mode 100644 app/src/main/assets/novnc/core/input/gesturehandler.js
 create mode 100644 app/src/main/assets/novnc/core/input/keyboard.js
 create mode 100644 app/src/main/assets/novnc/core/input/keysym.js
 create mode 100644 app/src/main/assets/novnc/core/input/keysymdef.js
 create mode 100644 app/src/main/assets/novnc/core/input/util.js
 create mode 100644 app/src/main/assets/novnc/core/input/vkeys.js
 create mode 100644 app/src/main/assets/novnc/core/input/xtscancodes.js
 create mode 100644 app/src/main/assets/novnc/core/rfb.js
 create mode 100644 app/src/main/assets/novnc/core/util/browser.js
 create mode 100644 app/src/main/assets/novnc/core/util/cursor.js
 create mode 100644 app/src/main/assets/novnc/core/util/element.js
 create mode 100644 app/src/main/assets/novnc/core/util/events.js
 create mode 100644 app/src/main/assets/novnc/core/util/eventtarget.js
 create mode 100644 app/src/main/assets/novnc/core/util/int.js
 create mode 100644 app/src/main/assets/novnc/core/util/logging.js
 create mode 100644 app/src/main/assets/novnc/core/util/strings.js
 create mode 100644 app/src/main/assets/novnc/core/websock.js
 create mode 100644 app/src/main/assets/novnc/karma.conf.js
 create mode 100644 app/src/main/assets/novnc/package.json
 create mode 100644 app/src/main/assets/novnc/po/.eslintrc
 create mode 100644 app/src/main/assets/novnc/po/Makefile
 create mode 100644 app/src/main/assets/novnc/po/cs.po
 create mode 100644 app/src/main/assets/novnc/po/de.po
 create mode 100644 app/src/main/assets/novnc/po/el.po
 create mode 100644 app/src/main/assets/novnc/po/es.po
 create mode 100644 app/src/main/assets/novnc/po/fr.po
 create mode 100644 app/src/main/assets/novnc/po/ja.po
 create mode 100644 app/src/main/assets/novnc/po/ko.po
 create mode 100644 app/src/main/assets/novnc/po/nl.po
 create mode 100644 app/src/main/assets/novnc/po/noVNC.pot
 create mode 100644 app/src/main/assets/novnc/po/pl.po
 create mode 100755 app/src/main/assets/novnc/po/po2js
 create mode 100644 app/src/main/assets/novnc/po/pt_BR.po
 create mode 100644 app/src/main/assets/novnc/po/ru.po
 create mode 100644 app/src/main/assets/novnc/po/sv.po
 create mode 100644 app/src/main/assets/novnc/po/tr.po
 create mode 100755 app/src/main/assets/novnc/po/xgettext-html
 create mode 100644 app/src/main/assets/novnc/po/zh_CN.po
 create mode 100644 app/src/main/assets/novnc/po/zh_TW.po
 create mode 100644 app/src/main/assets/novnc/snap/hooks/configure
 create mode 100755 app/src/main/assets/novnc/snap/local/svc_wrapper.sh
 create mode 100644 app/src/main/assets/novnc/snap/snapcraft.yaml
 create mode 100644 app/src/main/assets/novnc/utils/.eslintrc
 create mode 100644 app/src/main/assets/novnc/utils/README.md
 create mode 100755 app/src/main/assets/novnc/utils/b64-to-binary.pl
 create mode 100755 app/src/main/assets/novnc/utils/genkeysymdef.js
 create mode 100755 app/src/main/assets/novnc/utils/novnc_proxy
 create mode 100755 app/src/main/assets/novnc/utils/u2x11
 create mode 100755 app/src/main/assets/novnc/utils/use_require.js
 create mode 100755 app/src/main/assets/novnc/utils/validate
 create mode 100644 app/src/main/assets/novnc/vendor/pako/LICENSE
 create mode 100644 app/src/main/assets/novnc/vendor/pako/README.md
 create mode 100644 app/src/main/assets/novnc/vendor/pako/lib/utils/common.js
 create mode 100644 app/src/main/assets/novnc/vendor/pako/lib/zlib/adler32.js
 create mode 100644 app/src/main/assets/novnc/vendor/pako/lib/zlib/constants.js
 create mode 100644 app/src/main/assets/novnc/vendor/pako/lib/zlib/crc32.js
 create mode 100644 app/src/main/assets/novnc/vendor/pako/lib/zlib/deflate.js
 create mode 100644 app/src/main/assets/novnc/vendor/pako/lib/zlib/gzheader.js
 create mode 100644 app/src/main/assets/novnc/vendor/pako/lib/zlib/inffast.js
 create mode 100644 app/src/main/assets/novnc/vendor/pako/lib/zlib/inflate.js
 create mode 100644 app/src/main/assets/novnc/vendor/pako/lib/zlib/inftrees.js
 create mode 100644 app/src/main/assets/novnc/vendor/pako/lib/zlib/messages.js
 create mode 100644 app/src/main/assets/novnc/vendor/pako/lib/zlib/trees.js
 create mode 100644 app/src/main/assets/novnc/vendor/pako/lib/zlib/zstream.js
 create mode 100644 app/src/main/assets/novnc/vnc.html
 create mode 100644 app/src/main/assets/novnc/vnc_lite.html

diff --git a/app/src/main/assets/novnc/.eslintignore b/app/src/main/assets/novnc/.eslintignore
new file mode 100644
index 00000000..d3816280
--- /dev/null
+++ b/app/src/main/assets/novnc/.eslintignore
@@ -0,0 +1 @@
+**/xtscancodes.js
diff --git a/app/src/main/assets/novnc/.eslintrc b/app/src/main/assets/novnc/.eslintrc
new file mode 100644
index 00000000..a53bb402
--- /dev/null
+++ b/app/src/main/assets/novnc/.eslintrc
@@ -0,0 +1,50 @@
+{
+    "env": {
+        "browser": true,
+        "es6": true
+    },
+    "parserOptions": {
+        "sourceType": "module"
+    },
+    "extends": "eslint:recommended",
+    "rules": {
+        // Unsafe or confusing stuff that we forbid
+
+        "no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": true }],
+        "no-constant-condition": ["error", { "checkLoops": false }],
+        "no-var": "error",
+        "no-useless-constructor": "error",
+        "object-shorthand": ["error", "methods", { "avoidQuotes": true }],
+        "prefer-arrow-callback": "error",
+        "arrow-body-style": ["error", "as-needed", { "requireReturnForObjectLiteral": false } ],
+        "arrow-parens": ["error", "as-needed", { "requireForBlockBody": true }],
+        "arrow-spacing": ["error"],
+        "no-confusing-arrow": ["error", { "allowParens": true }],
+
+        // Enforced coding style
+
+        "brace-style": ["error", "1tbs", { "allowSingleLine": true }],
+        "indent": ["error", 4, { "SwitchCase": 1,
+                                 "FunctionDeclaration": { "parameters": "first" },
+                                 "CallExpression": { "arguments": "first" },
+                                 "ArrayExpression": "first",
+                                 "ObjectExpression": "first",
+                                 "ignoreComments": true }],
+        "comma-spacing": ["error"],
+        "comma-style": ["error"],
+        "curly": ["error", "multi-line"],
+        "func-call-spacing": ["error"],
+        "func-names": ["error"],
+        "func-style": ["error", "declaration", { "allowArrowFunctions": true }],
+        "key-spacing": ["error"],
+        "keyword-spacing": ["error"],
+        "no-trailing-spaces": ["error"],
+        "semi": ["error"],
+        "space-before-blocks": ["error"],
+        "space-before-function-paren": ["error", { "anonymous": "always",
+                                                   "named": "never",
+                                                   "asyncArrow": "always" }],
+        "switch-colon-spacing": ["error"],
+        "camelcase": ["error", { allow: ["^XK_", "^XF86XK_"] }],
+    }
+}
diff --git a/app/src/main/assets/novnc/.github/ISSUE_TEMPLATE/bug_report.md b/app/src/main/assets/novnc/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..94ac6f8d
--- /dev/null
+++ b/app/src/main/assets/novnc/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,34 @@
+---
+name: Bug report
+about: Create a report to help us improve
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Client (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser: [e.g. chrome, safari]
+ - Browser version: [e.g. 22]
+
+**Server (please complete the following information):**
+ - noVNC version: [e.g. 1.0.0 or git commit id]
+ - VNC server: [e.g. QEMU, TigerVNC]
+ - WebSocket proxy: [e.g. websockify]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/app/src/main/assets/novnc/.github/ISSUE_TEMPLATE/config.yml b/app/src/main/assets/novnc/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..cbd35aa7
--- /dev/null
+++ b/app/src/main/assets/novnc/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+  - name: Question or discussion
+    url: https://groups.google.com/forum/?fromgroups#!forum/novnc
+    about: Ask a question or start a discussion
diff --git a/app/src/main/assets/novnc/.github/ISSUE_TEMPLATE/feature_request.md b/app/src/main/assets/novnc/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000..066b2d92
--- /dev/null
+++ b/app/src/main/assets/novnc/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,17 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/app/src/main/assets/novnc/.github/workflows/deploy.yml b/app/src/main/assets/novnc/.github/workflows/deploy.yml
new file mode 100644
index 00000000..09bea6a4
--- /dev/null
+++ b/app/src/main/assets/novnc/.github/workflows/deploy.yml
@@ -0,0 +1,56 @@
+name: Publish
+
+on:
+  push:
+  pull_request:
+  release:
+    types: [published]
+
+jobs:
+  npm:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-node@v1
+        with:
+          # Needs to be explicitly specified for auth to work
+          registry-url: 'https://registry.npmjs.org'
+      - run: npm install
+      - uses: actions/upload-artifact@v2
+        with:
+          name: npm
+          path: lib
+      - run: npm publish --access public
+        env:
+          NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
+        if: ${{ github.event_name == 'release' && !github.event.release.prerelease }}
+      - run: npm publish --access public --tag beta
+        env:
+          NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
+        if: ${{ github.event_name == 'release' && github.event.release.prerelease }}
+  snap:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - run: |
+          VERSION=$(grep '"version"' package.json | cut -d '"' -f 4)
+          echo $VERSION
+          sed -i "s/@VERSION@/$VERSION/g" snap/snapcraft.yaml
+      - uses: snapcore/action-build@v1
+        id: snapcraft
+      - uses: actions/upload-artifact@v2
+        with:
+          name: snap
+          path: ${{ steps.snapcraft.outputs.snap }}
+      - uses: snapcore/action-publish@v1
+        with:
+          store_login: ${{ secrets.SNAPCRAFT_LOGIN }}
+          snap: ${{ steps.snapcraft.outputs.snap }}
+          release: stable
+        if: ${{ github.event_name == 'release' && !github.event.release.prerelease }}
+      - uses: snapcore/action-publish@v1
+        with:
+          store_login: ${{ secrets.SNAPCRAFT_LOGIN }}
+          snap: ${{ steps.snapcraft.outputs.snap }}
+          release: beta
+        if: ${{ github.event_name == 'release' && github.event.release.prerelease }}
diff --git a/app/src/main/assets/novnc/.github/workflows/lint.yml b/app/src/main/assets/novnc/.github/workflows/lint.yml
new file mode 100644
index 00000000..aaa36736
--- /dev/null
+++ b/app/src/main/assets/novnc/.github/workflows/lint.yml
@@ -0,0 +1,19 @@
+name: Lint
+
+on: [push, pull_request]
+
+jobs:
+  eslint:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-node@v1
+      - run: npm install
+      - run: npm run lint
+  html:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-node@v1
+      - run: npm install
+      - run: git ls-tree --name-only -r HEAD | grep -E "[.](html|css)$" | xargs ./utils/validate
diff --git a/app/src/main/assets/novnc/.github/workflows/test.yml b/app/src/main/assets/novnc/.github/workflows/test.yml
new file mode 100644
index 00000000..a0bcb36b
--- /dev/null
+++ b/app/src/main/assets/novnc/.github/workflows/test.yml
@@ -0,0 +1,28 @@
+name: Test
+
+on: [push, pull_request]
+
+jobs:
+  test:
+    strategy:
+      matrix:
+        os:
+          - ubuntu-latest
+          - windows-latest
+        browser:
+          - ChromeHeadless
+          - FirefoxHeadless
+        include:
+          - os: macos-latest
+            browser: Safari
+          - os: windows-latest
+            browser: EdgeHeadless
+      fail-fast: false
+    runs-on: ${{ matrix.os }}
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-node@v1
+      - run: npm install
+      - run: npm run test
+        env:
+          TEST_BROWSER_NAME: ${{ matrix.browser }}
diff --git a/app/src/main/assets/novnc/.gitignore b/app/src/main/assets/novnc/.gitignore
new file mode 100644
index 00000000..c178dbab
--- /dev/null
+++ b/app/src/main/assets/novnc/.gitignore
@@ -0,0 +1,12 @@
+*.pyc
+*.o
+tests/data_*.js
+utils/rebind.so
+utils/websockify
+/node_modules
+/build
+/lib
+recordings
+*.swp
+*~
+noVNC-*.tgz
diff --git a/app/src/main/assets/novnc/.gitmodules b/app/src/main/assets/novnc/.gitmodules
new file mode 100644
index 00000000..e69de29b
diff --git a/app/src/main/assets/novnc/AUTHORS b/app/src/main/assets/novnc/AUTHORS
new file mode 100644
index 00000000..dec0e893
--- /dev/null
+++ b/app/src/main/assets/novnc/AUTHORS
@@ -0,0 +1,13 @@
+maintainers:
+- Joel Martin (@kanaka)
+- Solly Ross (@directxman12)
+- Samuel Mannehed for Cendio AB (@samhed)
+- Pierre Ossman for Cendio AB (@CendioOssman)
+maintainersEmeritus:
+- @astrand 
+contributors:
+# There are a bunch of people that should be here.
+# If you want to be on this list, feel free send a PR
+# to add yourself.
+- jalf <git@jalf.dk>
+- NTT corp.
diff --git a/app/src/main/assets/novnc/LICENSE.txt b/app/src/main/assets/novnc/LICENSE.txt
new file mode 100644
index 00000000..ee81d202
--- /dev/null
+++ b/app/src/main/assets/novnc/LICENSE.txt
@@ -0,0 +1,62 @@
+noVNC is Copyright (C) 2019 The noVNC Authors
+(./AUTHORS)
+
+The noVNC core library files are licensed under the MPL 2.0 (Mozilla
+Public License 2.0). The noVNC core library is composed of the
+Javascript code necessary for full noVNC operation. This includes (but
+is not limited to):
+
+    core/**/*.js
+    app/*.js
+    test/playback.js
+
+The HTML, CSS, font and images files that included with the noVNC
+source distibution (or repository) are not considered part of the
+noVNC core library and are licensed under more permissive licenses.
+The intent is to allow easy integration of noVNC into existing web
+sites and web applications.
+
+The HTML, CSS, font and image files are licensed as follows:
+
+    *.html                     : 2-Clause BSD license
+
+    app/styles/*.css           : 2-Clause BSD license
+
+    app/styles/Orbitron*       : SIL Open Font License 1.1
+                                 (Copyright 2009 Matt McInerney)
+
+    app/images/                : Creative Commons Attribution-ShareAlike
+                                 http://creativecommons.org/licenses/by-sa/3.0/
+
+Some portions of noVNC are copyright to their individual authors.
+Please refer to the individual source files and/or to the noVNC commit
+history: https://github.com/novnc/noVNC/commits/master
+
+The are several files and projects that have been incorporated into
+the noVNC core library. Here is a list of those files and the original
+licenses (all MPL 2.0 compatible):
+
+    core/base64.js          : MPL 2.0
+
+    core/des.js             : Various BSD style licenses
+
+    vendor/pako/            : MIT
+
+Any other files not mentioned above are typically marked with
+a copyright/license header at the top of the file. The default noVNC
+license is MPL-2.0.
+
+The following license texts are included:
+
+    docs/LICENSE.MPL-2.0
+    docs/LICENSE.OFL-1.1
+    docs/LICENSE.BSD-3-Clause (New BSD)
+    docs/LICENSE.BSD-2-Clause (Simplified BSD / FreeBSD)
+    vendor/pako/LICENSE (MIT)
+
+Or alternatively the license texts may be found here:
+
+    http://www.mozilla.org/MPL/2.0/
+    http://scripts.sil.org/OFL
+    http://en.wikipedia.org/wiki/BSD_licenses
+    https://opensource.org/licenses/MIT
diff --git a/app/src/main/assets/novnc/README.md b/app/src/main/assets/novnc/README.md
new file mode 100644
index 00000000..3f8abf85
--- /dev/null
+++ b/app/src/main/assets/novnc/README.md
@@ -0,0 +1,214 @@
+## noVNC: HTML VNC Client Library and Application
+
+[![Test Status](https://github.com/novnc/noVNC/workflows/Test/badge.svg)](https://github.com/novnc/noVNC/actions?query=workflow%3ATest)
+[![Lint Status](https://github.com/novnc/noVNC/workflows/Lint/badge.svg)](https://github.com/novnc/noVNC/actions?query=workflow%3ALint)
+
+### Description
+
+noVNC is both a HTML VNC client JavaScript library and an application built on
+top of that library. noVNC runs well in any modern browser including mobile
+browsers (iOS and Android).
+
+Many companies, projects and products have integrated noVNC including
+[OpenStack](http://www.openstack.org),
+[OpenNebula](http://opennebula.org/),
+[LibVNCServer](http://libvncserver.sourceforge.net), and
+[ThinLinc](https://cendio.com/thinlinc). See
+[the Projects and Companies wiki page](https://github.com/novnc/noVNC/wiki/Projects-and-companies-using-noVNC)
+for a more complete list with additional info and links.
+
+### Table of Contents
+
+- [News/help/contact](#newshelpcontact)
+- [Features](#features)
+- [Screenshots](#screenshots)
+- [Browser Requirements](#browser-requirements)
+- [Server Requirements](#server-requirements)
+- [Quick Start](#quick-start)
+- [Installation from Snap Package](#installation-from-snap-package)
+- [Integration and Deployment](#integration-and-deployment)
+- [Authors/Contributors](#authorscontributors)
+
+### News/help/contact
+
+The project website is found at [novnc.com](http://novnc.com).
+Notable commits, announcements and news are posted to
+[@noVNC](http://www.twitter.com/noVNC).
+
+If you are a noVNC developer/integrator/user (or want to be) please join the
+[noVNC discussion group](https://groups.google.com/forum/?fromgroups#!forum/novnc).
+
+Bugs and feature requests can be submitted via
+[github issues](https://github.com/novnc/noVNC/issues). If you have questions
+about using noVNC then please first use the
+[discussion group](https://groups.google.com/forum/?fromgroups#!forum/novnc).
+We also have a [wiki](https://github.com/novnc/noVNC/wiki/) with lots of
+helpful information.
+
+If you are looking for a place to start contributing to noVNC, a good place to
+start would be the issues that are marked as
+["patchwelcome"](https://github.com/novnc/noVNC/issues?labels=patchwelcome).
+Please check our
+[contribution guide](https://github.com/novnc/noVNC/wiki/Contributing) though.
+
+If you want to show appreciation for noVNC you could donate to a great non-
+profits such as:
+[Compassion International](http://www.compassion.com/),
+[SIL](http://www.sil.org),
+[Habitat for Humanity](http://www.habitat.org),
+[Electronic Frontier Foundation](https://www.eff.org/),
+[Against Malaria Foundation](http://www.againstmalaria.com/),
+[Nothing But Nets](http://www.nothingbutnets.net/), etc.
+Please tweet [@noVNC](http://www.twitter.com/noVNC) if you do.
+
+
+### Features
+
+* Supports all modern browsers including mobile (iOS, Android)
+* Supported VNC encodings: raw, copyrect, rre, hextile, tight, tightPNG
+* Supports scaling, clipping and resizing the desktop
+* Local cursor rendering
+* Clipboard copy/paste
+* Translations
+* Touch gestures for emulating common mouse actions
+* Licensed mainly under the [MPL 2.0](http://www.mozilla.org/MPL/2.0/), see
+  [the license document](LICENSE.txt) for details
+
+### Screenshots
+
+Running in Firefox before and after connecting:
+
+<img src="http://novnc.com/img/noVNC-1-login.png" width=400>&nbsp;
+<img src="http://novnc.com/img/noVNC-3-connected.png" width=400>
+
+See more screenshots
+[here](http://novnc.com/screenshots.html).
+
+
+### Browser Requirements
+
+noVNC uses many modern web technologies so a formal requirement list is
+not available. However these are the minimum versions we are currently
+aware of:
+
+* Chrome 64, Firefox 79, Safari 13.4, Opera 51, Edge 79
+
+
+### Server Requirements
+
+noVNC follows the standard VNC protocol, but unlike other VNC clients it does
+require WebSockets support. Many servers include support (e.g.
+[x11vnc/libvncserver](http://libvncserver.sourceforge.net/),
+[QEMU](http://www.qemu.org/), and
+[MobileVNC](http://www.smartlab.at/mobilevnc/)), but for the others you need to
+use a WebSockets to TCP socket proxy. noVNC has a sister project
+[websockify](https://github.com/novnc/websockify) that provides a simple such
+proxy.
+
+
+### Quick Start
+
+* Use the `novnc_proxy` script to automatically download and start websockify, which
+  includes a mini-webserver and the WebSockets proxy. The `--vnc` option is
+  used to specify the location of a running VNC server:
+
+    `./utils/novnc_proxy --vnc localhost:5901`
+
+* Point your browser to the cut-and-paste URL that is output by the `novnc_procy`
+  script. Hit the Connect button, enter a password if the VNC server has one
+  configured, and enjoy!
+
+### Installation from Snap Package
+Running the command below will install the latest release of noVNC from Snap:
+
+`sudo snap install novnc`
+
+#### Running noVNC
+
+You can run the Snap-package installed novnc directly with, for example:
+
+`novnc --listen 6081 --vnc localhost:5901 # /snap/bin/novnc if /snap/bin is not in your PATH`
+
+#### Running as a Service (Daemon)
+The Snap package also has the capability to run a 'novnc' service which can be 
+configured to listen on multiple ports connecting to multiple VNC servers 
+(effectively a service runing multiple instances of novnc).
+Instructions (with example values):
+
+List current services (out-of-box this will be blank):
+
+```
+sudo snap get novnc services
+Key             Value
+services.n6080  {...}
+services.n6081  {...}
+```
+
+Create a new service that listens on port 6082 and connects to the VNC server 
+running on port 5902 on localhost:
+
+`sudo snap set novnc services.n6082.listen=6082 services.n6082.vnc=localhost:5902`
+
+(Any services you define with 'snap set' will be automatically started)
+Note that the name of the service, 'n6082' in this example, can be anything 
+as long as it doesn't start with a number or contain spaces/special characters.
+
+View the configuration of the service just created:
+
+```
+sudo snap get novnc services.n6082
+Key                    Value
+services.n6082.listen  6082
+services.n6082.vnc     localhost:5902
+```
+
+Disable a service (note that because of a limitation in  Snap it's currently not 
+possible to unset config variables, setting them to blank values is the way 
+to disable a service):
+
+`sudo snap set novnc services.n6082.listen='' services.n6082.vnc=''`
+
+(Any services you set to blank with 'snap set' like this will be automatically stopped)
+
+Verify that the service is disabled (blank values):
+
+```
+sudo snap get novnc services.n6082
+Key                    Value
+services.n6082.listen  
+services.n6082.vnc
+```
+
+### Integration and Deployment
+
+Please see our other documents for how to integrate noVNC in your own software,
+or deploying the noVNC application in production environments:
+
+* [Embedding](docs/EMBEDDING.md) - For the noVNC application
+* [Library](docs/LIBRARY.md) - For the noVNC JavaScript library
+
+
+### Authors/Contributors
+
+See [AUTHORS](AUTHORS) for a (full-ish) list of authors.  If you're not on
+that list and you think you should be, feel free to send a PR to fix that.
+
+* Core team:
+    * [Joel Martin](https://github.com/kanaka)
+    * [Samuel Mannehed](https://github.com/samhed) (Cendio)
+    * [Solly Ross](https://github.com/DirectXMan12) (Red Hat / OpenStack)
+    * [Pierre Ossman](https://github.com/CendioOssman) (Cendio)
+
+* Notable contributions:
+    * UI and Icons : Pierre Ossman, Chris Gordon
+    * Original Logo : Michael Sersen
+    * tight encoding : Michael Tinglof (Mercuri.ca)
+
+* Included libraries:
+    * base64 : Martijn Pieters (Digital Creations 2), Samuel Sieb (sieb.net)
+    * DES : Dave Zimmerman (Widget Workshop), Jef Poskanzer (ACME Labs)
+    * Pako : Vitaly Puzrin (https://github.com/nodeca/pako)
+
+Do you want to be on this list? Check out our
+[contribution guide](https://github.com/novnc/noVNC/wiki/Contributing) and
+start hacking!
diff --git a/app/src/main/assets/novnc/app/error-handler.js b/app/src/main/assets/novnc/app/error-handler.js
new file mode 100644
index 00000000..81a6cba8
--- /dev/null
+++ b/app/src/main/assets/novnc/app/error-handler.js
@@ -0,0 +1,66 @@
+/*
+ * 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.
+ */
+
+// NB: this should *not* be included as a module until we have
+// native support in the browsers, so that our error handler
+// can catch script-loading errors.
+
+// No ES6 can be used in this file since it's used for the translation
+/* eslint-disable prefer-arrow-callback */
+
+(function _scope() {
+    "use strict";
+
+    // Fallback for all uncought errors
+    function handleError(event, err) {
+        try {
+            const msg = document.getElementById('noVNC_fallback_errormsg');
+
+            // Only show the initial error
+            if (msg.hasChildNodes()) {
+                return false;
+            }
+
+            let div = document.createElement("div");
+            div.classList.add('noVNC_message');
+            div.appendChild(document.createTextNode(event.message));
+            msg.appendChild(div);
+
+            if (event.filename) {
+                div = document.createElement("div");
+                div.className = 'noVNC_location';
+                let text = event.filename;
+                if (event.lineno !== undefined) {
+                    text += ":" + event.lineno;
+                    if (event.colno !== undefined) {
+                        text += ":" + event.colno;
+                    }
+                }
+                div.appendChild(document.createTextNode(text));
+                msg.appendChild(div);
+            }
+
+            if (err && err.stack) {
+                div = document.createElement("div");
+                div.className = 'noVNC_stack';
+                div.appendChild(document.createTextNode(err.stack));
+                msg.appendChild(div);
+            }
+
+            document.getElementById('noVNC_fallback_error')
+                .classList.add("noVNC_open");
+        } catch (exc) {
+            document.write("noVNC encountered an error.");
+        }
+        // Don't return true since this would prevent the error
+        // from being printed to the browser console.
+        return false;
+    }
+    window.addEventListener('error', function onerror(evt) { handleError(evt, evt.error); });
+    window.addEventListener('unhandledrejection', function onreject(evt) { handleError(evt.reason, evt.reason); });
+})();
diff --git a/app/src/main/assets/novnc/app/images/alt.svg b/app/src/main/assets/novnc/app/images/alt.svg
new file mode 100644
index 00000000..e5bb4612
--- /dev/null
+++ b/app/src/main/assets/novnc/app/images/alt.svg
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="25"
+   height="25"
+   viewBox="0 0 25 25"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="alt.svg"
+   inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#959595"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="16"
+     inkscape:cx="18.205425"
+     inkscape:cy="17.531398"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:object-paths="true"
+     showguides="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1920"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-intersection-paths="true"
+     inkscape:snap-nodes="true"
+     inkscape:snap-global="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4136" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1027.3622)">
+    <g
+       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:48px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'Sans Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       id="text5290">
+      <path
+         d="m 9.9560547,1042.3329 -2.9394531,0 -0.4638672,1.3281 -1.8896485,0 2.7001953,-7.29 2.241211,0 2.7001958,7.29 -1.889649,0 -0.4589843,-1.3281 z m -2.4707031,-1.3526 1.9970703,0 -0.9960938,-2.9003 -1.0009765,2.9003 z"
+         style="font-size:10px;fill:#ffffff;fill-opacity:1"
+         id="path5340" />
+      <path
+         d="m 13.188477,1036.0634 1.748046,0 0,7.5976 -1.748046,0 0,-7.5976 z"
+         style="font-size:10px;fill:#ffffff;fill-opacity:1"
+         id="path5342" />
+      <path
+         d="m 18.535156,1036.6395 0,1.5528 1.801758,0 0,1.25 -1.801758,0 0,2.3193 q 0,0.3809 0.151367,0.5176 0.151368,0.1318 0.600586,0.1318 l 0.898438,0 0,1.25 -1.499024,0 q -1.035156,0 -1.469726,-0.4297 -0.429688,-0.4345 -0.429688,-1.4697 l 0,-2.3193 -0.86914,0 0,-1.25 0.86914,0 0,-1.5528 1.748047,0 z"
+         style="font-size:10px;fill:#ffffff;fill-opacity:1"
+         id="path5344" />
+    </g>
+  </g>
+</svg>
diff --git a/app/src/main/assets/novnc/app/images/clipboard.svg b/app/src/main/assets/novnc/app/images/clipboard.svg
new file mode 100644
index 00000000..79af2752
--- /dev/null
+++ b/app/src/main/assets/novnc/app/images/clipboard.svg
@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="25"
+   height="25"
+   viewBox="0 0 25 25"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="clipboard.svg"
+   inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#959595"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="15.366606"
+     inkscape:cy="16.42981"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:object-paths="true"
+     showguides="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1920"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-intersection-paths="true"
+     inkscape:snap-nodes="true"
+     inkscape:snap-global="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4136" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1027.3622)">
+    <path
+       style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       d="M 9,6 6,6 C 5.4459889,6 5,6.4459889 5,7 l 0,13 c 0,0.554011 0.4459889,1 1,1 l 13,0 c 0.554011,0 1,-0.445989 1,-1 L 20,7 C 20,6.4459889 19.554011,6 19,6 l -3,0"
+       transform="translate(0,1027.3622)"
+       id="rect6083"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cssssssssc" />
+    <rect
+       style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect6085"
+       width="7"
+       height="4"
+       x="9"
+       y="1031.3622"
+       ry="1.00002" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.50196081"
+       d="m 8.5071212,1038.8622 7.9999998,0"
+       id="path6087"
+       inkscape:connector-curvature="0" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.50196081"
+       d="m 8.5071212,1041.8622 3.9999998,0"
+       id="path6089"
+       inkscape:connector-curvature="0" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.50196081"
+       d="m 8.5071212,1044.8622 5.9999998,0"
+       id="path6091"
+       inkscape:connector-curvature="0" />
+  </g>
+</svg>
diff --git a/app/src/main/assets/novnc/app/images/connect.svg b/app/src/main/assets/novnc/app/images/connect.svg
new file mode 100644
index 00000000..56cde414
--- /dev/null
+++ b/app/src/main/assets/novnc/app/images/connect.svg
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="25"
+   height="25"
+   viewBox="0 0 25 25"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="connect.svg"
+   inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#959595"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="37.14834"
+     inkscape:cy="1.9525926"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:object-paths="true"
+     showguides="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1920"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-intersection-paths="true"
+     inkscape:snap-nodes="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4136" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1027.3622)">
+    <g
+       id="g5103"
+       transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,-729.15757,315.8823)">
+      <path
+         sodipodi:nodetypes="cssssc"
+         inkscape:connector-curvature="0"
+         id="rect5096"
+         d="m 11,1040.3622 -5,0 c -1.108,0 -2,-0.892 -2,-2 l 0,-4 c 0,-1.108 0.892,-2 2,-2 l 5,0"
+         style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+      <path
+         style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+         d="m 14,1032.3622 5,0 c 1.108,0 2,0.892 2,2 l 0,4 c 0,1.108 -0.892,2 -2,2 l -5,0"
+         id="path5099"
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="cssssc" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path5101"
+         d="m 9,1036.3622 7,0"
+         style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    </g>
+  </g>
+</svg>
diff --git a/app/src/main/assets/novnc/app/images/ctrl.svg b/app/src/main/assets/novnc/app/images/ctrl.svg
new file mode 100644
index 00000000..856e9395
--- /dev/null
+++ b/app/src/main/assets/novnc/app/images/ctrl.svg
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="25"
+   height="25"
+   viewBox="0 0 25 25"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="ctrl.svg"
+   inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#959595"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="16"
+     inkscape:cx="18.205425"
+     inkscape:cy="17.531398"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:object-paths="true"
+     showguides="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1920"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-intersection-paths="true"
+     inkscape:snap-nodes="true"
+     inkscape:snap-global="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4136" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1027.3622)">
+    <g
+       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:48px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'Sans Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       id="text5290">
+      <path
+         d="m 9.1210938,1043.1898 q -0.5175782,0.2686 -1.0791016,0.4053 -0.5615235,0.1367 -1.171875,0.1367 -1.8212891,0 -2.8857422,-1.0156 -1.0644531,-1.0205 -1.0644531,-2.7637 0,-1.748 1.0644531,-2.7637 1.0644531,-1.0205 2.8857422,-1.0205 0.6103515,0 1.171875,0.1368 0.5615234,0.1367 1.0791016,0.4052 l 0,1.5088 q -0.522461,-0.3564 -1.0302735,-0.5224 -0.5078125,-0.1661 -1.0693359,-0.1661 -1.0058594,0 -1.5820313,0.6446 -0.5761719,0.6445 -0.5761719,1.7773 0,1.1279 0.5761719,1.7725 0.5761719,0.6445 1.5820313,0.6445 0.5615234,0 1.0693359,-0.166 0.5078125,-0.166 1.0302735,-0.5225 l 0,1.5088 z"
+         style="font-size:10px;fill:#ffffff;fill-opacity:1"
+         id="path5370" />
+      <path
+         d="m 12.514648,1036.5687 0,1.5528 1.801758,0 0,1.25 -1.801758,0 0,2.3193 q 0,0.3809 0.151368,0.5176 0.151367,0.1318 0.600586,0.1318 l 0.898437,0 0,1.25 -1.499023,0 q -1.035157,0 -1.469727,-0.4297 -0.429687,-0.4345 -0.429687,-1.4697 l 0,-2.3193 -0.8691411,0 0,-1.25 0.8691411,0 0,-1.5528 1.748046,0 z"
+         style="font-size:10px;fill:#ffffff;fill-opacity:1"
+         id="path5372" />
+      <path
+         d="m 19.453125,1039.6107 q -0.229492,-0.1074 -0.458984,-0.1562 -0.22461,-0.054 -0.454102,-0.054 -0.673828,0 -1.040039,0.4345 -0.361328,0.4297 -0.361328,1.2354 l 0,2.5195 -1.748047,0 0,-5.4687 1.748047,0 0,0.8984 q 0.336914,-0.5371 0.771484,-0.7813 0.439453,-0.249 1.049805,-0.249 0.08789,0 0.19043,0.01 0.102539,0 0.297851,0.029 l 0.0049,1.582 z"
+         style="font-size:10px;fill:#ffffff;fill-opacity:1"
+         id="path5374" />
+      <path
+         d="m 20.332031,1035.9926 1.748047,0 0,7.5976 -1.748047,0 0,-7.5976 z"
+         style="font-size:10px;fill:#ffffff;fill-opacity:1"
+         id="path5376" />
+    </g>
+  </g>
+</svg>
diff --git a/app/src/main/assets/novnc/app/images/ctrlaltdel.svg b/app/src/main/assets/novnc/app/images/ctrlaltdel.svg
new file mode 100644
index 00000000..d7744ea3
--- /dev/null
+++ b/app/src/main/assets/novnc/app/images/ctrlaltdel.svg
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="25"
+   height="25"
+   viewBox="0 0 25 25"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="ctrlaltdel.svg"
+   inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#959595"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="8"
+     inkscape:cx="11.135667"
+     inkscape:cy="16.407428"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:object-paths="true"
+     showguides="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1920"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-intersection-paths="true"
+     inkscape:snap-nodes="true"
+     inkscape:snap-global="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4136" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1027.3622)">
+    <rect
+       style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect5253"
+       width="5"
+       height="5.0000172"
+       x="16"
+       y="1031.3622"
+       ry="1.0000174" />
+    <rect
+       y="1043.3622"
+       x="4"
+       height="5.0000172"
+       width="5"
+       id="rect5255"
+       style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       ry="1.0000174" />
+    <rect
+       style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect5257"
+       width="5"
+       height="5.0000172"
+       x="13"
+       y="1043.3622"
+       ry="1.0000174" />
+  </g>
+</svg>
diff --git a/app/src/main/assets/novnc/app/images/disconnect.svg b/app/src/main/assets/novnc/app/images/disconnect.svg
new file mode 100644
index 00000000..6be7d187
--- /dev/null
+++ b/app/src/main/assets/novnc/app/images/disconnect.svg
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="25"
+   height="25"
+   viewBox="0 0 25 25"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="disconnect.svg"
+   inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#959595"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="16"
+     inkscape:cx="25.05707"
+     inkscape:cy="11.594858"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:object-paths="true"
+     showguides="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1920"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-intersection-paths="true"
+     inkscape:snap-nodes="true"
+     inkscape:snap-global="false">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4136" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1027.3622)">
+    <g
+       id="g5171"
+       transform="translate(-24.062499,-6.15775e-4)">
+      <path
+         id="path5110"
+         transform="translate(0,1027.3622)"
+         d="m 39.744141,3.4960938 c -0.769923,0 -1.539607,0.2915468 -2.121094,0.8730468 l -2.566406,2.5664063 1.414062,1.4140625 2.566406,-2.5664063 c 0.403974,-0.404 1.010089,-0.404 1.414063,0 l 2.828125,2.828125 c 0.40398,0.4039 0.403907,1.0101621 0,1.4140629 l -2.566406,2.566406 1.414062,1.414062 2.566406,-2.566406 c 1.163041,-1.1629 1.162968,-3.0791874 0,-4.2421874 L 41.865234,4.3691406 C 41.283747,3.7876406 40.514063,3.4960937 39.744141,3.4960938 Z M 39.017578,9.015625 a 1.0001,1.0001 0 0 0 -0.6875,0.3027344 l -0.445312,0.4453125 1.414062,1.4140621 0.445313,-0.445312 A 1.0001,1.0001 0 0 0 39.017578,9.015625 Z m -6.363281,0.7070312 a 1.0001,1.0001 0 0 0 -0.6875,0.3027348 L 28.431641,13.5625 c -1.163042,1.163 -1.16297,3.079187 0,4.242188 l 2.828125,2.828124 c 1.162974,1.163101 3.079213,1.163101 4.242187,0 l 3.535156,-3.535156 a 1.0001,1.0001 0 1 0 -1.414062,-1.414062 l -3.535156,3.535156 c -0.403974,0.404 -1.010089,0.404 -1.414063,0 l -2.828125,-2.828125 c -0.403981,-0.404 -0.403908,-1.010162 0,-1.414063 l 3.535156,-3.537109 A 1.0001,1.0001 0 0 0 32.654297,9.7226562 Z m 3.109375,2.1621098 -2.382813,2.384765 a 1.0001,1.0001 0 1 0 1.414063,1.414063 l 2.382812,-2.384766 -1.414062,-1.414062 z"
+         style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+         inkscape:connector-curvature="0" />
+      <rect
+         transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
+         y="752.29541"
+         x="-712.31262"
+         height="18.000017"
+         width="3"
+         id="rect5116"
+         style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+    </g>
+  </g>
+</svg>
diff --git a/app/src/main/assets/novnc/app/images/drag.svg b/app/src/main/assets/novnc/app/images/drag.svg
new file mode 100644
index 00000000..139caf94
--- /dev/null
+++ b/app/src/main/assets/novnc/app/images/drag.svg
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="25"
+   height="25"
+   viewBox="0 0 25 25"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="drag.svg"
+   inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#959595"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="22.627417"
+     inkscape:cx="9.8789407"
+     inkscape:cy="9.5008608"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:object-paths="true"
+     showguides="false"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1920"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4136" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1027.3622)">
+    <path
+       style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       d="m 7.039733,1049.3037 c -0.4309106,-0.1233 -0.7932634,-0.4631 -0.9705434,-0.9103 -0.04922,-0.1241 -0.057118,-0.2988 -0.071321,-1.5771 l -0.015972,-1.4375 -0.328125,-0.082 c -0.7668138,-0.1927 -1.1897046,-0.4275 -1.7031253,-0.9457 -0.4586773,-0.4629 -0.6804297,-0.8433 -0.867034,-1.4875 -0.067215,-0.232 -0.068001,-0.2642 -0.078682,-3.2188 -0.012078,-3.341 -0.020337,-3.2012 0.2099452,-3.5555 0.2246623,-0.3458 0.5798271,-0.5892 0.9667343,-0.6626 0.092506,-0.017 0.531898,-0.032 0.9764271,-0.032 l 0.8082347,0 1.157e-4,1.336 c 1.125e-4,1.2779 0.00281,1.3403 0.062214,1.4378 0.091785,0.1505 0.2357707,0.226 0.4314082,0.2261 0.285389,2e-4 0.454884,-0.1352 0.5058962,-0.4042 0.019355,-0.102 0.031616,-0.982 0.031616,-2.269 0,-1.9756 0.00357,-2.1138 0.059205,-2.2926 0.1645475,-0.5287 0.6307616,-0.9246 1.19078,-1.0113 0.8000572,-0.1238 1.5711277,0.4446 1.6860387,1.2429 0.01732,0.1203 0.03177,0.8248 0.03211,1.5657 6.19e-4,1.3449 7.22e-4,1.347 0.07093,1.4499 0.108355,0.1587 0.255268,0.2248 0.46917,0.2108 0.204069,-0.013 0.316116,-0.08 0.413642,-0.2453 0.06028,-0.1024 0.06307,-0.1778 0.07862,-2.1218 0.01462,-1.8283 0.02124,-2.0285 0.07121,-2.1549 0.260673,-0.659 0.934894,-1.0527 1.621129,-0.9465 0.640523,0.099 1.152269,0.6104 1.243187,1.2421 0.01827,0.1269 0.03175,0.9943 0.03211,2.0657 l 6.19e-4,1.8469 0.07031,0.103 c 0.108355,0.1587 0.255267,0.2248 0.46917,0.2108 0.204069,-0.013 0.316115,-0.08 0.413642,-0.2453 0.05951,-0.1011 0.06329,-0.1786 0.07907,-1.6218 0.01469,-1.3438 0.02277,-1.5314 0.07121,-1.6549 0.257975,-0.6576 0.934425,-1.0527 1.620676,-0.9465 0.640522,0.099 1.152269,0.6104 1.243186,1.2421 0.0186,0.1292 0.03179,1.0759 0.03222,2.3125 7.15e-4,2.0335 0.0025,2.0966 0.06283,2.1956 0.09178,0.1505 0.235771,0.226 0.431409,0.2261 0.285388,2e-4 0.454884,-0.1352 0.505897,-0.4042 0.01874,-0.099 0.03161,-0.8192 0.03161,-1.769 0,-1.4848 0.0043,-1.6163 0.0592,-1.7926 0.164548,-0.5287 0.630762,-0.9246 1.19078,-1.0113 0.800057,-0.1238 1.571128,0.4446 1.686039,1.2429 0.04318,0.2999 0.04372,9.1764 5.78e-4,9.4531 -0.04431,0.2841 -0.217814,0.6241 -0.420069,0.8232 -0.320102,0.315 -0.63307,0.4268 -1.194973,0.4268 l -0.35281,0 -2.51e-4,1.2734 c -1.25e-4,0.7046 -0.01439,1.3642 -0.03191,1.4766 -0.06665,0.4274 -0.372966,0.8704 -0.740031,1.0702 -0.349999,0.1905 0.01748,0.18 -6.242199,0.1776 -5.3622439,0 -5.7320152,-0.01 -5.9121592,-0.057 l 1.4e-5,0 z"
+       id="path4379"
+       inkscape:connector-curvature="0" />
+  </g>
+</svg>
diff --git a/app/src/main/assets/novnc/app/images/error.svg b/app/src/main/assets/novnc/app/images/error.svg
new file mode 100644
index 00000000..8356d3f1
--- /dev/null
+++ b/app/src/main/assets/novnc/app/images/error.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="25"
+   height="25"
+   viewBox="0 0 25 25"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="error.svg"
+   inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#959595"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="14.00357"
+     inkscape:cy="12.443398"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:object-paths="true"
+     showguides="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1920"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-intersection-paths="true"
+     inkscape:snap-nodes="true"
+     inkscape:snap-global="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4136" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1027.3622)">
+    <path
+       style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       d="M 7 3 C 4.7839905 3 3 4.7839905 3 7 L 3 18 C 3 20.21601 4.7839905 22 7 22 L 18 22 C 20.21601 22 22 20.21601 22 18 L 22 7 C 22 4.7839905 20.21601 3 18 3 L 7 3 z M 7.6992188 6 A 1.6916875 1.6924297 0 0 1 8.9121094 6.5117188 L 12.5 10.101562 L 16.087891 6.5117188 A 1.6916875 1.6924297 0 0 1 17.251953 6 A 1.6916875 1.6924297 0 0 1 18.480469 8.90625 L 14.892578 12.496094 L 18.480469 16.085938 A 1.6916875 1.6924297 0 1 1 16.087891 18.478516 L 12.5 14.888672 L 8.9121094 18.478516 A 1.6916875 1.6924297 0 1 1 6.5214844 16.085938 L 10.109375 12.496094 L 6.5214844 8.90625 A 1.6916875 1.6924297 0 0 1 7.6992188 6 z "
+       transform="translate(0,1027.3622)"
+       id="rect4135" />
+  </g>
+</svg>
diff --git a/app/src/main/assets/novnc/app/images/esc.svg b/app/src/main/assets/novnc/app/images/esc.svg
new file mode 100644
index 00000000..830152b5
--- /dev/null
+++ b/app/src/main/assets/novnc/app/images/esc.svg
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="25"
+   height="25"
+   viewBox="0 0 25 25"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="esc.svg"
+   inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#959595"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="16"
+     inkscape:cx="18.205425"
+     inkscape:cy="17.531398"
+     inkscape:document-units="px"
+     inkscape:current-layer="text5290"
+     showgrid="false"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:object-paths="true"
+     showguides="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1920"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-intersection-paths="true"
+     inkscape:snap-nodes="true"
+     inkscape:snap-global="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4136" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1027.3622)">
+    <g
+       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:48px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'Sans Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       id="text5290">
+      <path
+         d="m 3.9331055,1036.1464 5.0732422,0 0,1.4209 -3.1933594,0 0,1.3574 3.0029297,0 0,1.4209 -3.0029297,0 0,1.6699 3.3007812,0 0,1.4209 -5.180664,0 0,-7.29 z"
+         style="font-size:10px;fill:#ffffff;fill-opacity:1"
+         id="path5314" />
+      <path
+         d="m 14.963379,1038.1385 0,1.3282 q -0.561524,-0.2344 -1.083984,-0.3516 -0.522461,-0.1172 -0.986329,-0.1172 -0.498046,0 -0.742187,0.127 -0.239258,0.122 -0.239258,0.3808 0,0.21 0.180664,0.3223 0.185547,0.1123 0.65918,0.166 l 0.307617,0.044 q 1.342773,0.1709 1.806641,0.5615 0.463867,0.3906 0.463867,1.2256 0,0.874 -0.644531,1.3134 -0.644532,0.4395 -1.923829,0.4395 -0.541992,0 -1.123046,-0.088 -0.576172,-0.083 -1.186524,-0.2539 l 0,-1.3281 q 0.522461,0.2539 1.069336,0.3808 0.551758,0.127 1.118164,0.127 0.512695,0 0.771485,-0.1416 0.258789,-0.1416 0.258789,-0.4199 0,-0.2344 -0.180664,-0.3467 -0.175782,-0.1172 -0.708008,-0.1807 l -0.307617,-0.039 q -1.166993,-0.1465 -1.635743,-0.542 -0.46875,-0.3955 -0.46875,-1.2012 0,-0.8691 0.595703,-1.2891 0.595704,-0.4199 1.826172,-0.4199 0.483399,0 1.015625,0.073 0.532227,0.073 1.157227,0.2294 z"
+         style="font-size:10px;fill:#ffffff;fill-opacity:1"
+         id="path5316" />
+      <path
+         d="m 21.066895,1038.1385 0,1.4258 q -0.356446,-0.2441 -0.717774,-0.3613 -0.356445,-0.1172 -0.742187,-0.1172 -0.732422,0 -1.142579,0.4297 -0.405273,0.4248 -0.405273,1.1914 0,0.7666 0.405273,1.1963 0.410157,0.4248 1.142579,0.4248 0.410156,0 0.776367,-0.1221 0.371094,-0.122 0.683594,-0.3613 l 0,1.4307 q -0.410157,0.1513 -0.834961,0.2246 -0.419922,0.078 -0.844727,0.078 -1.479492,0 -2.314453,-0.7568 -0.834961,-0.7618 -0.834961,-2.1143 0,-1.3525 0.834961,-2.1094 0.834961,-0.7617 2.314453,-0.7617 0.429688,0 0.844727,0.078 0.419921,0.073 0.834961,0.2246 z"
+         style="font-size:10px;fill:#ffffff;fill-opacity:1"
+         id="path5318" />
+    </g>
+  </g>
+</svg>
diff --git a/app/src/main/assets/novnc/app/images/expander.svg b/app/src/main/assets/novnc/app/images/expander.svg
new file mode 100644
index 00000000..e1635358
--- /dev/null
+++ b/app/src/main/assets/novnc/app/images/expander.svg
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="9"
+   height="10"
+   viewBox="0 0 9 10"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="expander.svg">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="45.254834"
+     inkscape:cx="9.8737281"
+     inkscape:cy="6.4583132"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     units="px"
+     inkscape:snap-object-midpoints="false"
+     inkscape:object-nodes="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="0"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4136" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1042.3622)">
+    <path
+       style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:4;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+       d="M 2.0800781,1042.3633 A 2.0002,2.0002 0 0 0 0,1044.3613 l 0,6 a 2.0002,2.0002 0 0 0 3.0292969,1.7168 l 5,-3 a 2.0002,2.0002 0 0 0 0,-3.4316 l -5,-3 a 2.0002,2.0002 0 0 0 -0.9492188,-0.2832 z"
+       id="path4138"
+       inkscape:connector-curvature="0" />
+  </g>
+</svg>
diff --git a/app/src/main/assets/novnc/app/images/fullscreen.svg b/app/src/main/assets/novnc/app/images/fullscreen.svg
new file mode 100644
index 00000000..29bd05da
--- /dev/null
+++ b/app/src/main/assets/novnc/app/images/fullscreen.svg
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="25"
+   height="25"
+   viewBox="0 0 25 25"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="fullscreen.svg"
+   inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#959595"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="16.400723"
+     inkscape:cy="15.083758"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:object-paths="true"
+     showguides="false"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1920"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-intersection-paths="true"
+     inkscape:snap-nodes="false">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4136" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1027.3622)">
+    <rect
+       style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect5006"
+       width="17"
+       height="17.000017"
+       x="4"
+       y="1031.3622"
+       ry="3.0000174" />
+    <path
+       style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
+       d="m 7.5,1044.8622 4,0 -1.5,-1.5 1.5,-1.5 -1,-1 -1.5,1.5 -1.5,-1.5 0,4 z"
+       id="path5017"
+       inkscape:connector-curvature="0" />
+    <path
+       inkscape:connector-curvature="0"
+       id="path5025"
+       d="m 17.5,1034.8622 -4,0 1.5,1.5 -1.5,1.5 1,1 1.5,-1.5 1.5,1.5 0,-4 z"
+       style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" />
+  </g>
+</svg>
diff --git a/app/src/main/assets/novnc/app/images/handle.svg b/app/src/main/assets/novnc/app/images/handle.svg
new file mode 100644
index 00000000..4a7a126f
--- /dev/null
+++ b/app/src/main/assets/novnc/app/images/handle.svg
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="5"
+   height="6"
+   viewBox="0 0 5 6"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="handle.svg"
+   inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#959595"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="32"
+     inkscape:cx="1.3551778"
+     inkscape:cy="8.7800329"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:object-paths="true"
+     showguides="false"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1920"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-intersection-paths="true"
+     inkscape:snap-nodes="true"
+     inkscape:snap-global="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4136" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1046.3622)">
+    <path
+       style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 4.0000803,1049.3622 -3,-2 0,4 z"
+       id="path4247"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cccc" />
+  </g>
+</svg>
diff --git a/app/src/main/assets/novnc/app/images/handle_bg.svg b/app/src/main/assets/novnc/app/images/handle_bg.svg
new file mode 100644
index 00000000..7579c42c
--- /dev/null
+++ b/app/src/main/assets/novnc/app/images/handle_bg.svg
@@ -0,0 +1,172 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="15"
+   height="50"
+   viewBox="0 0 15 50"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="handle_bg.svg"
+   inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#959595"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="16"
+     inkscape:cx="-10.001409"
+     inkscape:cy="24.512566"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:object-paths="true"
+     showguides="false"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1920"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-intersection-paths="true"
+     inkscape:snap-nodes="true"
+     inkscape:snap-global="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4136" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1002.3622)">
+    <rect
+       style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect4249"
+       width="1"
+       height="1.0000174"
+       x="9.5"
+       y="1008.8622"
+       ry="1.7382812e-05" />
+    <rect
+       ry="1.7382812e-05"
+       y="1013.8622"
+       x="9.5"
+       height="1.0000174"
+       width="1"
+       id="rect4255"
+       style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+    <rect
+       ry="1.7382812e-05"
+       y="1008.8622"
+       x="4.5"
+       height="1.0000174"
+       width="1"
+       id="rect4261"
+       style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+    <rect
+       style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect4263"
+       width="1"
+       height="1.0000174"
+       x="4.5"
+       y="1013.8622"
+       ry="1.7382812e-05" />
+    <rect
+       ry="1.7382812e-05"
+       y="1039.8622"
+       x="9.5"
+       height="1.0000174"
+       width="1"
+       id="rect4265"
+       style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+    <rect
+       style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect4267"
+       width="1"
+       height="1.0000174"
+       x="9.5"
+       y="1044.8622"
+       ry="1.7382812e-05" />
+    <rect
+       style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect4269"
+       width="1"
+       height="1.0000174"
+       x="4.5"
+       y="1039.8622"
+       ry="1.7382812e-05" />
+    <rect
+       ry="1.7382812e-05"
+       y="1044.8622"
+       x="4.5"
+       height="1.0000174"
+       width="1"
+       id="rect4271"
+       style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+    <rect
+       style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect4273"
+       width="1"
+       height="1.0000174"
+       x="9.5"
+       y="1018.8622"
+       ry="1.7382812e-05" />
+    <rect
+       ry="1.7382812e-05"
+       y="1018.8622"
+       x="4.5"
+       height="1.0000174"
+       width="1"
+       id="rect4275"
+       style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+    <rect
+       style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect4277"
+       width="1"
+       height="1.0000174"
+       x="9.5"
+       y="1034.8622"
+       ry="1.7382812e-05" />
+    <rect
+       ry="1.7382812e-05"
+       y="1034.8622"
+       x="4.5"
+       height="1.0000174"
+       width="1"
+       id="rect4279"
+       style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+  </g>
+</svg>
diff --git a/app/src/main/assets/novnc/app/images/icons/Makefile b/app/src/main/assets/novnc/app/images/icons/Makefile
new file mode 100644
index 00000000..be564b43
--- /dev/null
+++ b/app/src/main/assets/novnc/app/images/icons/Makefile
@@ -0,0 +1,42 @@
+ICONS := \
+	novnc-16x16.png \
+	novnc-24x24.png \
+	novnc-32x32.png \
+	novnc-48x48.png \
+	novnc-64x64.png
+
+ANDROID_LAUNCHER := \
+	novnc-48x48.png \
+	novnc-72x72.png \
+	novnc-96x96.png \
+	novnc-144x144.png \
+	novnc-192x192.png
+
+IPHONE_LAUNCHER := \
+	novnc-60x60.png \
+	novnc-120x120.png
+
+IPAD_LAUNCHER := \
+	novnc-76x76.png \
+	novnc-152x152.png
+
+ALL_ICONS := $(ICONS) $(ANDROID_LAUNCHER) $(IPHONE_LAUNCHER) $(IPAD_LAUNCHER)
+
+all: $(ALL_ICONS)
+
+novnc-16x16.png: novnc-icon-sm.svg
+	convert -density 90 \
+		-background transparent "$<" "$@"
+novnc-24x24.png: novnc-icon-sm.svg
+	convert -density 135 \
+		-background transparent "$<" "$@"
+novnc-32x32.png: novnc-icon-sm.svg
+	convert -density 180 \
+		-background transparent "$<" "$@"
+
+novnc-%.png: novnc-icon.svg
+	convert -density $$[`echo $* | cut -d x -f 1` * 90 / 48] \
+		-background transparent "$<" "$@"
+
+clean:
+	rm -f *.png
diff --git a/app/src/main/assets/novnc/app/images/icons/novnc-120x120.png b/app/src/main/assets/novnc/app/images/icons/novnc-120x120.png
new file mode 100644
index 0000000000000000000000000000000000000000..40823efbadf27f0286a84976a34d1cff8406f5a2
GIT binary patch
literal 4028
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
O0H%9ir%KB?_J05>Semo|

literal 0
HcmV?d00001

diff --git a/app/src/main/assets/novnc/app/images/icons/novnc-144x144.png b/app/src/main/assets/novnc/app/images/icons/novnc-144x144.png
new file mode 100644
index 0000000000000000000000000000000000000000..eee71f11c74fba3be7a05d668aa1c1dc7d993d09
GIT binary patch
literal 4582
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
P8v(kSw=`;ScCr5hJEV16

literal 0
HcmV?d00001

diff --git a/app/src/main/assets/novnc/app/images/icons/novnc-152x152.png b/app/src/main/assets/novnc/app/images/icons/novnc-152x152.png
new file mode 100644
index 0000000000000000000000000000000000000000..0694b2de39b8e709b442f10905a6f3cf39f7c7bc
GIT binary patch
literal 5216
zcmZ`-bySqk*I&9rx}?!n7Fa?-LZrJpmR>?Sq`O@fkPuKxP{O4_x>?CZ`3lGq@+DV5
zK#*>JkMDWU@0|CKcg~sT&b`mvJM+xv&OCGPOMP86Dsnb*5C}x2p{{HQ(7eBulo)uj
z3~=)RgwRDnM*#$CNT;~6CjxkGM|DFT5GeE>2oxO;0$l*2=uHsl1r!AOWd{OD0|uCV
z3OWpAffvLMT58Gw1<JWnXB0rleAF%cK_GAAzZI`=Hh>TWqL<T9Rxl2p-z^OD;TS9&
z?v)dlk?FK_Z?3wZiOYqot$_zHOqp?CrKAGCGoF}m5LI&EVrLnKS@Fpj`ky%{-H|6v
zr>!sa2P+vcg0&5Oe0i<m;bIAY`tA1%DHiN7In9}T+xFefGM$HQ@-u=*c^13B$9HyW
zFO&>8b@q+|CL&uaE>ACCNXBSPW3dyj;UV1+493``Pyo|tYhsdwLLpHo=fy+^-gsJC
zMFvInc=()F{HsGsbU2uF;CCDeO_3yyQ&qUOuC7>6V^Tbmf05~>^1*JT9mC<Wlvm%1
z&s7CCsUF)MD^2{Ws;W*)gY3l>pR;@O(zwU*wEMDAnf7_j#o-5|xBL+iVq7*24hd0q
zcFS@G4Bq7fZzD-#eMYtdZnTVyjBHD>{E+&Nj*jC}a1RQMK>tP<?!>|8lBc%1ZueKv
zQR?f5WMtJXErGURh6|TYLA6m1LWMI-2A`U#&T)GizPqdIGC@pVZYkIX9+UunLo#t#
z0TaeP($Uf3>7Iawl2K7nU7a6pOz8U>9FZoWznAE5ZYL%Z+w6&V;8PId7pw4QXm$!J
zjCv>=6flw!_DpMG8O%LBcb^(C^g0HuJeZ3CBh0>k|K45qdPrB+z|1VD6^uxqhq-D7
z@@P!HHZd`|8}n>=bM|me`drYodOrv#`bLBTjOID1!Lmh}4S|imbt5-7H>V%lz#n-r
zjY<rIr$a^Tp8dam{bEW`ESacGC~{y>pskhnTOY~o%VW|u`F{IuwKOjVh3a%1rld_p
z(5n2g?4(c~r5ae-r^4h2<x^QR<?(AwOwOb@FriS241*Oh-bAX`NCqRaBy<&CG{maK
zBhcI1J7tlmbe;2Qu{SjzVl^U=IX*K#Ua{l_5`xj%6($vsG%z_1LzaU5cm9WAfIof@
zMoj#U)%T-UF}MPJWcrbE1EEDuh^mRnP_QWj;vQuogH{8jiHU-h2%=0s#sHE)j&yin
zY{#Hgg8$x}oRMYFu(A)Fqe!IMK&eOyu$KR26YYWK%m8b<R5sVZsFkjzAyn02na!tv
z>8F~#7Edx;RE_MLtZx?IU-le4jXxaFHy@mhBlF!q3f2E%O{teYoGJZNI-b@Mzlkov
z2)e^HR3F76CmyDM?8ewuALtu*t{|~AjZ7AFn7<ZKP+Ve+*c9AW+InM{T?}r$%y7DE
zA&%hqlfJy3djC$p6J7$$zTl&bBdTpkAv)zdvv12m^<p~D_6S=HUbHb}#rWF;^-PK1
z;R}yEUlkZ4+VAP!zPOg+l7M^1x+$5GQGz~f3w-+2g1osKnDotdzPKo#et9xhYjbO>
z;ZoFzYEBXnfEO1Mx4D;&cwXr`vTP%uAx6^Xvx3hsI16&<=z}5?3a<!2p+;RfMEeAV
zpID!HyNGnzi15bh_PUckdmqy{L0oUX7XFl5gN~SlWaT)tCdBJWeDnJeZ%_a}1XCW+
zenns*6GIT&&kcIUBP}#a8eJ!RikG*}?)KWC1Tr&nc`(Js%36Z21#-87f;iJj03Ut;
zyA%ju7A~6phJ9aJia$6wsIk;pS4y+tqbmu^!SVa$_tZo3;So?x(~OA1a7|MMD=4DO
zsqii`Qw~b01b~{BcN`5MPWE5KhgEc{|1$4{{ug=$(!ZXNg#NROs<se<Dh9;%Qw}9>
z)n2GG$7v@w+>v0F+2SifC<iFUAV5Dz0{jZwKEpJnZ5O4QUM)WQN<GwK2dLTW|D($d
z8jIe?-D<C`;&wEo(XLJDc`xxt4$MAO3=;@v4=s(n+?02WD#;#QNA@La`wU8&eb{hb
z1ob3y{>0(tV8_UWwKiXa&ww=$SlW!xJ7sVM!w32oj~macHJ=<pO>cEt&CCKCzg!C0
zziK6M4s2C*!`mkT6@H`59vjo_tjhm}jF7`8mN~c1Q`*`xq@<>HaB^}AUsZn|G6a@`
zAu=-V0J1c_&6%64tF&|1#VLeVk5)8IG^XzhT-oIw5hy@ePn0sQ-G5_XH;}!`JWafT
zNc>(sZPEcf&eh!=t)81$N?FZn7+J~zgTYul*b53O4W28KI_G{-RfxjtxT&L1Qc{9C
zGSjE6Egy#(n3@t7ZBT9GZfi5JwEyndri`LMlx{}8`KW6}P)e-Vtx@}O-96w$4>k2~
zxuEO>dhaw{#$}9U2_<4*ttZHcwARw#tYIp;k$q6E0DzhOl283Fnsn4jN=;SDM5>}h
z-FF@Xo0g$Kx8i5WLny+YCGneQKjoVT9Mp_a7gHMJ1rKyJjTn||J8M6WW^FZxi<r}X
z=DH0gx6H{p3FLI2u}<)jV)JV9TzCSp{a13!AB~3mZm(dg&|~){(Jn_ljPDL>Hp6+j
zo{>mM>QT!-6iT(*olh}ubOM!?+3$ykzUMq1dz2GQ?5ZSW&aYVSm=q{MfCjNY{$%Is
z3a+79UrAahamiU|gcA!}Z@y{N4ha#9yj+;t4<*SE4h;)aGxI_=1h~oHTy4;|t)&SY
zn<Ctpb8Xu1m7azYY6q+QUfM&O*}C0My1T1DtY-`MLv2*s&woZvPrIh9&WhS?eB~s>
zkBPTdhr`7pPdlCW!=Xa(z0J*ME-o9JOIRD3c+Z8e{kX=U_OCB-lLLk^A0O^dYKgy?
zYV|hr*_u?ra1o*_gZKL>I=0ef=^{MCx^2euMEOc&>v}8)e|}kdW+G(C3nNK0`ZF$G
z<0B;&MHfA_h1?z$Ez62JswClxs=h80vL>yzStK%G$Qpn2B=QW+X>8&~^5u5dyDy1L
z6{d&7je1trJ0!#=aZ10QNr>vbT*NarHoxo|=e5};%9BEbpX10k&vPU_4MioUy(|P4
zy;!%gL9XQw$oD_fe{<|zqEVpD9C<P}-%K}CeLmlKptqjLKDpqn!>BltJu&qu`b6Bt
z`fS_w+PwMs0i<ZvVY%n>qQPZ?R+jVd^!j{MPq?&R|MJnv5c{_qW;7HmD5!PLsrQss
z(ZxlanyXf<`-=Y0W%KsIAambLP0mszM@)3&+oI!0Ncq>i*@pKVnB3C=+}j^P!Dsf%
zUnCdstlfSQfQ3?*zsht<Oeyl%O_gu@YG}MH2^Ln2X0~DJn{W1%dhy(O?3^Ee*zL6Y
zZGKF5STF2{5G+`B-r&K@E-GzCbC(JN?_OAJ=U&l|kIaO2YeRQrkBvpbpBB5FhA+ni
zlEwz!ZVocX%*<5Mw|w~UdB2;3+74SQ{KJ<dJ&k(pylkv1<dBZD(p-hNPOT4L>!wJq
zY;$|c^eR1yGCQ|5pU#aOM8Qnl;8%9yMbq8g=Rcv6ju{)vL{N45@XQBI@yc_}wm04N
z3VPd8|BajeCtUYYDAK;8FEP=)>*Q^qZ4xbTXCnn}Iho@o`N68+D~i()$c+#NMn-|x
zdn@R^we;<Zi6W|<`4_^Tj~lf=<21)H_Af;a3WE^e=23D_ntM3SyUz9!Q|nrPOs1=(
zd`d-ae8rOqQccHTFh&=H-b>Uw|JK}+*JD(3$m$_7Wg)zY0{AJ)q=CDre{_~QOceC=
z)aah*R=-0~u^*+P*d_5xSqjmvA9_y*C|;i}6GUXp+4ErtGewWhnRM<<Bz&^Nt9RB3
z8XLkUCVGt*R(TrB1yTlY6mI%9s(ybtB32)A`1<V1|GGiH!_(=_Gclu62>q<F@CTiI
zW5T#mc3YS6SH64Q7d5sIg-e2ydE?Z$V98A`$6VjD^b+fxG@3^BpyIyLg&I_OkW`YC
z4aI}Y_d{3>diRRuqA+QF{dbE|;;VEBwWwYK`RgtGfFSo%oWtYB1d0VnXyzNGM*4Ii
z8Y$$4V(2DM-<I~3)&mimwXZVG6@mFHfL2&1h5YR~M|{8xY0BKl*qC${Mk+<U+@A~W
z7gP9TZJ;M2@{2@KdXG(L82v)~Q9p@D^hUGHbX~7kniiOIIGuU<&#>U?{7B%Q*D#Xx
zWNxII|23R`l1{AK-{ncH!Y&rG+e*;n+^qA2uJhv89f)nMkg_;T(a3jGpR6d#W~tWH
z0I%!f`R}<AQXaB}o}o;pm_cjxc_NY)uh;)9-d#GoXew@2nkYD4O);pbwh%7)7`$Dh
z*>%%#1L9@&bn<GIDPz5I%Rd5eFt(zYp83UY>)qW<gSDx;JK*qHz$xC$=Hpw_$Bo8V
zWv%?a)%I5)yM@P$a&lRz71V0s?@@PhU)^Cy&}L`1K-Dn6`swEW$6EdHCtmAPBv}a=
z`V-f6B}J378@tmNA2L>Z=~-Zwi8IPtRej}tJaWg`2>nf~HTTFK<csa2wev52WUM?`
zPp4RJ*V-o;V7KC6_KAe`Lp=7!lt?yPY~5ZowrWvCd`iG6TiMeIhg%$l7vQm^KGW^|
zYtC~P)Mo8jSxNitpLg<&5(_}DwPA{SU&nCvxu%J?^yS&YKfGF36taZ0NIv`X{E^+q
z*e3eiUJG^5>(l@vmwo(5*n6&1;?+Skbq6wt{Qk+)DxbHA!tD#29ZqxCBp%LsHYid$
zY}yu&JUCfs0zhj_=<1R+(tg_FDM0VfQ)43+!erRhzs&9p8ctV(DVkYll<Tl$uD>=z
z>6-?U3!0U;v>QlOUw8K2gf2G>N0*k`>|fQxarfi7MMP-g1MF{a-mtK!FN!;C+o;F{
z<*Z#UVT%$z3@Yd9mwRo|HDxd|MkKI%Yd`fnT~*l)H8B}1uPO7c-=A|4NF;RLKF|Ty
z-tq(R;ql5IPNCr!Zas^GT)2@8KB>?n*D>ru&(qnzXU%Tm1<X5FYlPHjZsZ8qSbFoh
z-IGe0{7Q%SJ(&M)ES7x9g*7^{G2_Q#LdJ||-|L(moj%`exv4h5>_GKFbv+bh$sO^5
zy9myOmA~D5oh%EU@YyYPJp9z+YzDiZd51kym6uSYSC&3#k?|jow5V%$cWcGnUEKny
z6-B7j)4=T=i^J_;z=c~p$Q*H+o{sMNeBnFOA6eUWTh7fJKd=g#f-5VN<p)|~-}To&
zHfqTfO;P8MrN6Jcnb<a7EWZ>`)y=OV6?xU>Z296v^Hxgp(b3Vn=4KP|FKd6^b##PI
zp6i3-TUuH^Y>4C~EbXZQ5t=5}S($<fz2al{`$s!z()~TmaAUOqaAa5=39v^MktQT0
zEK5;mjI7%!%QdBNljg}sJsntqCMG3O?e$CK<b4tR_uc4-|41bhGBfhv@K9*66v`)k
z&JgA!U6`{Mr9t#q$DW1Qrf$&6AdcK)e)TwXs*(8~<E#X~h{!}H9+Jt}Zi+GqZ9gjA
z9;zRs!^e`S02E6(9zaf7hs{6X#(P-rrWhF+Ie!6jEWytwZd>%i?81P~I4Ru<`t$P>
zBB)^u4$3iH^6p*Vj2IvXa}4)y9L{mds1fDF)k7$XCvwT`aSt6^2Oj(!fNGZ~Q-D-i
z6xF>Tb}D#i=>Fw(B9ox#8-+yLOc~6+j-!~qHRzu&P|B=X@~&)T0(|b9`kZzw?Ew`U
zstrVQB_F7VeZdgOc=l7Q6obn7TYHO>-oMe?qu`>Xr9C|@YqN(?%0sfX0O=_2AO-L!
zAE}~h3Ptn>)Gswc5R;JQw1|e`Pe}5!mnp+*8t&R=2#)6-9@b-^X!Oci%3@iGHWd%y
z(_#_!&qO9BON7E!2Sf_Nv251j>}hNI#>P%sBRb@iTA4?}J{EQ)G@P;%hgZ3QxrowK
z3~p>Tqm;UnnnUUz8sr}pKaSRae;XPM(Np-Igm$;c)MRm2g2xWuzev6te)VTqjY#2a
z<oBdOsZ^mZ@Ee5xVEaT?55K*+j!M54{d%+=AZ@0`o1trwBku1x=Jw2kDh<NG1S+5=
zj+;oWnwXWhw6s+8|2giK1rKRn6evt<vlc!J??J-C0?Bz%sM@DK%g->gSy`c}Kx{r+
z@WkKi75|oIr<N?tM~Y+rJlv;qac&y>c7%^Be80bwUhmh;&+>dP>3fLPpdH~C!dbXF
zbi)p?FjtZ_gfMv}I2`9tF*Qey*a85p!=|jRu2wTM^Q_mrefu^FtCJq&tE5D*G&6Ie
z0wgKaIt4Q+q&TSC7Jp~A-219b$|J)hVsq73u9F`SXG$Nsf(4QZT$IFSbFdN*6<WeJ
z>spV7f%}RqeU7QAsUkYZ{qbShS~(Dl`-oNQ=L71LIdC<rD$2BA^@J%z>DBhl^`*Sl
zI$DFuB1afz$&FYI15z5rJGJn81HK#uy7vhcMof*AWMe@s#Bu$*z%C=JyC|N_Yb!7R
z$p-38SLS^L<bHPB=E3o_z%2J=y@Oq5DV_6Z57^Mqh11;yY|+kO;^zjBxyCub@Edjh
zM$JtAZLH%b*0i-g-yfBge)X>rJa|&os9*V+{N3#S))ldCHF^;d5oP=urw<W+RVY*w
zgUTO4#6T5FQ>^_pUp(@LM)^`Vo)%cWMEkzR9l*vb9uOMTLCUiPMjSjsLyId>7Y2@w
zYtgKdXHlF32A8rkw{bvrO&~zUJiy65050w52L}j93@Z9qNE9k0Dq$=tE)9iBOFR*T
ziUEx>=T_|hQSkJ3a&r#({}rZXXxsq>*1sbdc{>LL+55pkK=MY|&CA{2(cT9x?Cs~0
T|67&~=mgSG(N(Thw2S=@24lsF

literal 0
HcmV?d00001

diff --git a/app/src/main/assets/novnc/app/images/icons/novnc-16x16.png b/app/src/main/assets/novnc/app/images/icons/novnc-16x16.png
new file mode 100644
index 0000000000000000000000000000000000000000..42108f409990be6cf93cec396ae65f78a2bb3cd1
GIT binary patch
literal 675
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstU$g(vPY0F
z14ES>14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a><Y92N~{j>32}9Ba4-i0PcZQH
z^))gwGBh+~XkcI}VA9pqWhr8jmX=mgQBhS@H8C+UH8o|(V^CLDx3sj>)6-K^Q?s|X
zzjB4u+gmm~T;tLu=Kud0%F6{cG&CMOV6m{UXlW6dG>LuQJfX5Ow)^)PLPKR-TwEL-
zbsj!seDsLPz(Cs1PxQe9Mn^|Sp#9snv2WPGSX?Y}?;ewljg7Ifv6-2fv$M0St1HmG
zGk0752U2V$L4Ls+PoI2VEpq2}(jA}(W0JSKi^T%=rPqKQ&H|6fVg?3f4G?CWG4sh=
zpdfpRr>`sf11>&Z2E8R89dv*~D?MEtLnJPT_I2_#Iq<lgepnWn=*Y6ZtK!kgs@MPj
z&x}p#F`4Z7Z0TH)1A;%KA2M0okP>&1=5WwVU9jt#)FFkQC3$C_&1g(BO>_Lj+BjwJ
zl_tA{SsOz{p86hTD6o+77xA6gB`G+y;q%d=)-CrYEHlv3I`B5H-oa{~M8+Ckg?ajt
zllD#8_wnT1#z$3o=Zu%ipMUkg^rO)+=I_%dZ#O)~UhuZB<Mgb>&&%zyU-CQs+mLGt
zbhK)TYeY#(Vo9o1a#1RfVlXl=G}ARS&^0s(F*LR^Ft9Q)(>5>yGJ^Gv+o5R4%}>cp
ztHiBAskpugs6i5BLvVgtNqJ&XDljI?^)mCai<1)zQuXqS(r3T3kpe1W@O1TaS?83{
F1OT_7-I@RZ

literal 0
HcmV?d00001

diff --git a/app/src/main/assets/novnc/app/images/icons/novnc-192x192.png b/app/src/main/assets/novnc/app/images/icons/novnc-192x192.png
new file mode 100644
index 0000000000000000000000000000000000000000..ef9201f4370f886812acca9d780afb15c6b98452
GIT binary patch
literal 5787
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
TuB&r{Oo09!W8Eqp`|$q(4_V)v

literal 0
HcmV?d00001

diff --git a/app/src/main/assets/novnc/app/images/icons/novnc-24x24.png b/app/src/main/assets/novnc/app/images/icons/novnc-24x24.png
new file mode 100644
index 0000000000000000000000000000000000000000..110613594b8e305e26cafc6ac77e6f40bbdc76c9
GIT binary patch
literal 1000
zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM3?#3wJbMaAv7|ftIx;Y9?C1WI$O_~uBzpw;
zGB8xBF)%c=FfjZA3N^f7U???UV0e|lz+g3lfkC`r&aOZk1_s8Y0G|+7paN@aYcn%5
zBO@bob8|~eOCag#>FMF&0VE;7(9lrTSye1ZOgvCr*Hl;8Ls>jnT){;_G*pzKfq}!B
zgRzE@p_)NWO$}&>kdTn9tSqxLGm{e&uP?8>yu7ioG0=RVu|SIyoD`VdnH3ckx%{{|
zwK*khCD^psw6wGw9Ua}=+zJa7|Nm$3_LdC~*GNtlzH^7!(o$>MG_mY#<)1$pdwO_)
z?lCnrc>S7j!UWcZ3q@bOVvv`Y+Pan5$jD&oRMDwZ#k#tfy1SYE{kiAP<$L;+K~GQf
z`E%y}eg+*Ko#V$D{{Cg?>f&;DcMlI&pEix##)hZ4nYpiz@5~uiD=VwZms$S(V>o}F
z!N|yP(<ZjIHoo3ovA#a>88Z}fa}C?t#EOcT3=9mctaPtmXUfb}`0<0GrG-UBMMYIr
zRY5^PTU*=2#KhIr)y~e&!otGA!2uYCz~GJCTDKEO@stGl1v7X(|M$$}>4%kXWIi4^
zQ*{(5&Y0xw?!wT)DhpD}S>O>_%)nr>2ZR|n1iugl3bL1Y`ns||;Ns(D&|C7+K?kVE
z(9^{+MB{wvMfTuB4g#zXS_NEc$`&^{GHXQ$Twk}*d`-uV>f>BVzyCju?XXhXck;=k
z?`NN;sk`tkZ)(!en|k8o@@*cf>Rrcty$S<3o{B0>4PCKLF#GLxfmbFot4vNTy_o*J
zj?;J7I@SfIudBM#ZU{_Kh*M#iP`^#+7k^Z}Lj5uEErPcW3ockJ9Gf`t-3ngYkQ$yN
z9DDDy*U2&P^CixxRbH%PlqPgcUCq$cmigJuot0){6^3&C`kV(ooqBbvifPZpJ0c2t
zhH+kp#PUD$9oSucvR0bUhttBws?Wp1GWF-m&7ZqZYjdyrsiw=gy*%fR{fWc!x(y#y
z@@DExxUl%di<{3~Zs{1g>!o%5`5Smp{O0rts~7MuP06qF_`CJ!|E#NNE2n#u-hOg^
z@80@|(wWD-=bNl`-1J(i9~iI<swJ)wB`Jv|saDBFsX&Us$iUD{*U&)M&?Lmr*a{fK
zCT7|OMnFce-f=q=4Y~O#nQ4`{H7FI=Hvu(Bf@}!RPb(=;EJ|f4FE7{2%*!rLPAo{(
X%P&fw{mw=TsEEPS)z4*}Q$iB}Z|hP3

literal 0
HcmV?d00001

diff --git a/app/src/main/assets/novnc/app/images/icons/novnc-32x32.png b/app/src/main/assets/novnc/app/images/icons/novnc-32x32.png
new file mode 100644
index 0000000000000000000000000000000000000000..ff00dc305a756d9e87b2bf583caffb25fc8a9e6e
GIT binary patch
literal 1064
zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50
z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+0817lc#Plzi}fwi@@g@uK&
zu`!UewY3Fut*oqoY$GEhPbA>&?d{>=0TD1XGz7{rG%zsKG4O`-8W<Re#EZzu$#HXY
zi;Ig(NJyBNn3$WJ1I+;%$P>=P;KktN<fNjaA}cHF?(S}8W(G9p`gM-~{~5f!Wy8ZY
zu3TZ&(9o!=5>ZuEUA<c5&mRUoJ-v<&iM~GOMT^7=3K+Dsw0`|!2n&<^{hL8aNy*7c
z_vupxO-(gpW5X9OSQHevpFd}Ca8Tvp;g*z?+_{r0B!oXJOJ)6f26=h;*|V8Wo@6pH
z(Y3d?FDOu0yqIyz7Qw(kMp;?$Q>R$w&u0Vr%hc51^=rm2Ul_iAWqkFDL0nwO$w}+!
zQ$|Ti2|GKx`SS((`xzo56@UC-0ESFNgoddpYe|Xd;>A2NGBT#7rfzO-z;FYGp`#-(
zIvCCzQQZfmgi3<^f*Hae{(TTKW7_?{|DG@Tp%eLRscPG?r$AZ8ByV>Yh7ML)4<LuL
zz$3Dlfx#^Rgc<qgd@=(HvX^-Jy0Smu;^Sq|Tk_FC2dJmq)5S5w;&kie@bE(p0<GbS
zOj^@8JES%+MILWUa!!7~DCzjUqkaGWt8Z5km{27D&F5QPaZ$RT6UWMxr4F8!bGQ;$
zHGc|<T_dmM8S0Vi%5djMpyUUE6-sPBTK?oRW_*th<vj5GJwyGvQ_DphmM_b+i@y*i
z%J|2LDQs%+3aJaByf5?^)=V&4u$<w=)Z}kBm%T`Lh~RFvVHWJwPP*;9<&;|KsRl+F
z7KuX-1s!Bkr*{}iNV_kzU{@;;O5j?aK4a>Prw#(omh`i+KNIHe_CDPyn)mS4ECzw=
zm9uz1a@{zxHusKaQG)pQZ>kDzzJZJ~4;~829k(nf>PoM4@b!N1=h3HEzub;+-@CVv
z<=xHC%u^X`l<bbGFr3%_HbIhekJ7$I&I8u=_Gb(a3o$qwGpw1bc6M%^RNKPKpJrc)
zbkcU#tgYqVzwb`Ed>7YzGl6tZ`{Z9+CThH@kM>>ad;hje$LbjYo5QTXnti?>U07Z8
ztbg9$t-CV4wO{{d4sFj~ek^a>JYcM-mbgZgq$HN4S|t~y0x1R~14A=iLjzqylMq8=
zD+2>76EkfCBOoJK@3<X`hTQy=%(P0}8kCCbn}8Z5K{f>Er<If^7Ns(jmzV2h=4BTr
bCl;jY<rk&TerF>ERK(!v>gTe~DWM4f!E0>0

literal 0
HcmV?d00001

diff --git a/app/src/main/assets/novnc/app/images/icons/novnc-48x48.png b/app/src/main/assets/novnc/app/images/icons/novnc-48x48.png
new file mode 100644
index 0000000000000000000000000000000000000000..f24cd6cc939a073d4ba178995eac2854d6acf607
GIT binary patch
literal 1397
zcmZ`%X;2eJ6n;raKq8kF5+WghoB|;SNjMV&2?<z^B7tIo2}aDI)gT0cVmq1C7SJgL
zwVc|iwnasRid9;$o>-ZR7zzS%nNUSxKoB*6qUqZH>W}Wsd*AnVzxVCTzAa0LlUP|&
zECIkOS}ICJ$aohPIF#k1t1cigQwn2+0O!6Yu4Lj-?V^w-#sYle382{z@Ct2do&e;d
z#%Vb~NDTlf=V<!}VE~xPg!hxgNDv+#9uyQ57#J8F9L(qQc|0B}5$AHbDF1uPVzHwC
zSyHJKp+q7<8F86RhDc0I3=%MzOwa&MjiU)@?qYZRUOb!4hTY)f<3msrkUx<~B#}rI
z3I#cn$z)_68X77P2qGgR&2r6<r}IW<*aMIQ7&i>Eb$567_V#vkbQFulot;FJ2`VbA
z>+8u^u9$1JYdkzWluGx>NxV$v_2vy2jSv<_lSurGMl6Seqtoe1<=Tk}{Moa_6DM#C
z2BWog&GF+vrBW9cAvxJ;^=hI>B$7ycV`4}tDc&9)gvv_n%nV4SZX6D$uFmG_RZLfx
z)qw-x=H^DBkXu^`<Kvj{a4+;kd_H}47L%TCr&75+eF{!amOLK4s|(lNjpgzD%F65q
z2O%I}1Dnkl88Oex15Zy+KHqnE7}M5f#$p9<xr_%7uxd52q=c&11Bql46XQ8O4OLa<
zQBhHah3lrKU}grFk%5uPTwc5YM@Mp4SQwkl?Cmwv>A=BZtx`!@SpkCqhbE;^xJ5-_
z{QUeB3TJ}>TwPshG@4K-M1u<n35ke^KqEF`_l+SSs}tikLy~UkZld+Q{<*${p105X
zzFP4;e3|gP{Fk0ndyxjSb(2H{rFg0o6{~Wjsd)hFz260<ruN}1+B8>3$BNDMmey7f
z&}YnP2f)&!MZzTMtvdtCl>Uut3HnOiL5tGK=B)I=pYUn7o1VyoRt|oryp8P+Gruj4
z-z>KpiO)Y&8_llh=FL_A+VS|)+=gR^ZLjZ0t8IuBO>HT{8)T{WwGIvKfA$P^9{fnA
zC~I6Q(rn?HYM$H4B!3)PNQe*Qd{;@}=r;B*fQhCHmd@9)>>FaM(+fR5bz!dO^i5qC
zZqcR^&WKLjnY}KI(l(Jt=}r%mgc<ffpV5}P)c6-<9}$zZ@}9I%f$FoRiw!>J;_tmn
z@W%!&?k&$<9o$&R8J1m(z&IGTE)T9rtBaVK5ZHE9`%6;CDt^E=F^ZEHx*G3I)@6wD
z&cgltIOp>f)iCham;8?VrS;tNPDYg->^bAwXo0gH8c^(R`<=So)WfeD`J!V{s&_VI
zOv6yg>*D)4U{lpJdi+M=!zTi{4p&aS^&WUV>(73xXDgqLE`yHf45ayqA8)73PjjsG
zudFT=-L@Gz6s|W?FBRL&32b-b3zsZtuQhKw&GkO~;_`q*YfW{Fj?f~Om%h4I-&Q&|
z{y9;9Lo%>0_ipBpjh;PG-I4+6P0PkH%uUePDfaUw7O*Qh%`Lsj8?v<WrU~C<5S*qg
ziSJ!iO>2V!jvTG0(H?*EsEEJI{ZZwzYy7uyF7|dUV=ri`%cpBArq7++JU^Mesq^HG
zc}+l^*TrGlx(kM>ZG2nRj%@n}?EIH?s)CnlPu`FrK7Pm1(T}rC*vq|siG7aEz34M}
zsKp<uw`HocLKL}K2!O?4a{ZYMe`a73lO4ifgaq<@87xFzrgk6rPs1+Nw(RXi|KG6R
yOwC0Nj_+>RtlF+F$jr@xf`S5i_U@f|ip-oWx++&$|1^w(L=Y{G6E#Q3wSNO`Xb>9!

literal 0
HcmV?d00001

diff --git a/app/src/main/assets/novnc/app/images/icons/novnc-60x60.png b/app/src/main/assets/novnc/app/images/icons/novnc-60x60.png
new file mode 100644
index 0000000000000000000000000000000000000000..06b0d609a0f7c45b1ad0481debf061affd6a2a17
GIT binary patch
literal 1932
zcmZ{lc~FyC62LnbfrN01L<kTJ!AK5raD*V?3gM9Z5)qLrhy@Jcj=O+3!zdty&M*Qi
zIyx!{1FIm43hoTZDTj`?C^xbzDu<)1=qT=Ew`yzu*;n;mcmJxpzwYn7SFa?{-;+Wz
zA^`x!+e;RV<d(Imj$0!eH@hH#-|gz>3ea+x{3(`zY<j#`uphvmECC9O06rpA_#7ac
z5AY%eKvE50s65^tgc4yF1ARk1kb{ehi$o$3i^VoJHfVKla6pPkBtkGOEDVwVKu1SM
z#O>|v?d<Fj5ekLg-rhd{9$sEvJRT33NI?K8C=jjx9Vi>}7YGDM(M-{VLcnF?jC_nN
z-7MA9)nOlC@-d_Y65F0lP9{?YRBIb+yb6!ofkSC+ZEYbPKnCCeSSPHdt0ja2#G@v`
z*w`2q12JG^WMpM!1uj5$q2n#^24M#J`ueC5Gcz-41XWK@4;3$!N>QgWnateT9Os0y
zkXz^_=s^LXnpr{?D8RtL02Rz;vkiO<ARB-NOeT}z#2{=UsL|ET&CRJ2s+yr1hr^+<
zXhar~z#x!xNFE*@6%`x4e1W7S!^}+c;$rRTX}na*8XD5<>m$nLoZeoo4<9hj&dy@7
z^|NQ#@p0hsShlwO>1pEJ9O&q%@%jAWVXVD9XK;}G>#uO*hPJ1tXMVm>PLBGaL$tU!
zpwsDBuHc12>v!)ky}eLe42g+=wm>A}KYD~;Tm+HGTq@-)E#V$M1PVoCWd$lLiG_ub
zoJ>D_7=QZodLJLKu&^j8ftxq6`T3OYZsLazAe9>V`m#GaK}$<ZB4Lh=VLpF`-MiPJ
zeUFS}uC793Bi7p5y1iXTECv>f<?qj2Sb+EMAvDyewUzAeufKDr<+*bZ6=nA775a@E
z(b?$l2QHU$`!?Cq5}TT8I5B~7a@yqP=633oc3d1KCx?FI2pAd~78mOb3}Elv(QtL;
zzJ86FnISAMBaS<9g5>V*US7U#?_OwYQ(IcXPEA2?FR8DOJTO41ucwK{yum@->C-wH
z8MKZL2oHzuZuRJBaC5V2Yr~b5!T$XkZEe6{FicELOifMEiIvOcuCA_IwyfQqA=U3a
zNHDU3{X;<W$n=M)@;^S7sf`Q|m44Kko?LzX=u8-SJgD>#ZtTt3P{K32`M1a&6Svh<
z2E_yuFJx#ay|!lpushc#=H>%q2Ly>(-hLj$DUuci_|dER0RT9mx6C!vtN+0xh3cLQ
zmAIUe<$kckeV2G_8%?b@ygG((zLOf9aQ(T`#4h3a0qtCq_^TZq9XnbuicZgCHr~v9
zx0-^PFU+O6Ym;xHtgGIZ$39+JsS9c78mSK}_(fVq{qlCv&3uFKcbkyJfXy|WzlfU#
zuy|#Bcp~Ov<ln7of{#lbXB+QhS40j!%pNbB3BuJ^M-8U)<ktu*&Mi-jqe}a0HBJf@
zn8BkLIv*IY!*a;mbomW|mm{-Vf*65X$+D={^)*k5?l)}paylP$(k6*@@Eh4!eM=?9
z-0zs)-Ll3S{kv{j#HY>WY}N1suErkbLHFxjKTS;W)09`qW5<rzrH)==EGSwg2#bM1
zdACd)Z$?nE_ft+hebct}k@~ew%&D)w8~?M?<Mu$b{Q3B%@BJnVB%?R$qOVhfqGK*k
zkZHbzF|+i-g;kdxSGArl_my{nWe>HCrPy9sGDcpix}QW7g*VUk%@&miDy0>cJJNi+
z8rt_HRW(Qg`%9^Hj9WU(zCGL1J9CaXdG=(cPvi;6Svmo!;#hNDWD2*6)2kTj`dVLi
zTQN0VQ55b=FB*-F`1Y#gK;uLD`JMeU|1{RIe-iu6=e+p^LCTGh9gkl=)V-SO*sPi>
zKT&;Y$kxA3OO;Dn98JEq*!{z2V$);gvW}^AM(?0woVZk_Ii(`~I3<-vEmf4LUR)yQ
z9gs}_wM6xfcD#SYP;=vfL*A<Si|cl!_bCltKgsSfw(mMHy?$oF@m9o{s;Q3m%HjOH
zW&Urb-Hew%D?1N2;JfwPCPP+taNqi?`lkP}GBd}PUYL}5w<x~5NWQ1r{WO<Tu++Q-
zS4)*v`<l(Je5{xkIp4D0<=4=jKKV;c&)N1H%iW(sSeFx9mje_wHQWn9JprOB@tHf+
z7o?5~rBzQR&FAQB%+EC9{Ly$n&gFj7)S0^lqff`md|GPvQk}I6E_7FK9(mbS>9Bn%
zX^+%$pW6F+$#BVCY-NaCekCQZskN3c>MpO`7c!EjQTuI^=b6HN!GF8Io_FJ9)5XD*
z_xDzdwlcYqzvY~L^ZIr}Ml|=urBf?MnpvAUmFH9?-_!mezxwnUn@K3HLl=sf<q?sU
z5Syit#Ahgw03lys#})9o0-I2QNW$k!Y{YE75Ggp_>*y``pMcb~gguFS|9?P+FMbjQ
vY*_n3NLpf6Zfu4Ea&vQed%jA}jE_|+cxf5CYZkW{At&(m@Ryx;i~0Ir{{7o^

literal 0
HcmV?d00001

diff --git a/app/src/main/assets/novnc/app/images/icons/novnc-64x64.png b/app/src/main/assets/novnc/app/images/icons/novnc-64x64.png
new file mode 100644
index 0000000000000000000000000000000000000000..6d0fb34181b419fc0188004002d01c69227c60cb
GIT binary patch
literal 1946
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

literal 0
HcmV?d00001

diff --git a/app/src/main/assets/novnc/app/images/icons/novnc-72x72.png b/app/src/main/assets/novnc/app/images/icons/novnc-72x72.png
new file mode 100644
index 0000000000000000000000000000000000000000..23163a22d06001a508d01b83be1942d4e1032bf9
GIT binary patch
literal 2699
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

literal 0
HcmV?d00001

diff --git a/app/src/main/assets/novnc/app/images/icons/novnc-76x76.png b/app/src/main/assets/novnc/app/images/icons/novnc-76x76.png
new file mode 100644
index 0000000000000000000000000000000000000000..aef61c4804303f0c741d4b597bc512aec89e9779
GIT binary patch
literal 2874
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
T1zQ>d^di7O*F>jQ+bQlpIb%xa

literal 0
HcmV?d00001

diff --git a/app/src/main/assets/novnc/app/images/icons/novnc-96x96.png b/app/src/main/assets/novnc/app/images/icons/novnc-96x96.png
new file mode 100644
index 0000000000000000000000000000000000000000..1a77c53f4cb2d2bf63c86f654ce044fde3dac1aa
GIT binary patch
literal 2351
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

literal 0
HcmV?d00001

diff --git a/app/src/main/assets/novnc/app/images/icons/novnc-icon-sm.svg b/app/src/main/assets/novnc/app/images/icons/novnc-icon-sm.svg
new file mode 100644
index 00000000..aa1c6f18
--- /dev/null
+++ b/app/src/main/assets/novnc/app/images/icons/novnc-icon-sm.svg
@@ -0,0 +1,163 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="16"
+   height="16"
+   viewBox="0 0 16 16"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="novnc-icon-sm.svg">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="45.254834"
+     inkscape:cx="9.722703"
+     inkscape:cy="5.5311896"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:object-nodes="true"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:snap-midpoints="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1920"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4169" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1036.3621)">
+    <rect
+       style="opacity:1;fill:#494949;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect4167"
+       width="16"
+       height="15.999992"
+       x="0"
+       y="1036.3622"
+       ry="2.6666584" />
+    <path
+       style="opacity:1;fill:#313131;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       d="M 2.6666667,1036.3621 C 1.1893373,1036.3621 0,1037.5515 0,1039.0288 l 0,10.6666 c 0,1.4774 1.1893373,2.6667 2.6666667,2.6667 l 4,0 C 11.837333,1052.3621 16,1046.7128 16,1039.6955 l 0,-0.6667 c 0,-1.4773 -1.189337,-2.6667 -2.666667,-2.6667 l -10.6666663,0 z"
+       id="rect4173"
+       inkscape:connector-curvature="0" />
+    <g
+       id="g4381">
+      <g
+         transform="translate(0.25,0.25)"
+         style="fill:#000000;fill-opacity:1"
+         id="g4365">
+        <g
+           style="fill:#000000;fill-opacity:1"
+           id="g4367">
+          <path
+             inkscape:connector-curvature="0"
+             id="path4369"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+             d="m 4.3289754,1039.3621 c 0.1846149,0 0.3419956,0.071 0.4716623,0.2121 C 4.933546,1039.7121 5,1039.8793 5,1040.0759 l 0,3.2862 -1,0 0,-2.964 c 0,-0.024 -0.011592,-0.036 -0.034038,-0.036 l -1.931924,0 C 2.011349,1040.3621 2,1040.3741 2,1040.3981 l 0,2.964 -1,0 0,-4 z"
+             sodipodi:nodetypes="scsccsssscccs" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path4371"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+             d="m 6.6710244,1039.3621 2.6579513,0 c 0.184775,0 0.3419957,0.071 0.471662,0.2121 C 9.933546,1039.7121 10,1039.8793 10,1040.0759 l 0,2.5724 c 0,0.1966 -0.066454,0.3655 -0.1993623,0.5069 -0.1296663,0.1379 -0.286887,0.2069 -0.471662,0.2069 l -2.6579513,0 c -0.184775,0 -0.3436164,-0.069 -0.4765247,-0.2069 C 6.0648334,1043.0138 6,1042.8449 6,1042.6483 l 0,-2.5724 c 0,-0.1966 0.064833,-0.3638 0.1944997,-0.5017 0.1329083,-0.1414 0.2917497,-0.2121 0.4765247,-0.2121 z m 2.2949386,1 -1.931926,0 C 7.011344,1040.3621 7,1040.3741 7,1040.3981 l 0,1.928 c 0,0.024 0.011347,0.036 0.034037,0.036 l 1.931926,0 c 0.02269,0 0.034037,-0.012 0.034037,-0.036 l 0,-1.928 c 0,-0.024 -0.011347,-0.036 -0.034037,-0.036 z"
+             sodipodi:nodetypes="sscsscsscsscssssssssss" />
+        </g>
+        <g
+           style="fill:#000000;fill-opacity:1"
+           id="g4373">
+          <path
+             inkscape:connector-curvature="0"
+             id="path4375"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+             d="m 3,1047.1121 1,-2.75 1,0 -1.5,4 -1,0 -1.5,-4 1,0 z"
+             sodipodi:nodetypes="cccccccc" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path4377"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+             d="m 9,1046.8621 0,-2.5 1,0 0,4 -1,0 -2,-2.5 0,2.5 -1,0 0,-4 1,0 z"
+             sodipodi:nodetypes="ccccccccccc" />
+          <path
+             inkscape:connector-curvature="0"
+             id="path4379"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+             d="m 15,1045.3621 -2.96596,0 c -0.02269,0 -0.03404,0.012 -0.03404,0.036 l 0,1.928 c 0,0.024 0.01135,0.036 0.03404,0.036 l 2.96596,0 0,1 -3.324113,0 c -0.188017,0 -0.348479,-0.068 -0.481388,-0.2037 C 11.064833,1048.0192 11,1047.8511 11,1047.6542 l 0,-2.5842 c 0,-0.1969 0.06483,-0.3633 0.194499,-0.4991 0.132909,-0.1392 0.293371,-0.2088 0.481388,-0.2088 l 3.324113,0 z"
+             sodipodi:nodetypes="cssssccscsscscc" />
+        </g>
+      </g>
+      <g
+         id="g4356">
+        <g
+           id="g4347">
+          <path
+             sodipodi:nodetypes="scsccsssscccs"
+             d="m 4.3289754,1039.3621 c 0.1846149,0 0.3419956,0.071 0.4716623,0.2121 C 4.933546,1039.7121 5,1039.8793 5,1040.0759 l 0,3.2862 -1,0 0,-2.964 c 0,-0.024 -0.011592,-0.036 -0.034038,-0.036 l -1.931924,0 c -0.022689,0 -0.034038,0.012 -0.034038,0.036 l 0,2.964 -1,0 0,-4 z"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+             id="path4143"
+             inkscape:connector-curvature="0" />
+          <path
+             sodipodi:nodetypes="sscsscsscsscssssssssss"
+             d="m 6.6710244,1039.3621 2.6579513,0 c 0.184775,0 0.3419957,0.071 0.471662,0.2121 C 9.933546,1039.7121 10,1039.8793 10,1040.0759 l 0,2.5724 c 0,0.1966 -0.066454,0.3655 -0.1993623,0.5069 -0.1296663,0.1379 -0.286887,0.2069 -0.471662,0.2069 l -2.6579513,0 c -0.184775,0 -0.3436164,-0.069 -0.4765247,-0.2069 C 6.0648334,1043.0138 6,1042.8449 6,1042.6483 l 0,-2.5724 c 0,-0.1966 0.064833,-0.3638 0.1944997,-0.5017 0.1329083,-0.1414 0.2917497,-0.2121 0.4765247,-0.2121 z m 2.2949386,1 -1.931926,0 C 7.011344,1040.3621 7,1040.3741 7,1040.3981 l 0,1.928 c 0,0.024 0.011347,0.036 0.034037,0.036 l 1.931926,0 c 0.02269,0 0.034037,-0.012 0.034037,-0.036 l 0,-1.928 c 0,-0.024 -0.011347,-0.036 -0.034037,-0.036 z"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+             id="path4145"
+             inkscape:connector-curvature="0" />
+        </g>
+        <g
+           id="g4351">
+          <path
+             sodipodi:nodetypes="cccccccc"
+             d="m 3,1047.1121 1,-2.75 1,0 -1.5,4 -1,0 -1.5,-4 1,0 z"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+             id="path4147"
+             inkscape:connector-curvature="0" />
+          <path
+             sodipodi:nodetypes="ccccccccccc"
+             d="m 9,1046.8621 0,-2.5 1,0 0,4 -1,0 -2,-2.5 0,2.5 -1,0 0,-4 1,0 z"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+             id="path4149"
+             inkscape:connector-curvature="0" />
+          <path
+             sodipodi:nodetypes="cssssccscsscscc"
+             d="m 15,1045.3621 -2.96596,0 c -0.02269,0 -0.03404,0.012 -0.03404,0.036 l 0,1.928 c 0,0.024 0.01135,0.036 0.03404,0.036 l 2.96596,0 0,1 -3.324113,0 c -0.188017,0 -0.348479,-0.068 -0.481388,-0.2037 C 11.064833,1048.0192 11,1047.8511 11,1047.6542 l 0,-2.5842 c 0,-0.1969 0.06483,-0.3633 0.194499,-0.4991 0.132909,-0.1392 0.293371,-0.2088 0.481388,-0.2088 l 3.324113,0 z"
+             style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+             id="path4151"
+             inkscape:connector-curvature="0" />
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/app/src/main/assets/novnc/app/images/icons/novnc-icon.svg b/app/src/main/assets/novnc/app/images/icons/novnc-icon.svg
new file mode 100644
index 00000000..1efff912
--- /dev/null
+++ b/app/src/main/assets/novnc/app/images/icons/novnc-icon.svg
@@ -0,0 +1,163 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="48"
+   height="48"
+   viewBox="0 0 48 48.000001"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="novnc-icon.svg">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="11.313708"
+     inkscape:cx="27.187245"
+     inkscape:cy="17.700974"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:object-nodes="true"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:snap-midpoints="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1920"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4169" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1004.3621)">
+    <rect
+       style="opacity:1;fill:#494949;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="rect4167"
+       width="48"
+       height="48"
+       x="0"
+       y="1004.3621"
+       ry="7.9999785" />
+    <path
+       style="opacity:1;fill:#313131;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       d="m 8,1004.3621 c -4.4319881,0 -8,3.568 -8,8 l 0,32 c 0,4.432 3.5680119,8 8,8 l 12,0 c 15.512,0 28,-16.948 28,-38 l 0,-2 c 0,-4.432 -3.568012,-8 -8,-8 l -32,0 z"
+       id="rect4173"
+       inkscape:connector-curvature="0" />
+    <g
+       id="g4300"
+       style="fill:#000000;fill-opacity:1;stroke:none"
+       transform="translate(0.5,0.5)">
+      <g
+         id="g4302"
+         style="fill:#000000;fill-opacity:1;stroke:none">
+        <path
+           sodipodi:nodetypes="scsccsssscccs"
+           d="m 11.986926,1016.3621 c 0.554325,0 1.025987,0.2121 1.414987,0.6362 0.398725,0.4138 0.600909,0.9155 0.598087,1.5052 l 0,6.8586 -2,0 0,-6.8914 c 0,-0.072 -0.03404,-0.1086 -0.102113,-0.1086 l -4.7957745,0 C 7.0340375,1018.3621 7,1018.3983 7,1018.4707 l 0,6.8914 -2,0 0,-9 z"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           id="path4304"
+           inkscape:connector-curvature="0" />
+        <path
+           sodipodi:nodetypes="sscsscsscsscssssssssss"
+           d="m 17.013073,1016.3621 4.973854,0 c 0.554325,0 1.025987,0.2121 1.414986,0.6362 0.398725,0.4138 0.598087,0.9155 0.598087,1.5052 l 0,4.7172 c 0,0.5897 -0.199362,1.0966 -0.598087,1.5207 -0.388999,0.4138 -0.860661,0.6207 -1.414986,0.6207 l -4.973854,0 c -0.554325,0 -1.030849,-0.2069 -1.429574,-0.6207 C 15.1945,1024.3173 15,1023.8104 15,1023.2207 l 0,-4.7172 c 0,-0.5897 0.1945,-1.0914 0.583499,-1.5052 0.398725,-0.4241 0.875249,-0.6362 1.429574,-0.6362 z m 4.884815,2 -4.795776,0 c -0.06808,0 -0.102112,0.036 -0.102112,0.1086 l 0,4.7828 c 0,0.072 0.03404,0.1086 0.102112,0.1086 l 4.795776,0 c 0.06807,0 0.102112,-0.036 0.102112,-0.1086 l 0,-4.7828 c 0,-0.072 -0.03404,-0.1086 -0.102112,-0.1086 z"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           id="path4306"
+           inkscape:connector-curvature="0" />
+      </g>
+      <g
+         id="g4308"
+         style="fill:#000000;fill-opacity:1;stroke:none">
+        <path
+           sodipodi:nodetypes="cccccccc"
+           d="m 12,1036.9177 4.768114,-8.5556 2.231886,0 -6,11 -2,0 -6,-11 2.2318854,0 z"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           id="path4310"
+           inkscape:connector-curvature="0" />
+        <path
+           sodipodi:nodetypes="ccccccccccc"
+           d="m 29,1036.3621 0,-8 2,0 0,11 -2,0 -7,-8 0,8 -2,0 0,-11 2,0 z"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           id="path4312"
+           inkscape:connector-curvature="0" />
+        <path
+           sodipodi:nodetypes="cssssccscsscscc"
+           d="m 43,1030.3621 -8.897887,0 c -0.06808,0 -0.102113,0.036 -0.102113,0.1069 l 0,6.7862 c 0,0.071 0.03404,0.1069 0.102113,0.1069 l 8.897887,0 0,2 -8.972339,0 c -0.56405,0 -1.045437,-0.2037 -1.444162,-0.6111 C 32.1945,1038.3334 32,1037.8292 32,1037.2385 l 0,-6.7528 c 0,-0.5907 0.1945,-1.0898 0.583499,-1.4972 0.398725,-0.4176 0.880112,-0.6264 1.444162,-0.6264 l 8.972339,0 z"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           id="path4314"
+           inkscape:connector-curvature="0" />
+      </g>
+    </g>
+    <g
+       id="g4291"
+       style="stroke:none">
+      <g
+         id="g4282"
+         style="stroke:none">
+        <path
+           inkscape:connector-curvature="0"
+           id="path4143"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           d="m 11.986926,1016.3621 c 0.554325,0 1.025987,0.2121 1.414987,0.6362 0.398725,0.4138 0.600909,0.9155 0.598087,1.5052 l 0,6.8586 -2,0 0,-6.8914 c 0,-0.072 -0.03404,-0.1086 -0.102113,-0.1086 l -4.7957745,0 C 7.0340375,1018.3621 7,1018.3983 7,1018.4707 l 0,6.8914 -2,0 0,-9 z"
+           sodipodi:nodetypes="scsccsssscccs" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path4145"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           d="m 17.013073,1016.3621 4.973854,0 c 0.554325,0 1.025987,0.2121 1.414986,0.6362 0.398725,0.4138 0.598087,0.9155 0.598087,1.5052 l 0,4.7172 c 0,0.5897 -0.199362,1.0966 -0.598087,1.5207 -0.388999,0.4138 -0.860661,0.6207 -1.414986,0.6207 l -4.973854,0 c -0.554325,0 -1.030849,-0.2069 -1.429574,-0.6207 C 15.1945,1024.3173 15,1023.8104 15,1023.2207 l 0,-4.7172 c 0,-0.5897 0.1945,-1.0914 0.583499,-1.5052 0.398725,-0.4241 0.875249,-0.6362 1.429574,-0.6362 z m 4.884815,2 -4.795776,0 c -0.06808,0 -0.102112,0.036 -0.102112,0.1086 l 0,4.7828 c 0,0.072 0.03404,0.1086 0.102112,0.1086 l 4.795776,0 c 0.06807,0 0.102112,-0.036 0.102112,-0.1086 l 0,-4.7828 c 0,-0.072 -0.03404,-0.1086 -0.102112,-0.1086 z"
+           sodipodi:nodetypes="sscsscsscsscssssssssss" />
+      </g>
+      <g
+         id="g4286"
+         style="stroke:none">
+        <path
+           inkscape:connector-curvature="0"
+           id="path4147"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           d="m 12,1036.9177 4.768114,-8.5556 2.231886,0 -6,11 -2,0 -6,-11 2.2318854,0 z"
+           sodipodi:nodetypes="cccccccc" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path4149"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           d="m 29,1036.3621 0,-8 2,0 0,11 -2,0 -7,-8 0,8 -2,0 0,-11 2,0 z"
+           sodipodi:nodetypes="ccccccccccc" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path4151"
+           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           d="m 43,1030.3621 -8.897887,0 c -0.06808,0 -0.102113,0.036 -0.102113,0.1069 l 0,6.7862 c 0,0.071 0.03404,0.1069 0.102113,0.1069 l 8.897887,0 0,2 -8.972339,0 c -0.56405,0 -1.045437,-0.2037 -1.444162,-0.6111 C 32.1945,1038.3334 32,1037.8292 32,1037.2385 l 0,-6.7528 c 0,-0.5907 0.1945,-1.0898 0.583499,-1.4972 0.398725,-0.4176 0.880112,-0.6264 1.444162,-0.6264 l 8.972339,0 z"
+           sodipodi:nodetypes="cssssccscsscscc" />
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/app/src/main/assets/novnc/app/images/info.svg b/app/src/main/assets/novnc/app/images/info.svg
new file mode 100644
index 00000000..557b772f
--- /dev/null
+++ b/app/src/main/assets/novnc/app/images/info.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="25"
+   height="25"
+   viewBox="0 0 25 25"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="info.svg"
+   inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#959595"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="15.720838"
+     inkscape:cy="8.9111233"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:object-paths="true"
+     showguides="false"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1920"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-intersection-paths="true"
+     inkscape:snap-nodes="true"
+     inkscape:snap-global="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4136" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1027.3622)">
+    <path
+       style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       d="M 12.5 3 A 9.5 9.4999914 0 0 0 3 12.5 A 9.5 9.4999914 0 0 0 12.5 22 A 9.5 9.4999914 0 0 0 22 12.5 A 9.5 9.4999914 0 0 0 12.5 3 z M 12.5 5 A 1.5 1.5000087 0 0 1 14 6.5 A 1.5 1.5000087 0 0 1 12.5 8 A 1.5 1.5000087 0 0 1 11 6.5 A 1.5 1.5000087 0 0 1 12.5 5 z M 10.521484 8.9785156 L 12.521484 8.9785156 A 1.50015 1.50015 0 0 1 14.021484 10.478516 L 14.021484 15.972656 A 1.50015 1.50015 0 0 1 14.498047 18.894531 C 14.498047 18.894531 13.74301 19.228309 12.789062 18.912109 C 12.312092 18.754109 11.776235 18.366625 11.458984 17.828125 C 11.141734 17.289525 11.021484 16.668469 11.021484 15.980469 L 11.021484 11.980469 L 10.521484 11.980469 A 1.50015 1.50015 0 1 1 10.521484 8.9804688 L 10.521484 8.9785156 z "
+       transform="translate(0,1027.3622)"
+       id="path4136" />
+  </g>
+</svg>
diff --git a/app/src/main/assets/novnc/app/images/keyboard.svg b/app/src/main/assets/novnc/app/images/keyboard.svg
new file mode 100644
index 00000000..137b350a
--- /dev/null
+++ b/app/src/main/assets/novnc/app/images/keyboard.svg
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="25"
+   height="25"
+   viewBox="0 0 25 25"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="keyboard.svg"
+   inkscape:export-filename="/home/ossman/devel/noVNC/images/keyboard.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#717171"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="31.285341"
+     inkscape:cy="8.8028469"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:snap-bbox-midpoints="false"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1920"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     inkscape:object-paths="true"
+     inkscape:snap-intersection-paths="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-midpoints="true"
+     inkscape:snap-smooth-nodes="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4136" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1027.3622)">
+    <path
+       style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+       d="M 7,3 C 4.8012876,3 3,4.8013 3,7 3,11.166667 3,15.333333 3,19.5 3,20.8764 4.1236413,22 5.5,22 l 14,0 C 20.876358,22 22,20.8764 22,19.5 22,15.333333 22,11.166667 22,7 22,4.8013 20.198712,3 18,3 Z m 0,2 11,0 c 1.125307,0 2,0.8747 2,2 L 20,12 5,12 5,7 C 5,5.8747 5.8746931,5 7,5 Z M 6.5,14 C 6.777,14 7,14.223 7,14.5 7,14.777 6.777,15 6.5,15 6.223,15 6,14.777 6,14.5 6,14.223 6.223,14 6.5,14 Z m 2,0 C 8.777,14 9,14.223 9,14.5 9,14.777 8.777,15 8.5,15 8.223,15 8,14.777 8,14.5 8,14.223 8.223,14 8.5,14 Z m 2,0 C 10.777,14 11,14.223 11,14.5 11,14.777 10.777,15 10.5,15 10.223,15 10,14.777 10,14.5 10,14.223 10.223,14 10.5,14 Z m 2,0 C 12.777,14 13,14.223 13,14.5 13,14.777 12.777,15 12.5,15 12.223,15 12,14.777 12,14.5 12,14.223 12.223,14 12.5,14 Z m 2,0 C 14.777,14 15,14.223 15,14.5 15,14.777 14.777,15 14.5,15 14.223,15 14,14.777 14,14.5 14,14.223 14.223,14 14.5,14 Z m 2,0 C 16.777,14 17,14.223 17,14.5 17,14.777 16.777,15 16.5,15 16.223,15 16,14.777 16,14.5 16,14.223 16.223,14 16.5,14 Z m 2,0 C 18.777,14 19,14.223 19,14.5 19,14.777 18.777,15 18.5,15 18.223,15 18,14.777 18,14.5 18,14.223 18.223,14 18.5,14 Z m -13,2 C 5.777,16 6,16.223 6,16.5 6,16.777 5.777,17 5.5,17 5.223,17 5,16.777 5,16.5 5,16.223 5.223,16 5.5,16 Z m 2,0 C 7.777,16 8,16.223 8,16.5 8,16.777 7.777,17 7.5,17 7.223,17 7,16.777 7,16.5 7,16.223 7.223,16 7.5,16 Z m 2,0 C 9.777,16 10,16.223 10,16.5 10,16.777 9.777,17 9.5,17 9.223,17 9,16.777 9,16.5 9,16.223 9.223,16 9.5,16 Z m 2,0 C 11.777,16 12,16.223 12,16.5 12,16.777 11.777,17 11.5,17 11.223,17 11,16.777 11,16.5 11,16.223 11.223,16 11.5,16 Z m 2,0 C 13.777,16 14,16.223 14,16.5 14,16.777 13.777,17 13.5,17 13.223,17 13,16.777 13,16.5 13,16.223 13.223,16 13.5,16 Z m 2,0 C 15.777,16 16,16.223 16,16.5 16,16.777 15.777,17 15.5,17 15.223,17 15,16.777 15,16.5 15,16.223 15.223,16 15.5,16 Z m 2,0 C 17.777,16 18,16.223 18,16.5 18,16.777 17.777,17 17.5,17 17.223,17 17,16.777 17,16.5 17,16.223 17.223,16 17.5,16 Z m 2,0 C 19.777,16 20,16.223 20,16.5 20,16.777 19.777,17 19.5,17 19.223,17 19,16.777 19,16.5 19,16.223 19.223,16 19.5,16 Z M 6,18 c 0.554,0 1,0.446 1,1 0,0.554 -0.446,1 -1,1 -0.554,0 -1,-0.446 -1,-1 0,-0.554 0.446,-1 1,-1 z m 2.8261719,0 7.3476561,0 C 16.631643,18 17,18.368372 17,18.826172 l 0,0.347656 C 17,19.631628 16.631643,20 16.173828,20 L 8.8261719,20 C 8.3683573,20 8,19.631628 8,19.173828 L 8,18.826172 C 8,18.368372 8.3683573,18 8.8261719,18 Z m 10.1113281,0 0.125,0 C 19.581551,18 20,18.4184 20,18.9375 l 0,0.125 C 20,19.5816 19.581551,20 19.0625,20 l -0.125,0 C 18.418449,20 18,19.5816 18,19.0625 l 0,-0.125 C 18,18.4184 18.418449,18 18.9375,18 Z"
+       transform="translate(0,1027.3622)"
+       id="rect4160"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="sccssccsssssccssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" />
+    <path
+       style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
+       d="m 12.499929,1033.8622 -2,2 1.500071,0 0,2 1,0 0,-2 1.499929,0 z"
+       id="path4150"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cccccccc" />
+  </g>
+</svg>
diff --git a/app/src/main/assets/novnc/app/images/power.svg b/app/src/main/assets/novnc/app/images/power.svg
new file mode 100644
index 00000000..4925d3e8
--- /dev/null
+++ b/app/src/main/assets/novnc/app/images/power.svg
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="25"
+   height="25"
+   viewBox="0 0 25 25"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="power.svg"
+   inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#959595"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="9.3159849"
+     inkscape:cy="13.436208"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:object-paths="true"
+     showguides="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1920"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-intersection-paths="true"
+     inkscape:snap-nodes="true"
+     inkscape:snap-global="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4136" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1027.3622)">
+    <path
+       style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+       d="M 9 6.8183594 C 6.3418164 8.1213032 4.5 10.849161 4.5 14 C 4.5 18.4065 8.0935666 22 12.5 22 C 16.906433 22 20.5 18.4065 20.5 14 C 20.5 10.849161 18.658184 8.1213032 16 6.8183594 L 16 9.125 C 17.514327 10.211757 18.5 11.984508 18.5 14 C 18.5 17.3256 15.825553 20 12.5 20 C 9.1744469 20 6.5 17.3256 6.5 14 C 6.5 11.984508 7.4856727 10.211757 9 9.125 L 9 6.8183594 z "
+       transform="translate(0,1027.3622)"
+       id="path6140" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 12.5,1031.8836 0,6.4786"
+       id="path6142"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cc" />
+  </g>
+</svg>
diff --git a/app/src/main/assets/novnc/app/images/settings.svg b/app/src/main/assets/novnc/app/images/settings.svg
new file mode 100644
index 00000000..dbb2e80a
--- /dev/null
+++ b/app/src/main/assets/novnc/app/images/settings.svg
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="25"
+   height="25"
+   viewBox="0 0 25 25"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="settings.svg"
+   inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#959595"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="22.627417"
+     inkscape:cx="14.69683"
+     inkscape:cy="8.8039511"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:object-paths="true"
+     showguides="false"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1920"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4136" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1027.3622)">
+    <path
+       style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       d="M 11 3 L 11 5.1601562 A 7.5 7.5 0 0 0 8.3671875 6.2460938 L 6.84375 4.7226562 L 4.7226562 6.84375 L 6.2480469 8.3691406 A 7.5 7.5 0 0 0 5.1523438 11 L 3 11 L 3 14 L 5.1601562 14 A 7.5 7.5 0 0 0 6.2460938 16.632812 L 4.7226562 18.15625 L 6.84375 20.277344 L 8.3691406 18.751953 A 7.5 7.5 0 0 0 11 19.847656 L 11 22 L 14 22 L 14 19.839844 A 7.5 7.5 0 0 0 16.632812 18.753906 L 18.15625 20.277344 L 20.277344 18.15625 L 18.751953 16.630859 A 7.5 7.5 0 0 0 19.847656 14 L 22 14 L 22 11 L 19.839844 11 A 7.5 7.5 0 0 0 18.753906 8.3671875 L 20.277344 6.84375 L 18.15625 4.7226562 L 16.630859 6.2480469 A 7.5 7.5 0 0 0 14 5.1523438 L 14 3 L 11 3 z M 12.5 10 A 2.5 2.5 0 0 1 15 12.5 A 2.5 2.5 0 0 1 12.5 15 A 2.5 2.5 0 0 1 10 12.5 A 2.5 2.5 0 0 1 12.5 10 z "
+       transform="translate(0,1027.3622)"
+       id="rect4967" />
+  </g>
+</svg>
diff --git a/app/src/main/assets/novnc/app/images/tab.svg b/app/src/main/assets/novnc/app/images/tab.svg
new file mode 100644
index 00000000..1ccb3229
--- /dev/null
+++ b/app/src/main/assets/novnc/app/images/tab.svg
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="25"
+   height="25"
+   viewBox="0 0 25 25"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="tab.svg"
+   inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#959595"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="16"
+     inkscape:cx="11.67335"
+     inkscape:cy="17.881696"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:object-paths="true"
+     showguides="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1920"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-intersection-paths="true"
+     inkscape:snap-nodes="true"
+     inkscape:snap-global="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4136" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1027.3622)">
+    <path
+       style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       d="m 3,1031.3622 0,8 2,0 0,-4 0,-4 -2,0 z m 2,4 4,4 0,-3 13,0 0,-2 -13,0 0,-3 -4,4 z"
+       id="rect5194"
+       inkscape:connector-curvature="0" />
+    <path
+       id="path5211"
+       d="m 22,1048.3622 0,-8 -2,0 0,4 0,4 2,0 z m -2,-4 -4,-4 0,3 -13,0 0,2 13,0 0,3 4,-4 z"
+       style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       inkscape:connector-curvature="0" />
+  </g>
+</svg>
diff --git a/app/src/main/assets/novnc/app/images/toggleextrakeys.svg b/app/src/main/assets/novnc/app/images/toggleextrakeys.svg
new file mode 100644
index 00000000..b578c0d4
--- /dev/null
+++ b/app/src/main/assets/novnc/app/images/toggleextrakeys.svg
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="25"
+   height="25"
+   viewBox="0 0 25 25"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="extrakeys.svg"
+   inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#959595"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="15.234555"
+     inkscape:cy="9.9710826"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:object-paths="true"
+     showguides="false"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1920"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-intersection-paths="true"
+     inkscape:snap-nodes="false">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4136" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1027.3622)">
+    <path
+       style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+       d="m 8,1031.3622 c -2.1987124,0 -4,1.8013 -4,4 l 0,8.9996 c 0,2.1987 1.8012876,4 4,4 l 9,0 c 2.198712,0 4,-1.8013 4,-4 l 0,-8.9996 c 0,-2.1987 -1.801288,-4 -4,-4 z m 0,2 9,0 c 1.125307,0 2,0.8747 2,2 l 0,7.0005 c 0,1.1253 -0.874693,2 -2,2 l -9,0 c -1.1253069,0 -2,-0.8747 -2,-2 l 0,-7.0005 c 0,-1.1253 0.8746931,-2 2,-2 z"
+       id="rect5006"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ssssssssssssssssss" />
+    <g
+       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:10px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'Sans Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       id="text4167"
+       transform="matrix(0.96021948,0,0,0.96021948,0.18921715,41.80659)">
+      <path
+         d="m 14.292969,1040.6791 -2.939453,0 -0.463868,1.3281 -1.889648,0 2.700195,-7.29 2.241211,0 2.700196,7.29 -1.889649,0 -0.458984,-1.3281 z m -2.470703,-1.3526 1.99707,0 -0.996094,-2.9004 -1.000976,2.9004 z"
+         id="path4172"
+         inkscape:connector-curvature="0" />
+    </g>
+  </g>
+</svg>
diff --git a/app/src/main/assets/novnc/app/images/warning.svg b/app/src/main/assets/novnc/app/images/warning.svg
new file mode 100644
index 00000000..7114f9b1
--- /dev/null
+++ b/app/src/main/assets/novnc/app/images/warning.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="25"
+   height="25"
+   viewBox="0 0 25 25"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="warning.svg"
+   inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#959595"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="16.457343"
+     inkscape:cy="12.179552"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:object-paths="true"
+     showguides="false"
+     inkscape:window-width="1920"
+     inkscape:window-height="1136"
+     inkscape:window-x="1920"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:object-nodes="true"
+     inkscape:snap-intersection-paths="true"
+     inkscape:snap-nodes="true"
+     inkscape:snap-global="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4136" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1027.3622)">
+    <path
+       style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:4;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+       d="M 12.513672 3.0019531 C 11.751609 2.9919531 11.052563 3.4242687 10.710938 4.1054688 L 3.2109375 19.105469 C 2.5461937 20.435369 3.5132277 21.9999 5 22 L 20 22 C 21.486772 21.9999 22.453806 20.435369 21.789062 19.105469 L 14.289062 4.1054688 C 13.951849 3.4330688 13.265888 3.0066531 12.513672 3.0019531 z M 12.478516 6.9804688 A 1.50015 1.50015 0 0 1 14 8.5 L 14 14.5 A 1.50015 1.50015 0 1 1 11 14.5 L 11 8.5 A 1.50015 1.50015 0 0 1 12.478516 6.9804688 z M 12.5 17 A 1.5 1.5 0 0 1 14 18.5 A 1.5 1.5 0 0 1 12.5 20 A 1.5 1.5 0 0 1 11 18.5 A 1.5 1.5 0 0 1 12.5 17 z "
+       transform="translate(0,1027.3622)"
+       id="path4208" />
+  </g>
+</svg>
diff --git a/app/src/main/assets/novnc/app/images/windows.svg b/app/src/main/assets/novnc/app/images/windows.svg
new file mode 100644
index 00000000..ad5eec36
--- /dev/null
+++ b/app/src/main/assets/novnc/app/images/windows.svg
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   version="1.1"
+   id="svg2"
+   inkscape:export-ydpi="90"
+   inkscape:export-xdpi="90"
+   sodipodi:docname="windows.svg"
+   inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
+   inkscape:version="0.92.4 (unknown)"
+   x="0px"
+   y="0px"
+   viewBox="-293 384 25 25"
+   xml:space="preserve"
+   width="25"
+   height="25"><metadata
+   id="metadata21"><rdf:RDF><cc:Work
+       rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+         rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+   id="defs19" /><sodipodi:namedview
+   pagecolor="#959595"
+   bordercolor="#666666"
+   borderopacity="1"
+   objecttolerance="10"
+   gridtolerance="10"
+   guidetolerance="10"
+   inkscape:pageopacity="0"
+   inkscape:pageshadow="2"
+   inkscape:window-width="1920"
+   inkscape:window-height="1136"
+   id="namedview17"
+   showgrid="true"
+   inkscape:pagecheckerboard="false"
+   inkscape:zoom="32"
+   inkscape:cx="3.926913"
+   inkscape:cy="13.255959"
+   inkscape:window-x="1920"
+   inkscape:window-y="27"
+   inkscape:window-maximized="1"
+   inkscape:current-layer="svg2"><inkscape:grid
+     type="xygrid"
+     id="grid818" /></sodipodi:namedview>
+<style
+   type="text/css"
+   id="style2">
+	.st0{fill:#FFFFFF;}
+</style>
+
+<path
+   style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
+   d="M 21 4 L 11 5.1757812 L 11 12 L 21 12 L 21 4 z M 10 5.2949219 L 4 6 L 4 12 L 10 12 L 10 5.2949219 z "
+   transform="translate(-293,384)"
+   id="path853" /><path
+   id="path858"
+   d="m -272,405 -10,-1.17578 V 397 h 10 z M -283,403.70508 -289,403 v -6 h 6 z"
+   style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+   inkscape:connector-curvature="0" /></svg>
\ No newline at end of file
diff --git a/app/src/main/assets/novnc/app/locale/README b/app/src/main/assets/novnc/app/locale/README
new file mode 100644
index 00000000..ca4f548b
--- /dev/null
+++ b/app/src/main/assets/novnc/app/locale/README
@@ -0,0 +1 @@
+DO NOT MODIFY THE FILES IN THIS FOLDER, THEY ARE AUTOMATICALLY GENERATED FROM THE PO-FILES.
diff --git a/app/src/main/assets/novnc/app/locale/cs.json b/app/src/main/assets/novnc/app/locale/cs.json
new file mode 100644
index 00000000..589145ef
--- /dev/null
+++ b/app/src/main/assets/novnc/app/locale/cs.json
@@ -0,0 +1,71 @@
+{
+    "Connecting...": "Připojení...",
+    "Disconnecting...": "Odpojení...",
+    "Reconnecting...": "Obnova připojení...",
+    "Internal error": "Vnitřní chyba",
+    "Must set host": "Hostitel musí být nastavení",
+    "Connected (encrypted) to ": "Připojení (šifrované) k ",
+    "Connected (unencrypted) to ": "Připojení (nešifrované) k ",
+    "Something went wrong, connection is closed": "Něco se pokazilo, odpojeno",
+    "Failed to connect to server": "Chyba připojení k serveru",
+    "Disconnected": "Odpojeno",
+    "New connection has been rejected with reason: ": "Nové připojení bylo odmítnuto s odůvodněním: ",
+    "New connection has been rejected": "Nové připojení bylo odmítnuto",
+    "Password is required": "Je vyžadováno heslo",
+    "noVNC encountered an error:": "noVNC narazilo na chybu:",
+    "Hide/Show the control bar": "Skrýt/zobrazit ovládací panel",
+    "Move/Drag Viewport": "Přesunout/přetáhnout výřez",
+    "viewport drag": "přesun výřezu",
+    "Active Mouse Button": "Aktivní tlačítka myši",
+    "No mousebutton": "Žádné",
+    "Left mousebutton": "Levé tlačítko myši",
+    "Middle mousebutton": "Prostřední tlačítko myši",
+    "Right mousebutton": "Pravé tlačítko myši",
+    "Keyboard": "Klávesnice",
+    "Show Keyboard": "Zobrazit klávesnici",
+    "Extra keys": "Extra klávesy",
+    "Show Extra Keys": "Zobrazit extra klávesy",
+    "Ctrl": "Ctrl",
+    "Toggle Ctrl": "Přepnout Ctrl",
+    "Alt": "Alt",
+    "Toggle Alt": "Přepnout Alt",
+    "Send Tab": "Odeslat tabulátor",
+    "Tab": "Tab",
+    "Esc": "Esc",
+    "Send Escape": "Odeslat Esc",
+    "Ctrl+Alt+Del": "Ctrl+Alt+Del",
+    "Send Ctrl-Alt-Del": "Poslat Ctrl-Alt-Del",
+    "Shutdown/Reboot": "Vypnutí/Restart",
+    "Shutdown/Reboot...": "Vypnutí/Restart...",
+    "Power": "Napájení",
+    "Shutdown": "Vypnout",
+    "Reboot": "Restart",
+    "Reset": "Reset",
+    "Clipboard": "Schránka",
+    "Clear": "Vymazat",
+    "Fullscreen": "Celá obrazovka",
+    "Settings": "Nastavení",
+    "Shared Mode": "Sdílený režim",
+    "View Only": "Pouze prohlížení",
+    "Clip to Window": "Přizpůsobit oknu",
+    "Scaling Mode:": "Přizpůsobení velikosti",
+    "None": "Žádné",
+    "Local Scaling": "Místní",
+    "Remote Resizing": "Vzdálené",
+    "Advanced": "Pokročilé",
+    "Repeater ID:": "ID opakovače",
+    "WebSocket": "WebSocket",
+    "Encrypt": "Šifrování:",
+    "Host:": "Hostitel:",
+    "Port:": "Port:",
+    "Path:": "Cesta",
+    "Automatic Reconnect": "Automatická obnova připojení",
+    "Reconnect Delay (ms):": "Zpoždění připojení (ms)",
+    "Show Dot when No Cursor": "Tečka místo chybějícího kurzoru myši",
+    "Logging:": "Logování:",
+    "Disconnect": "Odpojit",
+    "Connect": "Připojit",
+    "Password:": "Heslo",
+    "Send Password": "Odeslat heslo",
+    "Cancel": "Zrušit"
+}
\ No newline at end of file
diff --git a/app/src/main/assets/novnc/app/locale/de.json b/app/src/main/assets/novnc/app/locale/de.json
new file mode 100644
index 00000000..62e73360
--- /dev/null
+++ b/app/src/main/assets/novnc/app/locale/de.json
@@ -0,0 +1,69 @@
+{
+    "Connecting...": "Verbinden...",
+    "Disconnecting...": "Verbindung trennen...",
+    "Reconnecting...": "Verbindung wiederherstellen...",
+    "Internal error": "Interner Fehler",
+    "Must set host": "Richten Sie den Server ein",
+    "Connected (encrypted) to ": "Verbunden mit (verschlüsselt) ",
+    "Connected (unencrypted) to ": "Verbunden mit (unverschlüsselt) ",
+    "Something went wrong, connection is closed": "Etwas lief schief, Verbindung wurde getrennt",
+    "Disconnected": "Verbindung zum Server getrennt",
+    "New connection has been rejected with reason: ": "Verbindung wurde aus folgendem Grund abgelehnt: ",
+    "New connection has been rejected": "Verbindung wurde abgelehnt",
+    "Password is required": "Passwort ist erforderlich",
+    "noVNC encountered an error:": "Ein Fehler ist aufgetreten:",
+    "Hide/Show the control bar": "Kontrollleiste verstecken/anzeigen",
+    "Move/Drag Viewport": "Ansichtsfenster verschieben/ziehen",
+    "viewport drag": "Ansichtsfenster ziehen",
+    "Active Mouse Button": "Aktive Maustaste",
+    "No mousebutton": "Keine Maustaste",
+    "Left mousebutton": "Linke Maustaste",
+    "Middle mousebutton": "Mittlere Maustaste",
+    "Right mousebutton": "Rechte Maustaste",
+    "Keyboard": "Tastatur",
+    "Show Keyboard": "Tastatur anzeigen",
+    "Extra keys": "Zusatztasten",
+    "Show Extra Keys": "Zusatztasten anzeigen",
+    "Ctrl": "Strg",
+    "Toggle Ctrl": "Strg umschalten",
+    "Alt": "Alt",
+    "Toggle Alt": "Alt umschalten",
+    "Send Tab": "Tab senden",
+    "Tab": "Tab",
+    "Esc": "Esc",
+    "Send Escape": "Escape senden",
+    "Ctrl+Alt+Del": "Strg+Alt+Entf",
+    "Send Ctrl-Alt-Del": "Strg+Alt+Entf senden",
+    "Shutdown/Reboot": "Herunterfahren/Neustarten",
+    "Shutdown/Reboot...": "Herunterfahren/Neustarten...",
+    "Power": "Energie",
+    "Shutdown": "Herunterfahren",
+    "Reboot": "Neustarten",
+    "Reset": "Zurücksetzen",
+    "Clipboard": "Zwischenablage",
+    "Clear": "Löschen",
+    "Fullscreen": "Vollbild",
+    "Settings": "Einstellungen",
+    "Shared Mode": "Geteilter Modus",
+    "View Only": "Nur betrachten",
+    "Clip to Window": "Auf Fenster begrenzen",
+    "Scaling Mode:": "Skalierungsmodus:",
+    "None": "Keiner",
+    "Local Scaling": "Lokales skalieren",
+    "Remote Resizing": "Serverseitiges skalieren",
+    "Advanced": "Erweitert",
+    "Repeater ID:": "Repeater ID:",
+    "WebSocket": "WebSocket",
+    "Encrypt": "Verschlüsselt",
+    "Host:": "Server:",
+    "Port:": "Port:",
+    "Path:": "Pfad:",
+    "Automatic Reconnect": "Automatisch wiederverbinden",
+    "Reconnect Delay (ms):": "Wiederverbindungsverzögerung (ms):",
+    "Logging:": "Protokollierung:",
+    "Disconnect": "Verbindung trennen",
+    "Connect": "Verbinden",
+    "Password:": "Passwort:",
+    "Cancel": "Abbrechen",
+    "Canvas not supported.": "Canvas nicht unterstützt."
+}
\ No newline at end of file
diff --git a/app/src/main/assets/novnc/app/locale/el.json b/app/src/main/assets/novnc/app/locale/el.json
new file mode 100644
index 00000000..f801251c
--- /dev/null
+++ b/app/src/main/assets/novnc/app/locale/el.json
@@ -0,0 +1,69 @@
+{
+    "Connecting...": "Συνδέεται...",
+    "Disconnecting...": "Aποσυνδέεται...",
+    "Reconnecting...": "Επανασυνδέεται...",
+    "Internal error": "Εσωτερικό σφάλμα",
+    "Must set host": "Πρέπει να οριστεί ο διακομιστής",
+    "Connected (encrypted) to ": "Συνδέθηκε (κρυπτογραφημένα) με το ",
+    "Connected (unencrypted) to ": "Συνδέθηκε (μη κρυπτογραφημένα) με το ",
+    "Something went wrong, connection is closed": "Κάτι πήγε στραβά, η σύνδεση διακόπηκε",
+    "Disconnected": "Αποσυνδέθηκε",
+    "New connection has been rejected with reason: ": "Η νέα σύνδεση απορρίφθηκε διότι: ",
+    "New connection has been rejected": "Η νέα σύνδεση απορρίφθηκε ",
+    "Password is required": "Απαιτείται ο κωδικός πρόσβασης",
+    "noVNC encountered an error:": "το noVNC αντιμετώπισε ένα σφάλμα:",
+    "Hide/Show the control bar": "Απόκρυψη/Εμφάνιση γραμμής ελέγχου",
+    "Move/Drag Viewport": "Μετακίνηση/Σύρσιμο Θεατού πεδίου",
+    "viewport drag": "σύρσιμο θεατού πεδίου",
+    "Active Mouse Button": "Ενεργό Πλήκτρο Ποντικιού",
+    "No mousebutton": "Χωρίς Πλήκτρο Ποντικιού",
+    "Left mousebutton": "Αριστερό Πλήκτρο Ποντικιού",
+    "Middle mousebutton": "Μεσαίο Πλήκτρο Ποντικιού",
+    "Right mousebutton": "Δεξί Πλήκτρο Ποντικιού",
+    "Keyboard": "Πληκτρολόγιο",
+    "Show Keyboard": "Εμφάνιση Πληκτρολογίου",
+    "Extra keys": "Επιπλέον πλήκτρα",
+    "Show Extra Keys": "Εμφάνιση Επιπλέον Πλήκτρων",
+    "Ctrl": "Ctrl",
+    "Toggle Ctrl": "Εναλλαγή Ctrl",
+    "Alt": "Alt",
+    "Toggle Alt": "Εναλλαγή Alt",
+    "Send Tab": "Αποστολή Tab",
+    "Tab": "Tab",
+    "Esc": "Esc",
+    "Send Escape": "Αποστολή Escape",
+    "Ctrl+Alt+Del": "Ctrl+Alt+Del",
+    "Send Ctrl-Alt-Del": "Αποστολή Ctrl-Alt-Del",
+    "Shutdown/Reboot": "Κλείσιμο/Επανεκκίνηση",
+    "Shutdown/Reboot...": "Κλείσιμο/Επανεκκίνηση...",
+    "Power": "Απενεργοποίηση",
+    "Shutdown": "Κλείσιμο",
+    "Reboot": "Επανεκκίνηση",
+    "Reset": "Επαναφορά",
+    "Clipboard": "Πρόχειρο",
+    "Clear": "Καθάρισμα",
+    "Fullscreen": "Πλήρης Οθόνη",
+    "Settings": "Ρυθμίσεις",
+    "Shared Mode": "Κοινόχρηστη Λειτουργία",
+    "View Only": "Μόνο Θέαση",
+    "Clip to Window": "Αποκοπή στο όριο του Παράθυρου",
+    "Scaling Mode:": "Λειτουργία Κλιμάκωσης:",
+    "None": "Καμία",
+    "Local Scaling": "Τοπική Κλιμάκωση",
+    "Remote Resizing": "Απομακρυσμένη Αλλαγή μεγέθους",
+    "Advanced": "Για προχωρημένους",
+    "Repeater ID:": "Repeater ID:",
+    "WebSocket": "WebSocket",
+    "Encrypt": "Κρυπτογράφηση",
+    "Host:": "Όνομα διακομιστή:",
+    "Port:": "Πόρτα διακομιστή:",
+    "Path:": "Διαδρομή:",
+    "Automatic Reconnect": "Αυτόματη επανασύνδεση",
+    "Reconnect Delay (ms):": "Καθυστέρηση επανασύνδεσης (ms):",
+    "Logging:": "Καταγραφή:",
+    "Disconnect": "Αποσύνδεση",
+    "Connect": "Σύνδεση",
+    "Password:": "Κωδικός Πρόσβασης:",
+    "Cancel": "Ακύρωση",
+    "Canvas not supported.": "Δεν υποστηρίζεται το στοιχείο Canvas"
+}
\ No newline at end of file
diff --git a/app/src/main/assets/novnc/app/locale/es.json b/app/src/main/assets/novnc/app/locale/es.json
new file mode 100644
index 00000000..b9e663a3
--- /dev/null
+++ b/app/src/main/assets/novnc/app/locale/es.json
@@ -0,0 +1,68 @@
+{
+    "Connecting...": "Conectando...",
+    "Connected (encrypted) to ": "Conectado (con encriptación) a",
+    "Connected (unencrypted) to ": "Conectado (sin encriptación) a",
+    "Disconnecting...": "Desconectando...",
+    "Disconnected": "Desconectado",
+    "Must set host": "Se debe configurar el host",
+    "Reconnecting...": "Reconectando...",
+    "Password is required": "La contraseña es obligatoria",
+    "Disconnect timeout": "Tiempo de desconexión agotado",
+    "noVNC encountered an error:": "noVNC ha encontrado un error:",
+    "Hide/Show the control bar": "Ocultar/Mostrar la barra de control",
+    "Move/Drag Viewport": "Mover/Arrastrar la ventana",
+    "viewport drag": "Arrastrar la ventana",
+    "Active Mouse Button": "Botón activo del ratón",
+    "No mousebutton": "Ningún botón del ratón",
+    "Left mousebutton": "Botón izquierdo del ratón",
+    "Middle mousebutton": "Botón central del ratón",
+    "Right mousebutton": "Botón derecho del ratón",
+    "Keyboard": "Teclado",
+    "Show Keyboard": "Mostrar teclado",
+    "Extra keys": "Teclas adicionales",
+    "Show Extra Keys": "Mostrar Teclas Adicionales",
+    "Ctrl": "Ctrl",
+    "Toggle Ctrl": "Pulsar/Soltar Ctrl",
+    "Alt": "Alt",
+    "Toggle Alt": "Pulsar/Soltar Alt",
+    "Send Tab": "Enviar Tabulación",
+    "Tab": "Tabulación",
+    "Esc": "Esc",
+    "Send Escape": "Enviar Escape",
+    "Ctrl+Alt+Del": "Ctrl+Alt+Del",
+    "Send Ctrl-Alt-Del": "Enviar Ctrl+Alt+Del",
+    "Shutdown/Reboot": "Apagar/Reiniciar",
+    "Shutdown/Reboot...": "Apagar/Reiniciar...",
+    "Power": "Encender",
+    "Shutdown": "Apagar",
+    "Reboot": "Reiniciar",
+    "Reset": "Restablecer",
+    "Clipboard": "Portapapeles",
+    "Clear": "Vaciar",
+    "Fullscreen": "Pantalla Completa",
+    "Settings": "Configuraciones",
+    "Encrypt": "Encriptar",
+    "Shared Mode": "Modo Compartido",
+    "View Only": "Solo visualización",
+    "Clip to Window": "Recortar al tamaño de la ventana",
+    "Scaling Mode:": "Modo de escalado:",
+    "None": "Ninguno",
+    "Local Scaling": "Escalado Local",
+    "Local Downscaling": "Reducción de escala local",
+    "Remote Resizing": "Cambio de tamaño remoto",
+    "Advanced": "Avanzado",
+    "Local Cursor": "Cursor Local",
+    "Repeater ID:": "ID del Repetidor:",
+    "WebSocket": "WebSocket",
+    "Host:": "Host:",
+    "Port:": "Puerto:",
+    "Path:": "Ruta:",
+    "Automatic Reconnect": "Reconexión automática",
+    "Reconnect Delay (ms):": "Retraso en la reconexión (ms):",
+    "Logging:": "Registrando:",
+    "Disconnect": "Desconectar",
+    "Connect": "Conectar",
+    "Password:": "Contraseña:",
+    "Cancel": "Cancelar",
+    "Canvas not supported.": "Canvas no soportado."
+}
\ No newline at end of file
diff --git a/app/src/main/assets/novnc/app/locale/fr.json b/app/src/main/assets/novnc/app/locale/fr.json
new file mode 100644
index 00000000..19e8255b
--- /dev/null
+++ b/app/src/main/assets/novnc/app/locale/fr.json
@@ -0,0 +1,72 @@
+{
+    "Connecting...": "En cours de connexion...",
+    "Disconnecting...": "Déconnexion en cours...",
+    "Reconnecting...": "Reconnexion en cours...",
+    "Internal error": "Erreur interne",
+    "Must set host": "Doit définir l'hôte",
+    "Connected (encrypted) to ": "Connecté (crypté) à ",
+    "Connected (unencrypted) to ": "Connecté (non crypté) à ",
+    "Something went wrong, connection is closed": "Quelque chose est arrivé, la connexion est fermée",
+    "Failed to connect to server": "Échec de connexion au serveur",
+    "Disconnected": "Déconnecté",
+    "New connection has been rejected with reason: ": "Une nouvelle connexion a été rejetée avec raison: ",
+    "New connection has been rejected": "Une nouvelle connexion a été rejetée",
+    "Credentials are required": "Les identifiants sont requis",
+    "noVNC encountered an error:": "noVNC a rencontré une erreur:",
+    "Hide/Show the control bar": "Masquer/Afficher la barre de contrôle",
+    "Drag": "Faire glisser",
+    "Move/Drag Viewport": "Déplacer/faire glisser Viewport",
+    "Keyboard": "Clavier",
+    "Show Keyboard": "Afficher le clavier",
+    "Extra keys": "Touches supplémentaires",
+    "Show Extra Keys": "Afficher les touches supplémentaires",
+    "Ctrl": "Ctrl",
+    "Toggle Ctrl": "Basculer Ctrl",
+    "Alt": "Alt",
+    "Toggle Alt": "Basculer Alt",
+    "Toggle Windows": "Basculer Windows",
+    "Windows": "Windows",
+    "Send Tab": "Envoyer l'onglet",
+    "Tab": "l'onglet",
+    "Esc": "Esc",
+    "Send Escape": "Envoyer Escape",
+    "Ctrl+Alt+Del": "Ctrl+Alt+Del",
+    "Send Ctrl-Alt-Del": "Envoyer Ctrl-Alt-Del",
+    "Shutdown/Reboot": "Arrêter/Redémarrer",
+    "Shutdown/Reboot...": "Arrêter/Redémarrer...",
+    "Power": "Alimentation",
+    "Shutdown": "Arrêter",
+    "Reboot": "Redémarrer",
+    "Reset": "Réinitialiser",
+    "Clipboard": "Presse-papiers",
+    "Clear": "Effacer",
+    "Fullscreen": "Plein écran",
+    "Settings": "Paramètres",
+    "Shared Mode": "Mode partagé",
+    "View Only": "Afficher uniquement",
+    "Clip to Window": "Clip à fenêtre",
+    "Scaling Mode:": "Mode mise à l'échelle:",
+    "None": "Aucun",
+    "Local Scaling": "Mise à l'échelle locale",
+    "Remote Resizing": "Redimensionnement à distance",
+    "Advanced": "Avancé",
+    "Quality:": "Qualité:",
+    "Compression level:": "Niveau de compression:",
+    "Repeater ID:": "ID Répéteur:",
+    "WebSocket": "WebSocket",
+    "Encrypt": "Crypter",
+    "Host:": "Hôte:",
+    "Port:": "Port:",
+    "Path:": "Chemin:",
+    "Automatic Reconnect": "Reconnecter automatiquemen",
+    "Reconnect Delay (ms):": "Délai de reconnexion (ms):",
+    "Show Dot when No Cursor": "Afficher le point lorsqu'il n'y a pas de curseur",
+    "Logging:": "Se connecter:",
+    "Version:": "Version:",
+    "Disconnect": "Déconnecter",
+    "Connect": "Connecter",
+    "Username:": "Nom d'utilisateur:",
+    "Password:": "Mot de passe:",
+    "Send Credentials": "Envoyer les identifiants",
+    "Cancel": "Annuler"
+}
\ No newline at end of file
diff --git a/app/src/main/assets/novnc/app/locale/ja.json b/app/src/main/assets/novnc/app/locale/ja.json
new file mode 100644
index 00000000..43fc5bf3
--- /dev/null
+++ b/app/src/main/assets/novnc/app/locale/ja.json
@@ -0,0 +1,72 @@
+{
+    "Connecting...": "接続しています...",
+    "Disconnecting...": "切断しています...",
+    "Reconnecting...": "再接続しています...",
+    "Internal error": "内部エラー",
+    "Must set host": "ホストを設定する必要があります",
+    "Connected (encrypted) to ": "接続しました (暗号化済み): ",
+    "Connected (unencrypted) to ": "接続しました (暗号化されていません): ",
+    "Something went wrong, connection is closed": "何らかの問題で、接続が閉じられました",
+    "Failed to connect to server": "サーバーへの接続に失敗しました",
+    "Disconnected": "切断しました",
+    "New connection has been rejected with reason: ": "新規接続は次の理由で拒否されました: ",
+    "New connection has been rejected": "新規接続は拒否されました",
+    "Credentials are required": "資格情報が必要です",
+    "noVNC encountered an error:": "noVNC でエラーが発生しました:",
+    "Hide/Show the control bar": "コントロールバーを隠す/表示する",
+    "Drag": "ドラッグ",
+    "Move/Drag Viewport": "ビューポートを移動/ドラッグ",
+    "Keyboard": "キーボード",
+    "Show Keyboard": "キーボードを表示",
+    "Extra keys": "追加キー",
+    "Show Extra Keys": "追加キーを表示",
+    "Ctrl": "Ctrl",
+    "Toggle Ctrl": "Ctrl キーを切り替え",
+    "Alt": "Alt",
+    "Toggle Alt": "Alt キーを切り替え",
+    "Toggle Windows": "Windows キーを切り替え",
+    "Windows": "Windows",
+    "Send Tab": "Tab キーを送信",
+    "Tab": "Tab",
+    "Esc": "Esc",
+    "Send Escape": "Escape キーを送信",
+    "Ctrl+Alt+Del": "Ctrl+Alt+Del",
+    "Send Ctrl-Alt-Del": "Ctrl-Alt-Del を送信",
+    "Shutdown/Reboot": "シャットダウン/再起動",
+    "Shutdown/Reboot...": "シャットダウン/再起動...",
+    "Power": "電源",
+    "Shutdown": "シャットダウン",
+    "Reboot": "再起動",
+    "Reset": "リセット",
+    "Clipboard": "クリップボード",
+    "Clear": "クリア",
+    "Fullscreen": "全画面表示",
+    "Settings": "設定",
+    "Shared Mode": "共有モード",
+    "View Only": "表示のみ",
+    "Clip to Window": "ウィンドウにクリップ",
+    "Scaling Mode:": "スケーリングモード:",
+    "None": "なし",
+    "Local Scaling": "ローカルスケーリング",
+    "Remote Resizing": "リモートでリサイズ",
+    "Advanced": "高度",
+    "Quality:": "品質:",
+    "Compression level:": "圧縮レベル:",
+    "Repeater ID:": "リピーター ID:",
+    "WebSocket": "WebSocket",
+    "Encrypt": "暗号化",
+    "Host:": "ホスト:",
+    "Port:": "ポート:",
+    "Path:": "パス:",
+    "Automatic Reconnect": "自動再接続",
+    "Reconnect Delay (ms):": "再接続する遅延 (ミリ秒):",
+    "Show Dot when No Cursor": "カーソルがないときにドットを表示",
+    "Logging:": "ロギング:",
+    "Version:": "バージョン:",
+    "Disconnect": "切断",
+    "Connect": "接続",
+    "Username:": "ユーザー名:",
+    "Password:": "パスワード:",
+    "Send Credentials": "資格情報を送信",
+    "Cancel": "キャンセル"
+}
\ No newline at end of file
diff --git a/app/src/main/assets/novnc/app/locale/ko.json b/app/src/main/assets/novnc/app/locale/ko.json
new file mode 100644
index 00000000..e4ecddcf
--- /dev/null
+++ b/app/src/main/assets/novnc/app/locale/ko.json
@@ -0,0 +1,70 @@
+{
+    "Connecting...": "연결중...",
+    "Disconnecting...": "연결 해제중...",
+    "Reconnecting...": "재연결중...",
+    "Internal error": "내부 오류",
+    "Must set host": "호스트는 설정되어야 합니다.",
+    "Connected (encrypted) to ": "다음과 (암호화되어) 연결되었습니다:",
+    "Connected (unencrypted) to ": "다음과 (암호화 없이) 연결되었습니다:",
+    "Something went wrong, connection is closed": "무언가 잘못되었습니다, 연결이 닫혔습니다.",
+    "Failed to connect to server": "서버에 연결하지 못했습니다.",
+    "Disconnected": "연결이 해제되었습니다.",
+    "New connection has been rejected with reason: ": "새 연결이 다음 이유로 거부되었습니다:",
+    "New connection has been rejected": "새 연결이 거부되었습니다.",
+    "Password is required": "비밀번호가 필요합니다.",
+    "noVNC encountered an error:": "noVNC에 오류가 발생했습니다:",
+    "Hide/Show the control bar": "컨트롤 바 숨기기/보이기",
+    "Move/Drag Viewport": "움직이기/드래그 뷰포트",
+    "viewport drag": "뷰포트 드래그",
+    "Active Mouse Button": "마우스 버튼 활성화",
+    "No mousebutton": "마우스 버튼 없음",
+    "Left mousebutton": "왼쪽 마우스 버튼",
+    "Middle mousebutton": "중간 마우스 버튼",
+    "Right mousebutton": "오른쪽 마우스 버튼",
+    "Keyboard": "키보드",
+    "Show Keyboard": "키보드 보이기",
+    "Extra keys": "기타 키들",
+    "Show Extra Keys": "기타 키들 보이기",
+    "Ctrl": "Ctrl",
+    "Toggle Ctrl": "Ctrl 켜기/끄기",
+    "Alt": "Alt",
+    "Toggle Alt": "Alt 켜기/끄기",
+    "Send Tab": "Tab 보내기",
+    "Tab": "Tab",
+    "Esc": "Esc",
+    "Send Escape": "Esc 보내기",
+    "Ctrl+Alt+Del": "Ctrl+Alt+Del",
+    "Send Ctrl-Alt-Del": "Ctrl+Alt+Del 보내기",
+    "Shutdown/Reboot": "셧다운/리붓",
+    "Shutdown/Reboot...": "셧다운/리붓...",
+    "Power": "전원",
+    "Shutdown": "셧다운",
+    "Reboot": "리붓",
+    "Reset": "리셋",
+    "Clipboard": "클립보드",
+    "Clear": "지우기",
+    "Fullscreen": "전체화면",
+    "Settings": "설정",
+    "Shared Mode": "공유 모드",
+    "View Only": "보기 전용",
+    "Clip to Window": "창에 클립",
+    "Scaling Mode:": "스케일링 모드:",
+    "None": "없음",
+    "Local Scaling": "로컬 스케일링",
+    "Remote Resizing": "원격 크기 조절",
+    "Advanced": "고급",
+    "Repeater ID:": "중계 ID",
+    "WebSocket": "웹소켓",
+    "Encrypt": "암호화",
+    "Host:": "호스트:",
+    "Port:": "포트:",
+    "Path:": "위치:",
+    "Automatic Reconnect": "자동 재연결",
+    "Reconnect Delay (ms):": "재연결 지연 시간 (ms)",
+    "Logging:": "로깅",
+    "Disconnect": "연결 해제",
+    "Connect": "연결",
+    "Password:": "비밀번호:",
+    "Send Password": "비밀번호 전송",
+    "Cancel": "취소"
+}
\ No newline at end of file
diff --git a/app/src/main/assets/novnc/app/locale/nl.json b/app/src/main/assets/novnc/app/locale/nl.json
new file mode 100644
index 00000000..0cdcc92a
--- /dev/null
+++ b/app/src/main/assets/novnc/app/locale/nl.json
@@ -0,0 +1,73 @@
+{
+    "Connecting...": "Verbinden...",
+    "Disconnecting...": "Verbinding verbreken...",
+    "Reconnecting...": "Opnieuw verbinding maken...",
+    "Internal error": "Interne fout",
+    "Must set host": "Host moeten worden ingesteld",
+    "Connected (encrypted) to ": "Verbonden (versleuteld) met ",
+    "Connected (unencrypted) to ": "Verbonden (onversleuteld) met ",
+    "Something went wrong, connection is closed": "Er iets fout gelopen, verbinding werd verbroken",
+    "Failed to connect to server": "Verbinding maken met server is mislukt",
+    "Disconnected": "Verbinding verbroken",
+    "New connection has been rejected with reason: ": "Nieuwe verbinding is geweigerd omwille van de volgende reden: ",
+    "New connection has been rejected": "Nieuwe verbinding is geweigerd",
+    "Password is required": "Wachtwoord is vereist",
+    "noVNC encountered an error:": "noVNC heeft een fout bemerkt:",
+    "Hide/Show the control bar": "Verberg/Toon de bedieningsbalk",
+    "Move/Drag Viewport": "Verplaats/Versleep Kijkvenster",
+    "viewport drag": "kijkvenster slepen",
+    "Active Mouse Button": "Actieve Muisknop",
+    "No mousebutton": "Geen muisknop",
+    "Left mousebutton": "Linker muisknop",
+    "Middle mousebutton": "Middelste muisknop",
+    "Right mousebutton": "Rechter muisknop",
+    "Keyboard": "Toetsenbord",
+    "Show Keyboard": "Toon Toetsenbord",
+    "Extra keys": "Extra toetsen",
+    "Show Extra Keys": "Toon Extra Toetsen",
+    "Ctrl": "Ctrl",
+    "Toggle Ctrl": "Ctrl omschakelen",
+    "Alt": "Alt",
+    "Toggle Alt": "Alt omschakelen",
+    "Toggle Windows": "Windows omschakelen",
+    "Windows": "Windows",
+    "Send Tab": "Tab Sturen",
+    "Tab": "Tab",
+    "Esc": "Esc",
+    "Send Escape": "Escape Sturen",
+    "Ctrl+Alt+Del": "Ctrl-Alt-Del",
+    "Send Ctrl-Alt-Del": "Ctrl-Alt-Del Sturen",
+    "Shutdown/Reboot": "Uitschakelen/Herstarten",
+    "Shutdown/Reboot...": "Uitschakelen/Herstarten...",
+    "Power": "Systeem",
+    "Shutdown": "Uitschakelen",
+    "Reboot": "Herstarten",
+    "Reset": "Resetten",
+    "Clipboard": "Klembord",
+    "Clear": "Wissen",
+    "Fullscreen": "Volledig Scherm",
+    "Settings": "Instellingen",
+    "Shared Mode": "Gedeelde Modus",
+    "View Only": "Alleen Kijken",
+    "Clip to Window": "Randen buiten venster afsnijden",
+    "Scaling Mode:": "Schaalmodus:",
+    "None": "Geen",
+    "Local Scaling": "Lokaal Schalen",
+    "Remote Resizing": "Op Afstand Formaat Wijzigen",
+    "Advanced": "Geavanceerd",
+    "Repeater ID:": "Repeater ID:",
+    "WebSocket": "WebSocket",
+    "Encrypt": "Versleutelen",
+    "Host:": "Host:",
+    "Port:": "Poort:",
+    "Path:": "Pad:",
+    "Automatic Reconnect": "Automatisch Opnieuw Verbinden",
+    "Reconnect Delay (ms):": "Vertraging voor Opnieuw Verbinden (ms):",
+    "Show Dot when No Cursor": "Geef stip weer indien geen cursor",
+    "Logging:": "Logmeldingen:",
+    "Disconnect": "Verbinding verbreken",
+    "Connect": "Verbinden",
+    "Password:": "Wachtwoord:",
+    "Send Password": "Verzend Wachtwoord:",
+    "Cancel": "Annuleren"
+}
\ No newline at end of file
diff --git a/app/src/main/assets/novnc/app/locale/pl.json b/app/src/main/assets/novnc/app/locale/pl.json
new file mode 100644
index 00000000..006ac7a5
--- /dev/null
+++ b/app/src/main/assets/novnc/app/locale/pl.json
@@ -0,0 +1,69 @@
+{
+    "Connecting...": "Łączenie...",
+    "Disconnecting...": "Rozłączanie...",
+    "Reconnecting...": "Łączenie...",
+    "Internal error": "Błąd wewnętrzny",
+    "Must set host": "Host i port są wymagane",
+    "Connected (encrypted) to ": "Połączenie (szyfrowane) z ",
+    "Connected (unencrypted) to ": "Połączenie (nieszyfrowane) z ",
+    "Something went wrong, connection is closed": "Coś poszło źle, połączenie zostało zamknięte",
+    "Disconnected": "Rozłączony",
+    "New connection has been rejected with reason: ": "Nowe połączenie zostało odrzucone z powodu: ",
+    "New connection has been rejected": "Nowe połączenie zostało odrzucone",
+    "Password is required": "Hasło jest wymagane",
+    "noVNC encountered an error:": "noVNC napotkało błąd:",
+    "Hide/Show the control bar": "Pokaż/Ukryj pasek ustawień",
+    "Move/Drag Viewport": "Ruszaj/Przeciągaj Viewport",
+    "viewport drag": "przeciągnij viewport",
+    "Active Mouse Button": "Aktywny Przycisk Myszy",
+    "No mousebutton": "Brak przycisku myszy",
+    "Left mousebutton": "Lewy przycisk myszy",
+    "Middle mousebutton": "Środkowy przycisk myszy",
+    "Right mousebutton": "Prawy przycisk myszy",
+    "Keyboard": "Klawiatura",
+    "Show Keyboard": "Pokaż klawiaturę",
+    "Extra keys": "Przyciski dodatkowe",
+    "Show Extra Keys": "Pokaż przyciski dodatkowe",
+    "Ctrl": "Ctrl",
+    "Toggle Ctrl": "Przełącz Ctrl",
+    "Alt": "Alt",
+    "Toggle Alt": "Przełącz Alt",
+    "Send Tab": "Wyślij Tab",
+    "Tab": "Tab",
+    "Esc": "Esc",
+    "Send Escape": "Wyślij Escape",
+    "Ctrl+Alt+Del": "Ctrl+Alt+Del",
+    "Send Ctrl-Alt-Del": "Wyślij Ctrl-Alt-Del",
+    "Shutdown/Reboot": "Wyłącz/Uruchom ponownie",
+    "Shutdown/Reboot...": "Wyłącz/Uruchom ponownie...",
+    "Power": "Włączony",
+    "Shutdown": "Wyłącz",
+    "Reboot": "Uruchom ponownie",
+    "Reset": "Resetuj",
+    "Clipboard": "Schowek",
+    "Clear": "Wyczyść",
+    "Fullscreen": "Pełny ekran",
+    "Settings": "Ustawienia",
+    "Shared Mode": "Tryb Współdzielenia",
+    "View Only": "Tylko Podgląd",
+    "Clip to Window": "Przytnij do Okna",
+    "Scaling Mode:": "Tryb Skalowania:",
+    "None": "Brak",
+    "Local Scaling": "Skalowanie lokalne",
+    "Remote Resizing": "Skalowanie zdalne",
+    "Advanced": "Zaawansowane",
+    "Repeater ID:": "ID Repeatera:",
+    "WebSocket": "WebSocket",
+    "Encrypt": "Szyfrowanie",
+    "Host:": "Host:",
+    "Port:": "Port:",
+    "Path:": "Ścieżka:",
+    "Automatic Reconnect": "Automatycznie wznawiaj połączenie",
+    "Reconnect Delay (ms):": "Opóźnienie wznawiania (ms):",
+    "Logging:": "Poziom logowania:",
+    "Disconnect": "Rozłącz",
+    "Connect": "Połącz",
+    "Password:": "Hasło:",
+    "Cancel": "Anuluj",
+    "Canvas not supported.": "Element Canvas nie jest wspierany."
+}
\ No newline at end of file
diff --git a/app/src/main/assets/novnc/app/locale/pt_BR.json b/app/src/main/assets/novnc/app/locale/pt_BR.json
new file mode 100644
index 00000000..aa130f76
--- /dev/null
+++ b/app/src/main/assets/novnc/app/locale/pt_BR.json
@@ -0,0 +1,72 @@
+{
+    "Connecting...": "Conectando...",
+    "Disconnecting...": "Desconectando...",
+    "Reconnecting...": "Reconectando...",
+    "Internal error": "Erro interno",
+    "Must set host": "É necessário definir o host",
+    "Connected (encrypted) to ": "Conectado (com criptografia) a ",
+    "Connected (unencrypted) to ": "Conectado (sem criptografia) a ",
+    "Something went wrong, connection is closed": "Algo deu errado. A conexão foi encerrada.",
+    "Failed to connect to server": "Falha ao conectar-se ao servidor",
+    "Disconnected": "Desconectado",
+    "New connection has been rejected with reason: ": "A nova conexão foi rejeitada pelo motivo: ",
+    "New connection has been rejected": "A nova conexão foi rejeitada",
+    "Credentials are required": "Credenciais são obrigatórias",
+    "noVNC encountered an error:": "O noVNC encontrou um erro:",
+    "Hide/Show the control bar": "Esconder/mostrar a barra de controles",
+    "Drag": "Arrastar",
+    "Move/Drag Viewport": "Mover/arrastar a janela",
+    "Keyboard": "Teclado",
+    "Show Keyboard": "Mostrar teclado",
+    "Extra keys": "Teclas adicionais",
+    "Show Extra Keys": "Mostar teclas adicionais",
+    "Ctrl": "Ctrl",
+    "Toggle Ctrl": "Pressionar/soltar Ctrl",
+    "Alt": "Alt",
+    "Toggle Alt": "Pressionar/soltar Alt",
+    "Toggle Windows": "Pressionar/soltar Windows",
+    "Windows": "Windows",
+    "Send Tab": "Enviar Tab",
+    "Tab": "Tab",
+    "Esc": "Esc",
+    "Send Escape": "Enviar Esc",
+    "Ctrl+Alt+Del": "Ctrl+Alt+Del",
+    "Send Ctrl-Alt-Del": "Enviar Ctrl-Alt-Del",
+    "Shutdown/Reboot": "Desligar/reiniciar",
+    "Shutdown/Reboot...": "Desligar/reiniciar...",
+    "Power": "Ligar",
+    "Shutdown": "Desligar",
+    "Reboot": "Reiniciar",
+    "Reset": "Reiniciar (forçado)",
+    "Clipboard": "Área de transferência",
+    "Clear": "Limpar",
+    "Fullscreen": "Tela cheia",
+    "Settings": "Configurações",
+    "Shared Mode": "Modo compartilhado",
+    "View Only": "Apenas visualizar",
+    "Clip to Window": "Recortar à janela",
+    "Scaling Mode:": "Modo de dimensionamento:",
+    "None": "Nenhum",
+    "Local Scaling": "Local",
+    "Remote Resizing": "Remoto",
+    "Advanced": "Avançado",
+    "Quality:": "Qualidade:",
+    "Compression level:": "Nível de compressão:",
+    "Repeater ID:": "ID do repetidor:",
+    "WebSocket": "WebSocket",
+    "Encrypt": "Criptografar",
+    "Host:": "Host:",
+    "Port:": "Porta:",
+    "Path:": "Caminho:",
+    "Automatic Reconnect": "Reconexão automática",
+    "Reconnect Delay (ms):": "Atraso da reconexão (ms)",
+    "Show Dot when No Cursor": "Mostrar ponto quando não há cursor",
+    "Logging:": "Registros:",
+    "Version:": "Versão:",
+    "Disconnect": "Desconectar",
+    "Connect": "Conectar",
+    "Username:": "Nome de usuário:",
+    "Password:": "Senha:",
+    "Send Credentials": "Enviar credenciais",
+    "Cancel": "Cancelar"
+}
\ No newline at end of file
diff --git a/app/src/main/assets/novnc/app/locale/ru.json b/app/src/main/assets/novnc/app/locale/ru.json
new file mode 100644
index 00000000..cab97396
--- /dev/null
+++ b/app/src/main/assets/novnc/app/locale/ru.json
@@ -0,0 +1,72 @@
+{
+    "Connecting...": "Подключение...",
+    "Disconnecting...": "Отключение...",
+    "Reconnecting...": "Переподключение...",
+    "Internal error": "Внутренняя ошибка",
+    "Must set host": "Задайте имя сервера или IP",
+    "Connected (encrypted) to ": "Подключено (с шифрованием) к ",
+    "Connected (unencrypted) to ": "Подключено (без шифрования) к ",
+    "Something went wrong, connection is closed": "Что-то пошло не так, подключение разорвано",
+    "Failed to connect to server": "Ошибка подключения к серверу",
+    "Disconnected": "Отключено",
+    "New connection has been rejected with reason: ": "Новое соединение отклонено по причине: ",
+    "New connection has been rejected": "Новое соединение отклонено",
+    "Credentials are required": "Требуются учетные данные",
+    "noVNC encountered an error:": "Ошибка noVNC: ",
+    "Hide/Show the control bar": "Скрыть/Показать контрольную панель",
+    "Drag": "Переместить",
+    "Move/Drag Viewport": "Переместить окно",
+    "Keyboard": "Клавиатура",
+    "Show Keyboard": "Показать клавиатуру",
+    "Extra keys": "Дополнительные Кнопки",
+    "Show Extra Keys": "Показать Дополнительные Кнопки",
+    "Ctrl": "Ctrl",
+    "Toggle Ctrl": "Переключение нажатия Ctrl",
+    "Alt": "Alt",
+    "Toggle Alt": "Переключение нажатия Alt",
+    "Toggle Windows": "Переключение вкладок",
+    "Windows": "Вкладка",
+    "Send Tab": "Передать нажатие Tab",
+    "Tab": "Tab",
+    "Esc": "Esc",
+    "Send Escape": "Передать нажатие Escape",
+    "Ctrl+Alt+Del": "Ctrl+Alt+Del",
+    "Send Ctrl-Alt-Del": "Передать нажатие Ctrl-Alt-Del",
+    "Shutdown/Reboot": "Выключить/Перезагрузить",
+    "Shutdown/Reboot...": "Выключить/Перезагрузить...",
+    "Power": "Питание",
+    "Shutdown": "Выключить",
+    "Reboot": "Перезагрузить",
+    "Reset": "Сброс",
+    "Clipboard": "Буфер обмена",
+    "Clear": "Очистить",
+    "Fullscreen": "Во весь экран",
+    "Settings": "Настройки",
+    "Shared Mode": "Общий режим",
+    "View Only": "Только Просмотр",
+    "Clip to Window": "В окно",
+    "Scaling Mode:": "Масштаб:",
+    "None": "Нет",
+    "Local Scaling": "Локльный масштаб",
+    "Remote Resizing": "Удаленная перенастройка размера",
+    "Advanced": "Дополнительно",
+    "Quality:": "Качество",
+    "Compression level:": "Уровень Сжатия",
+    "Repeater ID:": "Идентификатор ID:",
+    "WebSocket": "WebSocket",
+    "Encrypt": "Шифрование",
+    "Host:": "Сервер:",
+    "Port:": "Порт:",
+    "Path:": "Путь:",
+    "Automatic Reconnect": "Автоматическое переподключение",
+    "Reconnect Delay (ms):": "Задержка переподключения (мс):",
+    "Show Dot when No Cursor": "Показать точку вместо курсора",
+    "Logging:": "Лог:",
+    "Version:": "Версия",
+    "Disconnect": "Отключение",
+    "Connect": "Подключение",
+    "Username:": "Имя Пользователя",
+    "Password:": "Пароль:",
+    "Send Credentials": "Передача Учетных Данных",
+    "Cancel": "Выход"
+}
\ No newline at end of file
diff --git a/app/src/main/assets/novnc/app/locale/sv.json b/app/src/main/assets/novnc/app/locale/sv.json
new file mode 100644
index 00000000..e46df45b
--- /dev/null
+++ b/app/src/main/assets/novnc/app/locale/sv.json
@@ -0,0 +1,72 @@
+{
+    "Connecting...": "Ansluter...",
+    "Disconnecting...": "Kopplar ner...",
+    "Reconnecting...": "Återansluter...",
+    "Internal error": "Internt fel",
+    "Must set host": "Du måste specifiera en värd",
+    "Connected (encrypted) to ": "Ansluten (krypterat) till ",
+    "Connected (unencrypted) to ": "Ansluten (okrypterat) till ",
+    "Something went wrong, connection is closed": "Något gick fel, anslutningen avslutades",
+    "Failed to connect to server": "Misslyckades att ansluta till servern",
+    "Disconnected": "Frånkopplad",
+    "New connection has been rejected with reason: ": "Ny anslutning har blivit nekad med följande skäl: ",
+    "New connection has been rejected": "Ny anslutning har blivit nekad",
+    "Credentials are required": "Användaruppgifter krävs",
+    "noVNC encountered an error:": "noVNC stötte på ett problem:",
+    "Hide/Show the control bar": "Göm/Visa kontrollbaren",
+    "Drag": "Dra",
+    "Move/Drag Viewport": "Flytta/Dra Vyn",
+    "Keyboard": "Tangentbord",
+    "Show Keyboard": "Visa Tangentbord",
+    "Extra keys": "Extraknappar",
+    "Show Extra Keys": "Visa Extraknappar",
+    "Ctrl": "Ctrl",
+    "Toggle Ctrl": "Växla Ctrl",
+    "Alt": "Alt",
+    "Toggle Alt": "Växla Alt",
+    "Toggle Windows": "Växla Windows",
+    "Windows": "Windows",
+    "Send Tab": "Skicka Tab",
+    "Tab": "Tab",
+    "Esc": "Esc",
+    "Send Escape": "Skicka Escape",
+    "Ctrl+Alt+Del": "Ctrl+Alt+Del",
+    "Send Ctrl-Alt-Del": "Skicka Ctrl-Alt-Del",
+    "Shutdown/Reboot": "Stäng av/Boota om",
+    "Shutdown/Reboot...": "Stäng av/Boota om...",
+    "Power": "Ström",
+    "Shutdown": "Stäng av",
+    "Reboot": "Boota om",
+    "Reset": "Återställ",
+    "Clipboard": "Urklipp",
+    "Clear": "Rensa",
+    "Fullscreen": "Fullskärm",
+    "Settings": "Inställningar",
+    "Shared Mode": "Delat Läge",
+    "View Only": "Endast Visning",
+    "Clip to Window": "Begränsa till Fönster",
+    "Scaling Mode:": "Skalningsläge:",
+    "None": "Ingen",
+    "Local Scaling": "Lokal Skalning",
+    "Remote Resizing": "Ändra Storlek",
+    "Advanced": "Avancerat",
+    "Quality:": "Kvalitet:",
+    "Compression level:": "Kompressionsnivå:",
+    "Repeater ID:": "Repeater-ID:",
+    "WebSocket": "WebSocket",
+    "Encrypt": "Kryptera",
+    "Host:": "Värd:",
+    "Port:": "Port:",
+    "Path:": "Sökväg:",
+    "Automatic Reconnect": "Automatisk Återanslutning",
+    "Reconnect Delay (ms):": "Fördröjning (ms):",
+    "Show Dot when No Cursor": "Visa prick när ingen muspekare finns",
+    "Logging:": "Loggning:",
+    "Version:": "Version:",
+    "Disconnect": "Koppla från",
+    "Connect": "Anslut",
+    "Username:": "Användarnamn:",
+    "Password:": "Lösenord:",
+    "Send Credentials": "Skicka Användaruppgifter",
+    "Cancel": "Avbryt"
+}
\ No newline at end of file
diff --git a/app/src/main/assets/novnc/app/locale/tr.json b/app/src/main/assets/novnc/app/locale/tr.json
new file mode 100644
index 00000000..451c1b8a
--- /dev/null
+++ b/app/src/main/assets/novnc/app/locale/tr.json
@@ -0,0 +1,69 @@
+{
+    "Connecting...": "Bağlanıyor...",
+    "Disconnecting...": "Bağlantı kesiliyor...",
+    "Reconnecting...": "Yeniden bağlantı kuruluyor...",
+    "Internal error": "İç hata",
+    "Must set host": "Sunucuyu kur",
+    "Connected (encrypted) to ": "Bağlı (şifrelenmiş)",
+    "Connected (unencrypted) to ": "Bağlandı (şifrelenmemiş)",
+    "Something went wrong, connection is closed": "Bir şeyler ters gitti, bağlantı kesildi",
+    "Disconnected": "Bağlantı kesildi",
+    "New connection has been rejected with reason: ": "Bağlantı aşağıdaki nedenlerden dolayı reddedildi: ",
+    "New connection has been rejected": "Bağlantı reddedildi",
+    "Password is required": "Şifre gerekli",
+    "noVNC encountered an error:": "Bir hata oluştu:",
+    "Hide/Show the control bar": "Denetim masasını Gizle/Göster",
+    "Move/Drag Viewport": "Görünümü Taşı/Sürükle",
+    "viewport drag": "Görüntü penceresini sürükle",
+    "Active Mouse Button": "Aktif Fare Düğmesi",
+    "No mousebutton": "Fare düğmesi yok",
+    "Left mousebutton": "Farenin sol düğmesi",
+    "Middle mousebutton": "Farenin orta düğmesi",
+    "Right mousebutton": "Farenin sağ düğmesi",
+    "Keyboard": "Klavye",
+    "Show Keyboard": "Klavye Düzenini Göster",
+    "Extra keys": "Ekstra tuşlar",
+    "Show Extra Keys": "Ekstra tuşları göster",
+    "Ctrl": "Ctrl",
+    "Toggle Ctrl": "Ctrl Değiştir ",
+    "Alt": "Alt",
+    "Toggle Alt": "Alt Değiştir",
+    "Send Tab": "Sekme Gönder",
+    "Tab": "Sekme",
+    "Esc": "Esc",
+    "Send Escape": "Boşluk Gönder",
+    "Ctrl+Alt+Del": "Ctrl + Alt + Del",
+    "Send Ctrl-Alt-Del": "Ctrl-Alt-Del Gönder",
+    "Shutdown/Reboot": "Kapat/Yeniden Başlat",
+    "Shutdown/Reboot...": "Kapat/Yeniden Başlat...",
+    "Power": "Güç",
+    "Shutdown": "Kapat",
+    "Reboot": "Yeniden Başlat",
+    "Reset": "Sıfırla",
+    "Clipboard": "Pano",
+    "Clear": "Temizle",
+    "Fullscreen": "Tam Ekran",
+    "Settings": "Ayarlar",
+    "Shared Mode": "Paylaşım Modu",
+    "View Only": "Sadece Görüntüle",
+    "Clip to Window": "Pencereye Tıkla",
+    "Scaling Mode:": "Ölçekleme Modu:",
+    "None": "Bilinmeyen",
+    "Local Scaling": "Yerel Ölçeklendirme",
+    "Remote Resizing": "Uzaktan Yeniden Boyutlandırma",
+    "Advanced": "Gelişmiş",
+    "Repeater ID:": "Tekralayıcı ID:",
+    "WebSocket": "WebSocket",
+    "Encrypt": "Şifrele",
+    "Host:": "Ana makine:",
+    "Port:": "Port:",
+    "Path:": "Yol:",
+    "Automatic Reconnect": "Otomatik Yeniden Bağlan",
+    "Reconnect Delay (ms):": "Yeniden Bağlanma Süreci (ms):",
+    "Logging:": "Giriş yapılıyor:",
+    "Disconnect": "Bağlantıyı Kes",
+    "Connect": "Bağlan",
+    "Password:": "Parola:",
+    "Cancel": "Vazgeç",
+    "Canvas not supported.": "Tuval desteklenmiyor."
+}
\ No newline at end of file
diff --git a/app/src/main/assets/novnc/app/locale/zh_CN.json b/app/src/main/assets/novnc/app/locale/zh_CN.json
new file mode 100644
index 00000000..f0aea9af
--- /dev/null
+++ b/app/src/main/assets/novnc/app/locale/zh_CN.json
@@ -0,0 +1,69 @@
+{
+    "Connecting...": "连接中...",
+    "Disconnecting...": "正在断开连接...",
+    "Reconnecting...": "重新连接中...",
+    "Internal error": "内部错误",
+    "Must set host": "请提供主机名",
+    "Connected (encrypted) to ": "已连接到(加密)",
+    "Connected (unencrypted) to ": "已连接到(未加密)",
+    "Something went wrong, connection is closed": "发生错误,连接已关闭",
+    "Failed to connect to server": "无法连接到服务器",
+    "Disconnected": "已断开连接",
+    "New connection has been rejected with reason: ": "连接被拒绝,原因:",
+    "New connection has been rejected": "连接被拒绝",
+    "Password is required": "请提供密码",
+    "noVNC encountered an error:": "noVNC 遇到一个错误:",
+    "Hide/Show the control bar": "显示/隐藏控制栏",
+    "Move/Drag Viewport": "拖放显示范围",
+    "viewport drag": "显示范围拖放",
+    "Active Mouse Button": "启动鼠标按鍵",
+    "No mousebutton": "禁用鼠标按鍵",
+    "Left mousebutton": "鼠标左鍵",
+    "Middle mousebutton": "鼠标中鍵",
+    "Right mousebutton": "鼠标右鍵",
+    "Keyboard": "键盘",
+    "Show Keyboard": "显示键盘",
+    "Extra keys": "额外按键",
+    "Show Extra Keys": "显示额外按键",
+    "Ctrl": "Ctrl",
+    "Toggle Ctrl": "切换 Ctrl",
+    "Alt": "Alt",
+    "Toggle Alt": "切换 Alt",
+    "Send Tab": "发送 Tab 键",
+    "Tab": "Tab",
+    "Esc": "Esc",
+    "Send Escape": "发送 Escape 键",
+    "Ctrl+Alt+Del": "Ctrl-Alt-Del",
+    "Send Ctrl-Alt-Del": "发送 Ctrl-Alt-Del 键",
+    "Shutdown/Reboot": "关机/重新启动",
+    "Shutdown/Reboot...": "关机/重新启动...",
+    "Power": "电源",
+    "Shutdown": "关机",
+    "Reboot": "重新启动",
+    "Reset": "重置",
+    "Clipboard": "剪贴板",
+    "Clear": "清除",
+    "Fullscreen": "全屏",
+    "Settings": "设置",
+    "Shared Mode": "分享模式",
+    "View Only": "仅查看",
+    "Clip to Window": "限制/裁切窗口大小",
+    "Scaling Mode:": "缩放模式:",
+    "None": "无",
+    "Local Scaling": "本地缩放",
+    "Remote Resizing": "远程调整大小",
+    "Advanced": "高级",
+    "Repeater ID:": "中继站 ID",
+    "WebSocket": "WebSocket",
+    "Encrypt": "加密",
+    "Host:": "主机:",
+    "Port:": "端口:",
+    "Path:": "路径:",
+    "Automatic Reconnect": "自动重新连接",
+    "Reconnect Delay (ms):": "重新连接间隔 (ms):",
+    "Logging:": "日志级别:",
+    "Disconnect": "中断连接",
+    "Connect": "连接",
+    "Password:": "密码:",
+    "Cancel": "取消"
+}
\ No newline at end of file
diff --git a/app/src/main/assets/novnc/app/locale/zh_TW.json b/app/src/main/assets/novnc/app/locale/zh_TW.json
new file mode 100644
index 00000000..8ddf813f
--- /dev/null
+++ b/app/src/main/assets/novnc/app/locale/zh_TW.json
@@ -0,0 +1,69 @@
+{
+    "Connecting...": "連線中...",
+    "Disconnecting...": "正在中斷連線...",
+    "Reconnecting...": "重新連線中...",
+    "Internal error": "內部錯誤",
+    "Must set host": "請提供主機資訊",
+    "Connected (encrypted) to ": "已加密連線到",
+    "Connected (unencrypted) to ": "未加密連線到",
+    "Something went wrong, connection is closed": "發生錯誤,連線已關閉",
+    "Failed to connect to server": "無法連線到伺服器",
+    "Disconnected": "連線已中斷",
+    "New connection has been rejected with reason: ": "連線被拒絕,原因:",
+    "New connection has been rejected": "連線被拒絕",
+    "Password is required": "請提供密碼",
+    "noVNC encountered an error:": "noVNC 遇到一個錯誤:",
+    "Hide/Show the control bar": "顯示/隱藏控制列",
+    "Move/Drag Viewport": "拖放顯示範圍",
+    "viewport drag": "顯示範圍拖放",
+    "Active Mouse Button": "啟用滑鼠按鍵",
+    "No mousebutton": "無滑鼠按鍵",
+    "Left mousebutton": "滑鼠左鍵",
+    "Middle mousebutton": "滑鼠中鍵",
+    "Right mousebutton": "滑鼠右鍵",
+    "Keyboard": "鍵盤",
+    "Show Keyboard": "顯示鍵盤",
+    "Extra keys": "額外按鍵",
+    "Show Extra Keys": "顯示額外按鍵",
+    "Ctrl": "Ctrl",
+    "Toggle Ctrl": "切換 Ctrl",
+    "Alt": "Alt",
+    "Toggle Alt": "切換 Alt",
+    "Send Tab": "送出 Tab 鍵",
+    "Tab": "Tab",
+    "Esc": "Esc",
+    "Send Escape": "送出 Escape 鍵",
+    "Ctrl+Alt+Del": "Ctrl-Alt-Del",
+    "Send Ctrl-Alt-Del": "送出 Ctrl-Alt-Del 快捷鍵",
+    "Shutdown/Reboot": "關機/重新啟動",
+    "Shutdown/Reboot...": "關機/重新啟動...",
+    "Power": "電源",
+    "Shutdown": "關機",
+    "Reboot": "重新啟動",
+    "Reset": "重設",
+    "Clipboard": "剪貼簿",
+    "Clear": "清除",
+    "Fullscreen": "全螢幕",
+    "Settings": "設定",
+    "Shared Mode": "分享模式",
+    "View Only": "僅檢視",
+    "Clip to Window": "限制/裁切視窗大小",
+    "Scaling Mode:": "縮放模式:",
+    "None": "無",
+    "Local Scaling": "本機縮放",
+    "Remote Resizing": "遠端調整大小",
+    "Advanced": "進階",
+    "Repeater ID:": "中繼站 ID",
+    "WebSocket": "WebSocket",
+    "Encrypt": "加密",
+    "Host:": "主機:",
+    "Port:": "連接埠:",
+    "Path:": "路徑:",
+    "Automatic Reconnect": "自動重新連線",
+    "Reconnect Delay (ms):": "重新連線間隔 (ms):",
+    "Logging:": "日誌級別:",
+    "Disconnect": "中斷連線",
+    "Connect": "連線",
+    "Password:": "密碼:",
+    "Cancel": "取消"
+}
\ No newline at end of file
diff --git a/app/src/main/assets/novnc/app/localization.js b/app/src/main/assets/novnc/app/localization.js
new file mode 100644
index 00000000..100901c9
--- /dev/null
+++ b/app/src/main/assets/novnc/app/localization.js
@@ -0,0 +1,172 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2018 The noVNC Authors
+ * Licensed under MPL 2.0 (see LICENSE.txt)
+ *
+ * See README.md for usage and integration instructions.
+ */
+
+/*
+ * Localization Utilities
+ */
+
+export class Localizer {
+    constructor() {
+        // Currently configured language
+        this.language = 'en';
+
+        // Current dictionary of translations
+        this.dictionary = undefined;
+    }
+
+    // Configure suitable language based on user preferences
+    setup(supportedLanguages) {
+        this.language = 'en'; // Default: US English
+
+        /*
+         * Navigator.languages only available in Chrome (32+) and FireFox (32+)
+         * Fall back to navigator.language for other browsers
+         */
+        let userLanguages;
+        if (typeof window.navigator.languages == 'object') {
+            userLanguages = window.navigator.languages;
+        } else {
+            userLanguages = [navigator.language || navigator.userLanguage];
+        }
+
+        for (let i = 0;i < userLanguages.length;i++) {
+            const userLang = userLanguages[i]
+                .toLowerCase()
+                .replace("_", "-")
+                .split("-");
+
+            // Built-in default?
+            if ((userLang[0] === 'en') &&
+                ((userLang[1] === undefined) || (userLang[1] === 'us'))) {
+                return;
+            }
+
+            // First pass: perfect match
+            for (let j = 0; j < supportedLanguages.length; j++) {
+                const supLang = supportedLanguages[j]
+                    .toLowerCase()
+                    .replace("_", "-")
+                    .split("-");
+
+                if (userLang[0] !== supLang[0]) {
+                    continue;
+                }
+                if (userLang[1] !== supLang[1]) {
+                    continue;
+                }
+
+                this.language = supportedLanguages[j];
+                return;
+            }
+
+            // Second pass: fallback
+            for (let j = 0;j < supportedLanguages.length;j++) {
+                const supLang = supportedLanguages[j]
+                    .toLowerCase()
+                    .replace("_", "-")
+                    .split("-");
+
+                if (userLang[0] !== supLang[0]) {
+                    continue;
+                }
+                if (supLang[1] !== undefined) {
+                    continue;
+                }
+
+                this.language = supportedLanguages[j];
+                return;
+            }
+        }
+    }
+
+    // Retrieve localised text
+    get(id) {
+        if (typeof this.dictionary !== 'undefined' && this.dictionary[id]) {
+            return this.dictionary[id];
+        } else {
+            return id;
+        }
+    }
+
+    // Traverses the DOM and translates relevant fields
+    // See https://html.spec.whatwg.org/multipage/dom.html#attr-translate
+    translateDOM() {
+        const self = this;
+
+        function process(elem, enabled) {
+            function isAnyOf(searchElement, items) {
+                return items.indexOf(searchElement) !== -1;
+            }
+
+            function translateAttribute(elem, attr) {
+                const str = self.get(elem.getAttribute(attr));
+                elem.setAttribute(attr, str);
+            }
+
+            function translateTextNode(node) {
+                const str = self.get(node.data.trim());
+                node.data = str;
+            }
+
+            if (elem.hasAttribute("translate")) {
+                if (isAnyOf(elem.getAttribute("translate"), ["", "yes"])) {
+                    enabled = true;
+                } else if (isAnyOf(elem.getAttribute("translate"), ["no"])) {
+                    enabled = false;
+                }
+            }
+
+            if (enabled) {
+                if (elem.hasAttribute("abbr") &&
+                    elem.tagName === "TH") {
+                    translateAttribute(elem, "abbr");
+                }
+                if (elem.hasAttribute("alt") &&
+                    isAnyOf(elem.tagName, ["AREA", "IMG", "INPUT"])) {
+                    translateAttribute(elem, "alt");
+                }
+                if (elem.hasAttribute("download") &&
+                    isAnyOf(elem.tagName, ["A", "AREA"])) {
+                    translateAttribute(elem, "download");
+                }
+                if (elem.hasAttribute("label") &&
+                    isAnyOf(elem.tagName, ["MENUITEM", "MENU", "OPTGROUP",
+                                           "OPTION", "TRACK"])) {
+                    translateAttribute(elem, "label");
+                }
+                // FIXME: Should update "lang"
+                if (elem.hasAttribute("placeholder") &&
+                    isAnyOf(elem.tagName, ["INPUT", "TEXTAREA"])) {
+                    translateAttribute(elem, "placeholder");
+                }
+                if (elem.hasAttribute("title")) {
+                    translateAttribute(elem, "title");
+                }
+                if (elem.hasAttribute("value") &&
+                    elem.tagName === "INPUT" &&
+                    isAnyOf(elem.getAttribute("type"), ["reset", "button", "submit"])) {
+                    translateAttribute(elem, "value");
+                }
+            }
+
+            for (let i = 0; i < elem.childNodes.length; i++) {
+                const node = elem.childNodes[i];
+                if (node.nodeType === node.ELEMENT_NODE) {
+                    process(node, enabled);
+                } else if (node.nodeType === node.TEXT_NODE && enabled) {
+                    translateTextNode(node);
+                }
+            }
+        }
+
+        process(document.body, true);
+    }
+}
+
+export const l10n = new Localizer();
+export default l10n.get.bind(l10n);
diff --git a/app/src/main/assets/novnc/app/sounds/CREDITS b/app/src/main/assets/novnc/app/sounds/CREDITS
new file mode 100644
index 00000000..ec1fb556
--- /dev/null
+++ b/app/src/main/assets/novnc/app/sounds/CREDITS
@@ -0,0 +1,4 @@
+bell
+        Copyright: Dr. Richard Boulanger et al
+        URL: http://www.archive.org/details/Berklee44v12
+        License: CC-BY Attribution 3.0 Unported
diff --git a/app/src/main/assets/novnc/app/sounds/bell.mp3 b/app/src/main/assets/novnc/app/sounds/bell.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..fdbf149a1e9e58aa6a39adb6c681b910347d161a
GIT binary patch
literal 4531
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
DLJX+-

literal 0
HcmV?d00001

diff --git a/app/src/main/assets/novnc/app/sounds/bell.oga b/app/src/main/assets/novnc/app/sounds/bell.oga
new file mode 100644
index 0000000000000000000000000000000000000000..144d2b3670e8e294ee7cfe343355bf90123cb687
GIT binary patch
literal 8495
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

literal 0
HcmV?d00001

diff --git a/app/src/main/assets/novnc/app/styles/Orbitron700.ttf b/app/src/main/assets/novnc/app/styles/Orbitron700.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..e28729dc56b0662b5d2506ddfde5a368784f5755
GIT binary patch
literal 38580
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
YaN=?Mu0Qj~<BJ#F#P26F-oE1h0<?nDhX4Qo

literal 0
HcmV?d00001

diff --git a/app/src/main/assets/novnc/app/styles/Orbitron700.woff b/app/src/main/assets/novnc/app/styles/Orbitron700.woff
new file mode 100644
index 0000000000000000000000000000000000000000..61db630cce1c18fd78c71462d82184a32af6089a
GIT binary patch
literal 17472
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
aYg+X)x}OQee;ks?lK=p?0sn=t1dRYj;d3$o

literal 0
HcmV?d00001

diff --git a/app/src/main/assets/novnc/app/styles/base.css b/app/src/main/assets/novnc/app/styles/base.css
new file mode 100644
index 00000000..fd78b79c
--- /dev/null
+++ b/app/src/main/assets/novnc/app/styles/base.css
@@ -0,0 +1,970 @@
+/*
+ * noVNC base CSS
+ * Copyright (C) 2019 The noVNC Authors
+ * noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
+ * This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
+ */
+
+/*
+ * Z index layers:
+ *
+ * 0: Main screen
+ * 10: Control bar
+ * 50: Transition blocker
+ * 60: Connection popups
+ * 100: Status bar
+ * ...
+ * 1000: Javascript crash
+ * ...
+ * 10000: Max (used for polyfills)
+ */
+
+body {
+  margin:0;
+  padding:0;
+  font-family: Helvetica;
+  /*Background image with light grey curve.*/
+  background-color:#494949;
+  background-repeat:no-repeat;
+  background-position:right bottom;
+  height:100%;
+  touch-action: none;
+}
+
+html {
+  height:100%;
+}
+
+.noVNC_only_touch.noVNC_hidden {
+  display: none;
+}
+
+.noVNC_disabled {
+  color: rgb(128, 128, 128);
+}
+
+/* ----------------------------------------
+ * Spinner
+ * ----------------------------------------
+ */
+
+.noVNC_spinner {
+  position: relative;
+}
+.noVNC_spinner, .noVNC_spinner::before, .noVNC_spinner::after {
+  width: 10px;
+  height: 10px;
+  border-radius: 2px;
+  box-shadow: -60px 10px 0 rgba(255, 255, 255, 0);
+  animation: noVNC_spinner 1.0s linear infinite;
+}
+.noVNC_spinner::before {
+  content: "";
+  position: absolute;
+  left: 0px;
+  top: 0px;
+  animation-delay: -0.1s;
+}
+.noVNC_spinner::after {
+  content: "";
+  position: absolute;
+  top: 0px;
+  left: 0px;
+  animation-delay: 0.1s;
+}
+@keyframes noVNC_spinner {
+  0% { box-shadow: -60px 10px 0 rgba(255, 255, 255, 0); width: 20px; }
+  25% { box-shadow: 20px 10px 0 rgba(255, 255, 255, 1); width: 10px; }
+  50% { box-shadow: 60px 10px 0 rgba(255, 255, 255, 0); width: 10px; }
+}
+
+/* ----------------------------------------
+ * Input Elements
+ * ----------------------------------------
+ */
+
+input:not([type]),
+input[type=date],
+input[type=datetime-local],
+input[type=email],
+input[type=month],
+input[type=number],
+input[type=password],
+input[type=search],
+input[type=tel],
+input[type=text],
+input[type=time],
+input[type=url],
+input[type=week],
+textarea {
+  /* Disable default rendering */
+  -webkit-appearance: none;
+  -moz-appearance: none;
+  background: none;
+
+  margin: 2px;
+  padding: 2px;
+  border: 1px solid rgb(192, 192, 192);
+  border-radius: 5px;
+  color: black;
+  background: linear-gradient(to top, rgb(255, 255, 255) 80%, rgb(240, 240, 240));
+}
+
+input[type=button],
+input[type=color],
+input[type=reset],
+input[type=submit],
+select {
+  /* Disable default rendering */
+  -webkit-appearance: none;
+  -moz-appearance: none;
+  background: none;
+
+  margin: 2px;
+  padding: 2px;
+  border: 1px solid rgb(192, 192, 192);
+  border-bottom-width: 2px;
+  border-radius: 5px;
+  color: black;
+  background: linear-gradient(to top, rgb(255, 255, 255), rgb(240, 240, 240));
+
+  /* This avoids it jumping around when :active */
+  vertical-align: middle;
+}
+
+input[type=button],
+input[type=color],
+input[type=reset],
+input[type=submit] {
+  padding-left: 20px;
+  padding-right: 20px;
+}
+
+option {
+  color: black;
+  background: white;
+}
+
+input:not([type]):focus,
+input[type=button]:focus,
+input[type=color]:focus,
+input[type=date]:focus,
+input[type=datetime-local]:focus,
+input[type=email]:focus,
+input[type=month]:focus,
+input[type=number]:focus,
+input[type=password]:focus,
+input[type=reset]:focus,
+input[type=search]:focus,
+input[type=submit]:focus,
+input[type=tel]:focus,
+input[type=text]:focus,
+input[type=time]:focus,
+input[type=url]:focus,
+input[type=week]:focus,
+select:focus,
+textarea:focus {
+  box-shadow: 0px 0px 3px rgba(74, 144, 217, 0.5);
+  border-color: rgb(74, 144, 217);
+  outline: none;
+}
+
+input[type=button]::-moz-focus-inner,
+input[type=color]::-moz-focus-inner,
+input[type=reset]::-moz-focus-inner,
+input[type=submit]::-moz-focus-inner {
+  border: none;
+}
+
+input:not([type]):disabled,
+input[type=button]:disabled,
+input[type=color]:disabled,
+input[type=date]:disabled,
+input[type=datetime-local]:disabled,
+input[type=email]:disabled,
+input[type=month]:disabled,
+input[type=number]:disabled,
+input[type=password]:disabled,
+input[type=reset]:disabled,
+input[type=search]:disabled,
+input[type=submit]:disabled,
+input[type=tel]:disabled,
+input[type=text]:disabled,
+input[type=time]:disabled,
+input[type=url]:disabled,
+input[type=week]:disabled,
+select:disabled,
+textarea:disabled {
+  color: rgb(128, 128, 128);
+  background: rgb(240, 240, 240);
+}
+
+input[type=button]:active,
+input[type=color]:active,
+input[type=reset]:active,
+input[type=submit]:active,
+select:active {
+  border-bottom-width: 1px;
+  margin-top: 3px;
+}
+
+:root:not(.noVNC_touch) input[type=button]:hover:not(:disabled),
+:root:not(.noVNC_touch) input[type=color]:hover:not(:disabled),
+:root:not(.noVNC_touch) input[type=reset]:hover:not(:disabled),
+:root:not(.noVNC_touch) input[type=submit]:hover:not(:disabled),
+:root:not(.noVNC_touch) select:hover:not(:disabled) {
+  background: linear-gradient(to top, rgb(255, 255, 255), rgb(250, 250, 250));
+}
+
+/* ----------------------------------------
+ * WebKit centering hacks
+ * ----------------------------------------
+ */
+
+.noVNC_center {
+  /*
+   * This is a workaround because webkit misrenders transforms and
+   * uses non-integer coordinates, resulting in blurry content.
+   * Ideally we'd use "top: 50%; transform: translateY(-50%);" on
+   * the objects instead.
+   */
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  pointer-events: none;
+}
+.noVNC_center > * {
+  pointer-events: auto;
+}
+.noVNC_vcenter {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  position: fixed;
+  top: 0;
+  left: 0;
+  height: 100%;
+  pointer-events: none;
+}
+.noVNC_vcenter > * {
+  pointer-events: auto;
+}
+
+/* ----------------------------------------
+ * Layering
+ * ----------------------------------------
+ */
+
+.noVNC_connect_layer {
+  z-index: 60;
+}
+
+/* ----------------------------------------
+ * Fallback error
+ * ----------------------------------------
+ */
+
+#noVNC_fallback_error {
+  z-index: 1000;
+  visibility: hidden;
+}
+#noVNC_fallback_error.noVNC_open {
+  visibility: visible;
+}
+
+#noVNC_fallback_error > div {
+  max-width: 90%;
+  padding: 15px;
+
+  transition: 0.5s ease-in-out;
+
+  transform: translateY(-50px);
+  opacity: 0;
+
+  text-align: center;
+  font-weight: bold;
+  color: #fff;
+
+  border-radius: 10px;
+  box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
+  background: rgba(200,55,55,0.8);
+}
+#noVNC_fallback_error.noVNC_open > div {
+  transform: translateY(0);
+  opacity: 1;
+}
+
+#noVNC_fallback_errormsg {
+  font-weight: normal;
+}
+
+#noVNC_fallback_errormsg .noVNC_message {
+  display: inline-block;
+  text-align: left;
+  font-family: monospace;
+  white-space: pre-wrap;
+}
+
+#noVNC_fallback_error .noVNC_location {
+  font-style: italic;
+  font-size: 0.8em;
+  color: rgba(255, 255, 255, 0.8);
+}
+
+#noVNC_fallback_error .noVNC_stack {
+  max-height: 50vh;
+  padding: 10px;
+  margin: 10px;
+  font-size: 0.8em;
+  text-align: left;
+  font-family: monospace;
+  white-space: pre;
+  border: 1px solid rgba(0, 0, 0, 0.5);
+  background: rgba(0, 0, 0, 0.2);
+  overflow: auto;
+}
+
+/* ----------------------------------------
+ * Control Bar
+ * ----------------------------------------
+ */
+
+#noVNC_control_bar_anchor {
+  /* The anchor is needed to get z-stacking to work */
+  position: fixed;
+  z-index: 10;
+
+  transition: 0.5s ease-in-out;
+
+  /* Edge misrenders animations wihthout this */
+  transform: translateX(0);
+}
+:root.noVNC_connected #noVNC_control_bar_anchor.noVNC_idle {
+  opacity: 0.8;
+}
+#noVNC_control_bar_anchor.noVNC_right {
+  left: auto;
+  right: 0;
+}
+
+#noVNC_control_bar {
+  position: relative;
+  left: -100%;
+
+  transition: 0.5s ease-in-out;
+
+  background-color: rgb(110, 132, 163);
+  border-radius: 0 10px 10px 0;
+
+}
+#noVNC_control_bar.noVNC_open {
+  box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
+  left: 0;
+}
+#noVNC_control_bar::before {
+  /* This extra element is to get a proper shadow */
+  content: "";
+  position: absolute;
+  z-index: -1;
+  height: 100%;
+  width: 30px;
+  left: -30px;
+  transition: box-shadow 0.5s ease-in-out;
+}
+#noVNC_control_bar.noVNC_open::before {
+  box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
+}
+.noVNC_right #noVNC_control_bar {
+  left: 100%;
+  border-radius: 10px 0 0 10px;
+}
+.noVNC_right #noVNC_control_bar.noVNC_open {
+  left: 0;
+}
+.noVNC_right #noVNC_control_bar::before {
+  visibility: hidden;
+}
+
+#noVNC_control_bar_handle {
+  position: absolute;
+  left: -15px;
+  top: 0;
+  transform: translateY(35px);
+  width: calc(100% + 30px);
+  height: 50px;
+  z-index: -1;
+  cursor: pointer;
+  border-radius: 5px;
+  background-color: rgb(83, 99, 122);
+  background-image: url("../images/handle_bg.svg");
+  background-repeat: no-repeat;
+  background-position: right;
+  box-shadow: 3px 3px 0px rgba(0, 0, 0, 0.5);
+}
+#noVNC_control_bar_handle:after {
+  content: "";
+  transition: transform 0.5s ease-in-out;
+  background: url("../images/handle.svg");
+  position: absolute;
+  top: 22px; /* (50px-6px)/2 */
+  right: 5px;
+  width: 5px;
+  height: 6px;
+}
+#noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
+  transform: translateX(1px) rotate(180deg);
+}
+:root:not(.noVNC_connected) #noVNC_control_bar_handle {
+  display: none;
+}
+.noVNC_right #noVNC_control_bar_handle {
+  background-position: left;
+}
+.noVNC_right #noVNC_control_bar_handle:after {
+  left: 5px;
+  right: 0;
+  transform: translateX(1px) rotate(180deg);
+}
+.noVNC_right #noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
+  transform: none;
+}
+#noVNC_control_bar_handle div {
+  position: absolute;
+  right: -35px;
+  top: 0;
+  width: 50px;
+  height: 50px;
+}
+:root:not(.noVNC_touch) #noVNC_control_bar_handle div {
+  display: none;
+}
+.noVNC_right #noVNC_control_bar_handle div {
+  left: -35px;
+  right: auto;
+}
+
+#noVNC_control_bar .noVNC_scroll {
+  max-height: 100vh; /* Chrome is buggy with 100% */
+  overflow-x: hidden;
+  overflow-y: auto;
+  padding: 0 10px 0 5px;
+}
+.noVNC_right #noVNC_control_bar .noVNC_scroll {
+  padding: 0 5px 0 10px;
+}
+
+/* Control bar hint */
+#noVNC_control_bar_hint {
+  position: fixed;
+  left: calc(100vw - 50px);
+  right: auto;
+  top: 50%;
+  transform: translateY(-50%) scale(0);
+  width: 100px;
+  height: 50%;
+  max-height: 600px;
+
+  visibility: hidden;
+  opacity: 0;
+  transition: 0.2s ease-in-out;
+  background: transparent;
+  box-shadow: 0 0 10px black, inset 0 0 10px 10px rgba(110, 132, 163, 0.8);
+  border-radius: 10px;
+  transition-delay: 0s;
+}
+#noVNC_control_bar_anchor.noVNC_right #noVNC_control_bar_hint{
+  left: auto;
+  right: calc(100vw - 50px);
+}
+#noVNC_control_bar_hint.noVNC_active {
+  visibility: visible;
+  opacity: 1;
+  transition-delay: 0.2s;
+  transform: translateY(-50%) scale(1);
+}
+
+/* General button style */
+.noVNC_button {
+  display: block;
+  padding: 4px 4px;
+  margin: 10px 0;
+  vertical-align: middle;
+  border:1px solid rgba(255, 255, 255, 0.2);
+  border-radius: 6px;
+}
+.noVNC_button.noVNC_selected {
+  border-color: rgba(0, 0, 0, 0.8);
+  background: rgba(0, 0, 0, 0.5);
+}
+.noVNC_button:disabled {
+  opacity: 0.4;
+}
+.noVNC_button:focus {
+  outline: none;
+}
+.noVNC_button:active {
+  padding-top: 5px;
+  padding-bottom: 3px;
+}
+/* Android browsers don't properly update hover state if touch events
+ * are intercepted, but focus should be safe to display */
+:root:not(.noVNC_touch) .noVNC_button.noVNC_selected:hover,
+.noVNC_button.noVNC_selected:focus {
+  border-color: rgba(0, 0, 0, 0.4);
+  background: rgba(0, 0, 0, 0.2);
+}
+:root:not(.noVNC_touch) .noVNC_button:hover,
+.noVNC_button:focus {
+  background: rgba(255, 255, 255, 0.2);
+}
+.noVNC_button.noVNC_hidden {
+  display: none;
+}
+
+/* Panels */
+.noVNC_panel {
+  transform: translateX(25px);
+
+  transition: 0.5s ease-in-out;
+
+  max-height: 100vh; /* Chrome is buggy with 100% */
+  overflow-x: hidden;
+  overflow-y: auto;
+
+  visibility: hidden;
+  opacity: 0;
+
+  padding: 15px;
+
+  background: #fff;
+  border-radius: 10px;
+  color: #000;
+  border: 2px solid #E0E0E0;
+  box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
+}
+.noVNC_panel.noVNC_open {
+  visibility: visible;
+  opacity: 1;
+  transform: translateX(75px);
+}
+.noVNC_right .noVNC_vcenter {
+  left: auto;
+  right: 0;
+}
+.noVNC_right .noVNC_panel {
+  transform: translateX(-25px);
+}
+.noVNC_right .noVNC_panel.noVNC_open {
+  transform: translateX(-75px);
+}
+
+.noVNC_panel hr {
+  border: none;
+  border-top: 1px solid rgb(192, 192, 192);
+}
+
+.noVNC_panel label {
+  display: block;
+  white-space: nowrap;
+}
+
+.noVNC_panel .noVNC_heading {
+  background-color: rgb(110, 132, 163);
+  border-radius: 5px;
+  padding: 5px;
+  /* Compensate for padding in image */
+  padding-right: 8px;
+  color: white;
+  font-size: 20px;
+  margin-bottom: 10px;
+  white-space: nowrap;
+}
+.noVNC_panel .noVNC_heading img {
+  vertical-align: bottom;
+}
+
+.noVNC_submit {
+  float: right;
+}
+
+/* Expanders */
+.noVNC_expander {
+  cursor: pointer;
+}
+.noVNC_expander::before {
+  content: url("../images/expander.svg");
+  display: inline-block;
+  margin-right: 5px;
+  transition: 0.2s ease-in-out;
+}
+.noVNC_expander.noVNC_open::before {
+  transform: rotateZ(90deg);
+}
+.noVNC_expander ~ * {
+  margin: 5px;
+  margin-left: 10px;
+  padding: 5px;
+  background: rgba(0, 0, 0, 0.05);
+  border-radius: 5px;
+}
+.noVNC_expander:not(.noVNC_open) ~ * {
+  display: none;
+}
+
+/* Control bar content */
+
+#noVNC_control_bar .noVNC_logo {
+  font-size: 13px;
+}
+
+:root:not(.noVNC_connected) #noVNC_view_drag_button {
+  display: none;
+}
+
+/* noVNC Touch Device only buttons */
+:root:not(.noVNC_connected) #noVNC_mobile_buttons {
+  display: none;
+}
+:root:not(.noVNC_touch) #noVNC_mobile_buttons {
+  display: none;
+}
+
+/* Extra manual keys */
+:root:not(.noVNC_connected) #noVNC_toggle_extra_keys_button {
+  display: none;
+}
+
+#noVNC_modifiers {
+  background-color: rgb(92, 92, 92);
+  border: none;
+  padding: 0 10px;
+}
+
+/* Shutdown/Reboot */
+:root:not(.noVNC_connected) #noVNC_power_button {
+  display: none;
+}
+#noVNC_power {
+}
+#noVNC_power_buttons {
+  display: none;
+}
+
+#noVNC_power input[type=button] {
+  width: 100%;
+}
+
+/* Clipboard */
+:root:not(.noVNC_connected) #noVNC_clipboard_button {
+  display: none;
+}
+#noVNC_clipboard {
+  /* Full screen, minus padding and left and right margins */
+  max-width: calc(100vw - 2*15px - 75px - 25px);
+}
+#noVNC_clipboard_text {
+  width: 500px;
+  max-width: 100%;
+}
+
+/* Settings */
+#noVNC_settings {
+}
+#noVNC_settings ul {
+  list-style: none;
+  margin: 0px;
+  padding: 0px;
+}
+#noVNC_setting_port {
+  width: 80px;
+}
+#noVNC_setting_path {
+  width: 100px;
+}
+
+/* Version */
+
+.noVNC_version_wrapper {
+  font-size: small;
+}
+
+.noVNC_version {
+  margin-left: 1rem;
+}
+
+/* Connection Controls */
+:root:not(.noVNC_connected) #noVNC_disconnect_button {
+  display: none;
+}
+
+/* ----------------------------------------
+ * Status Dialog
+ * ----------------------------------------
+ */
+
+#noVNC_status {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  z-index: 100;
+  transform: translateY(-100%);
+
+  cursor: pointer;
+
+  transition: 0.5s ease-in-out;
+
+  visibility: hidden;
+  opacity: 0;
+
+  padding: 5px;
+
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-content: center;
+
+  line-height: 25px;
+  word-wrap: break-word;
+  color: #fff;
+
+  border-bottom: 1px solid rgba(0, 0, 0, 0.9);
+}
+#noVNC_status.noVNC_open {
+  transform: translateY(0);
+  visibility: visible;
+  opacity: 1;
+}
+
+#noVNC_status::before {
+  content: "";
+  display: inline-block;
+  width: 25px;
+  height: 25px;
+  margin-right: 5px;
+}
+
+#noVNC_status.noVNC_status_normal {
+  background: rgba(128,128,128,0.9);
+}
+#noVNC_status.noVNC_status_normal::before {
+  content: url("../images/info.svg") " ";
+}
+#noVNC_status.noVNC_status_error {
+  background: rgba(200,55,55,0.9);
+}
+#noVNC_status.noVNC_status_error::before {
+  content: url("../images/error.svg") " ";
+}
+#noVNC_status.noVNC_status_warn {
+  background: rgba(180,180,30,0.9);
+}
+#noVNC_status.noVNC_status_warn::before {
+  content: url("../images/warning.svg") " ";
+}
+
+/* ----------------------------------------
+ * Connect Dialog
+ * ----------------------------------------
+ */
+
+#noVNC_connect_dlg {
+  transition: 0.5s ease-in-out;
+
+  transform: scale(0, 0);
+  visibility: hidden;
+  opacity: 0;
+}
+#noVNC_connect_dlg.noVNC_open {
+  transform: scale(1, 1);
+  visibility: visible;
+  opacity: 1;
+}
+#noVNC_connect_dlg .noVNC_logo {
+  transition: 0.5s ease-in-out;
+  padding: 10px;
+  margin-bottom: 10px;
+
+  font-size: 80px;
+  text-align: center;
+
+  border-radius: 5px;
+}
+@media (max-width: 440px) {
+  #noVNC_connect_dlg {
+    max-width: calc(100vw - 100px);
+  }
+  #noVNC_connect_dlg .noVNC_logo {
+    font-size: calc(25vw - 30px);
+  }
+}
+#noVNC_connect_button {
+  cursor: pointer;
+
+  padding: 10px;
+
+  color: white;
+  background-color: rgb(110, 132, 163);
+  border-radius: 12px;
+
+  text-align: center;
+  font-size: 20px;
+
+  box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
+}
+#noVNC_connect_button div {
+  margin: 2px;
+  padding: 5px 30px;
+  border: 1px solid rgb(83, 99, 122);
+  border-bottom-width: 2px;
+  border-radius: 5px;
+  background: linear-gradient(to top, rgb(110, 132, 163), rgb(99, 119, 147));
+
+  /* This avoids it jumping around when :active */
+  vertical-align: middle;
+}
+#noVNC_connect_button div:active {
+  border-bottom-width: 1px;
+  margin-top: 3px;
+}
+:root:not(.noVNC_touch) #noVNC_connect_button div:hover {
+  background: linear-gradient(to top, rgb(110, 132, 163), rgb(105, 125, 155));
+}
+
+#noVNC_connect_button img {
+  vertical-align: bottom;
+  height: 1.3em;
+}
+
+/* ----------------------------------------
+ * Password Dialog
+ * ----------------------------------------
+ */
+
+#noVNC_credentials_dlg {
+  position: relative;
+
+  transform: translateY(-50px);
+}
+#noVNC_credentials_dlg.noVNC_open {
+  transform: translateY(0);
+}
+#noVNC_credentials_dlg ul {
+  list-style: none;
+  margin: 0px;
+  padding: 0px;
+}
+.noVNC_hidden {
+  display: none;
+}
+
+
+/* ----------------------------------------
+ * Main Area
+ * ----------------------------------------
+ */
+
+/* Transition screen */
+#noVNC_transition {
+  display: none;
+
+  position: fixed;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+
+  color: white;
+  background: rgba(0, 0, 0, 0.5);
+  z-index: 50;
+
+  /*display: flex;*/
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+}
+:root.noVNC_loading #noVNC_transition,
+:root.noVNC_connecting #noVNC_transition,
+:root.noVNC_disconnecting #noVNC_transition,
+:root.noVNC_reconnecting #noVNC_transition {
+  display: flex;
+}
+:root:not(.noVNC_reconnecting) #noVNC_cancel_reconnect_button {
+  display: none;
+}
+#noVNC_transition_text {
+  font-size: 1.5em;
+}
+
+/* Main container */
+#noVNC_container {
+  width: 100%;
+  height: 100%;
+  background-color: #313131;
+  border-bottom-right-radius: 800px 600px;
+  /*border-top-left-radius: 800px 600px;*/
+}
+
+#noVNC_keyboardinput {
+  width: 1px;
+  height: 1px;
+  background-color: #fff;
+  color: #fff;
+  border: 0;
+  position: absolute;
+  left: -40px;
+  z-index: -1;
+  ime-mode: disabled;
+}
+
+/*Default noVNC logo.*/
+/* From: http://fonts.googleapis.com/css?family=Orbitron:700 */
+@font-face {
+  font-family: 'Orbitron';
+  font-style: normal;
+  font-weight: 700;
+  src: local('?'), url('Orbitron700.woff') format('woff'),
+                   url('Orbitron700.ttf') format('truetype');
+}
+
+.noVNC_logo {
+  color:yellow;
+  font-family: 'Orbitron', 'OrbitronTTF', sans-serif;
+  line-height:90%;
+  text-shadow: 0.1em 0.1em 0 black;
+}
+.noVNC_logo span{
+  color:green;
+}
+
+#noVNC_bell {
+  display: none;
+}
+
+/* ----------------------------------------
+ * Media sizing
+ * ----------------------------------------
+ */
+
+@media screen and (max-width: 640px){
+  #noVNC_logo {
+    font-size: 150px;
+  }
+}
+
+@media screen and (min-width: 321px) and (max-width: 480px) {
+  #noVNC_logo {
+    font-size: 110px;
+  }
+}
+
+@media screen and (max-width: 320px) {
+  #noVNC_logo {
+    font-size: 90px;
+  }
+}
diff --git a/app/src/main/assets/novnc/app/ui.js b/app/src/main/assets/novnc/app/ui.js
new file mode 100644
index 00000000..cb6a9fda
--- /dev/null
+++ b/app/src/main/assets/novnc/app/ui.js
@@ -0,0 +1,1715 @@
+/*
+ * 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.
+ */
+
+import * as Log from '../core/util/logging.js';
+import _, { l10n } from './localization.js';
+import { isTouchDevice, isSafari, hasScrollbarGutter, dragThreshold }
+    from '../core/util/browser.js';
+import { setCapture, getPointerEvent } from '../core/util/events.js';
+import KeyTable from "../core/input/keysym.js";
+import keysyms from "../core/input/keysymdef.js";
+import Keyboard from "../core/input/keyboard.js";
+import RFB from "../core/rfb.js";
+import * as WebUtil from "./webutil.js";
+
+const PAGE_TITLE = "noVNC";
+
+const UI = {
+
+    connected: false,
+    desktopName: "",
+
+    statusTimeout: null,
+    hideKeyboardTimeout: null,
+    idleControlbarTimeout: null,
+    closeControlbarTimeout: null,
+
+    controlbarGrabbed: false,
+    controlbarDrag: false,
+    controlbarMouseDownClientY: 0,
+    controlbarMouseDownOffsetY: 0,
+
+    lastKeyboardinput: null,
+    defaultKeyboardinputLen: 100,
+
+    inhibitReconnect: true,
+    reconnectCallback: null,
+    reconnectPassword: null,
+
+    prime() {
+        return WebUtil.initSettings().then(() => {
+            if (document.readyState === "interactive" || document.readyState === "complete") {
+                return UI.start();
+            }
+
+            return new Promise((resolve, reject) => {
+                document.addEventListener('DOMContentLoaded', () => UI.start().then(resolve).catch(reject));
+            });
+        });
+    },
+
+    // Render default UI and initialize settings menu
+    start() {
+
+        UI.initSettings();
+
+        // Translate the DOM
+        l10n.translateDOM();
+
+        fetch('./package.json')
+            .then((response) => {
+                if (!response.ok) {
+                    throw Error("" + response.status + " " + response.statusText);
+                }
+                return response.json();
+            })
+            .then((packageInfo) => {
+                Array.from(document.getElementsByClassName('noVNC_version')).forEach(el => el.innerText = packageInfo.version);
+            })
+            .catch((err) => {
+                Log.Error("Couldn't fetch package.json: " + err);
+                Array.from(document.getElementsByClassName('noVNC_version_wrapper'))
+                    .concat(Array.from(document.getElementsByClassName('noVNC_version_separator')))
+                    .forEach(el => el.style.display = 'none');
+            });
+
+        // Adapt the interface for touch screen devices
+        if (isTouchDevice) {
+            document.documentElement.classList.add("noVNC_touch");
+            // Remove the address bar
+            setTimeout(() => window.scrollTo(0, 1), 100);
+        }
+
+        // Restore control bar position
+        if (WebUtil.readSetting('controlbar_pos') === 'right') {
+            UI.toggleControlbarSide();
+        }
+
+        UI.initFullscreen();
+
+        // Setup event handlers
+        UI.addControlbarHandlers();
+        UI.addTouchSpecificHandlers();
+        UI.addExtraKeysHandlers();
+        UI.addMachineHandlers();
+        UI.addConnectionControlHandlers();
+        UI.addClipboardHandlers();
+        UI.addSettingsHandlers();
+        document.getElementById("noVNC_status")
+            .addEventListener('click', UI.hideStatus);
+
+        // Bootstrap fallback input handler
+        UI.keyboardinputReset();
+
+        UI.openControlbar();
+
+        UI.updateVisualState('init');
+
+        document.documentElement.classList.remove("noVNC_loading");
+
+        let autoconnect = WebUtil.getConfigVar('autoconnect', false);
+        if (autoconnect === 'true' || autoconnect == '1') {
+            autoconnect = true;
+            UI.connect();
+        } else {
+            autoconnect = false;
+            // Show the connect panel on first load unless autoconnecting
+            UI.openConnectPanel();
+        }
+
+        return Promise.resolve(UI.rfb);
+    },
+
+    initFullscreen() {
+        // Only show the button if fullscreen is properly supported
+        // * Safari doesn't support alphanumerical input while in fullscreen
+        if (!isSafari() &&
+            (document.documentElement.requestFullscreen ||
+             document.documentElement.mozRequestFullScreen ||
+             document.documentElement.webkitRequestFullscreen ||
+             document.body.msRequestFullscreen)) {
+            document.getElementById('noVNC_fullscreen_button')
+                .classList.remove("noVNC_hidden");
+            UI.addFullscreenHandlers();
+        }
+    },
+
+    initSettings() {
+        // Logging selection dropdown
+        const llevels = ['error', 'warn', 'info', 'debug'];
+        for (let i = 0; i < llevels.length; i += 1) {
+            UI.addOption(document.getElementById('noVNC_setting_logging'), llevels[i], llevels[i]);
+        }
+
+        // Settings with immediate effects
+        UI.initSetting('logging', 'warn');
+        UI.updateLogging();
+
+        // if port == 80 (or 443) then it won't be present and should be
+        // set manually
+        let port = window.location.port;
+        if (!port) {
+            if (window.location.protocol.substring(0, 5) == 'https') {
+                port = 443;
+            } else if (window.location.protocol.substring(0, 4) == 'http') {
+                port = 80;
+            }
+        }
+
+        /* Populate the controls if defaults are provided in the URL */
+        UI.initSetting('host', window.location.hostname);
+        UI.initSetting('port', port);
+        UI.initSetting('encrypt', (window.location.protocol === "https:"));
+        UI.initSetting('view_clip', false);
+        UI.initSetting('resize', 'off');
+        UI.initSetting('quality', 6);
+        UI.initSetting('compression', 2);
+        UI.initSetting('shared', true);
+        UI.initSetting('view_only', false);
+        UI.initSetting('show_dot', false);
+        UI.initSetting('path', 'websockify');
+        UI.initSetting('repeaterID', '');
+        UI.initSetting('reconnect', false);
+        UI.initSetting('reconnect_delay', 5000);
+
+        UI.setupSettingLabels();
+    },
+    // Adds a link to the label elements on the corresponding input elements
+    setupSettingLabels() {
+        const labels = document.getElementsByTagName('LABEL');
+        for (let i = 0; i < labels.length; i++) {
+            const htmlFor = labels[i].htmlFor;
+            if (htmlFor != '') {
+                const elem = document.getElementById(htmlFor);
+                if (elem) elem.label = labels[i];
+            } else {
+                // If 'for' isn't set, use the first input element child
+                const children = labels[i].children;
+                for (let j = 0; j < children.length; j++) {
+                    if (children[j].form !== undefined) {
+                        children[j].label = labels[i];
+                        break;
+                    }
+                }
+            }
+        }
+    },
+
+/* ------^-------
+*     /INIT
+* ==============
+* EVENT HANDLERS
+* ------v------*/
+
+    addControlbarHandlers() {
+        document.getElementById("noVNC_control_bar")
+            .addEventListener('mousemove', UI.activateControlbar);
+        document.getElementById("noVNC_control_bar")
+            .addEventListener('mouseup', UI.activateControlbar);
+        document.getElementById("noVNC_control_bar")
+            .addEventListener('mousedown', UI.activateControlbar);
+        document.getElementById("noVNC_control_bar")
+            .addEventListener('keydown', UI.activateControlbar);
+
+        document.getElementById("noVNC_control_bar")
+            .addEventListener('mousedown', UI.keepControlbar);
+        document.getElementById("noVNC_control_bar")
+            .addEventListener('keydown', UI.keepControlbar);
+
+        document.getElementById("noVNC_view_drag_button")
+            .addEventListener('click', UI.toggleViewDrag);
+
+        document.getElementById("noVNC_control_bar_handle")
+            .addEventListener('mousedown', UI.controlbarHandleMouseDown);
+        document.getElementById("noVNC_control_bar_handle")
+            .addEventListener('mouseup', UI.controlbarHandleMouseUp);
+        document.getElementById("noVNC_control_bar_handle")
+            .addEventListener('mousemove', UI.dragControlbarHandle);
+        // resize events aren't available for elements
+        window.addEventListener('resize', UI.updateControlbarHandle);
+
+        const exps = document.getElementsByClassName("noVNC_expander");
+        for (let i = 0;i < exps.length;i++) {
+            exps[i].addEventListener('click', UI.toggleExpander);
+        }
+    },
+
+    addTouchSpecificHandlers() {
+        document.getElementById("noVNC_keyboard_button")
+            .addEventListener('click', UI.toggleVirtualKeyboard);
+
+        UI.touchKeyboard = new Keyboard(document.getElementById('noVNC_keyboardinput'));
+        UI.touchKeyboard.onkeyevent = UI.keyEvent;
+        UI.touchKeyboard.grab();
+        document.getElementById("noVNC_keyboardinput")
+            .addEventListener('input', UI.keyInput);
+        document.getElementById("noVNC_keyboardinput")
+            .addEventListener('focus', UI.onfocusVirtualKeyboard);
+        document.getElementById("noVNC_keyboardinput")
+            .addEventListener('blur', UI.onblurVirtualKeyboard);
+        document.getElementById("noVNC_keyboardinput")
+            .addEventListener('submit', () => false);
+
+        document.documentElement
+            .addEventListener('mousedown', UI.keepVirtualKeyboard, true);
+
+        document.getElementById("noVNC_control_bar")
+            .addEventListener('touchstart', UI.activateControlbar);
+        document.getElementById("noVNC_control_bar")
+            .addEventListener('touchmove', UI.activateControlbar);
+        document.getElementById("noVNC_control_bar")
+            .addEventListener('touchend', UI.activateControlbar);
+        document.getElementById("noVNC_control_bar")
+            .addEventListener('input', UI.activateControlbar);
+
+        document.getElementById("noVNC_control_bar")
+            .addEventListener('touchstart', UI.keepControlbar);
+        document.getElementById("noVNC_control_bar")
+            .addEventListener('input', UI.keepControlbar);
+
+        document.getElementById("noVNC_control_bar_handle")
+            .addEventListener('touchstart', UI.controlbarHandleMouseDown);
+        document.getElementById("noVNC_control_bar_handle")
+            .addEventListener('touchend', UI.controlbarHandleMouseUp);
+        document.getElementById("noVNC_control_bar_handle")
+            .addEventListener('touchmove', UI.dragControlbarHandle);
+    },
+
+    addExtraKeysHandlers() {
+        document.getElementById("noVNC_toggle_extra_keys_button")
+            .addEventListener('click', UI.toggleExtraKeys);
+        document.getElementById("noVNC_toggle_ctrl_button")
+            .addEventListener('click', UI.toggleCtrl);
+        document.getElementById("noVNC_toggle_windows_button")
+            .addEventListener('click', UI.toggleWindows);
+        document.getElementById("noVNC_toggle_alt_button")
+            .addEventListener('click', UI.toggleAlt);
+        document.getElementById("noVNC_send_tab_button")
+            .addEventListener('click', UI.sendTab);
+        document.getElementById("noVNC_send_esc_button")
+            .addEventListener('click', UI.sendEsc);
+        document.getElementById("noVNC_send_ctrl_alt_del_button")
+            .addEventListener('click', UI.sendCtrlAltDel);
+    },
+
+    addMachineHandlers() {
+        document.getElementById("noVNC_shutdown_button")
+            .addEventListener('click', () => UI.rfb.machineShutdown());
+        document.getElementById("noVNC_reboot_button")
+            .addEventListener('click', () => UI.rfb.machineReboot());
+        document.getElementById("noVNC_reset_button")
+            .addEventListener('click', () => UI.rfb.machineReset());
+        document.getElementById("noVNC_power_button")
+            .addEventListener('click', UI.togglePowerPanel);
+    },
+
+    addConnectionControlHandlers() {
+        document.getElementById("noVNC_disconnect_button")
+            .addEventListener('click', UI.disconnect);
+        document.getElementById("noVNC_connect_button")
+            .addEventListener('click', UI.connect);
+        document.getElementById("noVNC_cancel_reconnect_button")
+            .addEventListener('click', UI.cancelReconnect);
+
+        document.getElementById("noVNC_credentials_button")
+            .addEventListener('click', UI.setCredentials);
+    },
+
+    addClipboardHandlers() {
+        document.getElementById("noVNC_clipboard_button")
+            .addEventListener('click', UI.toggleClipboardPanel);
+        document.getElementById("noVNC_clipboard_text")
+            .addEventListener('change', UI.clipboardSend);
+        document.getElementById("noVNC_clipboard_clear_button")
+            .addEventListener('click', UI.clipboardClear);
+    },
+
+    // Add a call to save settings when the element changes,
+    // unless the optional parameter changeFunc is used instead.
+    addSettingChangeHandler(name, changeFunc) {
+        const settingElem = document.getElementById("noVNC_setting_" + name);
+        if (changeFunc === undefined) {
+            changeFunc = () => UI.saveSetting(name);
+        }
+        settingElem.addEventListener('change', changeFunc);
+    },
+
+    addSettingsHandlers() {
+        document.getElementById("noVNC_settings_button")
+            .addEventListener('click', UI.toggleSettingsPanel);
+
+        UI.addSettingChangeHandler('encrypt');
+        UI.addSettingChangeHandler('resize');
+        UI.addSettingChangeHandler('resize', UI.applyResizeMode);
+        UI.addSettingChangeHandler('resize', UI.updateViewClip);
+        UI.addSettingChangeHandler('quality');
+        UI.addSettingChangeHandler('quality', UI.updateQuality);
+        UI.addSettingChangeHandler('compression');
+        UI.addSettingChangeHandler('compression', UI.updateCompression);
+        UI.addSettingChangeHandler('view_clip');
+        UI.addSettingChangeHandler('view_clip', UI.updateViewClip);
+        UI.addSettingChangeHandler('shared');
+        UI.addSettingChangeHandler('view_only');
+        UI.addSettingChangeHandler('view_only', UI.updateViewOnly);
+        UI.addSettingChangeHandler('show_dot');
+        UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor);
+        UI.addSettingChangeHandler('host');
+        UI.addSettingChangeHandler('port');
+        UI.addSettingChangeHandler('path');
+        UI.addSettingChangeHandler('repeaterID');
+        UI.addSettingChangeHandler('logging');
+        UI.addSettingChangeHandler('logging', UI.updateLogging);
+        UI.addSettingChangeHandler('reconnect');
+        UI.addSettingChangeHandler('reconnect_delay');
+    },
+
+    addFullscreenHandlers() {
+        document.getElementById("noVNC_fullscreen_button")
+            .addEventListener('click', UI.toggleFullscreen);
+
+        window.addEventListener('fullscreenchange', UI.updateFullscreenButton);
+        window.addEventListener('mozfullscreenchange', UI.updateFullscreenButton);
+        window.addEventListener('webkitfullscreenchange', UI.updateFullscreenButton);
+        window.addEventListener('msfullscreenchange', UI.updateFullscreenButton);
+    },
+
+/* ------^-------
+ * /EVENT HANDLERS
+ * ==============
+ *     VISUAL
+ * ------v------*/
+
+    // Disable/enable controls depending on connection state
+    updateVisualState(state) {
+
+        document.documentElement.classList.remove("noVNC_connecting");
+        document.documentElement.classList.remove("noVNC_connected");
+        document.documentElement.classList.remove("noVNC_disconnecting");
+        document.documentElement.classList.remove("noVNC_reconnecting");
+
+        const transitionElem = document.getElementById("noVNC_transition_text");
+        switch (state) {
+            case 'init':
+                break;
+            case 'connecting':
+                transitionElem.textContent = _("Connecting...");
+                document.documentElement.classList.add("noVNC_connecting");
+                break;
+            case 'connected':
+                document.documentElement.classList.add("noVNC_connected");
+                break;
+            case 'disconnecting':
+                transitionElem.textContent = _("Disconnecting...");
+                document.documentElement.classList.add("noVNC_disconnecting");
+                break;
+            case 'disconnected':
+                break;
+            case 'reconnecting':
+                transitionElem.textContent = _("Reconnecting...");
+                document.documentElement.classList.add("noVNC_reconnecting");
+                break;
+            default:
+                Log.Error("Invalid visual state: " + state);
+                UI.showStatus(_("Internal error"), 'error');
+                return;
+        }
+
+        if (UI.connected) {
+            UI.updateViewClip();
+
+            UI.disableSetting('encrypt');
+            UI.disableSetting('shared');
+            UI.disableSetting('host');
+            UI.disableSetting('port');
+            UI.disableSetting('path');
+            UI.disableSetting('repeaterID');
+
+            // Hide the controlbar after 2 seconds
+            UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000);
+        } else {
+            UI.enableSetting('encrypt');
+            UI.enableSetting('shared');
+            UI.enableSetting('host');
+            UI.enableSetting('port');
+            UI.enableSetting('path');
+            UI.enableSetting('repeaterID');
+            UI.updatePowerButton();
+            UI.keepControlbar();
+        }
+
+        // State change closes dialogs as they may not be relevant
+        // anymore
+        UI.closeAllPanels();
+        document.getElementById('noVNC_credentials_dlg')
+            .classList.remove('noVNC_open');
+    },
+
+    showStatus(text, statusType, time) {
+        const statusElem = document.getElementById('noVNC_status');
+
+        if (typeof statusType === 'undefined') {
+            statusType = 'normal';
+        }
+
+        // Don't overwrite more severe visible statuses and never
+        // errors. Only shows the first error.
+        if (statusElem.classList.contains("noVNC_open")) {
+            if (statusElem.classList.contains("noVNC_status_error")) {
+                return;
+            }
+            if (statusElem.classList.contains("noVNC_status_warn") &&
+                statusType === 'normal') {
+                return;
+            }
+        }
+
+        clearTimeout(UI.statusTimeout);
+
+        switch (statusType) {
+            case 'error':
+                statusElem.classList.remove("noVNC_status_warn");
+                statusElem.classList.remove("noVNC_status_normal");
+                statusElem.classList.add("noVNC_status_error");
+                break;
+            case 'warning':
+            case 'warn':
+                statusElem.classList.remove("noVNC_status_error");
+                statusElem.classList.remove("noVNC_status_normal");
+                statusElem.classList.add("noVNC_status_warn");
+                break;
+            case 'normal':
+            case 'info':
+            default:
+                statusElem.classList.remove("noVNC_status_error");
+                statusElem.classList.remove("noVNC_status_warn");
+                statusElem.classList.add("noVNC_status_normal");
+                break;
+        }
+
+        statusElem.textContent = text;
+        statusElem.classList.add("noVNC_open");
+
+        // If no time was specified, show the status for 1.5 seconds
+        if (typeof time === 'undefined') {
+            time = 1500;
+        }
+
+        // Error messages do not timeout
+        if (statusType !== 'error') {
+            UI.statusTimeout = window.setTimeout(UI.hideStatus, time);
+        }
+    },
+
+    hideStatus() {
+        clearTimeout(UI.statusTimeout);
+        document.getElementById('noVNC_status').classList.remove("noVNC_open");
+    },
+
+    activateControlbar(event) {
+        clearTimeout(UI.idleControlbarTimeout);
+        // We manipulate the anchor instead of the actual control
+        // bar in order to avoid creating new a stacking group
+        document.getElementById('noVNC_control_bar_anchor')
+            .classList.remove("noVNC_idle");
+        UI.idleControlbarTimeout = window.setTimeout(UI.idleControlbar, 2000);
+    },
+
+    idleControlbar() {
+        // Don't fade if a child of the control bar has focus
+        if (document.getElementById('noVNC_control_bar')
+            .contains(document.activeElement) && document.hasFocus()) {
+            UI.activateControlbar();
+            return;
+        }
+
+        document.getElementById('noVNC_control_bar_anchor')
+            .classList.add("noVNC_idle");
+    },
+
+    keepControlbar() {
+        clearTimeout(UI.closeControlbarTimeout);
+    },
+
+    openControlbar() {
+        document.getElementById('noVNC_control_bar')
+            .classList.add("noVNC_open");
+    },
+
+    closeControlbar() {
+        UI.closeAllPanels();
+        document.getElementById('noVNC_control_bar')
+            .classList.remove("noVNC_open");
+        UI.rfb.focus();
+    },
+
+    toggleControlbar() {
+        if (document.getElementById('noVNC_control_bar')
+            .classList.contains("noVNC_open")) {
+            UI.closeControlbar();
+        } else {
+            UI.openControlbar();
+        }
+    },
+
+    toggleControlbarSide() {
+        // Temporarily disable animation, if bar is displayed, to avoid weird
+        // movement. The transitionend-event will not fire when display=none.
+        const bar = document.getElementById('noVNC_control_bar');
+        const barDisplayStyle = window.getComputedStyle(bar).display;
+        if (barDisplayStyle !== 'none') {
+            bar.style.transitionDuration = '0s';
+            bar.addEventListener('transitionend', () => bar.style.transitionDuration = '');
+        }
+
+        const anchor = document.getElementById('noVNC_control_bar_anchor');
+        if (anchor.classList.contains("noVNC_right")) {
+            WebUtil.writeSetting('controlbar_pos', 'left');
+            anchor.classList.remove("noVNC_right");
+        } else {
+            WebUtil.writeSetting('controlbar_pos', 'right');
+            anchor.classList.add("noVNC_right");
+        }
+
+        // Consider this a movement of the handle
+        UI.controlbarDrag = true;
+    },
+
+    showControlbarHint(show) {
+        const hint = document.getElementById('noVNC_control_bar_hint');
+        if (show) {
+            hint.classList.add("noVNC_active");
+        } else {
+            hint.classList.remove("noVNC_active");
+        }
+    },
+
+    dragControlbarHandle(e) {
+        if (!UI.controlbarGrabbed) return;
+
+        const ptr = getPointerEvent(e);
+
+        const anchor = document.getElementById('noVNC_control_bar_anchor');
+        if (ptr.clientX < (window.innerWidth * 0.1)) {
+            if (anchor.classList.contains("noVNC_right")) {
+                UI.toggleControlbarSide();
+            }
+        } else if (ptr.clientX > (window.innerWidth * 0.9)) {
+            if (!anchor.classList.contains("noVNC_right")) {
+                UI.toggleControlbarSide();
+            }
+        }
+
+        if (!UI.controlbarDrag) {
+            const dragDistance = Math.abs(ptr.clientY - UI.controlbarMouseDownClientY);
+
+            if (dragDistance < dragThreshold) return;
+
+            UI.controlbarDrag = true;
+        }
+
+        const eventY = ptr.clientY - UI.controlbarMouseDownOffsetY;
+
+        UI.moveControlbarHandle(eventY);
+
+        e.preventDefault();
+        e.stopPropagation();
+        UI.keepControlbar();
+        UI.activateControlbar();
+    },
+
+    // Move the handle but don't allow any position outside the bounds
+    moveControlbarHandle(viewportRelativeY) {
+        const handle = document.getElementById("noVNC_control_bar_handle");
+        const handleHeight = handle.getBoundingClientRect().height;
+        const controlbarBounds = document.getElementById("noVNC_control_bar")
+            .getBoundingClientRect();
+        const margin = 10;
+
+        // These heights need to be non-zero for the below logic to work
+        if (handleHeight === 0 || controlbarBounds.height === 0) {
+            return;
+        }
+
+        let newY = viewportRelativeY;
+
+        // Check if the coordinates are outside the control bar
+        if (newY < controlbarBounds.top + margin) {
+            // Force coordinates to be below the top of the control bar
+            newY = controlbarBounds.top + margin;
+
+        } else if (newY > controlbarBounds.top +
+                   controlbarBounds.height - handleHeight - margin) {
+            // Force coordinates to be above the bottom of the control bar
+            newY = controlbarBounds.top +
+                controlbarBounds.height - handleHeight - margin;
+        }
+
+        // Corner case: control bar too small for stable position
+        if (controlbarBounds.height < (handleHeight + margin * 2)) {
+            newY = controlbarBounds.top +
+                (controlbarBounds.height - handleHeight) / 2;
+        }
+
+        // The transform needs coordinates that are relative to the parent
+        const parentRelativeY = newY - controlbarBounds.top;
+        handle.style.transform = "translateY(" + parentRelativeY + "px)";
+    },
+
+    updateControlbarHandle() {
+        // Since the control bar is fixed on the viewport and not the page,
+        // the move function expects coordinates relative the the viewport.
+        const handle = document.getElementById("noVNC_control_bar_handle");
+        const handleBounds = handle.getBoundingClientRect();
+        UI.moveControlbarHandle(handleBounds.top);
+    },
+
+    controlbarHandleMouseUp(e) {
+        if ((e.type == "mouseup") && (e.button != 0)) return;
+
+        // mouseup and mousedown on the same place toggles the controlbar
+        if (UI.controlbarGrabbed && !UI.controlbarDrag) {
+            UI.toggleControlbar();
+            e.preventDefault();
+            e.stopPropagation();
+            UI.keepControlbar();
+            UI.activateControlbar();
+        }
+        UI.controlbarGrabbed = false;
+        UI.showControlbarHint(false);
+    },
+
+    controlbarHandleMouseDown(e) {
+        if ((e.type == "mousedown") && (e.button != 0)) return;
+
+        const ptr = getPointerEvent(e);
+
+        const handle = document.getElementById("noVNC_control_bar_handle");
+        const bounds = handle.getBoundingClientRect();
+
+        // Touch events have implicit capture
+        if (e.type === "mousedown") {
+            setCapture(handle);
+        }
+
+        UI.controlbarGrabbed = true;
+        UI.controlbarDrag = false;
+
+        UI.showControlbarHint(true);
+
+        UI.controlbarMouseDownClientY = ptr.clientY;
+        UI.controlbarMouseDownOffsetY = ptr.clientY - bounds.top;
+        e.preventDefault();
+        e.stopPropagation();
+        UI.keepControlbar();
+        UI.activateControlbar();
+    },
+
+    toggleExpander(e) {
+        if (this.classList.contains("noVNC_open")) {
+            this.classList.remove("noVNC_open");
+        } else {
+            this.classList.add("noVNC_open");
+        }
+    },
+
+/* ------^-------
+ *    /VISUAL
+ * ==============
+ *    SETTINGS
+ * ------v------*/
+
+    // Initial page load read/initialization of settings
+    initSetting(name, defVal) {
+        // Check Query string followed by cookie
+        let val = WebUtil.getConfigVar(name);
+        if (val === null) {
+            val = WebUtil.readSetting(name, defVal);
+        }
+        WebUtil.setSetting(name, val);
+        UI.updateSetting(name);
+        return val;
+    },
+
+    // Set the new value, update and disable form control setting
+    forceSetting(name, val) {
+        WebUtil.setSetting(name, val);
+        UI.updateSetting(name);
+        UI.disableSetting(name);
+    },
+
+    // Update cookie and form control setting. If value is not set, then
+    // updates from control to current cookie setting.
+    updateSetting(name) {
+
+        // Update the settings control
+        let value = UI.getSetting(name);
+
+        const ctrl = document.getElementById('noVNC_setting_' + name);
+        if (ctrl.type === 'checkbox') {
+            ctrl.checked = value;
+
+        } else if (typeof ctrl.options !== 'undefined') {
+            for (let i = 0; i < ctrl.options.length; i += 1) {
+                if (ctrl.options[i].value === value) {
+                    ctrl.selectedIndex = i;
+                    break;
+                }
+            }
+        } else {
+            ctrl.value = value;
+        }
+    },
+
+    // Save control setting to cookie
+    saveSetting(name) {
+        const ctrl = document.getElementById('noVNC_setting_' + name);
+        let val;
+        if (ctrl.type === 'checkbox') {
+            val = ctrl.checked;
+        } else if (typeof ctrl.options !== 'undefined') {
+            val = ctrl.options[ctrl.selectedIndex].value;
+        } else {
+            val = ctrl.value;
+        }
+        WebUtil.writeSetting(name, val);
+        //Log.Debug("Setting saved '" + name + "=" + val + "'");
+        return val;
+    },
+
+    // Read form control compatible setting from cookie
+    getSetting(name) {
+        const ctrl = document.getElementById('noVNC_setting_' + name);
+        let val = WebUtil.readSetting(name);
+        if (typeof val !== 'undefined' && val !== null && ctrl.type === 'checkbox') {
+            if (val.toString().toLowerCase() in {'0': 1, 'no': 1, 'false': 1}) {
+                val = false;
+            } else {
+                val = true;
+            }
+        }
+        return val;
+    },
+
+    // These helpers compensate for the lack of parent-selectors and
+    // previous-sibling-selectors in CSS which are needed when we want to
+    // disable the labels that belong to disabled input elements.
+    disableSetting(name) {
+        const ctrl = document.getElementById('noVNC_setting_' + name);
+        ctrl.disabled = true;
+        ctrl.label.classList.add('noVNC_disabled');
+    },
+
+    enableSetting(name) {
+        const ctrl = document.getElementById('noVNC_setting_' + name);
+        ctrl.disabled = false;
+        ctrl.label.classList.remove('noVNC_disabled');
+    },
+
+/* ------^-------
+ *   /SETTINGS
+ * ==============
+ *    PANELS
+ * ------v------*/
+
+    closeAllPanels() {
+        UI.closeSettingsPanel();
+        UI.closePowerPanel();
+        UI.closeClipboardPanel();
+        UI.closeExtraKeys();
+    },
+
+/* ------^-------
+ *   /PANELS
+ * ==============
+ * SETTINGS (panel)
+ * ------v------*/
+
+    openSettingsPanel() {
+        UI.closeAllPanels();
+        UI.openControlbar();
+
+        // Refresh UI elements from saved cookies
+        UI.updateSetting('encrypt');
+        UI.updateSetting('view_clip');
+        UI.updateSetting('resize');
+        UI.updateSetting('quality');
+        UI.updateSetting('compression');
+        UI.updateSetting('shared');
+        UI.updateSetting('view_only');
+        UI.updateSetting('path');
+        UI.updateSetting('repeaterID');
+        UI.updateSetting('logging');
+        UI.updateSetting('reconnect');
+        UI.updateSetting('reconnect_delay');
+
+        document.getElementById('noVNC_settings')
+            .classList.add("noVNC_open");
+        document.getElementById('noVNC_settings_button')
+            .classList.add("noVNC_selected");
+    },
+
+    closeSettingsPanel() {
+        document.getElementById('noVNC_settings')
+            .classList.remove("noVNC_open");
+        document.getElementById('noVNC_settings_button')
+            .classList.remove("noVNC_selected");
+    },
+
+    toggleSettingsPanel() {
+        if (document.getElementById('noVNC_settings')
+            .classList.contains("noVNC_open")) {
+            UI.closeSettingsPanel();
+        } else {
+            UI.openSettingsPanel();
+        }
+    },
+
+/* ------^-------
+ *   /SETTINGS
+ * ==============
+ *     POWER
+ * ------v------*/
+
+    openPowerPanel() {
+        UI.closeAllPanels();
+        UI.openControlbar();
+
+        document.getElementById('noVNC_power')
+            .classList.add("noVNC_open");
+        document.getElementById('noVNC_power_button')
+            .classList.add("noVNC_selected");
+    },
+
+    closePowerPanel() {
+        document.getElementById('noVNC_power')
+            .classList.remove("noVNC_open");
+        document.getElementById('noVNC_power_button')
+            .classList.remove("noVNC_selected");
+    },
+
+    togglePowerPanel() {
+        if (document.getElementById('noVNC_power')
+            .classList.contains("noVNC_open")) {
+            UI.closePowerPanel();
+        } else {
+            UI.openPowerPanel();
+        }
+    },
+
+    // Disable/enable power button
+    updatePowerButton() {
+        if (UI.connected &&
+            UI.rfb.capabilities.power &&
+            !UI.rfb.viewOnly) {
+            document.getElementById('noVNC_power_button')
+                .classList.remove("noVNC_hidden");
+        } else {
+            document.getElementById('noVNC_power_button')
+                .classList.add("noVNC_hidden");
+            // Close power panel if open
+            UI.closePowerPanel();
+        }
+    },
+
+/* ------^-------
+ *    /POWER
+ * ==============
+ *   CLIPBOARD
+ * ------v------*/
+
+    openClipboardPanel() {
+        UI.closeAllPanels();
+        UI.openControlbar();
+
+        document.getElementById('noVNC_clipboard')
+            .classList.add("noVNC_open");
+        document.getElementById('noVNC_clipboard_button')
+            .classList.add("noVNC_selected");
+    },
+
+    closeClipboardPanel() {
+        document.getElementById('noVNC_clipboard')
+            .classList.remove("noVNC_open");
+        document.getElementById('noVNC_clipboard_button')
+            .classList.remove("noVNC_selected");
+    },
+
+    toggleClipboardPanel() {
+        if (document.getElementById('noVNC_clipboard')
+            .classList.contains("noVNC_open")) {
+            UI.closeClipboardPanel();
+        } else {
+            UI.openClipboardPanel();
+        }
+    },
+
+    clipboardReceive(e) {
+        Log.Debug(">> UI.clipboardReceive: " + e.detail.text.substr(0, 40) + "...");
+        document.getElementById('noVNC_clipboard_text').value = e.detail.text;
+        Log.Debug("<< UI.clipboardReceive");
+    },
+
+    clipboardClear() {
+        document.getElementById('noVNC_clipboard_text').value = "";
+        UI.rfb.clipboardPasteFrom("");
+    },
+
+    clipboardSend() {
+        const text = document.getElementById('noVNC_clipboard_text').value;
+        Log.Debug(">> UI.clipboardSend: " + text.substr(0, 40) + "...");
+        UI.rfb.clipboardPasteFrom(text);
+        Log.Debug("<< UI.clipboardSend");
+    },
+
+/* ------^-------
+ *  /CLIPBOARD
+ * ==============
+ *  CONNECTION
+ * ------v------*/
+
+    openConnectPanel() {
+        document.getElementById('noVNC_connect_dlg')
+            .classList.add("noVNC_open");
+    },
+
+    closeConnectPanel() {
+        document.getElementById('noVNC_connect_dlg')
+            .classList.remove("noVNC_open");
+    },
+
+    connect(event, password) {
+
+        // Ignore when rfb already exists
+        if (typeof UI.rfb !== 'undefined') {
+            return;
+        }
+
+        const host = UI.getSetting('host');
+        const port = UI.getSetting('port');
+        const path = UI.getSetting('path');
+
+        if (typeof password === 'undefined') {
+            password = WebUtil.getConfigVar('password');
+            UI.reconnectPassword = password;
+        }
+
+        if (password === null) {
+            password = undefined;
+        }
+
+        UI.hideStatus();
+
+        if (!host) {
+            Log.Error("Can't connect when host is: " + host);
+            UI.showStatus(_("Must set host"), 'error');
+            return;
+        }
+
+        UI.closeConnectPanel();
+
+        UI.updateVisualState('connecting');
+
+        let url;
+
+        url = UI.getSetting('encrypt') ? 'wss' : 'ws';
+
+        url += '://' + host;
+        if (port) {
+            url += ':' + port;
+        }
+        url += '/' + path;
+
+        UI.rfb = new RFB(document.getElementById('noVNC_container'), url,
+                         { shared: UI.getSetting('shared'),
+                           repeaterID: UI.getSetting('repeaterID'),
+                           credentials: { password: password } });
+        UI.rfb.addEventListener("connect", UI.connectFinished);
+        UI.rfb.addEventListener("disconnect", UI.disconnectFinished);
+        UI.rfb.addEventListener("credentialsrequired", UI.credentials);
+        UI.rfb.addEventListener("securityfailure", UI.securityFailed);
+        UI.rfb.addEventListener("capabilities", UI.updatePowerButton);
+        UI.rfb.addEventListener("clipboard", UI.clipboardReceive);
+        UI.rfb.addEventListener("bell", UI.bell);
+        UI.rfb.addEventListener("desktopname", UI.updateDesktopName);
+        UI.rfb.clipViewport = UI.getSetting('view_clip');
+        UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';
+        UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';
+        UI.rfb.qualityLevel = parseInt(UI.getSetting('quality'));
+        UI.rfb.compressionLevel = parseInt(UI.getSetting('compression'));
+        UI.rfb.showDotCursor = UI.getSetting('show_dot');
+
+        UI.updateViewOnly(); // requires UI.rfb
+    },
+
+    disconnect() {
+        UI.rfb.disconnect();
+
+        UI.connected = false;
+
+        // Disable automatic reconnecting
+        UI.inhibitReconnect = true;
+
+        UI.updateVisualState('disconnecting');
+
+        // Don't display the connection settings until we're actually disconnected
+    },
+
+    reconnect() {
+        UI.reconnectCallback = null;
+
+        // if reconnect has been disabled in the meantime, do nothing.
+        if (UI.inhibitReconnect) {
+            return;
+        }
+
+        UI.connect(null, UI.reconnectPassword);
+    },
+
+    cancelReconnect() {
+        if (UI.reconnectCallback !== null) {
+            clearTimeout(UI.reconnectCallback);
+            UI.reconnectCallback = null;
+        }
+
+        UI.updateVisualState('disconnected');
+
+        UI.openControlbar();
+        UI.openConnectPanel();
+    },
+
+    connectFinished(e) {
+        UI.connected = true;
+        UI.inhibitReconnect = false;
+
+        let msg;
+        if (UI.getSetting('encrypt')) {
+            msg = _("Connected (encrypted) to ") + UI.desktopName;
+        } else {
+            msg = _("Connected (unencrypted) to ") + UI.desktopName;
+        }
+        UI.showStatus(msg);
+        UI.updateVisualState('connected');
+
+        // Do this last because it can only be used on rendered elements
+        UI.rfb.focus();
+    },
+
+    disconnectFinished(e) {
+        const wasConnected = UI.connected;
+
+        // This variable is ideally set when disconnection starts, but
+        // when the disconnection isn't clean or if it is initiated by
+        // the server, we need to do it here as well since
+        // UI.disconnect() won't be used in those cases.
+        UI.connected = false;
+
+        UI.rfb = undefined;
+
+        if (!e.detail.clean) {
+            UI.updateVisualState('disconnected');
+            if (wasConnected) {
+                UI.showStatus(_("Something went wrong, connection is closed"),
+                              'error');
+            } else {
+                UI.showStatus(_("Failed to connect to server"), 'error');
+            }
+        } else if (UI.getSetting('reconnect', false) === true && !UI.inhibitReconnect) {
+            UI.updateVisualState('reconnecting');
+
+            const delay = parseInt(UI.getSetting('reconnect_delay'));
+            UI.reconnectCallback = setTimeout(UI.reconnect, delay);
+            return;
+        } else {
+            UI.updateVisualState('disconnected');
+            UI.showStatus(_("Disconnected"), 'normal');
+        }
+
+        document.title = PAGE_TITLE;
+
+        UI.openControlbar();
+        UI.openConnectPanel();
+    },
+
+    securityFailed(e) {
+        let msg = "";
+        // On security failures we might get a string with a reason
+        // directly from the server. Note that we can't control if
+        // this string is translated or not.
+        if ('reason' in e.detail) {
+            msg = _("New connection has been rejected with reason: ") +
+                e.detail.reason;
+        } else {
+            msg = _("New connection has been rejected");
+        }
+        UI.showStatus(msg, 'error');
+    },
+
+/* ------^-------
+ *  /CONNECTION
+ * ==============
+ *   PASSWORD
+ * ------v------*/
+
+    credentials(e) {
+        // FIXME: handle more types
+
+        document.getElementById("noVNC_username_block").classList.remove("noVNC_hidden");
+        document.getElementById("noVNC_password_block").classList.remove("noVNC_hidden");
+
+        let inputFocus = "none";
+        if (e.detail.types.indexOf("username") === -1) {
+            document.getElementById("noVNC_username_block").classList.add("noVNC_hidden");
+        } else {
+            inputFocus = inputFocus === "none" ? "noVNC_username_input" : inputFocus;
+        }
+        if (e.detail.types.indexOf("password") === -1) {
+            document.getElementById("noVNC_password_block").classList.add("noVNC_hidden");
+        } else {
+            inputFocus = inputFocus === "none" ? "noVNC_password_input" : inputFocus;
+        }
+        document.getElementById('noVNC_credentials_dlg')
+            .classList.add('noVNC_open');
+
+        setTimeout(() => document
+            .getElementById(inputFocus).focus(), 100);
+
+        Log.Warn("Server asked for credentials");
+        UI.showStatus(_("Credentials are required"), "warning");
+    },
+
+    setCredentials(e) {
+        // Prevent actually submitting the form
+        e.preventDefault();
+
+        let inputElemUsername = document.getElementById('noVNC_username_input');
+        const username = inputElemUsername.value;
+
+        let inputElemPassword = document.getElementById('noVNC_password_input');
+        const password = inputElemPassword.value;
+        // Clear the input after reading the password
+        inputElemPassword.value = "";
+
+        UI.rfb.sendCredentials({ username: username, password: password });
+        UI.reconnectPassword = password;
+        document.getElementById('noVNC_credentials_dlg')
+            .classList.remove('noVNC_open');
+    },
+
+/* ------^-------
+ *  /PASSWORD
+ * ==============
+ *   FULLSCREEN
+ * ------v------*/
+
+    toggleFullscreen() {
+        if (document.fullscreenElement || // alternative standard method
+            document.mozFullScreenElement || // currently working methods
+            document.webkitFullscreenElement ||
+            document.msFullscreenElement) {
+            if (document.exitFullscreen) {
+                document.exitFullscreen();
+            } else if (document.mozCancelFullScreen) {
+                document.mozCancelFullScreen();
+            } else if (document.webkitExitFullscreen) {
+                document.webkitExitFullscreen();
+            } else if (document.msExitFullscreen) {
+                document.msExitFullscreen();
+            }
+        } else {
+            if (document.documentElement.requestFullscreen) {
+                document.documentElement.requestFullscreen();
+            } else if (document.documentElement.mozRequestFullScreen) {
+                document.documentElement.mozRequestFullScreen();
+            } else if (document.documentElement.webkitRequestFullscreen) {
+                document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
+            } else if (document.body.msRequestFullscreen) {
+                document.body.msRequestFullscreen();
+            }
+        }
+        UI.updateFullscreenButton();
+    },
+
+    updateFullscreenButton() {
+        if (document.fullscreenElement || // alternative standard method
+            document.mozFullScreenElement || // currently working methods
+            document.webkitFullscreenElement ||
+            document.msFullscreenElement ) {
+            document.getElementById('noVNC_fullscreen_button')
+                .classList.add("noVNC_selected");
+        } else {
+            document.getElementById('noVNC_fullscreen_button')
+                .classList.remove("noVNC_selected");
+        }
+    },
+
+/* ------^-------
+ *  /FULLSCREEN
+ * ==============
+ *     RESIZE
+ * ------v------*/
+
+    // Apply remote resizing or local scaling
+    applyResizeMode() {
+        if (!UI.rfb) return;
+
+        UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';
+        UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';
+    },
+
+/* ------^-------
+ *    /RESIZE
+ * ==============
+ * VIEW CLIPPING
+ * ------v------*/
+
+    // Update viewport clipping property for the connection. The normal
+    // case is to get the value from the setting. There are special cases
+    // for when the viewport is scaled or when a touch device is used.
+    updateViewClip() {
+        if (!UI.rfb) return;
+
+        const scaling = UI.getSetting('resize') === 'scale';
+
+        if (scaling) {
+            // Can't be clipping if viewport is scaled to fit
+            UI.forceSetting('view_clip', false);
+            UI.rfb.clipViewport  = false;
+        } else if (!hasScrollbarGutter) {
+            // Some platforms have scrollbars that are difficult
+            // to use in our case, so we always use our own panning
+            UI.forceSetting('view_clip', true);
+            UI.rfb.clipViewport = true;
+        } else {
+            UI.enableSetting('view_clip');
+            UI.rfb.clipViewport = UI.getSetting('view_clip');
+        }
+
+        // Changing the viewport may change the state of
+        // the dragging button
+        UI.updateViewDrag();
+    },
+
+/* ------^-------
+ * /VIEW CLIPPING
+ * ==============
+ *    VIEWDRAG
+ * ------v------*/
+
+    toggleViewDrag() {
+        if (!UI.rfb) return;
+
+        UI.rfb.dragViewport = !UI.rfb.dragViewport;
+        UI.updateViewDrag();
+    },
+
+    updateViewDrag() {
+        if (!UI.connected) return;
+
+        const viewDragButton = document.getElementById('noVNC_view_drag_button');
+
+        if (!UI.rfb.clipViewport && UI.rfb.dragViewport) {
+            // We are no longer clipping the viewport. Make sure
+            // viewport drag isn't active when it can't be used.
+            UI.rfb.dragViewport = false;
+        }
+
+        if (UI.rfb.dragViewport) {
+            viewDragButton.classList.add("noVNC_selected");
+        } else {
+            viewDragButton.classList.remove("noVNC_selected");
+        }
+
+        if (UI.rfb.clipViewport) {
+            viewDragButton.classList.remove("noVNC_hidden");
+        } else {
+            viewDragButton.classList.add("noVNC_hidden");
+        }
+    },
+
+/* ------^-------
+ *   /VIEWDRAG
+ * ==============
+ *    QUALITY
+ * ------v------*/
+
+    updateQuality() {
+        if (!UI.rfb) return;
+
+        UI.rfb.qualityLevel = parseInt(UI.getSetting('quality'));
+    },
+
+/* ------^-------
+ *   /QUALITY
+ * ==============
+ *  COMPRESSION
+ * ------v------*/
+
+    updateCompression() {
+        if (!UI.rfb) return;
+
+        UI.rfb.compressionLevel = parseInt(UI.getSetting('compression'));
+    },
+
+/* ------^-------
+ *  /COMPRESSION
+ * ==============
+ *    KEYBOARD
+ * ------v------*/
+
+    showVirtualKeyboard() {
+        if (!isTouchDevice) return;
+
+        const input = document.getElementById('noVNC_keyboardinput');
+
+        if (document.activeElement == input) return;
+
+        input.focus();
+
+        try {
+            const l = input.value.length;
+            // Move the caret to the end
+            input.setSelectionRange(l, l);
+        } catch (err) {
+            // setSelectionRange is undefined in Google Chrome
+        }
+    },
+
+    hideVirtualKeyboard() {
+        if (!isTouchDevice) return;
+
+        const input = document.getElementById('noVNC_keyboardinput');
+
+        if (document.activeElement != input) return;
+
+        input.blur();
+    },
+
+    toggleVirtualKeyboard() {
+        if (document.getElementById('noVNC_keyboard_button')
+            .classList.contains("noVNC_selected")) {
+            UI.hideVirtualKeyboard();
+        } else {
+            UI.showVirtualKeyboard();
+        }
+    },
+
+    onfocusVirtualKeyboard(event) {
+        document.getElementById('noVNC_keyboard_button')
+            .classList.add("noVNC_selected");
+        if (UI.rfb) {
+            UI.rfb.focusOnClick = false;
+        }
+    },
+
+    onblurVirtualKeyboard(event) {
+        document.getElementById('noVNC_keyboard_button')
+            .classList.remove("noVNC_selected");
+        if (UI.rfb) {
+            UI.rfb.focusOnClick = true;
+        }
+    },
+
+    keepVirtualKeyboard(event) {
+        const input = document.getElementById('noVNC_keyboardinput');
+
+        // Only prevent focus change if the virtual keyboard is active
+        if (document.activeElement != input) {
+            return;
+        }
+
+        // Only allow focus to move to other elements that need
+        // focus to function properly
+        if (event.target.form !== undefined) {
+            switch (event.target.type) {
+                case 'text':
+                case 'email':
+                case 'search':
+                case 'password':
+                case 'tel':
+                case 'url':
+                case 'textarea':
+                case 'select-one':
+                case 'select-multiple':
+                    return;
+            }
+        }
+
+        event.preventDefault();
+    },
+
+    keyboardinputReset() {
+        const kbi = document.getElementById('noVNC_keyboardinput');
+        kbi.value = new Array(UI.defaultKeyboardinputLen).join("_");
+        UI.lastKeyboardinput = kbi.value;
+    },
+
+    keyEvent(keysym, code, down) {
+        if (!UI.rfb) return;
+
+        UI.rfb.sendKey(keysym, code, down);
+    },
+
+    // When normal keyboard events are left uncought, use the input events from
+    // the keyboardinput element instead and generate the corresponding key events.
+    // This code is required since some browsers on Android are inconsistent in
+    // sending keyCodes in the normal keyboard events when using on screen keyboards.
+    keyInput(event) {
+
+        if (!UI.rfb) return;
+
+        const newValue = event.target.value;
+
+        if (!UI.lastKeyboardinput) {
+            UI.keyboardinputReset();
+        }
+        const oldValue = UI.lastKeyboardinput;
+
+        let newLen;
+        try {
+            // Try to check caret position since whitespace at the end
+            // will not be considered by value.length in some browsers
+            newLen = Math.max(event.target.selectionStart, newValue.length);
+        } catch (err) {
+            // selectionStart is undefined in Google Chrome
+            newLen = newValue.length;
+        }
+        const oldLen = oldValue.length;
+
+        let inputs = newLen - oldLen;
+        let backspaces = inputs < 0 ? -inputs : 0;
+
+        // Compare the old string with the new to account for
+        // text-corrections or other input that modify existing text
+        for (let i = 0; i < Math.min(oldLen, newLen); i++) {
+            if (newValue.charAt(i) != oldValue.charAt(i)) {
+                inputs = newLen - i;
+                backspaces = oldLen - i;
+                break;
+            }
+        }
+
+        // Send the key events
+        for (let i = 0; i < backspaces; i++) {
+            UI.rfb.sendKey(KeyTable.XK_BackSpace, "Backspace");
+        }
+        for (let i = newLen - inputs; i < newLen; i++) {
+            UI.rfb.sendKey(keysyms.lookup(newValue.charCodeAt(i)));
+        }
+
+        // Control the text content length in the keyboardinput element
+        if (newLen > 2 * UI.defaultKeyboardinputLen) {
+            UI.keyboardinputReset();
+        } else if (newLen < 1) {
+            // There always have to be some text in the keyboardinput
+            // element with which backspace can interact.
+            UI.keyboardinputReset();
+            // This sometimes causes the keyboard to disappear for a second
+            // but it is required for the android keyboard to recognize that
+            // text has been added to the field
+            event.target.blur();
+            // This has to be ran outside of the input handler in order to work
+            setTimeout(event.target.focus.bind(event.target), 0);
+        } else {
+            UI.lastKeyboardinput = newValue;
+        }
+    },
+
+/* ------^-------
+ *   /KEYBOARD
+ * ==============
+ *   EXTRA KEYS
+ * ------v------*/
+
+    openExtraKeys() {
+        UI.closeAllPanels();
+        UI.openControlbar();
+
+        document.getElementById('noVNC_modifiers')
+            .classList.add("noVNC_open");
+        document.getElementById('noVNC_toggle_extra_keys_button')
+            .classList.add("noVNC_selected");
+    },
+
+    closeExtraKeys() {
+        document.getElementById('noVNC_modifiers')
+            .classList.remove("noVNC_open");
+        document.getElementById('noVNC_toggle_extra_keys_button')
+            .classList.remove("noVNC_selected");
+    },
+
+    toggleExtraKeys() {
+        if (document.getElementById('noVNC_modifiers')
+            .classList.contains("noVNC_open")) {
+            UI.closeExtraKeys();
+        } else  {
+            UI.openExtraKeys();
+        }
+    },
+
+    sendEsc() {
+        UI.sendKey(KeyTable.XK_Escape, "Escape");
+    },
+
+    sendTab() {
+        UI.sendKey(KeyTable.XK_Tab, "Tab");
+    },
+
+    toggleCtrl() {
+        const btn = document.getElementById('noVNC_toggle_ctrl_button');
+        if (btn.classList.contains("noVNC_selected")) {
+            UI.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
+            btn.classList.remove("noVNC_selected");
+        } else {
+            UI.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
+            btn.classList.add("noVNC_selected");
+        }
+    },
+
+    toggleWindows() {
+        const btn = document.getElementById('noVNC_toggle_windows_button');
+        if (btn.classList.contains("noVNC_selected")) {
+            UI.sendKey(KeyTable.XK_Super_L, "MetaLeft", false);
+            btn.classList.remove("noVNC_selected");
+        } else {
+            UI.sendKey(KeyTable.XK_Super_L, "MetaLeft", true);
+            btn.classList.add("noVNC_selected");
+        }
+    },
+
+    toggleAlt() {
+        const btn = document.getElementById('noVNC_toggle_alt_button');
+        if (btn.classList.contains("noVNC_selected")) {
+            UI.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
+            btn.classList.remove("noVNC_selected");
+        } else {
+            UI.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);
+            btn.classList.add("noVNC_selected");
+        }
+    },
+
+    sendCtrlAltDel() {
+        UI.rfb.sendCtrlAltDel();
+        // See below
+        UI.rfb.focus();
+        UI.idleControlbar();
+    },
+
+    sendKey(keysym, code, down) {
+        UI.rfb.sendKey(keysym, code, down);
+
+        // Move focus to the screen in order to be able to use the
+        // keyboard right after these extra keys.
+        // The exception is when a virtual keyboard is used, because
+        // if we focus the screen the virtual keyboard would be closed.
+        // In this case we focus our special virtual keyboard input
+        // element instead.
+        if (document.getElementById('noVNC_keyboard_button')
+            .classList.contains("noVNC_selected")) {
+            document.getElementById('noVNC_keyboardinput').focus();
+        } else {
+            UI.rfb.focus();
+        }
+        // fade out the controlbar to highlight that
+        // the focus has been moved to the screen
+        UI.idleControlbar();
+    },
+
+/* ------^-------
+ *   /EXTRA KEYS
+ * ==============
+ *     MISC
+ * ------v------*/
+
+    updateViewOnly() {
+        if (!UI.rfb) return;
+        UI.rfb.viewOnly = UI.getSetting('view_only');
+
+        // Hide input related buttons in view only mode
+        if (UI.rfb.viewOnly) {
+            document.getElementById('noVNC_keyboard_button')
+                .classList.add('noVNC_hidden');
+            document.getElementById('noVNC_toggle_extra_keys_button')
+                .classList.add('noVNC_hidden');
+            document.getElementById('noVNC_clipboard_button')
+                .classList.add('noVNC_hidden');
+        } else {
+            document.getElementById('noVNC_keyboard_button')
+                .classList.remove('noVNC_hidden');
+            document.getElementById('noVNC_toggle_extra_keys_button')
+                .classList.remove('noVNC_hidden');
+            document.getElementById('noVNC_clipboard_button')
+                .classList.remove('noVNC_hidden');
+        }
+    },
+
+    updateShowDotCursor() {
+        if (!UI.rfb) return;
+        UI.rfb.showDotCursor = UI.getSetting('show_dot');
+    },
+
+    updateLogging() {
+        WebUtil.initLogging(UI.getSetting('logging'));
+    },
+
+    updateDesktopName(e) {
+        UI.desktopName = e.detail.name;
+        // Display the desktop name in the document title
+        document.title = e.detail.name + " - " + PAGE_TITLE;
+    },
+
+    bell(e) {
+        if (WebUtil.getConfigVar('bell', 'on') === 'on') {
+            const promise = document.getElementById('noVNC_bell').play();
+            // The standards disagree on the return value here
+            if (promise) {
+                promise.catch((e) => {
+                    if (e.name === "NotAllowedError") {
+                        // Ignore when the browser doesn't let us play audio.
+                        // It is common that the browsers require audio to be
+                        // initiated from a user action.
+                    } else {
+                        Log.Error("Unable to play bell: " + e);
+                    }
+                });
+            }
+        }
+    },
+
+    //Helper to add options to dropdown.
+    addOption(selectbox, text, value) {
+        const optn = document.createElement("OPTION");
+        optn.text = text;
+        optn.value = value;
+        selectbox.options.add(optn);
+    },
+
+/* ------^-------
+ *    /MISC
+ * ==============
+ */
+};
+
+// Set up translations
+const LINGUAS = ["cs", "de", "el", "es", "fr", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"];
+l10n.setup(LINGUAS);
+if (l10n.language === "en" || l10n.dictionary !== undefined) {
+    UI.prime();
+} else {
+    fetch('app/locale/' + l10n.language + '.json')
+        .then((response) => {
+            if (!response.ok) {
+                throw Error("" + response.status + " " + response.statusText);
+            }
+            return response.json();
+        })
+        .then((translations) => { l10n.dictionary = translations; })
+        .catch(err => Log.Error("Failed to load translations: " + err))
+        .then(UI.prime);
+}
+
+export default UI;
diff --git a/app/src/main/assets/novnc/app/webutil.js b/app/src/main/assets/novnc/app/webutil.js
new file mode 100644
index 00000000..d42b7f25
--- /dev/null
+++ b/app/src/main/assets/novnc/app/webutil.js
@@ -0,0 +1,186 @@
+/*
+ * 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.
+ */
+
+import { initLogging as mainInitLogging } from '../core/util/logging.js';
+
+// init log level reading the logging HTTP param
+export function initLogging(level) {
+    "use strict";
+    if (typeof level !== "undefined") {
+        mainInitLogging(level);
+    } else {
+        const param = document.location.href.match(/logging=([A-Za-z0-9._-]*)/);
+        mainInitLogging(param || undefined);
+    }
+}
+
+// Read a query string variable
+// A URL with a query parameter can look like this (But will most probably get logged on the http server):
+// https://www.example.com?myqueryparam=myvalue
+//
+// For privacy (Using a hastag #, the parameters will not be sent to the server)
+// the url can be requested in the following way:
+// https://www.example.com#myqueryparam=myvalue&password=secreatvalue
+//
+// Even Mixing public and non public parameters will work:
+// https://www.example.com?nonsecretparam=example.com#password=secreatvalue
+export function getQueryVar(name, defVal) {
+    "use strict";
+    const re = new RegExp('.*[?&]' + name + '=([^&#]*)'),
+        match = ''.concat(document.location.href, window.location.hash).match(re);
+    if (typeof defVal === 'undefined') { defVal = null; }
+
+    if (match) {
+        return decodeURIComponent(match[1]);
+    }
+
+    return defVal;
+}
+
+// Read a hash fragment variable
+export function getHashVar(name, defVal) {
+    "use strict";
+    const re = new RegExp('.*[&#]' + name + '=([^&]*)'),
+        match = document.location.hash.match(re);
+    if (typeof defVal === 'undefined') { defVal = null; }
+
+    if (match) {
+        return decodeURIComponent(match[1]);
+    }
+
+    return defVal;
+}
+
+// Read a variable from the fragment or the query string
+// Fragment takes precedence
+export function getConfigVar(name, defVal) {
+    "use strict";
+    const val = getHashVar(name);
+
+    if (val === null) {
+        return getQueryVar(name, defVal);
+    }
+
+    return val;
+}
+
+/*
+ * Cookie handling. Dervied from: http://www.quirksmode.org/js/cookies.html
+ */
+
+// No days means only for this browser session
+export function createCookie(name, value, days) {
+    "use strict";
+    let date, expires;
+    if (days) {
+        date = new Date();
+        date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
+        expires = "; expires=" + date.toGMTString();
+    } else {
+        expires = "";
+    }
+
+    let secure;
+    if (document.location.protocol === "https:") {
+        secure = "; secure";
+    } else {
+        secure = "";
+    }
+    document.cookie = name + "=" + value + expires + "; path=/" + secure;
+}
+
+export function readCookie(name, defaultValue) {
+    "use strict";
+    const nameEQ = name + "=";
+    const ca = document.cookie.split(';');
+
+    for (let i = 0; i < ca.length; i += 1) {
+        let c = ca[i];
+        while (c.charAt(0) === ' ') {
+            c = c.substring(1, c.length);
+        }
+        if (c.indexOf(nameEQ) === 0) {
+            return c.substring(nameEQ.length, c.length);
+        }
+    }
+
+    return (typeof defaultValue !== 'undefined') ? defaultValue : null;
+}
+
+export function eraseCookie(name) {
+    "use strict";
+    createCookie(name, "", -1);
+}
+
+/*
+ * Setting handling.
+ */
+
+let settings = {};
+
+export function initSettings() {
+    if (!window.chrome || !window.chrome.storage) {
+        settings = {};
+        return Promise.resolve();
+    }
+
+    return new Promise(resolve => window.chrome.storage.sync.get(resolve))
+        .then((cfg) => { settings = cfg; });
+}
+
+// Update the settings cache, but do not write to permanent storage
+export function setSetting(name, value) {
+    settings[name] = value;
+}
+
+// No days means only for this browser session
+export function writeSetting(name, value) {
+    "use strict";
+    if (settings[name] === value) return;
+    settings[name] = value;
+    if (window.chrome && window.chrome.storage) {
+        window.chrome.storage.sync.set(settings);
+    } else {
+        localStorage.setItem(name, value);
+    }
+}
+
+export function readSetting(name, defaultValue) {
+    "use strict";
+    let value;
+    if ((name in settings) || (window.chrome && window.chrome.storage)) {
+        value = settings[name];
+    } else {
+        value = localStorage.getItem(name);
+        settings[name] = value;
+    }
+    if (typeof value === "undefined") {
+        value = null;
+    }
+
+    if (value === null && typeof defaultValue !== "undefined") {
+        return defaultValue;
+    }
+
+    return value;
+}
+
+export function eraseSetting(name) {
+    "use strict";
+    // Deleting here means that next time the setting is read when using local
+    // storage, it will be pulled from local storage again.
+    // If the setting in local storage is changed (e.g. in another tab)
+    // between this delete and the next read, it could lead to an unexpected
+    // value change.
+    delete settings[name];
+    if (window.chrome && window.chrome.storage) {
+        window.chrome.storage.sync.remove(name);
+    } else {
+        localStorage.removeItem(name);
+    }
+}
diff --git a/app/src/main/assets/novnc/core/base64.js b/app/src/main/assets/novnc/core/base64.js
new file mode 100644
index 00000000..db572c2d
--- /dev/null
+++ b/app/src/main/assets/novnc/core/base64.js
@@ -0,0 +1,104 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// From: http://hg.mozilla.org/mozilla-central/raw-file/ec10630b1a54/js/src/devtools/jint/sunspider/string-base64.js
+
+import * as Log from './util/logging.js';
+
+export default {
+    /* Convert data (an array of integers) to a Base64 string. */
+    toBase64Table: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split(''),
+    base64Pad: '=',
+
+    encode(data) {
+        "use strict";
+        let result = '';
+        const length = data.length;
+        const lengthpad = (length % 3);
+        // Convert every three bytes to 4 ascii characters.
+
+        for (let i = 0; i < (length - 2); i += 3) {
+            result += this.toBase64Table[data[i] >> 2];
+            result += this.toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)];
+            result += this.toBase64Table[((data[i + 1] & 0x0f) << 2) + (data[i + 2] >> 6)];
+            result += this.toBase64Table[data[i + 2] & 0x3f];
+        }
+
+        // Convert the remaining 1 or 2 bytes, pad out to 4 characters.
+        const j = length - lengthpad;
+        if (lengthpad === 2) {
+            result += this.toBase64Table[data[j] >> 2];
+            result += this.toBase64Table[((data[j] & 0x03) << 4) + (data[j + 1] >> 4)];
+            result += this.toBase64Table[(data[j + 1] & 0x0f) << 2];
+            result += this.toBase64Table[64];
+        } else if (lengthpad === 1) {
+            result += this.toBase64Table[data[j] >> 2];
+            result += this.toBase64Table[(data[j] & 0x03) << 4];
+            result += this.toBase64Table[64];
+            result += this.toBase64Table[64];
+        }
+
+        return result;
+    },
+
+    /* Convert Base64 data to a string */
+    /* eslint-disable comma-spacing */
+    toBinaryTable: [
+        -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+        -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
+        -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63,
+        52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1,
+        -1, 0, 1, 2,  3, 4, 5, 6,  7, 8, 9,10, 11,12,13,14,
+        15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
+        -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
+        41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
+    ],
+    /* eslint-enable comma-spacing */
+
+    decode(data, offset = 0) {
+        let dataLength = data.indexOf('=') - offset;
+        if (dataLength < 0) { dataLength = data.length - offset; }
+
+        /* Every four characters is 3 resulting numbers */
+        const resultLength = (dataLength >> 2) * 3 + Math.floor((dataLength % 4) / 1.5);
+        const result = new Array(resultLength);
+
+        // Convert one by one.
+
+        let leftbits = 0; // number of bits decoded, but yet to be appended
+        let leftdata = 0; // bits decoded, but yet to be appended
+        for (let idx = 0, i = offset; i < data.length; i++) {
+            const c = this.toBinaryTable[data.charCodeAt(i) & 0x7f];
+            const padding = (data.charAt(i) === this.base64Pad);
+            // Skip illegal characters and whitespace
+            if (c === -1) {
+                Log.Error("Illegal character code " + data.charCodeAt(i) + " at position " + i);
+                continue;
+            }
+
+            // Collect data into leftdata, update bitcount
+            leftdata = (leftdata << 6) | c;
+            leftbits += 6;
+
+            // If we have 8 or more bits, append 8 bits to the result
+            if (leftbits >= 8) {
+                leftbits -= 8;
+                // Append if not padding.
+                if (!padding) {
+                    result[idx++] = (leftdata >> leftbits) & 0xff;
+                }
+                leftdata &= (1 << leftbits) - 1;
+            }
+        }
+
+        // If there are any bits left, the base64 string was corrupted
+        if (leftbits) {
+            const err = new Error('Corrupted base64 string');
+            err.name = 'Base64-Error';
+            throw err;
+        }
+
+        return result;
+    }
+}; /* End of Base64 namespace */
diff --git a/app/src/main/assets/novnc/core/decoders/copyrect.js b/app/src/main/assets/novnc/core/decoders/copyrect.js
new file mode 100644
index 00000000..9e6391a1
--- /dev/null
+++ b/app/src/main/assets/novnc/core/decoders/copyrect.js
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ *
+ */
+
+export default class CopyRectDecoder {
+    decodeRect(x, y, width, height, sock, display, depth) {
+        if (sock.rQwait("COPYRECT", 4)) {
+            return false;
+        }
+
+        let deltaX = sock.rQshift16();
+        let deltaY = sock.rQshift16();
+
+        if ((width === 0) || (height === 0)) {
+            return true;
+        }
+
+        display.copyImage(deltaX, deltaY, x, y, width, height);
+
+        return true;
+    }
+}
diff --git a/app/src/main/assets/novnc/core/decoders/hextile.js b/app/src/main/assets/novnc/core/decoders/hextile.js
new file mode 100644
index 00000000..ac21eff0
--- /dev/null
+++ b/app/src/main/assets/novnc/core/decoders/hextile.js
@@ -0,0 +1,191 @@
+/*
+ * 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.
+ *
+ */
+
+import * as Log from '../util/logging.js';
+
+export default class HextileDecoder {
+    constructor() {
+        this._tiles = 0;
+        this._lastsubencoding = 0;
+        this._tileBuffer = new Uint8Array(16 * 16 * 4);
+    }
+
+    decodeRect(x, y, width, height, sock, display, depth) {
+        if (this._tiles === 0) {
+            this._tilesX = Math.ceil(width / 16);
+            this._tilesY = Math.ceil(height / 16);
+            this._totalTiles = this._tilesX * this._tilesY;
+            this._tiles = this._totalTiles;
+        }
+
+        while (this._tiles > 0) {
+            let bytes = 1;
+
+            if (sock.rQwait("HEXTILE", bytes)) {
+                return false;
+            }
+
+            let rQ = sock.rQ;
+            let rQi = sock.rQi;
+
+            let subencoding = rQ[rQi];  // Peek
+            if (subencoding > 30) {  // Raw
+                throw new Error("Illegal hextile subencoding (subencoding: " +
+                            subencoding + ")");
+            }
+
+            const currTile = this._totalTiles - this._tiles;
+            const tileX = currTile % this._tilesX;
+            const tileY = Math.floor(currTile / this._tilesX);
+            const tx = x + tileX * 16;
+            const ty = y + tileY * 16;
+            const tw = Math.min(16, (x + width) - tx);
+            const th = Math.min(16, (y + height) - ty);
+
+            // Figure out how much we are expecting
+            if (subencoding & 0x01) {  // Raw
+                bytes += tw * th * 4;
+            } else {
+                if (subencoding & 0x02) {  // Background
+                    bytes += 4;
+                }
+                if (subencoding & 0x04) {  // Foreground
+                    bytes += 4;
+                }
+                if (subencoding & 0x08) {  // AnySubrects
+                    bytes++;  // Since we aren't shifting it off
+
+                    if (sock.rQwait("HEXTILE", bytes)) {
+                        return false;
+                    }
+
+                    let subrects = rQ[rQi + bytes - 1];  // Peek
+                    if (subencoding & 0x10) {  // SubrectsColoured
+                        bytes += subrects * (4 + 2);
+                    } else {
+                        bytes += subrects * 2;
+                    }
+                }
+            }
+
+            if (sock.rQwait("HEXTILE", bytes)) {
+                return false;
+            }
+
+            // We know the encoding and have a whole tile
+            rQi++;
+            if (subencoding === 0) {
+                if (this._lastsubencoding & 0x01) {
+                    // Weird: ignore blanks are RAW
+                    Log.Debug("     Ignoring blank after RAW");
+                } else {
+                    display.fillRect(tx, ty, tw, th, this._background);
+                }
+            } else if (subencoding & 0x01) {  // Raw
+                let pixels = tw * th;
+                // Max sure the image is fully opaque
+                for (let i = 0;i <  pixels;i++) {
+                    rQ[rQi + i * 4 + 3] = 255;
+                }
+                display.blitImage(tx, ty, tw, th, rQ, rQi);
+                rQi += bytes - 1;
+            } else {
+                if (subencoding & 0x02) {  // Background
+                    this._background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
+                    rQi += 4;
+                }
+                if (subencoding & 0x04) {  // Foreground
+                    this._foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
+                    rQi += 4;
+                }
+
+                this._startTile(tx, ty, tw, th, this._background);
+                if (subencoding & 0x08) {  // AnySubrects
+                    let subrects = rQ[rQi];
+                    rQi++;
+
+                    for (let s = 0; s < subrects; s++) {
+                        let color;
+                        if (subencoding & 0x10) {  // SubrectsColoured
+                            color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
+                            rQi += 4;
+                        } else {
+                            color = this._foreground;
+                        }
+                        const xy = rQ[rQi];
+                        rQi++;
+                        const sx = (xy >> 4);
+                        const sy = (xy & 0x0f);
+
+                        const wh = rQ[rQi];
+                        rQi++;
+                        const sw = (wh >> 4) + 1;
+                        const sh = (wh & 0x0f) + 1;
+
+                        this._subTile(sx, sy, sw, sh, color);
+                    }
+                }
+                this._finishTile(display);
+            }
+            sock.rQi = rQi;
+            this._lastsubencoding = subencoding;
+            this._tiles--;
+        }
+
+        return true;
+    }
+
+    // start updating a tile
+    _startTile(x, y, width, height, color) {
+        this._tileX = x;
+        this._tileY = y;
+        this._tileW = width;
+        this._tileH = height;
+
+        const red = color[0];
+        const green = color[1];
+        const blue = color[2];
+
+        const data = this._tileBuffer;
+        for (let i = 0; i < width * height * 4; i += 4) {
+            data[i]     = red;
+            data[i + 1] = green;
+            data[i + 2] = blue;
+            data[i + 3] = 255;
+        }
+    }
+
+    // update sub-rectangle of the current tile
+    _subTile(x, y, w, h, color) {
+        const red = color[0];
+        const green = color[1];
+        const blue = color[2];
+        const xend = x + w;
+        const yend = y + h;
+
+        const data = this._tileBuffer;
+        const width = this._tileW;
+        for (let j = y; j < yend; j++) {
+            for (let i = x; i < xend; i++) {
+                const p = (i + (j * width)) * 4;
+                data[p]     = red;
+                data[p + 1] = green;
+                data[p + 2] = blue;
+                data[p + 3] = 255;
+            }
+        }
+    }
+
+    // draw the current tile to the screen
+    _finishTile(display) {
+        display.blitImage(this._tileX, this._tileY,
+                          this._tileW, this._tileH,
+                          this._tileBuffer, 0);
+    }
+}
diff --git a/app/src/main/assets/novnc/core/decoders/raw.js b/app/src/main/assets/novnc/core/decoders/raw.js
new file mode 100644
index 00000000..e8ea178e
--- /dev/null
+++ b/app/src/main/assets/novnc/core/decoders/raw.js
@@ -0,0 +1,66 @@
+/*
+ * 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.
+ *
+ */
+
+export default class RawDecoder {
+    constructor() {
+        this._lines = 0;
+    }
+
+    decodeRect(x, y, width, height, sock, display, depth) {
+        if ((width === 0) || (height === 0)) {
+            return true;
+        }
+
+        if (this._lines === 0) {
+            this._lines = height;
+        }
+
+        const pixelSize = depth == 8 ? 1 : 4;
+        const bytesPerLine = width * pixelSize;
+
+        if (sock.rQwait("RAW", bytesPerLine)) {
+            return false;
+        }
+
+        const curY = y + (height - this._lines);
+        const currHeight = Math.min(this._lines,
+                                    Math.floor(sock.rQlen / bytesPerLine));
+        const pixels = width * currHeight;
+
+        let data = sock.rQ;
+        let index = sock.rQi;
+
+        // Convert data if needed
+        if (depth == 8) {
+            const newdata = new Uint8Array(pixels * 4);
+            for (let i = 0; i < pixels; i++) {
+                newdata[i * 4 + 0] = ((data[index + i] >> 0) & 0x3) * 255 / 3;
+                newdata[i * 4 + 1] = ((data[index + i] >> 2) & 0x3) * 255 / 3;
+                newdata[i * 4 + 2] = ((data[index + i] >> 4) & 0x3) * 255 / 3;
+                newdata[i * 4 + 3] = 255;
+            }
+            data = newdata;
+            index = 0;
+        }
+
+        // Max sure the image is fully opaque
+        for (let i = 0; i < pixels; i++) {
+            data[i * 4 + 3] = 255;
+        }
+
+        display.blitImage(x, curY, width, currHeight, data, index);
+        sock.rQskipBytes(currHeight * bytesPerLine);
+        this._lines -= currHeight;
+        if (this._lines > 0) {
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/app/src/main/assets/novnc/core/decoders/rre.js b/app/src/main/assets/novnc/core/decoders/rre.js
new file mode 100644
index 00000000..6219369d
--- /dev/null
+++ b/app/src/main/assets/novnc/core/decoders/rre.js
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ *
+ */
+
+export default class RREDecoder {
+    constructor() {
+        this._subrects = 0;
+    }
+
+    decodeRect(x, y, width, height, sock, display, depth) {
+        if (this._subrects === 0) {
+            if (sock.rQwait("RRE", 4 + 4)) {
+                return false;
+            }
+
+            this._subrects = sock.rQshift32();
+
+            let color = sock.rQshiftBytes(4);  // Background
+            display.fillRect(x, y, width, height, color);
+        }
+
+        while (this._subrects > 0) {
+            if (sock.rQwait("RRE", 4 + 8)) {
+                return false;
+            }
+
+            let color = sock.rQshiftBytes(4);
+            let sx = sock.rQshift16();
+            let sy = sock.rQshift16();
+            let swidth = sock.rQshift16();
+            let sheight = sock.rQshift16();
+            display.fillRect(x + sx, y + sy, swidth, sheight, color);
+
+            this._subrects--;
+        }
+
+        return true;
+    }
+}
diff --git a/app/src/main/assets/novnc/core/decoders/tight.js b/app/src/main/assets/novnc/core/decoders/tight.js
new file mode 100644
index 00000000..7952707c
--- /dev/null
+++ b/app/src/main/assets/novnc/core/decoders/tight.js
@@ -0,0 +1,331 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2019 The noVNC Authors
+ * (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca)
+ * Licensed under MPL 2.0 (see LICENSE.txt)
+ *
+ * See README.md for usage and integration instructions.
+ *
+ */
+
+import * as Log from '../util/logging.js';
+import Inflator from "../inflator.js";
+
+export default class TightDecoder {
+    constructor() {
+        this._ctl = null;
+        this._filter = null;
+        this._numColors = 0;
+        this._palette = new Uint8Array(1024);  // 256 * 4 (max palette size * max bytes-per-pixel)
+        this._len = 0;
+
+        this._zlibs = [];
+        for (let i = 0; i < 4; i++) {
+            this._zlibs[i] = new Inflator();
+        }
+    }
+
+    decodeRect(x, y, width, height, sock, display, depth) {
+        if (this._ctl === null) {
+            if (sock.rQwait("TIGHT compression-control", 1)) {
+                return false;
+            }
+
+            this._ctl = sock.rQshift8();
+
+            // Reset streams if the server requests it
+            for (let i = 0; i < 4; i++) {
+                if ((this._ctl >> i) & 1) {
+                    this._zlibs[i].reset();
+                    Log.Info("Reset zlib stream " + i);
+                }
+            }
+
+            // Figure out filter
+            this._ctl = this._ctl >> 4;
+        }
+
+        let ret;
+
+        if (this._ctl === 0x08) {
+            ret = this._fillRect(x, y, width, height,
+                                 sock, display, depth);
+        } else if (this._ctl === 0x09) {
+            ret = this._jpegRect(x, y, width, height,
+                                 sock, display, depth);
+        } else if (this._ctl === 0x0A) {
+            ret = this._pngRect(x, y, width, height,
+                                sock, display, depth);
+        } else if ((this._ctl & 0x08) == 0) {
+            ret = this._basicRect(this._ctl, x, y, width, height,
+                                  sock, display, depth);
+        } else {
+            throw new Error("Illegal tight compression received (ctl: " +
+                                   this._ctl + ")");
+        }
+
+        if (ret) {
+            this._ctl = null;
+        }
+
+        return ret;
+    }
+
+    _fillRect(x, y, width, height, sock, display, depth) {
+        if (sock.rQwait("TIGHT", 3)) {
+            return false;
+        }
+
+        const rQi = sock.rQi;
+        const rQ = sock.rQ;
+
+        display.fillRect(x, y, width, height,
+                         [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2]], false);
+        sock.rQskipBytes(3);
+
+        return true;
+    }
+
+    _jpegRect(x, y, width, height, sock, display, depth) {
+        let data = this._readData(sock);
+        if (data === null) {
+            return false;
+        }
+
+        display.imageRect(x, y, width, height, "image/jpeg", data);
+
+        return true;
+    }
+
+    _pngRect(x, y, width, height, sock, display, depth) {
+        throw new Error("PNG received in standard Tight rect");
+    }
+
+    _basicRect(ctl, x, y, width, height, sock, display, depth) {
+        if (this._filter === null) {
+            if (ctl & 0x4) {
+                if (sock.rQwait("TIGHT", 1)) {
+                    return false;
+                }
+
+                this._filter = sock.rQshift8();
+            } else {
+                // Implicit CopyFilter
+                this._filter = 0;
+            }
+        }
+
+        let streamId = ctl & 0x3;
+
+        let ret;
+
+        switch (this._filter) {
+            case 0: // CopyFilter
+                ret = this._copyFilter(streamId, x, y, width, height,
+                                       sock, display, depth);
+                break;
+            case 1: // PaletteFilter
+                ret = this._paletteFilter(streamId, x, y, width, height,
+                                          sock, display, depth);
+                break;
+            case 2: // GradientFilter
+                ret = this._gradientFilter(streamId, x, y, width, height,
+                                           sock, display, depth);
+                break;
+            default:
+                throw new Error("Illegal tight filter received (ctl: " +
+                                       this._filter + ")");
+        }
+
+        if (ret) {
+            this._filter = null;
+        }
+
+        return ret;
+    }
+
+    _copyFilter(streamId, x, y, width, height, sock, display, depth) {
+        const uncompressedSize = width * height * 3;
+        let data;
+
+        if (uncompressedSize === 0) {
+            return true;
+        }
+
+        if (uncompressedSize < 12) {
+            if (sock.rQwait("TIGHT", uncompressedSize)) {
+                return false;
+            }
+
+            data = sock.rQshiftBytes(uncompressedSize);
+        } else {
+            data = this._readData(sock);
+            if (data === null) {
+                return false;
+            }
+
+            this._zlibs[streamId].setInput(data);
+            data = this._zlibs[streamId].inflate(uncompressedSize);
+            this._zlibs[streamId].setInput(null);
+        }
+
+        let rgbx = new Uint8Array(width * height * 4);
+        for (let i = 0, j = 0; i < width * height * 4; i += 4, j += 3) {
+            rgbx[i]     = data[j];
+            rgbx[i + 1] = data[j + 1];
+            rgbx[i + 2] = data[j + 2];
+            rgbx[i + 3] = 255;  // Alpha
+        }
+
+        display.blitImage(x, y, width, height, rgbx, 0, false);
+
+        return true;
+    }
+
+    _paletteFilter(streamId, x, y, width, height, sock, display, depth) {
+        if (this._numColors === 0) {
+            if (sock.rQwait("TIGHT palette", 1)) {
+                return false;
+            }
+
+            const numColors = sock.rQpeek8() + 1;
+            const paletteSize = numColors * 3;
+
+            if (sock.rQwait("TIGHT palette", 1 + paletteSize)) {
+                return false;
+            }
+
+            this._numColors = numColors;
+            sock.rQskipBytes(1);
+
+            sock.rQshiftTo(this._palette, paletteSize);
+        }
+
+        const bpp = (this._numColors <= 2) ? 1 : 8;
+        const rowSize = Math.floor((width * bpp + 7) / 8);
+        const uncompressedSize = rowSize * height;
+
+        let data;
+
+        if (uncompressedSize === 0) {
+            return true;
+        }
+
+        if (uncompressedSize < 12) {
+            if (sock.rQwait("TIGHT", uncompressedSize)) {
+                return false;
+            }
+
+            data = sock.rQshiftBytes(uncompressedSize);
+        } else {
+            data = this._readData(sock);
+            if (data === null) {
+                return false;
+            }
+
+            this._zlibs[streamId].setInput(data);
+            data = this._zlibs[streamId].inflate(uncompressedSize);
+            this._zlibs[streamId].setInput(null);
+        }
+
+        // Convert indexed (palette based) image data to RGB
+        if (this._numColors == 2) {
+            this._monoRect(x, y, width, height, data, this._palette, display);
+        } else {
+            this._paletteRect(x, y, width, height, data, this._palette, display);
+        }
+
+        this._numColors = 0;
+
+        return true;
+    }
+
+    _monoRect(x, y, width, height, data, palette, display) {
+        // Convert indexed (palette based) image data to RGB
+        // TODO: reduce number of calculations inside loop
+        const dest = this._getScratchBuffer(width * height * 4);
+        const w = Math.floor((width + 7) / 8);
+        const w1 = Math.floor(width / 8);
+
+        for (let y = 0; y < height; y++) {
+            let dp, sp, x;
+            for (x = 0; x < w1; x++) {
+                for (let b = 7; b >= 0; b--) {
+                    dp = (y * width + x * 8 + 7 - b) * 4;
+                    sp = (data[y * w + x] >> b & 1) * 3;
+                    dest[dp]     = palette[sp];
+                    dest[dp + 1] = palette[sp + 1];
+                    dest[dp + 2] = palette[sp + 2];
+                    dest[dp + 3] = 255;
+                }
+            }
+
+            for (let b = 7; b >= 8 - width % 8; b--) {
+                dp = (y * width + x * 8 + 7 - b) * 4;
+                sp = (data[y * w + x] >> b & 1) * 3;
+                dest[dp]     = palette[sp];
+                dest[dp + 1] = palette[sp + 1];
+                dest[dp + 2] = palette[sp + 2];
+                dest[dp + 3] = 255;
+            }
+        }
+
+        display.blitImage(x, y, width, height, dest, 0, false);
+    }
+
+    _paletteRect(x, y, width, height, data, palette, display) {
+        // Convert indexed (palette based) image data to RGB
+        const dest = this._getScratchBuffer(width * height * 4);
+        const total = width * height * 4;
+        for (let i = 0, j = 0; i < total; i += 4, j++) {
+            const sp = data[j] * 3;
+            dest[i]     = palette[sp];
+            dest[i + 1] = palette[sp + 1];
+            dest[i + 2] = palette[sp + 2];
+            dest[i + 3] = 255;
+        }
+
+        display.blitImage(x, y, width, height, dest, 0, false);
+    }
+
+    _gradientFilter(streamId, x, y, width, height, sock, display, depth) {
+        throw new Error("Gradient filter not implemented");
+    }
+
+    _readData(sock) {
+        if (this._len === 0) {
+            if (sock.rQwait("TIGHT", 3)) {
+                return null;
+            }
+
+            let byte;
+
+            byte = sock.rQshift8();
+            this._len = byte & 0x7f;
+            if (byte & 0x80) {
+                byte = sock.rQshift8();
+                this._len |= (byte & 0x7f) << 7;
+                if (byte & 0x80) {
+                    byte = sock.rQshift8();
+                    this._len |= byte << 14;
+                }
+            }
+        }
+
+        if (sock.rQwait("TIGHT", this._len)) {
+            return null;
+        }
+
+        let data = sock.rQshiftBytes(this._len);
+        this._len = 0;
+
+        return data;
+    }
+
+    _getScratchBuffer(size) {
+        if (!this._scratchBuffer || (this._scratchBuffer.length < size)) {
+            this._scratchBuffer = new Uint8Array(size);
+        }
+        return this._scratchBuffer;
+    }
+}
diff --git a/app/src/main/assets/novnc/core/decoders/tightpng.js b/app/src/main/assets/novnc/core/decoders/tightpng.js
new file mode 100644
index 00000000..82f492de
--- /dev/null
+++ b/app/src/main/assets/novnc/core/decoders/tightpng.js
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ *
+ */
+
+import TightDecoder from './tight.js';
+
+export default class TightPNGDecoder extends TightDecoder {
+    _pngRect(x, y, width, height, sock, display, depth) {
+        let data = this._readData(sock);
+        if (data === null) {
+            return false;
+        }
+
+        display.imageRect(x, y, width, height, "image/png", data);
+
+        return true;
+    }
+
+    _basicRect(ctl, x, y, width, height, sock, display, depth) {
+        throw new Error("BasicCompression received in TightPNG rect");
+    }
+}
diff --git a/app/src/main/assets/novnc/core/deflator.js b/app/src/main/assets/novnc/core/deflator.js
new file mode 100644
index 00000000..fe2a8f70
--- /dev/null
+++ b/app/src/main/assets/novnc/core/deflator.js
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+
+import { deflateInit, deflate } from "../vendor/pako/lib/zlib/deflate.js";
+import { Z_FULL_FLUSH } from "../vendor/pako/lib/zlib/deflate.js";
+import ZStream from "../vendor/pako/lib/zlib/zstream.js";
+
+export default class Deflator {
+    constructor() {
+        this.strm = new ZStream();
+        this.chunkSize = 1024 * 10 * 10;
+        this.outputBuffer = new Uint8Array(this.chunkSize);
+        this.windowBits = 5;
+
+        deflateInit(this.strm, this.windowBits);
+    }
+
+    deflate(inData) {
+        /* eslint-disable camelcase */
+        this.strm.input = inData;
+        this.strm.avail_in = this.strm.input.length;
+        this.strm.next_in = 0;
+        this.strm.output = this.outputBuffer;
+        this.strm.avail_out = this.chunkSize;
+        this.strm.next_out = 0;
+        /* eslint-enable camelcase */
+
+        let lastRet = deflate(this.strm, Z_FULL_FLUSH);
+        let outData = new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out);
+
+        if (lastRet < 0) {
+            throw new Error("zlib deflate failed");
+        }
+
+        if (this.strm.avail_in > 0) {
+            // Read chunks until done
+
+            let chunks = [outData];
+            let totalLen = outData.length;
+            do {
+                /* eslint-disable camelcase */
+                this.strm.output = new Uint8Array(this.chunkSize);
+                this.strm.next_out = 0;
+                this.strm.avail_out = this.chunkSize;
+                /* eslint-enable camelcase */
+
+                lastRet = deflate(this.strm, Z_FULL_FLUSH);
+
+                if (lastRet < 0) {
+                    throw new Error("zlib deflate failed");
+                }
+
+                let chunk = new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out);
+                totalLen += chunk.length;
+                chunks.push(chunk);
+            } while (this.strm.avail_in > 0);
+
+            // Combine chunks into a single data
+
+            let newData = new Uint8Array(totalLen);
+            let offset = 0;
+
+            for (let i = 0; i < chunks.length; i++) {
+                newData.set(chunks[i], offset);
+                offset += chunks[i].length;
+            }
+
+            outData = newData;
+        }
+
+        /* eslint-disable camelcase */
+        this.strm.input = null;
+        this.strm.avail_in = 0;
+        this.strm.next_in = 0;
+        /* eslint-enable camelcase */
+
+        return outData;
+    }
+
+}
diff --git a/app/src/main/assets/novnc/core/des.js b/app/src/main/assets/novnc/core/des.js
new file mode 100644
index 00000000..d2f807b8
--- /dev/null
+++ b/app/src/main/assets/novnc/core/des.js
@@ -0,0 +1,266 @@
+/*
+ * Ported from Flashlight VNC ActionScript implementation:
+ *     http://www.wizhelp.com/flashlight-vnc/
+ *
+ * Full attribution follows:
+ *
+ * -------------------------------------------------------------------------
+ *
+ * This DES class has been extracted from package Acme.Crypto for use in VNC.
+ * The unnecessary odd parity code has been removed.
+ *
+ * These changes are:
+ *  Copyright (C) 1999 AT&T Laboratories Cambridge.  All Rights Reserved.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+
+ * DesCipher - the DES encryption method
+ *
+ * The meat of this code is by Dave Zimmerman <dzimm@widget.com>, and is:
+ *
+ * Copyright (c) 1996 Widget Workshop, Inc. All Rights Reserved.
+ *
+ * Permission to use, copy, modify, and distribute this software
+ * and its documentation for NON-COMMERCIAL or COMMERCIAL purposes and
+ * without fee is hereby granted, provided that this copyright notice is kept
+ * intact.
+ *
+ * WIDGET WORKSHOP MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY
+ * OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+ * TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE, OR NON-INFRINGEMENT. WIDGET WORKSHOP SHALL NOT BE LIABLE
+ * FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
+ * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES.
+ *
+ * THIS SOFTWARE IS NOT DESIGNED OR INTENDED FOR USE OR RESALE AS ON-LINE
+ * CONTROL EQUIPMENT IN HAZARDOUS ENVIRONMENTS REQUIRING FAIL-SAFE
+ * PERFORMANCE, SUCH AS IN THE OPERATION OF NUCLEAR FACILITIES, AIRCRAFT
+ * NAVIGATION OR COMMUNICATION SYSTEMS, AIR TRAFFIC CONTROL, DIRECT LIFE
+ * SUPPORT MACHINES, OR WEAPONS SYSTEMS, IN WHICH THE FAILURE OF THE
+ * SOFTWARE COULD LEAD DIRECTLY TO DEATH, PERSONAL INJURY, OR SEVERE
+ * PHYSICAL OR ENVIRONMENTAL DAMAGE ("HIGH RISK ACTIVITIES").  WIDGET WORKSHOP
+ * SPECIFICALLY DISCLAIMS ANY EXPRESS OR IMPLIED WARRANTY OF FITNESS FOR
+ * HIGH RISK ACTIVITIES.
+ *
+ *
+ * The rest is:
+ *
+ * Copyright (C) 1996 by Jef Poskanzer <jef@acme.com>.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ * Visit the ACME Labs Java page for up-to-date versions of this and other
+ * fine Java utilities: http://www.acme.com/java/
+ */
+
+/* eslint-disable comma-spacing */
+
+// Tables, permutations, S-boxes, etc.
+const PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3,
+             25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39,
+             50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ],
+    totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28];
+
+const z = 0x0;
+let a,b,c,d,e,f;
+a=1<<16; b=1<<24; c=a|b; d=1<<2; e=1<<10; f=d|e;
+const SP1 = [c|e,z|z,a|z,c|f,c|d,a|f,z|d,a|z,z|e,c|e,c|f,z|e,b|f,c|d,b|z,z|d,
+             z|f,b|e,b|e,a|e,a|e,c|z,c|z,b|f,a|d,b|d,b|d,a|d,z|z,z|f,a|f,b|z,
+             a|z,c|f,z|d,c|z,c|e,b|z,b|z,z|e,c|d,a|z,a|e,b|d,z|e,z|d,b|f,a|f,
+             c|f,a|d,c|z,b|f,b|d,z|f,a|f,c|e,z|f,b|e,b|e,z|z,a|d,a|e,z|z,c|d];
+a=1<<20; b=1<<31; c=a|b; d=1<<5; e=1<<15; f=d|e;
+const SP2 = [c|f,b|e,z|e,a|f,a|z,z|d,c|d,b|f,b|d,c|f,c|e,b|z,b|e,a|z,z|d,c|d,
+             a|e,a|d,b|f,z|z,b|z,z|e,a|f,c|z,a|d,b|d,z|z,a|e,z|f,c|e,c|z,z|f,
+             z|z,a|f,c|d,a|z,b|f,c|z,c|e,z|e,c|z,b|e,z|d,c|f,a|f,z|d,z|e,b|z,
+             z|f,c|e,a|z,b|d,a|d,b|f,b|d,a|d,a|e,z|z,b|e,z|f,b|z,c|d,c|f,a|e];
+a=1<<17; b=1<<27; c=a|b; d=1<<3; e=1<<9; f=d|e;
+const SP3 = [z|f,c|e,z|z,c|d,b|e,z|z,a|f,b|e,a|d,b|d,b|d,a|z,c|f,a|d,c|z,z|f,
+             b|z,z|d,c|e,z|e,a|e,c|z,c|d,a|f,b|f,a|e,a|z,b|f,z|d,c|f,z|e,b|z,
+             c|e,b|z,a|d,z|f,a|z,c|e,b|e,z|z,z|e,a|d,c|f,b|e,b|d,z|e,z|z,c|d,
+             b|f,a|z,b|z,c|f,z|d,a|f,a|e,b|d,c|z,b|f,z|f,c|z,a|f,z|d,c|d,a|e];
+a=1<<13; b=1<<23; c=a|b; d=1<<0; e=1<<7; f=d|e;
+const SP4 = [c|d,a|f,a|f,z|e,c|e,b|f,b|d,a|d,z|z,c|z,c|z,c|f,z|f,z|z,b|e,b|d,
+             z|d,a|z,b|z,c|d,z|e,b|z,a|d,a|e,b|f,z|d,a|e,b|e,a|z,c|e,c|f,z|f,
+             b|e,b|d,c|z,c|f,z|f,z|z,z|z,c|z,a|e,b|e,b|f,z|d,c|d,a|f,a|f,z|e,
+             c|f,z|f,z|d,a|z,b|d,a|d,c|e,b|f,a|d,a|e,b|z,c|d,z|e,b|z,a|z,c|e];
+a=1<<25; b=1<<30; c=a|b; d=1<<8; e=1<<19; f=d|e;
+const SP5 = [z|d,a|f,a|e,c|d,z|e,z|d,b|z,a|e,b|f,z|e,a|d,b|f,c|d,c|e,z|f,b|z,
+             a|z,b|e,b|e,z|z,b|d,c|f,c|f,a|d,c|e,b|d,z|z,c|z,a|f,a|z,c|z,z|f,
+             z|e,c|d,z|d,a|z,b|z,a|e,c|d,b|f,a|d,b|z,c|e,a|f,b|f,z|d,a|z,c|e,
+             c|f,z|f,c|z,c|f,a|e,z|z,b|e,c|z,z|f,a|d,b|d,z|e,z|z,b|e,a|f,b|d];
+a=1<<22; b=1<<29; c=a|b; d=1<<4; e=1<<14; f=d|e;
+const SP6 = [b|d,c|z,z|e,c|f,c|z,z|d,c|f,a|z,b|e,a|f,a|z,b|d,a|d,b|e,b|z,z|f,
+             z|z,a|d,b|f,z|e,a|e,b|f,z|d,c|d,c|d,z|z,a|f,c|e,z|f,a|e,c|e,b|z,
+             b|e,z|d,c|d,a|e,c|f,a|z,z|f,b|d,a|z,b|e,b|z,z|f,b|d,c|f,a|e,c|z,
+             a|f,c|e,z|z,c|d,z|d,z|e,c|z,a|f,z|e,a|d,b|f,z|z,c|e,b|z,a|d,b|f];
+a=1<<21; b=1<<26; c=a|b; d=1<<1; e=1<<11; f=d|e;
+const SP7 = [a|z,c|d,b|f,z|z,z|e,b|f,a|f,c|e,c|f,a|z,z|z,b|d,z|d,b|z,c|d,z|f,
+             b|e,a|f,a|d,b|e,b|d,c|z,c|e,a|d,c|z,z|e,z|f,c|f,a|e,z|d,b|z,a|e,
+             b|z,a|e,a|z,b|f,b|f,c|d,c|d,z|d,a|d,b|z,b|e,a|z,c|e,z|f,a|f,c|e,
+             z|f,b|d,c|f,c|z,a|e,z|z,z|d,c|f,z|z,a|f,c|z,z|e,b|d,b|e,z|e,a|d];
+a=1<<18; b=1<<28; c=a|b; d=1<<6; e=1<<12; f=d|e;
+const SP8 = [b|f,z|e,a|z,c|f,b|z,b|f,z|d,b|z,a|d,c|z,c|f,a|e,c|e,a|f,z|e,z|d,
+             c|z,b|d,b|e,z|f,a|e,a|d,c|d,c|e,z|f,z|z,z|z,c|d,b|d,b|e,a|f,a|z,
+             a|f,a|z,c|e,z|e,z|d,c|d,z|e,a|f,b|e,z|d,b|d,c|z,c|d,b|z,a|z,b|f,
+             z|z,c|f,a|d,b|d,c|z,b|e,b|f,z|z,c|f,a|e,a|e,z|f,z|f,a|d,b|z,c|e];
+
+/* eslint-enable comma-spacing */
+
+export default class DES {
+    constructor(password) {
+        this.keys = [];
+
+        // Set the key.
+        const pc1m = [], pcr = [], kn = [];
+
+        for (let j = 0, l = 56; j < 56; ++j, l -= 8) {
+            l += l < -5 ? 65 : l < -3 ? 31 : l < -1 ? 63 : l === 27 ? 35 : 0; // PC1
+            const m = l & 0x7;
+            pc1m[j] = ((password[l >>> 3] & (1<<m)) !== 0) ? 1: 0;
+        }
+
+        for (let i = 0; i < 16; ++i) {
+            const m = i << 1;
+            const n = m + 1;
+            kn[m] = kn[n] = 0;
+            for (let o = 28; o < 59; o += 28) {
+                for (let j = o - 28; j < o; ++j) {
+                    const l = j + totrot[i];
+                    pcr[j] = l < o ? pc1m[l] : pc1m[l - 28];
+                }
+            }
+            for (let j = 0; j < 24; ++j) {
+                if (pcr[PC2[j]] !== 0) {
+                    kn[m] |= 1 << (23 - j);
+                }
+                if (pcr[PC2[j + 24]] !== 0) {
+                    kn[n] |= 1 << (23 - j);
+                }
+            }
+        }
+
+        // cookey
+        for (let i = 0, rawi = 0, KnLi = 0; i < 16; ++i) {
+            const raw0 = kn[rawi++];
+            const raw1 = kn[rawi++];
+            this.keys[KnLi] = (raw0 & 0x00fc0000) << 6;
+            this.keys[KnLi] |= (raw0 & 0x00000fc0) << 10;
+            this.keys[KnLi] |= (raw1 & 0x00fc0000) >>> 10;
+            this.keys[KnLi] |= (raw1 & 0x00000fc0) >>> 6;
+            ++KnLi;
+            this.keys[KnLi] = (raw0 & 0x0003f000) << 12;
+            this.keys[KnLi] |= (raw0 & 0x0000003f) << 16;
+            this.keys[KnLi] |= (raw1 & 0x0003f000) >>> 4;
+            this.keys[KnLi] |= (raw1 & 0x0000003f);
+            ++KnLi;
+        }
+    }
+
+    // Encrypt 8 bytes of text
+    enc8(text) {
+        const b = text.slice();
+        let i = 0, l, r, x; // left, right, accumulator
+
+        // Squash 8 bytes to 2 ints
+        l = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++];
+        r = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++];
+
+        x = ((l >>> 4) ^ r) & 0x0f0f0f0f;
+        r ^= x;
+        l ^= (x << 4);
+        x = ((l >>> 16) ^ r) & 0x0000ffff;
+        r ^= x;
+        l ^= (x << 16);
+        x = ((r >>> 2) ^ l) & 0x33333333;
+        l ^= x;
+        r ^= (x << 2);
+        x = ((r >>> 8) ^ l) & 0x00ff00ff;
+        l ^= x;
+        r ^= (x << 8);
+        r = (r << 1) | ((r >>> 31) & 1);
+        x = (l ^ r) & 0xaaaaaaaa;
+        l ^= x;
+        r ^= x;
+        l = (l << 1) | ((l >>> 31) & 1);
+
+        for (let i = 0, keysi = 0; i < 8; ++i) {
+            x = (r << 28) | (r >>> 4);
+            x ^= this.keys[keysi++];
+            let fval =  SP7[x & 0x3f];
+            fval |= SP5[(x >>> 8) & 0x3f];
+            fval |= SP3[(x >>> 16) & 0x3f];
+            fval |= SP1[(x >>> 24) & 0x3f];
+            x = r ^ this.keys[keysi++];
+            fval |= SP8[x & 0x3f];
+            fval |= SP6[(x >>> 8) & 0x3f];
+            fval |= SP4[(x >>> 16) & 0x3f];
+            fval |= SP2[(x >>> 24) & 0x3f];
+            l ^= fval;
+            x = (l << 28) | (l >>> 4);
+            x ^= this.keys[keysi++];
+            fval =  SP7[x & 0x3f];
+            fval |= SP5[(x >>> 8) & 0x3f];
+            fval |= SP3[(x >>> 16) & 0x3f];
+            fval |= SP1[(x >>> 24) & 0x3f];
+            x = l ^ this.keys[keysi++];
+            fval |= SP8[x & 0x0000003f];
+            fval |= SP6[(x >>> 8) & 0x3f];
+            fval |= SP4[(x >>> 16) & 0x3f];
+            fval |= SP2[(x >>> 24) & 0x3f];
+            r ^= fval;
+        }
+
+        r = (r << 31) | (r >>> 1);
+        x = (l ^ r) & 0xaaaaaaaa;
+        l ^= x;
+        r ^= x;
+        l = (l << 31) | (l >>> 1);
+        x = ((l >>> 8) ^ r) & 0x00ff00ff;
+        r ^= x;
+        l ^= (x << 8);
+        x = ((l >>> 2) ^ r) & 0x33333333;
+        r ^= x;
+        l ^= (x << 2);
+        x = ((r >>> 16) ^ l) & 0x0000ffff;
+        l ^= x;
+        r ^= (x << 16);
+        x = ((r >>> 4) ^ l) & 0x0f0f0f0f;
+        l ^= x;
+        r ^= (x << 4);
+
+        // Spread ints to bytes
+        x = [r, l];
+        for (i = 0; i < 8; i++) {
+            b[i] = (x[i>>>2] >>> (8 * (3 - (i % 4)))) % 256;
+            if (b[i] < 0) { b[i] += 256; } // unsigned
+        }
+        return b;
+    }
+
+    // Encrypt 16 bytes of text using passwd as key
+    encrypt(t) {
+        return this.enc8(t.slice(0, 8)).concat(this.enc8(t.slice(8, 16)));
+    }
+}
diff --git a/app/src/main/assets/novnc/core/display.js b/app/src/main/assets/novnc/core/display.js
new file mode 100644
index 00000000..701eba4a
--- /dev/null
+++ b/app/src/main/assets/novnc/core/display.js
@@ -0,0 +1,513 @@
+/*
+ * 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.
+ */
+
+import * as Log from './util/logging.js';
+import Base64 from "./base64.js";
+import { toSigned32bit } from './util/int.js';
+
+export default class Display {
+    constructor(target) {
+        this._drawCtx = null;
+
+        this._renderQ = [];  // queue drawing actions for in-oder rendering
+        this._flushing = false;
+
+        // the full frame buffer (logical canvas) size
+        this._fbWidth = 0;
+        this._fbHeight = 0;
+
+        this._prevDrawStyle = "";
+
+        Log.Debug(">> Display.constructor");
+
+        // The visible canvas
+        this._target = target;
+
+        if (!this._target) {
+            throw new Error("Target must be set");
+        }
+
+        if (typeof this._target === 'string') {
+            throw new Error('target must be a DOM element');
+        }
+
+        if (!this._target.getContext) {
+            throw new Error("no getContext method");
+        }
+
+        this._targetCtx = this._target.getContext('2d');
+
+        // the visible canvas viewport (i.e. what actually gets seen)
+        this._viewportLoc = { 'x': 0, 'y': 0, 'w': this._target.width, 'h': this._target.height };
+
+        // The hidden canvas, where we do the actual rendering
+        this._backbuffer = document.createElement('canvas');
+        this._drawCtx = this._backbuffer.getContext('2d');
+
+        this._damageBounds = { left: 0, top: 0,
+                               right: this._backbuffer.width,
+                               bottom: this._backbuffer.height };
+
+        Log.Debug("User Agent: " + navigator.userAgent);
+
+        Log.Debug("<< Display.constructor");
+
+        // ===== PROPERTIES =====
+
+        this._scale = 1.0;
+        this._clipViewport = false;
+
+        // ===== EVENT HANDLERS =====
+
+        this.onflush = () => {}; // A flush request has finished
+    }
+
+    // ===== PROPERTIES =====
+
+    get scale() { return this._scale; }
+    set scale(scale) {
+        this._rescale(scale);
+    }
+
+    get clipViewport() { return this._clipViewport; }
+    set clipViewport(viewport) {
+        this._clipViewport = viewport;
+        // May need to readjust the viewport dimensions
+        const vp = this._viewportLoc;
+        this.viewportChangeSize(vp.w, vp.h);
+        this.viewportChangePos(0, 0);
+    }
+
+    get width() {
+        return this._fbWidth;
+    }
+
+    get height() {
+        return this._fbHeight;
+    }
+
+    // ===== PUBLIC METHODS =====
+
+    viewportChangePos(deltaX, deltaY) {
+        const vp = this._viewportLoc;
+        deltaX = Math.floor(deltaX);
+        deltaY = Math.floor(deltaY);
+
+        if (!this._clipViewport) {
+            deltaX = -vp.w;  // clamped later of out of bounds
+            deltaY = -vp.h;
+        }
+
+        const vx2 = vp.x + vp.w - 1;
+        const vy2 = vp.y + vp.h - 1;
+
+        // Position change
+
+        if (deltaX < 0 && vp.x + deltaX < 0) {
+            deltaX = -vp.x;
+        }
+        if (vx2 + deltaX >= this._fbWidth) {
+            deltaX -= vx2 + deltaX - this._fbWidth + 1;
+        }
+
+        if (vp.y + deltaY < 0) {
+            deltaY = -vp.y;
+        }
+        if (vy2 + deltaY >= this._fbHeight) {
+            deltaY -= (vy2 + deltaY - this._fbHeight + 1);
+        }
+
+        if (deltaX === 0 && deltaY === 0) {
+            return;
+        }
+        Log.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY);
+
+        vp.x += deltaX;
+        vp.y += deltaY;
+
+        this._damage(vp.x, vp.y, vp.w, vp.h);
+
+        this.flip();
+    }
+
+    viewportChangeSize(width, height) {
+
+        if (!this._clipViewport ||
+            typeof(width) === "undefined" ||
+            typeof(height) === "undefined") {
+
+            Log.Debug("Setting viewport to full display region");
+            width = this._fbWidth;
+            height = this._fbHeight;
+        }
+
+        width = Math.floor(width);
+        height = Math.floor(height);
+
+        if (width > this._fbWidth) {
+            width = this._fbWidth;
+        }
+        if (height > this._fbHeight) {
+            height = this._fbHeight;
+        }
+
+        const vp = this._viewportLoc;
+        if (vp.w !== width || vp.h !== height) {
+            vp.w = width;
+            vp.h = height;
+
+            const canvas = this._target;
+            canvas.width = width;
+            canvas.height = height;
+
+            // The position might need to be updated if we've grown
+            this.viewportChangePos(0, 0);
+
+            this._damage(vp.x, vp.y, vp.w, vp.h);
+            this.flip();
+
+            // Update the visible size of the target canvas
+            this._rescale(this._scale);
+        }
+    }
+
+    absX(x) {
+        if (this._scale === 0) {
+            return 0;
+        }
+        return toSigned32bit(x / this._scale + this._viewportLoc.x);
+    }
+
+    absY(y) {
+        if (this._scale === 0) {
+            return 0;
+        }
+        return toSigned32bit(y / this._scale + this._viewportLoc.y);
+    }
+
+    resize(width, height) {
+        this._prevDrawStyle = "";
+
+        this._fbWidth = width;
+        this._fbHeight = height;
+
+        const canvas = this._backbuffer;
+        if (canvas.width !== width || canvas.height !== height) {
+
+            // We have to save the canvas data since changing the size will clear it
+            let saveImg = null;
+            if (canvas.width > 0 && canvas.height > 0) {
+                saveImg = this._drawCtx.getImageData(0, 0, canvas.width, canvas.height);
+            }
+
+            if (canvas.width !== width) {
+                canvas.width = width;
+            }
+            if (canvas.height !== height) {
+                canvas.height = height;
+            }
+
+            if (saveImg) {
+                this._drawCtx.putImageData(saveImg, 0, 0);
+            }
+        }
+
+        // Readjust the viewport as it may be incorrectly sized
+        // and positioned
+        const vp = this._viewportLoc;
+        this.viewportChangeSize(vp.w, vp.h);
+        this.viewportChangePos(0, 0);
+    }
+
+    // Track what parts of the visible canvas that need updating
+    _damage(x, y, w, h) {
+        if (x < this._damageBounds.left) {
+            this._damageBounds.left = x;
+        }
+        if (y < this._damageBounds.top) {
+            this._damageBounds.top = y;
+        }
+        if ((x + w) > this._damageBounds.right) {
+            this._damageBounds.right = x + w;
+        }
+        if ((y + h) > this._damageBounds.bottom) {
+            this._damageBounds.bottom = y + h;
+        }
+    }
+
+    // Update the visible canvas with the contents of the
+    // rendering canvas
+    flip(fromQueue) {
+        if (this._renderQ.length !== 0 && !fromQueue) {
+            this._renderQPush({
+                'type': 'flip'
+            });
+        } else {
+            let x = this._damageBounds.left;
+            let y = this._damageBounds.top;
+            let w = this._damageBounds.right - x;
+            let h = this._damageBounds.bottom - y;
+
+            let vx = x - this._viewportLoc.x;
+            let vy = y - this._viewportLoc.y;
+
+            if (vx < 0) {
+                w += vx;
+                x -= vx;
+                vx = 0;
+            }
+            if (vy < 0) {
+                h += vy;
+                y -= vy;
+                vy = 0;
+            }
+
+            if ((vx + w) > this._viewportLoc.w) {
+                w = this._viewportLoc.w - vx;
+            }
+            if ((vy + h) > this._viewportLoc.h) {
+                h = this._viewportLoc.h - vy;
+            }
+
+            if ((w > 0) && (h > 0)) {
+                // FIXME: We may need to disable image smoothing here
+                //        as well (see copyImage()), but we haven't
+                //        noticed any problem yet.
+                this._targetCtx.drawImage(this._backbuffer,
+                                          x, y, w, h,
+                                          vx, vy, w, h);
+            }
+
+            this._damageBounds.left = this._damageBounds.top = 65535;
+            this._damageBounds.right = this._damageBounds.bottom = 0;
+        }
+    }
+
+    pending() {
+        return this._renderQ.length > 0;
+    }
+
+    flush() {
+        if (this._renderQ.length === 0) {
+            this.onflush();
+        } else {
+            this._flushing = true;
+        }
+    }
+
+    fillRect(x, y, width, height, color, fromQueue) {
+        if (this._renderQ.length !== 0 && !fromQueue) {
+            this._renderQPush({
+                'type': 'fill',
+                'x': x,
+                'y': y,
+                'width': width,
+                'height': height,
+                'color': color
+            });
+        } else {
+            this._setFillColor(color);
+            this._drawCtx.fillRect(x, y, width, height);
+            this._damage(x, y, width, height);
+        }
+    }
+
+    copyImage(oldX, oldY, newX, newY, w, h, fromQueue) {
+        if (this._renderQ.length !== 0 && !fromQueue) {
+            this._renderQPush({
+                'type': 'copy',
+                'oldX': oldX,
+                'oldY': oldY,
+                'x': newX,
+                'y': newY,
+                'width': w,
+                'height': h,
+            });
+        } else {
+            // Due to this bug among others [1] we need to disable the image-smoothing to
+            // avoid getting a blur effect when copying data.
+            //
+            // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719
+            //
+            // We need to set these every time since all properties are reset
+            // when the the size is changed
+            this._drawCtx.mozImageSmoothingEnabled = false;
+            this._drawCtx.webkitImageSmoothingEnabled = false;
+            this._drawCtx.msImageSmoothingEnabled = false;
+            this._drawCtx.imageSmoothingEnabled = false;
+
+            this._drawCtx.drawImage(this._backbuffer,
+                                    oldX, oldY, w, h,
+                                    newX, newY, w, h);
+            this._damage(newX, newY, w, h);
+        }
+    }
+
+    imageRect(x, y, width, height, mime, arr) {
+        /* The internal logic cannot handle empty images, so bail early */
+        if ((width === 0) || (height === 0)) {
+            return;
+        }
+
+        const img = new Image();
+        img.src = "data: " + mime + ";base64," + Base64.encode(arr);
+
+        this._renderQPush({
+            'type': 'img',
+            'img': img,
+            'x': x,
+            'y': y,
+            'width': width,
+            'height': height
+        });
+    }
+
+    blitImage(x, y, width, height, arr, offset, fromQueue) {
+        if (this._renderQ.length !== 0 && !fromQueue) {
+            // NB(directxman12): it's technically more performant here to use preallocated arrays,
+            // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
+            // this probably isn't getting called *nearly* as much
+            const newArr = new Uint8Array(width * height * 4);
+            newArr.set(new Uint8Array(arr.buffer, 0, newArr.length));
+            this._renderQPush({
+                'type': 'blit',
+                'data': newArr,
+                'x': x,
+                'y': y,
+                'width': width,
+                'height': height,
+            });
+        } else {
+            // NB(directxman12): arr must be an Type Array view
+            let data = new Uint8ClampedArray(arr.buffer,
+                                             arr.byteOffset + offset,
+                                             width * height * 4);
+            let img = new ImageData(data, width, height);
+            this._drawCtx.putImageData(img, x, y);
+            this._damage(x, y, width, height);
+        }
+    }
+
+    drawImage(img, x, y) {
+        this._drawCtx.drawImage(img, x, y);
+        this._damage(x, y, img.width, img.height);
+    }
+
+    autoscale(containerWidth, containerHeight) {
+        let scaleRatio;
+
+        if (containerWidth === 0 || containerHeight === 0) {
+            scaleRatio = 0;
+
+        } else {
+
+            const vp = this._viewportLoc;
+            const targetAspectRatio = containerWidth / containerHeight;
+            const fbAspectRatio = vp.w / vp.h;
+
+            if (fbAspectRatio >= targetAspectRatio) {
+                scaleRatio = containerWidth / vp.w;
+            } else {
+                scaleRatio = containerHeight / vp.h;
+            }
+        }
+
+        this._rescale(scaleRatio);
+    }
+
+    // ===== PRIVATE METHODS =====
+
+    _rescale(factor) {
+        this._scale = factor;
+        const vp = this._viewportLoc;
+
+        // NB(directxman12): If you set the width directly, or set the
+        //                   style width to a number, the canvas is cleared.
+        //                   However, if you set the style width to a string
+        //                   ('NNNpx'), the canvas is scaled without clearing.
+        const width = factor * vp.w + 'px';
+        const height = factor * vp.h + 'px';
+
+        if ((this._target.style.width !== width) ||
+            (this._target.style.height !== height)) {
+            this._target.style.width = width;
+            this._target.style.height = height;
+        }
+    }
+
+    _setFillColor(color) {
+        const newStyle = 'rgb(' + color[0] + ',' + color[1] + ',' + color[2] + ')';
+        if (newStyle !== this._prevDrawStyle) {
+            this._drawCtx.fillStyle = newStyle;
+            this._prevDrawStyle = newStyle;
+        }
+    }
+
+    _renderQPush(action) {
+        this._renderQ.push(action);
+        if (this._renderQ.length === 1) {
+            // If this can be rendered immediately it will be, otherwise
+            // the scanner will wait for the relevant event
+            this._scanRenderQ();
+        }
+    }
+
+    _resumeRenderQ() {
+        // "this" is the object that is ready, not the
+        // display object
+        this.removeEventListener('load', this._noVNCDisplay._resumeRenderQ);
+        this._noVNCDisplay._scanRenderQ();
+    }
+
+    _scanRenderQ() {
+        let ready = true;
+        while (ready && this._renderQ.length > 0) {
+            const a = this._renderQ[0];
+            switch (a.type) {
+                case 'flip':
+                    this.flip(true);
+                    break;
+                case 'copy':
+                    this.copyImage(a.oldX, a.oldY, a.x, a.y, a.width, a.height, true);
+                    break;
+                case 'fill':
+                    this.fillRect(a.x, a.y, a.width, a.height, a.color, true);
+                    break;
+                case 'blit':
+                    this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true);
+                    break;
+                case 'img':
+                    if (a.img.complete) {
+                        if (a.img.width !== a.width || a.img.height !== a.height) {
+                            Log.Error("Decoded image has incorrect dimensions. Got " +
+                                      a.img.width + "x" + a.img.height + ". Expected " +
+                                      a.width + "x" + a.height + ".");
+                            return;
+                        }
+                        this.drawImage(a.img, a.x, a.y);
+                    } else {
+                        a.img._noVNCDisplay = this;
+                        a.img.addEventListener('load', this._resumeRenderQ);
+                        // We need to wait for this image to 'load'
+                        // to keep things in-order
+                        ready = false;
+                    }
+                    break;
+            }
+
+            if (ready) {
+                this._renderQ.shift();
+            }
+        }
+
+        if (this._renderQ.length === 0 && this._flushing) {
+            this._flushing = false;
+            this.onflush();
+        }
+    }
+}
diff --git a/app/src/main/assets/novnc/core/encodings.js b/app/src/main/assets/novnc/core/encodings.js
new file mode 100644
index 00000000..51c09929
--- /dev/null
+++ b/app/src/main/assets/novnc/core/encodings.js
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+
+export const encodings = {
+    encodingRaw: 0,
+    encodingCopyRect: 1,
+    encodingRRE: 2,
+    encodingHextile: 5,
+    encodingTight: 7,
+    encodingTightPNG: -260,
+
+    pseudoEncodingQualityLevel9: -23,
+    pseudoEncodingQualityLevel0: -32,
+    pseudoEncodingDesktopSize: -223,
+    pseudoEncodingLastRect: -224,
+    pseudoEncodingCursor: -239,
+    pseudoEncodingQEMUExtendedKeyEvent: -258,
+    pseudoEncodingDesktopName: -307,
+    pseudoEncodingExtendedDesktopSize: -308,
+    pseudoEncodingXvp: -309,
+    pseudoEncodingFence: -312,
+    pseudoEncodingContinuousUpdates: -313,
+    pseudoEncodingCompressLevel9: -247,
+    pseudoEncodingCompressLevel0: -256,
+    pseudoEncodingVMwareCursor: 0x574d5664,
+    pseudoEncodingExtendedClipboard: 0xc0a1e5ce
+};
+
+export function encodingName(num) {
+    switch (num) {
+        case encodings.encodingRaw:      return "Raw";
+        case encodings.encodingCopyRect: return "CopyRect";
+        case encodings.encodingRRE:      return "RRE";
+        case encodings.encodingHextile:  return "Hextile";
+        case encodings.encodingTight:    return "Tight";
+        case encodings.encodingTightPNG: return "TightPNG";
+        default:                         return "[unknown encoding " + num + "]";
+    }
+}
diff --git a/app/src/main/assets/novnc/core/inflator.js b/app/src/main/assets/novnc/core/inflator.js
new file mode 100644
index 00000000..4b337607
--- /dev/null
+++ b/app/src/main/assets/novnc/core/inflator.js
@@ -0,0 +1,66 @@
+/*
+ * 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.
+ */
+
+import { inflateInit, inflate, inflateReset } from "../vendor/pako/lib/zlib/inflate.js";
+import ZStream from "../vendor/pako/lib/zlib/zstream.js";
+
+export default class Inflate {
+    constructor() {
+        this.strm = new ZStream();
+        this.chunkSize = 1024 * 10 * 10;
+        this.strm.output = new Uint8Array(this.chunkSize);
+        this.windowBits = 5;
+
+        inflateInit(this.strm, this.windowBits);
+    }
+
+    setInput(data) {
+        if (!data) {
+            //FIXME: flush remaining data.
+            /* eslint-disable camelcase */
+            this.strm.input = null;
+            this.strm.avail_in = 0;
+            this.strm.next_in = 0;
+        } else {
+            this.strm.input = data;
+            this.strm.avail_in = this.strm.input.length;
+            this.strm.next_in = 0;
+            /* eslint-enable camelcase */
+        }
+    }
+
+    inflate(expected) {
+        // resize our output buffer if it's too small
+        // (we could just use multiple chunks, but that would cause an extra
+        // allocation each time to flatten the chunks)
+        if (expected > this.chunkSize) {
+            this.chunkSize = expected;
+            this.strm.output = new Uint8Array(this.chunkSize);
+        }
+
+        /* eslint-disable camelcase */
+        this.strm.next_out = 0;
+        this.strm.avail_out = expected;
+        /* eslint-enable camelcase */
+
+        let ret = inflate(this.strm, 0); // Flush argument not used.
+        if (ret < 0) {
+            throw new Error("zlib inflate failed");
+        }
+
+        if (this.strm.next_out != expected) {
+            throw new Error("Incomplete zlib block");
+        }
+
+        return new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out);
+    }
+
+    reset() {
+        inflateReset(this.strm);
+    }
+}
diff --git a/app/src/main/assets/novnc/core/input/domkeytable.js b/app/src/main/assets/novnc/core/input/domkeytable.js
new file mode 100644
index 00000000..f79aeadf
--- /dev/null
+++ b/app/src/main/assets/novnc/core/input/domkeytable.js
@@ -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;
diff --git a/app/src/main/assets/novnc/core/input/fixedkeys.js b/app/src/main/assets/novnc/core/input/fixedkeys.js
new file mode 100644
index 00000000..4d09f2f7
--- /dev/null
+++ b/app/src/main/assets/novnc/core/input/fixedkeys.js
@@ -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',
+};
diff --git a/app/src/main/assets/novnc/core/input/gesturehandler.js b/app/src/main/assets/novnc/core/input/gesturehandler.js
new file mode 100644
index 00000000..6fa72d2a
--- /dev/null
+++ b/app/src/main/assets/novnc/core/input/gesturehandler.js
@@ -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 } };
+    }
+}
diff --git a/app/src/main/assets/novnc/core/input/keyboard.js b/app/src/main/assets/novnc/core/input/keyboard.js
new file mode 100644
index 00000000..48f65cf6
--- /dev/null
+++ b/app/src/main/assets/novnc/core/input/keyboard.js
@@ -0,0 +1,273 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2019 The noVNC Authors
+ * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
+ */
+
+import * as Log from '../util/logging.js';
+import { stopEvent } from '../util/events.js';
+import * as KeyboardUtil from "./util.js";
+import KeyTable from "./keysym.js";
+import * as browser from "../util/browser.js";
+
+//
+// Keyboard event handler
+//
+
+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];
+        }
+
+        Log.Debug("onkeyevent " + (down ? "down" : "up") +
+                  ", keysym: " + keysym, ", code: " + code);
+        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;
+        }
+
+        // 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).
+        if (browser.isMac() || browser.isIOS()) {
+            switch (keysym) {
+                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;
+        }
+
+        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;
+        }
+
+        this._sendKeyEvent(keysym, code, true);
+    }
+
+    _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() {
+        Log.Debug(">> Keyboard.allKeysUp");
+        for (let code in this._keyDownList) {
+            this._sendKeyEvent(this._keyDownList[code], code, false);
+        }
+        Log.Debug("<< Keyboard.allKeysUp");
+    }
+
+    // ===== PUBLIC METHODS =====
+
+    grab() {
+        //Log.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);
+
+        //Log.Debug("<< Keyboard.grab");
+    }
+
+    ungrab() {
+        //Log.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();
+
+        //Log.Debug(">> Keyboard.ungrab");
+    }
+}
diff --git a/app/src/main/assets/novnc/core/input/keysym.js b/app/src/main/assets/novnc/core/input/keysym.js
new file mode 100644
index 00000000..22ba0584
--- /dev/null
+++ b/app/src/main/assets/novnc/core/input/keysym.js
@@ -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,
+};
diff --git a/app/src/main/assets/novnc/core/input/keysymdef.js b/app/src/main/assets/novnc/core/input/keysymdef.js
new file mode 100644
index 00000000..951cacab
--- /dev/null
+++ b/app/src/main/assets/novnc/core/input/keysymdef.js
@@ -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;
+    },
+};
diff --git a/app/src/main/assets/novnc/core/input/util.js b/app/src/main/assets/novnc/core/input/util.js
new file mode 100644
index 00000000..58f84e55
--- /dev/null
+++ b/app/src/main/assets/novnc/core/input/util.js
@@ -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 "../util/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;
+}
diff --git a/app/src/main/assets/novnc/core/input/vkeys.js b/app/src/main/assets/novnc/core/input/vkeys.js
new file mode 100644
index 00000000..dacc3580
--- /dev/null
+++ b/app/src/main/assets/novnc/core/input/vkeys.js
@@ -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
+};
diff --git a/app/src/main/assets/novnc/core/input/xtscancodes.js b/app/src/main/assets/novnc/core/input/xtscancodes.js
new file mode 100644
index 00000000..8ab9c17f
--- /dev/null
+++ b/app/src/main/assets/novnc/core/input/xtscancodes.js
@@ -0,0 +1,173 @@
+/*
+ * This file is auto-generated from keymaps.csv
+ * Database checksum sha256(76d68c10e97d37fe2ea459e210125ae41796253fb217e900bf2983ade13a7920)
+ * To re-generate, run:
+ *   keymap-gen code-map --lang=js keymaps.csv html atset1
+*/
+export default {
+  "Again": 0xe005, /* html:Again (Again) -> linux:129 (KEY_AGAIN) -> atset1:57349 */
+  "AltLeft": 0x38, /* html:AltLeft (AltLeft) -> linux:56 (KEY_LEFTALT) -> atset1:56 */
+  "AltRight": 0xe038, /* html:AltRight (AltRight) -> linux:100 (KEY_RIGHTALT) -> atset1:57400 */
+  "ArrowDown": 0xe050, /* html:ArrowDown (ArrowDown) -> linux:108 (KEY_DOWN) -> atset1:57424 */
+  "ArrowLeft": 0xe04b, /* html:ArrowLeft (ArrowLeft) -> linux:105 (KEY_LEFT) -> atset1:57419 */
+  "ArrowRight": 0xe04d, /* html:ArrowRight (ArrowRight) -> linux:106 (KEY_RIGHT) -> atset1:57421 */
+  "ArrowUp": 0xe048, /* html:ArrowUp (ArrowUp) -> linux:103 (KEY_UP) -> atset1:57416 */
+  "AudioVolumeDown": 0xe02e, /* html:AudioVolumeDown (AudioVolumeDown) -> linux:114 (KEY_VOLUMEDOWN) -> atset1:57390 */
+  "AudioVolumeMute": 0xe020, /* html:AudioVolumeMute (AudioVolumeMute) -> linux:113 (KEY_MUTE) -> atset1:57376 */
+  "AudioVolumeUp": 0xe030, /* html:AudioVolumeUp (AudioVolumeUp) -> linux:115 (KEY_VOLUMEUP) -> atset1:57392 */
+  "Backquote": 0x29, /* html:Backquote (Backquote) -> linux:41 (KEY_GRAVE) -> atset1:41 */
+  "Backslash": 0x2b, /* html:Backslash (Backslash) -> linux:43 (KEY_BACKSLASH) -> atset1:43 */
+  "Backspace": 0xe, /* html:Backspace (Backspace) -> linux:14 (KEY_BACKSPACE) -> atset1:14 */
+  "BracketLeft": 0x1a, /* html:BracketLeft (BracketLeft) -> linux:26 (KEY_LEFTBRACE) -> atset1:26 */
+  "BracketRight": 0x1b, /* html:BracketRight (BracketRight) -> linux:27 (KEY_RIGHTBRACE) -> atset1:27 */
+  "BrowserBack": 0xe06a, /* html:BrowserBack (BrowserBack) -> linux:158 (KEY_BACK) -> atset1:57450 */
+  "BrowserFavorites": 0xe066, /* html:BrowserFavorites (BrowserFavorites) -> linux:156 (KEY_BOOKMARKS) -> atset1:57446 */
+  "BrowserForward": 0xe069, /* html:BrowserForward (BrowserForward) -> linux:159 (KEY_FORWARD) -> atset1:57449 */
+  "BrowserHome": 0xe032, /* html:BrowserHome (BrowserHome) -> linux:172 (KEY_HOMEPAGE) -> atset1:57394 */
+  "BrowserRefresh": 0xe067, /* html:BrowserRefresh (BrowserRefresh) -> linux:173 (KEY_REFRESH) -> atset1:57447 */
+  "BrowserSearch": 0xe065, /* html:BrowserSearch (BrowserSearch) -> linux:217 (KEY_SEARCH) -> atset1:57445 */
+  "BrowserStop": 0xe068, /* html:BrowserStop (BrowserStop) -> linux:128 (KEY_STOP) -> atset1:57448 */
+  "CapsLock": 0x3a, /* html:CapsLock (CapsLock) -> linux:58 (KEY_CAPSLOCK) -> atset1:58 */
+  "Comma": 0x33, /* html:Comma (Comma) -> linux:51 (KEY_COMMA) -> atset1:51 */
+  "ContextMenu": 0xe05d, /* html:ContextMenu (ContextMenu) -> linux:127 (KEY_COMPOSE) -> atset1:57437 */
+  "ControlLeft": 0x1d, /* html:ControlLeft (ControlLeft) -> linux:29 (KEY_LEFTCTRL) -> atset1:29 */
+  "ControlRight": 0xe01d, /* html:ControlRight (ControlRight) -> linux:97 (KEY_RIGHTCTRL) -> atset1:57373 */
+  "Convert": 0x79, /* html:Convert (Convert) -> linux:92 (KEY_HENKAN) -> atset1:121 */
+  "Copy": 0xe078, /* html:Copy (Copy) -> linux:133 (KEY_COPY) -> atset1:57464 */
+  "Cut": 0xe03c, /* html:Cut (Cut) -> linux:137 (KEY_CUT) -> atset1:57404 */
+  "Delete": 0xe053, /* html:Delete (Delete) -> linux:111 (KEY_DELETE) -> atset1:57427 */
+  "Digit0": 0xb, /* html:Digit0 (Digit0) -> linux:11 (KEY_0) -> atset1:11 */
+  "Digit1": 0x2, /* html:Digit1 (Digit1) -> linux:2 (KEY_1) -> atset1:2 */
+  "Digit2": 0x3, /* html:Digit2 (Digit2) -> linux:3 (KEY_2) -> atset1:3 */
+  "Digit3": 0x4, /* html:Digit3 (Digit3) -> linux:4 (KEY_3) -> atset1:4 */
+  "Digit4": 0x5, /* html:Digit4 (Digit4) -> linux:5 (KEY_4) -> atset1:5 */
+  "Digit5": 0x6, /* html:Digit5 (Digit5) -> linux:6 (KEY_5) -> atset1:6 */
+  "Digit6": 0x7, /* html:Digit6 (Digit6) -> linux:7 (KEY_6) -> atset1:7 */
+  "Digit7": 0x8, /* html:Digit7 (Digit7) -> linux:8 (KEY_7) -> atset1:8 */
+  "Digit8": 0x9, /* html:Digit8 (Digit8) -> linux:9 (KEY_8) -> atset1:9 */
+  "Digit9": 0xa, /* html:Digit9 (Digit9) -> linux:10 (KEY_9) -> atset1:10 */
+  "Eject": 0xe07d, /* html:Eject (Eject) -> linux:162 (KEY_EJECTCLOSECD) -> atset1:57469 */
+  "End": 0xe04f, /* html:End (End) -> linux:107 (KEY_END) -> atset1:57423 */
+  "Enter": 0x1c, /* html:Enter (Enter) -> linux:28 (KEY_ENTER) -> atset1:28 */
+  "Equal": 0xd, /* html:Equal (Equal) -> linux:13 (KEY_EQUAL) -> atset1:13 */
+  "Escape": 0x1, /* html:Escape (Escape) -> linux:1 (KEY_ESC) -> atset1:1 */
+  "F1": 0x3b, /* html:F1 (F1) -> linux:59 (KEY_F1) -> atset1:59 */
+  "F10": 0x44, /* html:F10 (F10) -> linux:68 (KEY_F10) -> atset1:68 */
+  "F11": 0x57, /* html:F11 (F11) -> linux:87 (KEY_F11) -> atset1:87 */
+  "F12": 0x58, /* html:F12 (F12) -> linux:88 (KEY_F12) -> atset1:88 */
+  "F13": 0x5d, /* html:F13 (F13) -> linux:183 (KEY_F13) -> atset1:93 */
+  "F14": 0x5e, /* html:F14 (F14) -> linux:184 (KEY_F14) -> atset1:94 */
+  "F15": 0x5f, /* html:F15 (F15) -> linux:185 (KEY_F15) -> atset1:95 */
+  "F16": 0x55, /* html:F16 (F16) -> linux:186 (KEY_F16) -> atset1:85 */
+  "F17": 0xe003, /* html:F17 (F17) -> linux:187 (KEY_F17) -> atset1:57347 */
+  "F18": 0xe077, /* html:F18 (F18) -> linux:188 (KEY_F18) -> atset1:57463 */
+  "F19": 0xe004, /* html:F19 (F19) -> linux:189 (KEY_F19) -> atset1:57348 */
+  "F2": 0x3c, /* html:F2 (F2) -> linux:60 (KEY_F2) -> atset1:60 */
+  "F20": 0x5a, /* html:F20 (F20) -> linux:190 (KEY_F20) -> atset1:90 */
+  "F21": 0x74, /* html:F21 (F21) -> linux:191 (KEY_F21) -> atset1:116 */
+  "F22": 0xe079, /* html:F22 (F22) -> linux:192 (KEY_F22) -> atset1:57465 */
+  "F23": 0x6d, /* html:F23 (F23) -> linux:193 (KEY_F23) -> atset1:109 */
+  "F24": 0x6f, /* html:F24 (F24) -> linux:194 (KEY_F24) -> atset1:111 */
+  "F3": 0x3d, /* html:F3 (F3) -> linux:61 (KEY_F3) -> atset1:61 */
+  "F4": 0x3e, /* html:F4 (F4) -> linux:62 (KEY_F4) -> atset1:62 */
+  "F5": 0x3f, /* html:F5 (F5) -> linux:63 (KEY_F5) -> atset1:63 */
+  "F6": 0x40, /* html:F6 (F6) -> linux:64 (KEY_F6) -> atset1:64 */
+  "F7": 0x41, /* html:F7 (F7) -> linux:65 (KEY_F7) -> atset1:65 */
+  "F8": 0x42, /* html:F8 (F8) -> linux:66 (KEY_F8) -> atset1:66 */
+  "F9": 0x43, /* html:F9 (F9) -> linux:67 (KEY_F9) -> atset1:67 */
+  "Find": 0xe041, /* html:Find (Find) -> linux:136 (KEY_FIND) -> atset1:57409 */
+  "Help": 0xe075, /* html:Help (Help) -> linux:138 (KEY_HELP) -> atset1:57461 */
+  "Hiragana": 0x77, /* html:Hiragana (Lang4) -> linux:91 (KEY_HIRAGANA) -> atset1:119 */
+  "Home": 0xe047, /* html:Home (Home) -> linux:102 (KEY_HOME) -> atset1:57415 */
+  "Insert": 0xe052, /* html:Insert (Insert) -> linux:110 (KEY_INSERT) -> atset1:57426 */
+  "IntlBackslash": 0x56, /* html:IntlBackslash (IntlBackslash) -> linux:86 (KEY_102ND) -> atset1:86 */
+  "IntlRo": 0x73, /* html:IntlRo (IntlRo) -> linux:89 (KEY_RO) -> atset1:115 */
+  "IntlYen": 0x7d, /* html:IntlYen (IntlYen) -> linux:124 (KEY_YEN) -> atset1:125 */
+  "KanaMode": 0x70, /* html:KanaMode (KanaMode) -> linux:93 (KEY_KATAKANAHIRAGANA) -> atset1:112 */
+  "Katakana": 0x78, /* html:Katakana (Lang3) -> linux:90 (KEY_KATAKANA) -> atset1:120 */
+  "KeyA": 0x1e, /* html:KeyA (KeyA) -> linux:30 (KEY_A) -> atset1:30 */
+  "KeyB": 0x30, /* html:KeyB (KeyB) -> linux:48 (KEY_B) -> atset1:48 */
+  "KeyC": 0x2e, /* html:KeyC (KeyC) -> linux:46 (KEY_C) -> atset1:46 */
+  "KeyD": 0x20, /* html:KeyD (KeyD) -> linux:32 (KEY_D) -> atset1:32 */
+  "KeyE": 0x12, /* html:KeyE (KeyE) -> linux:18 (KEY_E) -> atset1:18 */
+  "KeyF": 0x21, /* html:KeyF (KeyF) -> linux:33 (KEY_F) -> atset1:33 */
+  "KeyG": 0x22, /* html:KeyG (KeyG) -> linux:34 (KEY_G) -> atset1:34 */
+  "KeyH": 0x23, /* html:KeyH (KeyH) -> linux:35 (KEY_H) -> atset1:35 */
+  "KeyI": 0x17, /* html:KeyI (KeyI) -> linux:23 (KEY_I) -> atset1:23 */
+  "KeyJ": 0x24, /* html:KeyJ (KeyJ) -> linux:36 (KEY_J) -> atset1:36 */
+  "KeyK": 0x25, /* html:KeyK (KeyK) -> linux:37 (KEY_K) -> atset1:37 */
+  "KeyL": 0x26, /* html:KeyL (KeyL) -> linux:38 (KEY_L) -> atset1:38 */
+  "KeyM": 0x32, /* html:KeyM (KeyM) -> linux:50 (KEY_M) -> atset1:50 */
+  "KeyN": 0x31, /* html:KeyN (KeyN) -> linux:49 (KEY_N) -> atset1:49 */
+  "KeyO": 0x18, /* html:KeyO (KeyO) -> linux:24 (KEY_O) -> atset1:24 */
+  "KeyP": 0x19, /* html:KeyP (KeyP) -> linux:25 (KEY_P) -> atset1:25 */
+  "KeyQ": 0x10, /* html:KeyQ (KeyQ) -> linux:16 (KEY_Q) -> atset1:16 */
+  "KeyR": 0x13, /* html:KeyR (KeyR) -> linux:19 (KEY_R) -> atset1:19 */
+  "KeyS": 0x1f, /* html:KeyS (KeyS) -> linux:31 (KEY_S) -> atset1:31 */
+  "KeyT": 0x14, /* html:KeyT (KeyT) -> linux:20 (KEY_T) -> atset1:20 */
+  "KeyU": 0x16, /* html:KeyU (KeyU) -> linux:22 (KEY_U) -> atset1:22 */
+  "KeyV": 0x2f, /* html:KeyV (KeyV) -> linux:47 (KEY_V) -> atset1:47 */
+  "KeyW": 0x11, /* html:KeyW (KeyW) -> linux:17 (KEY_W) -> atset1:17 */
+  "KeyX": 0x2d, /* html:KeyX (KeyX) -> linux:45 (KEY_X) -> atset1:45 */
+  "KeyY": 0x15, /* html:KeyY (KeyY) -> linux:21 (KEY_Y) -> atset1:21 */
+  "KeyZ": 0x2c, /* html:KeyZ (KeyZ) -> linux:44 (KEY_Z) -> atset1:44 */
+  "Lang1": 0x72, /* html:Lang1 (Lang1) -> linux:122 (KEY_HANGEUL) -> atset1:114 */
+  "Lang2": 0x71, /* html:Lang2 (Lang2) -> linux:123 (KEY_HANJA) -> atset1:113 */
+  "Lang3": 0x78, /* html:Lang3 (Lang3) -> linux:90 (KEY_KATAKANA) -> atset1:120 */
+  "Lang4": 0x77, /* html:Lang4 (Lang4) -> linux:91 (KEY_HIRAGANA) -> atset1:119 */
+  "Lang5": 0x76, /* html:Lang5 (Lang5) -> linux:85 (KEY_ZENKAKUHANKAKU) -> atset1:118 */
+  "LaunchApp1": 0xe06b, /* html:LaunchApp1 (LaunchApp1) -> linux:157 (KEY_COMPUTER) -> atset1:57451 */
+  "LaunchApp2": 0xe021, /* html:LaunchApp2 (LaunchApp2) -> linux:140 (KEY_CALC) -> atset1:57377 */
+  "LaunchMail": 0xe06c, /* html:LaunchMail (LaunchMail) -> linux:155 (KEY_MAIL) -> atset1:57452 */
+  "MediaPlayPause": 0xe022, /* html:MediaPlayPause (MediaPlayPause) -> linux:164 (KEY_PLAYPAUSE) -> atset1:57378 */
+  "MediaSelect": 0xe06d, /* html:MediaSelect (MediaSelect) -> linux:226 (KEY_MEDIA) -> atset1:57453 */
+  "MediaStop": 0xe024, /* html:MediaStop (MediaStop) -> linux:166 (KEY_STOPCD) -> atset1:57380 */
+  "MediaTrackNext": 0xe019, /* html:MediaTrackNext (MediaTrackNext) -> linux:163 (KEY_NEXTSONG) -> atset1:57369 */
+  "MediaTrackPrevious": 0xe010, /* html:MediaTrackPrevious (MediaTrackPrevious) -> linux:165 (KEY_PREVIOUSSONG) -> atset1:57360 */
+  "MetaLeft": 0xe05b, /* html:MetaLeft (MetaLeft) -> linux:125 (KEY_LEFTMETA) -> atset1:57435 */
+  "MetaRight": 0xe05c, /* html:MetaRight (MetaRight) -> linux:126 (KEY_RIGHTMETA) -> atset1:57436 */
+  "Minus": 0xc, /* html:Minus (Minus) -> linux:12 (KEY_MINUS) -> atset1:12 */
+  "NonConvert": 0x7b, /* html:NonConvert (NonConvert) -> linux:94 (KEY_MUHENKAN) -> atset1:123 */
+  "NumLock": 0x45, /* html:NumLock (NumLock) -> linux:69 (KEY_NUMLOCK) -> atset1:69 */
+  "Numpad0": 0x52, /* html:Numpad0 (Numpad0) -> linux:82 (KEY_KP0) -> atset1:82 */
+  "Numpad1": 0x4f, /* html:Numpad1 (Numpad1) -> linux:79 (KEY_KP1) -> atset1:79 */
+  "Numpad2": 0x50, /* html:Numpad2 (Numpad2) -> linux:80 (KEY_KP2) -> atset1:80 */
+  "Numpad3": 0x51, /* html:Numpad3 (Numpad3) -> linux:81 (KEY_KP3) -> atset1:81 */
+  "Numpad4": 0x4b, /* html:Numpad4 (Numpad4) -> linux:75 (KEY_KP4) -> atset1:75 */
+  "Numpad5": 0x4c, /* html:Numpad5 (Numpad5) -> linux:76 (KEY_KP5) -> atset1:76 */
+  "Numpad6": 0x4d, /* html:Numpad6 (Numpad6) -> linux:77 (KEY_KP6) -> atset1:77 */
+  "Numpad7": 0x47, /* html:Numpad7 (Numpad7) -> linux:71 (KEY_KP7) -> atset1:71 */
+  "Numpad8": 0x48, /* html:Numpad8 (Numpad8) -> linux:72 (KEY_KP8) -> atset1:72 */
+  "Numpad9": 0x49, /* html:Numpad9 (Numpad9) -> linux:73 (KEY_KP9) -> atset1:73 */
+  "NumpadAdd": 0x4e, /* html:NumpadAdd (NumpadAdd) -> linux:78 (KEY_KPPLUS) -> atset1:78 */
+  "NumpadComma": 0x7e, /* html:NumpadComma (NumpadComma) -> linux:121 (KEY_KPCOMMA) -> atset1:126 */
+  "NumpadDecimal": 0x53, /* html:NumpadDecimal (NumpadDecimal) -> linux:83 (KEY_KPDOT) -> atset1:83 */
+  "NumpadDivide": 0xe035, /* html:NumpadDivide (NumpadDivide) -> linux:98 (KEY_KPSLASH) -> atset1:57397 */
+  "NumpadEnter": 0xe01c, /* html:NumpadEnter (NumpadEnter) -> linux:96 (KEY_KPENTER) -> atset1:57372 */
+  "NumpadEqual": 0x59, /* html:NumpadEqual (NumpadEqual) -> linux:117 (KEY_KPEQUAL) -> atset1:89 */
+  "NumpadMultiply": 0x37, /* html:NumpadMultiply (NumpadMultiply) -> linux:55 (KEY_KPASTERISK) -> atset1:55 */
+  "NumpadParenLeft": 0xe076, /* html:NumpadParenLeft (NumpadParenLeft) -> linux:179 (KEY_KPLEFTPAREN) -> atset1:57462 */
+  "NumpadParenRight": 0xe07b, /* html:NumpadParenRight (NumpadParenRight) -> linux:180 (KEY_KPRIGHTPAREN) -> atset1:57467 */
+  "NumpadSubtract": 0x4a, /* html:NumpadSubtract (NumpadSubtract) -> linux:74 (KEY_KPMINUS) -> atset1:74 */
+  "Open": 0x64, /* html:Open (Open) -> linux:134 (KEY_OPEN) -> atset1:100 */
+  "PageDown": 0xe051, /* html:PageDown (PageDown) -> linux:109 (KEY_PAGEDOWN) -> atset1:57425 */
+  "PageUp": 0xe049, /* html:PageUp (PageUp) -> linux:104 (KEY_PAGEUP) -> atset1:57417 */
+  "Paste": 0x65, /* html:Paste (Paste) -> linux:135 (KEY_PASTE) -> atset1:101 */
+  "Pause": 0xe046, /* html:Pause (Pause) -> linux:119 (KEY_PAUSE) -> atset1:57414 */
+  "Period": 0x34, /* html:Period (Period) -> linux:52 (KEY_DOT) -> atset1:52 */
+  "Power": 0xe05e, /* html:Power (Power) -> linux:116 (KEY_POWER) -> atset1:57438 */
+  "PrintScreen": 0x54, /* html:PrintScreen (PrintScreen) -> linux:99 (KEY_SYSRQ) -> atset1:84 */
+  "Props": 0xe006, /* html:Props (Props) -> linux:130 (KEY_PROPS) -> atset1:57350 */
+  "Quote": 0x28, /* html:Quote (Quote) -> linux:40 (KEY_APOSTROPHE) -> atset1:40 */
+  "ScrollLock": 0x46, /* html:ScrollLock (ScrollLock) -> linux:70 (KEY_SCROLLLOCK) -> atset1:70 */
+  "Semicolon": 0x27, /* html:Semicolon (Semicolon) -> linux:39 (KEY_SEMICOLON) -> atset1:39 */
+  "ShiftLeft": 0x2a, /* html:ShiftLeft (ShiftLeft) -> linux:42 (KEY_LEFTSHIFT) -> atset1:42 */
+  "ShiftRight": 0x36, /* html:ShiftRight (ShiftRight) -> linux:54 (KEY_RIGHTSHIFT) -> atset1:54 */
+  "Slash": 0x35, /* html:Slash (Slash) -> linux:53 (KEY_SLASH) -> atset1:53 */
+  "Sleep": 0xe05f, /* html:Sleep (Sleep) -> linux:142 (KEY_SLEEP) -> atset1:57439 */
+  "Space": 0x39, /* html:Space (Space) -> linux:57 (KEY_SPACE) -> atset1:57 */
+  "Suspend": 0xe025, /* html:Suspend (Suspend) -> linux:205 (KEY_SUSPEND) -> atset1:57381 */
+  "Tab": 0xf, /* html:Tab (Tab) -> linux:15 (KEY_TAB) -> atset1:15 */
+  "Undo": 0xe007, /* html:Undo (Undo) -> linux:131 (KEY_UNDO) -> atset1:57351 */
+  "WakeUp": 0xe063, /* html:WakeUp (WakeUp) -> linux:143 (KEY_WAKEUP) -> atset1:57443 */
+};
diff --git a/app/src/main/assets/novnc/core/rfb.js b/app/src/main/assets/novnc/core/rfb.js
new file mode 100644
index 00000000..ea3bf58a
--- /dev/null
+++ b/app/src/main/assets/novnc/core/rfb.js
@@ -0,0 +1,2988 @@
+/*
+ * 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.
+ *
+ */
+
+import { toUnsigned32bit, toSigned32bit } from './util/int.js';
+import * as Log from './util/logging.js';
+import { encodeUTF8, decodeUTF8 } from './util/strings.js';
+import { dragThreshold } from './util/browser.js';
+import { clientToElement } from './util/element.js';
+import { setCapture } from './util/events.js';
+import EventTargetMixin from './util/eventtarget.js';
+import Display from "./display.js";
+import Inflator from "./inflator.js";
+import Deflator from "./deflator.js";
+import Keyboard from "./input/keyboard.js";
+import GestureHandler from "./input/gesturehandler.js";
+import Cursor from "./util/cursor.js";
+import Websock from "./websock.js";
+import DES from "./des.js";
+import KeyTable from "./input/keysym.js";
+import XtScancode from "./input/xtscancodes.js";
+import { encodings } from "./encodings.js";
+
+import RawDecoder from "./decoders/raw.js";
+import CopyRectDecoder from "./decoders/copyrect.js";
+import RREDecoder from "./decoders/rre.js";
+import HextileDecoder from "./decoders/hextile.js";
+import TightDecoder from "./decoders/tight.js";
+import TightPNGDecoder from "./decoders/tightpng.js";
+
+// How many seconds to wait for a disconnect to finish
+const DISCONNECT_TIMEOUT = 3;
+const DEFAULT_BACKGROUND = 'rgb(40, 40, 40)';
+
+// Minimum wait (ms) between two mouse moves
+const MOUSE_MOVE_DELAY = 17;
+
+// Wheel thresholds
+const WHEEL_STEP = 50; // 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;
+
+// Extended clipboard pseudo-encoding formats
+const extendedClipboardFormatText   = 1;
+/*eslint-disable no-unused-vars */
+const extendedClipboardFormatRtf    = 1 << 1;
+const extendedClipboardFormatHtml   = 1 << 2;
+const extendedClipboardFormatDib    = 1 << 3;
+const extendedClipboardFormatFiles  = 1 << 4;
+/*eslint-enable */
+
+// Extended clipboard pseudo-encoding actions
+const extendedClipboardActionCaps    = 1 << 24;
+const extendedClipboardActionRequest = 1 << 25;
+const extendedClipboardActionPeek    = 1 << 26;
+const extendedClipboardActionNotify  = 1 << 27;
+const extendedClipboardActionProvide = 1 << 28;
+
+export default class RFB extends EventTargetMixin {
+    constructor(target, urlOrChannel, options) {
+        if (!target) {
+            throw new Error("Must specify target");
+        }
+        if (!urlOrChannel) {
+            throw new Error("Must specify URL, WebSocket or RTCDataChannel");
+        }
+
+        super();
+
+        this._target = target;
+
+        if (typeof urlOrChannel === "string") {
+            this._url = urlOrChannel;
+        } else {
+            this._url = null;
+            this._rawChannel = urlOrChannel;
+        }
+
+        // Connection details
+        options = options || {};
+        this._rfbCredentials = options.credentials || {};
+        this._shared = 'shared' in options ? !!options.shared : true;
+        this._repeaterID = options.repeaterID || '';
+        this._wsProtocols = options.wsProtocols || [];
+
+        // Internal state
+        this._rfbConnectionState = '';
+        this._rfbInitState = '';
+        this._rfbAuthScheme = -1;
+        this._rfbCleanDisconnect = true;
+
+        // Server capabilities
+        this._rfbVersion = 0;
+        this._rfbMaxVersion = 3.8;
+        this._rfbTightVNC = false;
+        this._rfbVeNCryptState = 0;
+        this._rfbXvpVer = 0;
+
+        this._fbWidth = 0;
+        this._fbHeight = 0;
+
+        this._fbName = "";
+
+        this._capabilities = { power: false };
+
+        this._supportsFence = false;
+
+        this._supportsContinuousUpdates = false;
+        this._enabledContinuousUpdates = false;
+
+        this._supportsSetDesktopSize = false;
+        this._screenID = 0;
+        this._screenFlags = 0;
+
+        this._qemuExtKeyEventSupported = false;
+
+        this._clipboardText = null;
+        this._clipboardServerCapabilitiesActions = {};
+        this._clipboardServerCapabilitiesFormats = {};
+
+        // Internal objects
+        this._sock = null;              // Websock object
+        this._display = null;           // Display object
+        this._flushing = false;         // Display flushing state
+        this._keyboard = null;          // Keyboard input handler object
+        this._gestures = null;          // Gesture input handler object
+        this._resizeObserver = null;    // Resize observer object
+
+        // Timers
+        this._disconnTimer = null;      // disconnection timer
+        this._resizeTimeout = null;     // resize rate limiting
+        this._mouseMoveTimer = null;
+
+        // Decoder states
+        this._decoders = {};
+
+        this._FBU = {
+            rects: 0,
+            x: 0,
+            y: 0,
+            width: 0,
+            height: 0,
+            encoding: null,
+        };
+
+        // Mouse state
+        this._mousePos = {};
+        this._mouseButtonMask = 0;
+        this._mouseLastMoveTime = 0;
+        this._viewportDragging = false;
+        this._viewportDragPos = {};
+        this._viewportHasMoved = false;
+        this._accumulatedWheelDeltaX = 0;
+        this._accumulatedWheelDeltaY = 0;
+
+        // Gesture state
+        this._gestureLastTapTime = null;
+        this._gestureFirstDoubleTapEv = null;
+        this._gestureLastMagnitudeX = 0;
+        this._gestureLastMagnitudeY = 0;
+
+        // Bound event handlers
+        this._eventHandlers = {
+            focusCanvas: this._focusCanvas.bind(this),
+            handleResize: this._handleResize.bind(this),
+            handleMouse: this._handleMouse.bind(this),
+            handleWheel: this._handleWheel.bind(this),
+            handleGesture: this._handleGesture.bind(this),
+        };
+
+        // main setup
+        Log.Debug(">> RFB.constructor");
+
+        // Create DOM elements
+        this._screen = document.createElement('div');
+        this._screen.style.display = 'flex';
+        this._screen.style.width = '100%';
+        this._screen.style.height = '100%';
+        this._screen.style.overflow = 'auto';
+        this._screen.style.background = DEFAULT_BACKGROUND;
+        this._canvas = document.createElement('canvas');
+        this._canvas.style.margin = 'auto';
+        // Some browsers add an outline on focus
+        this._canvas.style.outline = 'none';
+        this._canvas.width = 0;
+        this._canvas.height = 0;
+        this._canvas.tabIndex = -1;
+        this._screen.appendChild(this._canvas);
+
+        // Cursor
+        this._cursor = new Cursor();
+
+        // XXX: TightVNC 2.8.11 sends no cursor at all until Windows changes
+        // it. Result: no cursor at all until a window border or an edit field
+        // is hit blindly. But there are also VNC servers that draw the cursor
+        // in the framebuffer and don't send the empty local cursor. There is
+        // no way to satisfy both sides.
+        //
+        // The spec is unclear on this "initial cursor" issue. Many other
+        // viewers (TigerVNC, RealVNC, Remmina) display an arrow as the
+        // initial cursor instead.
+        this._cursorImage = RFB.cursors.none;
+
+        // populate decoder array with objects
+        this._decoders[encodings.encodingRaw] = new RawDecoder();
+        this._decoders[encodings.encodingCopyRect] = new CopyRectDecoder();
+        this._decoders[encodings.encodingRRE] = new RREDecoder();
+        this._decoders[encodings.encodingHextile] = new HextileDecoder();
+        this._decoders[encodings.encodingTight] = new TightDecoder();
+        this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder();
+
+        // NB: nothing that needs explicit teardown should be done
+        // before this point, since this can throw an exception
+        try {
+            this._display = new Display(this._canvas);
+        } catch (exc) {
+            Log.Error("Display exception: " + exc);
+            throw exc;
+        }
+        this._display.onflush = this._onFlush.bind(this);
+
+        this._keyboard = new Keyboard(this._canvas);
+        this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
+
+        this._gestures = new GestureHandler();
+
+        this._sock = new Websock();
+        this._sock.on('open', this._socketOpen.bind(this));
+        this._sock.on('close', this._socketClose.bind(this));
+        this._sock.on('message', this._handleMessage.bind(this));
+        this._sock.on('error', this._socketError.bind(this));
+
+        this._resizeObserver = new ResizeObserver(this._eventHandlers.handleResize);
+
+        // All prepared, kick off the connection
+        this._updateConnectionState('connecting');
+
+        Log.Debug("<< RFB.constructor");
+
+        // ===== PROPERTIES =====
+
+        this.dragViewport = false;
+        this.focusOnClick = true;
+
+        this._viewOnly = false;
+        this._clipViewport = false;
+        this._scaleViewport = false;
+        this._resizeSession = false;
+
+        this._showDotCursor = false;
+        if (options.showDotCursor !== undefined) {
+            Log.Warn("Specifying showDotCursor as a RFB constructor argument is deprecated");
+            this._showDotCursor = options.showDotCursor;
+        }
+
+        this._qualityLevel = 6;
+        this._compressionLevel = 2;
+    }
+
+    // ===== PROPERTIES =====
+
+    get viewOnly() { return this._viewOnly; }
+    set viewOnly(viewOnly) {
+        this._viewOnly = viewOnly;
+
+        if (this._rfbConnectionState === "connecting" ||
+            this._rfbConnectionState === "connected") {
+            if (viewOnly) {
+                this._keyboard.ungrab();
+            } else {
+                this._keyboard.grab();
+            }
+        }
+    }
+
+    get capabilities() { return this._capabilities; }
+
+    get touchButton() { return 0; }
+    set touchButton(button) { Log.Warn("Using old API!"); }
+
+    get clipViewport() { return this._clipViewport; }
+    set clipViewport(viewport) {
+        this._clipViewport = viewport;
+        this._updateClip();
+    }
+
+    get scaleViewport() { return this._scaleViewport; }
+    set scaleViewport(scale) {
+        this._scaleViewport = scale;
+        // Scaling trumps clipping, so we may need to adjust
+        // clipping when enabling or disabling scaling
+        if (scale && this._clipViewport) {
+            this._updateClip();
+        }
+        this._updateScale();
+        if (!scale && this._clipViewport) {
+            this._updateClip();
+        }
+    }
+
+    get resizeSession() { return this._resizeSession; }
+    set resizeSession(resize) {
+        this._resizeSession = resize;
+        if (resize) {
+            this._requestRemoteResize();
+        }
+    }
+
+    get showDotCursor() { return this._showDotCursor; }
+    set showDotCursor(show) {
+        this._showDotCursor = show;
+        this._refreshCursor();
+    }
+
+    get background() { return this._screen.style.background; }
+    set background(cssValue) { this._screen.style.background = cssValue; }
+
+    get qualityLevel() {
+        return this._qualityLevel;
+    }
+    set qualityLevel(qualityLevel) {
+        if (!Number.isInteger(qualityLevel) || qualityLevel < 0 || qualityLevel > 9) {
+            Log.Error("qualityLevel must be an integer between 0 and 9");
+            return;
+        }
+
+        if (this._qualityLevel === qualityLevel) {
+            return;
+        }
+
+        this._qualityLevel = qualityLevel;
+
+        if (this._rfbConnectionState === 'connected') {
+            this._sendEncodings();
+        }
+    }
+
+    get compressionLevel() {
+        return this._compressionLevel;
+    }
+    set compressionLevel(compressionLevel) {
+        if (!Number.isInteger(compressionLevel) || compressionLevel < 0 || compressionLevel > 9) {
+            Log.Error("compressionLevel must be an integer between 0 and 9");
+            return;
+        }
+
+        if (this._compressionLevel === compressionLevel) {
+            return;
+        }
+
+        this._compressionLevel = compressionLevel;
+
+        if (this._rfbConnectionState === 'connected') {
+            this._sendEncodings();
+        }
+    }
+
+    // ===== PUBLIC METHODS =====
+
+    disconnect() {
+        this._updateConnectionState('disconnecting');
+        this._sock.off('error');
+        this._sock.off('message');
+        this._sock.off('open');
+    }
+
+    sendCredentials(creds) {
+        this._rfbCredentials = creds;
+        setTimeout(this._initMsg.bind(this), 0);
+    }
+
+    sendCtrlAltDel() {
+        if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }
+        Log.Info("Sending Ctrl-Alt-Del");
+
+        this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
+        this.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);
+        this.sendKey(KeyTable.XK_Delete, "Delete", true);
+        this.sendKey(KeyTable.XK_Delete, "Delete", false);
+        this.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
+        this.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
+    }
+
+    machineShutdown() {
+        this._xvpOp(1, 2);
+    }
+
+    machineReboot() {
+        this._xvpOp(1, 3);
+    }
+
+    machineReset() {
+        this._xvpOp(1, 4);
+    }
+
+    // Send a key press. If 'down' is not specified then send a down key
+    // followed by an up key.
+    sendKey(keysym, code, down) {
+        if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }
+
+        if (down === undefined) {
+            this.sendKey(keysym, code, true);
+            this.sendKey(keysym, code, false);
+            return;
+        }
+
+        const scancode = XtScancode[code];
+
+        if (this._qemuExtKeyEventSupported && scancode) {
+            // 0 is NoSymbol
+            keysym = keysym || 0;
+
+            Log.Info("Sending key (" + (down ? "down" : "up") + "): keysym " + keysym + ", scancode " + scancode);
+
+            RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode);
+        } else {
+            if (!keysym) {
+                return;
+            }
+            Log.Info("Sending keysym (" + (down ? "down" : "up") + "): " + keysym);
+            RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0);
+        }
+    }
+
+    focus() {
+        this._canvas.focus();
+    }
+
+    blur() {
+        this._canvas.blur();
+    }
+
+    clipboardPasteFrom(text) {
+        if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }
+
+        if (this._clipboardServerCapabilitiesFormats[extendedClipboardFormatText] &&
+            this._clipboardServerCapabilitiesActions[extendedClipboardActionNotify]) {
+
+            this._clipboardText = text;
+            RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]);
+        } else {
+            let data = new Uint8Array(text.length);
+            for (let i = 0; i < text.length; i++) {
+                // FIXME: text can have values outside of Latin1/Uint8
+                data[i] = text.charCodeAt(i);
+            }
+
+            RFB.messages.clientCutText(this._sock, data);
+        }
+    }
+
+    // ===== PRIVATE METHODS =====
+
+    _connect() {
+        Log.Debug(">> RFB.connect");
+
+        if (this._url) {
+            Log.Info(`connecting to ${this._url}`);
+            this._sock.open(this._url, this._wsProtocols);
+        } else {
+            Log.Info(`attaching ${this._rawChannel} to Websock`);
+            this._sock.attach(this._rawChannel);
+
+            if (this._sock.readyState === 'closed') {
+                throw Error("Cannot use already closed WebSocket/RTCDataChannel");
+            }
+
+            if (this._sock.readyState === 'open') {
+                // FIXME: _socketOpen() can in theory call _fail(), which
+                //        isn't allowed this early, but I'm not sure that can
+                //        happen without a bug messing up our state variables
+                this._socketOpen();
+            }
+        }
+
+        // Make our elements part of the page
+        this._target.appendChild(this._screen);
+
+        this._gestures.attach(this._canvas);
+
+        this._cursor.attach(this._canvas);
+        this._refreshCursor();
+
+        // Monitor size changes of the screen element
+        this._resizeObserver.observe(this._screen);
+
+        // Always grab focus on some kind of click event
+        this._canvas.addEventListener("mousedown", this._eventHandlers.focusCanvas);
+        this._canvas.addEventListener("touchstart", this._eventHandlers.focusCanvas);
+
+        // Mouse events
+        this._canvas.addEventListener('mousedown', this._eventHandlers.handleMouse);
+        this._canvas.addEventListener('mouseup', this._eventHandlers.handleMouse);
+        this._canvas.addEventListener('mousemove', this._eventHandlers.handleMouse);
+        // Prevent middle-click pasting (see handler for why we bind to document)
+        this._canvas.addEventListener('click', this._eventHandlers.handleMouse);
+        // preventDefault() on mousedown doesn't stop this event for some
+        // reason so we have to explicitly block it
+        this._canvas.addEventListener('contextmenu', this._eventHandlers.handleMouse);
+
+        // Wheel events
+        this._canvas.addEventListener("wheel", this._eventHandlers.handleWheel);
+
+        // Gesture events
+        this._canvas.addEventListener("gesturestart", this._eventHandlers.handleGesture);
+        this._canvas.addEventListener("gesturemove", this._eventHandlers.handleGesture);
+        this._canvas.addEventListener("gestureend", this._eventHandlers.handleGesture);
+
+        Log.Debug("<< RFB.connect");
+    }
+
+    _disconnect() {
+        Log.Debug(">> RFB.disconnect");
+        this._cursor.detach();
+        this._canvas.removeEventListener("gesturestart", this._eventHandlers.handleGesture);
+        this._canvas.removeEventListener("gesturemove", this._eventHandlers.handleGesture);
+        this._canvas.removeEventListener("gestureend", this._eventHandlers.handleGesture);
+        this._canvas.removeEventListener("wheel", this._eventHandlers.handleWheel);
+        this._canvas.removeEventListener('mousedown', this._eventHandlers.handleMouse);
+        this._canvas.removeEventListener('mouseup', this._eventHandlers.handleMouse);
+        this._canvas.removeEventListener('mousemove', this._eventHandlers.handleMouse);
+        this._canvas.removeEventListener('click', this._eventHandlers.handleMouse);
+        this._canvas.removeEventListener('contextmenu', this._eventHandlers.handleMouse);
+        this._canvas.removeEventListener("mousedown", this._eventHandlers.focusCanvas);
+        this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas);
+        this._resizeObserver.disconnect();
+        this._keyboard.ungrab();
+        this._gestures.detach();
+        this._sock.close();
+        try {
+            this._target.removeChild(this._screen);
+        } catch (e) {
+            if (e.name === 'NotFoundError') {
+                // Some cases where the initial connection fails
+                // can disconnect before the _screen is created
+            } else {
+                throw e;
+            }
+        }
+        clearTimeout(this._resizeTimeout);
+        clearTimeout(this._mouseMoveTimer);
+        Log.Debug("<< RFB.disconnect");
+    }
+
+    _socketOpen() {
+        if ((this._rfbConnectionState === 'connecting') &&
+            (this._rfbInitState === '')) {
+            this._rfbInitState = 'ProtocolVersion';
+            Log.Debug("Starting VNC handshake");
+        } else {
+            this._fail("Unexpected server connection while " +
+                       this._rfbConnectionState);
+        }
+    }
+
+    _socketClose(e) {
+        Log.Debug("WebSocket on-close event");
+        let msg = "";
+        if (e.code) {
+            msg = "(code: " + e.code;
+            if (e.reason) {
+                msg += ", reason: " + e.reason;
+            }
+            msg += ")";
+        }
+        switch (this._rfbConnectionState) {
+            case 'connecting':
+                this._fail("Connection closed " + msg);
+                break;
+            case 'connected':
+                // Handle disconnects that were initiated server-side
+                this._updateConnectionState('disconnecting');
+                this._updateConnectionState('disconnected');
+                break;
+            case 'disconnecting':
+                // Normal disconnection path
+                this._updateConnectionState('disconnected');
+                break;
+            case 'disconnected':
+                this._fail("Unexpected server disconnect " +
+                           "when already disconnected " + msg);
+                break;
+            default:
+                this._fail("Unexpected server disconnect before connecting " +
+                           msg);
+                break;
+        }
+        this._sock.off('close');
+        // Delete reference to raw channel to allow cleanup.
+        this._rawChannel = null;
+    }
+
+    _socketError(e) {
+        Log.Warn("WebSocket on-error event");
+    }
+
+    _focusCanvas(event) {
+        if (!this.focusOnClick) {
+            return;
+        }
+
+        this.focus();
+    }
+
+    _setDesktopName(name) {
+        this._fbName = name;
+        this.dispatchEvent(new CustomEvent(
+            "desktopname",
+            { detail: { name: this._fbName } }));
+    }
+
+    _handleResize() {
+        // If the window resized then our screen element might have
+        // as well. Update the viewport dimensions.
+        window.requestAnimationFrame(() => {
+            this._updateClip();
+            this._updateScale();
+        });
+
+        if (this._resizeSession) {
+            // Request changing the resolution of the remote display to
+            // the size of the local browser viewport.
+
+            // In order to not send multiple requests before the browser-resize
+            // is finished we wait 0.5 seconds before sending the request.
+            clearTimeout(this._resizeTimeout);
+            this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this), 500);
+        }
+    }
+
+    // Update state of clipping in Display object, and make sure the
+    // configured viewport matches the current screen size
+    _updateClip() {
+        const curClip = this._display.clipViewport;
+        let newClip = this._clipViewport;
+
+        if (this._scaleViewport) {
+            // Disable viewport clipping if we are scaling
+            newClip = false;
+        }
+
+        if (curClip !== newClip) {
+            this._display.clipViewport = newClip;
+        }
+
+        if (newClip) {
+            // When clipping is enabled, the screen is limited to
+            // the size of the container.
+            const size = this._screenSize();
+            this._display.viewportChangeSize(size.w, size.h);
+            this._fixScrollbars();
+        }
+    }
+
+    _updateScale() {
+        if (!this._scaleViewport) {
+            this._display.scale = 1.0;
+        } else {
+            const size = this._screenSize();
+            this._display.autoscale(size.w, size.h);
+        }
+        this._fixScrollbars();
+    }
+
+    // Requests a change of remote desktop size. This message is an extension
+    // and may only be sent if we have received an ExtendedDesktopSize message
+    _requestRemoteResize() {
+        clearTimeout(this._resizeTimeout);
+        this._resizeTimeout = null;
+
+        if (!this._resizeSession || this._viewOnly ||
+            !this._supportsSetDesktopSize) {
+            return;
+        }
+
+        const size = this._screenSize();
+        RFB.messages.setDesktopSize(this._sock,
+                                    Math.floor(size.w), Math.floor(size.h),
+                                    this._screenID, this._screenFlags);
+
+        Log.Debug('Requested new desktop size: ' +
+                   size.w + 'x' + size.h);
+    }
+
+    // Gets the the size of the available screen
+    _screenSize() {
+        let r = this._screen.getBoundingClientRect();
+        return { w: r.width, h: r.height };
+    }
+
+    _fixScrollbars() {
+        // This is a hack because Chrome screws up the calculation
+        // for when scrollbars are needed. So to fix it we temporarily
+        // toggle them off and on.
+        const orig = this._screen.style.overflow;
+        this._screen.style.overflow = 'hidden';
+        // Force Chrome to recalculate the layout by asking for
+        // an element's dimensions
+        this._screen.getBoundingClientRect();
+        this._screen.style.overflow = orig;
+    }
+
+    /*
+     * Connection states:
+     *   connecting
+     *   connected
+     *   disconnecting
+     *   disconnected - permanent state
+     */
+    _updateConnectionState(state) {
+        const oldstate = this._rfbConnectionState;
+
+        if (state === oldstate) {
+            Log.Debug("Already in state '" + state + "', ignoring");
+            return;
+        }
+
+        // The 'disconnected' state is permanent for each RFB object
+        if (oldstate === 'disconnected') {
+            Log.Error("Tried changing state of a disconnected RFB object");
+            return;
+        }
+
+        // Ensure proper transitions before doing anything
+        switch (state) {
+            case 'connected':
+                if (oldstate !== 'connecting') {
+                    Log.Error("Bad transition to connected state, " +
+                               "previous connection state: " + oldstate);
+                    return;
+                }
+                break;
+
+            case 'disconnected':
+                if (oldstate !== 'disconnecting') {
+                    Log.Error("Bad transition to disconnected state, " +
+                               "previous connection state: " + oldstate);
+                    return;
+                }
+                break;
+
+            case 'connecting':
+                if (oldstate !== '') {
+                    Log.Error("Bad transition to connecting state, " +
+                               "previous connection state: " + oldstate);
+                    return;
+                }
+                break;
+
+            case 'disconnecting':
+                if (oldstate !== 'connected' && oldstate !== 'connecting') {
+                    Log.Error("Bad transition to disconnecting state, " +
+                               "previous connection state: " + oldstate);
+                    return;
+                }
+                break;
+
+            default:
+                Log.Error("Unknown connection state: " + state);
+                return;
+        }
+
+        // State change actions
+
+        this._rfbConnectionState = state;
+
+        Log.Debug("New state '" + state + "', was '" + oldstate + "'.");
+
+        if (this._disconnTimer && state !== 'disconnecting') {
+            Log.Debug("Clearing disconnect timer");
+            clearTimeout(this._disconnTimer);
+            this._disconnTimer = null;
+
+            // make sure we don't get a double event
+            this._sock.off('close');
+        }
+
+        switch (state) {
+            case 'connecting':
+                this._connect();
+                break;
+
+            case 'connected':
+                this.dispatchEvent(new CustomEvent("connect", { detail: {} }));
+                break;
+
+            case 'disconnecting':
+                this._disconnect();
+
+                this._disconnTimer = setTimeout(() => {
+                    Log.Error("Disconnection timed out.");
+                    this._updateConnectionState('disconnected');
+                }, DISCONNECT_TIMEOUT * 1000);
+                break;
+
+            case 'disconnected':
+                this.dispatchEvent(new CustomEvent(
+                    "disconnect", { detail:
+                                    { clean: this._rfbCleanDisconnect } }));
+                break;
+        }
+    }
+
+    /* Print errors and disconnect
+     *
+     * The parameter 'details' is used for information that
+     * should be logged but not sent to the user interface.
+     */
+    _fail(details) {
+        switch (this._rfbConnectionState) {
+            case 'disconnecting':
+                Log.Error("Failed when disconnecting: " + details);
+                break;
+            case 'connected':
+                Log.Error("Failed while connected: " + details);
+                break;
+            case 'connecting':
+                Log.Error("Failed when connecting: " + details);
+                break;
+            default:
+                Log.Error("RFB failure: " + details);
+                break;
+        }
+        this._rfbCleanDisconnect = false; //This is sent to the UI
+
+        // Transition to disconnected without waiting for socket to close
+        this._updateConnectionState('disconnecting');
+        this._updateConnectionState('disconnected');
+
+        return false;
+    }
+
+    _setCapability(cap, val) {
+        this._capabilities[cap] = val;
+        this.dispatchEvent(new CustomEvent("capabilities",
+                                           { detail: { capabilities: this._capabilities } }));
+    }
+
+    _handleMessage() {
+        if (this._sock.rQlen === 0) {
+            Log.Warn("handleMessage called on an empty receive queue");
+            return;
+        }
+
+        switch (this._rfbConnectionState) {
+            case 'disconnected':
+                Log.Error("Got data while disconnected");
+                break;
+            case 'connected':
+                while (true) {
+                    if (this._flushing) {
+                        break;
+                    }
+                    if (!this._normalMsg()) {
+                        break;
+                    }
+                    if (this._sock.rQlen === 0) {
+                        break;
+                    }
+                }
+                break;
+            default:
+                this._initMsg();
+                break;
+        }
+    }
+
+    _handleKeyEvent(keysym, code, down) {
+        this.sendKey(keysym, code, down);
+    }
+
+    _handleMouse(ev) {
+        /*
+         * We don't check connection status or viewOnly here as the
+         * mouse events might be used to control the viewport
+         */
+
+        if (ev.type === 'click') {
+            /*
+             * Note: This is only needed for the 'click' event as it fails
+             *       to fire properly for the target element so we have
+             *       to listen on the document element instead.
+             */
+            if (ev.target !== this._canvas) {
+                return;
+            }
+        }
+
+        // FIXME: if we're in view-only and not dragging,
+        //        should we stop events?
+        ev.stopPropagation();
+        ev.preventDefault();
+
+        if ((ev.type === 'click') || (ev.type === 'contextmenu')) {
+            return;
+        }
+
+        let pos = clientToElement(ev.clientX, ev.clientY,
+                                  this._canvas);
+
+        switch (ev.type) {
+            case 'mousedown':
+                setCapture(this._canvas);
+                this._handleMouseButton(pos.x, pos.y,
+                                        true, 1 << ev.button);
+                break;
+            case 'mouseup':
+                this._handleMouseButton(pos.x, pos.y,
+                                        false, 1 << ev.button);
+                break;
+            case 'mousemove':
+                this._handleMouseMove(pos.x, pos.y);
+                break;
+        }
+    }
+
+    _handleMouseButton(x, y, down, bmask) {
+        if (this.dragViewport) {
+            if (down && !this._viewportDragging) {
+                this._viewportDragging = true;
+                this._viewportDragPos = {'x': x, 'y': y};
+                this._viewportHasMoved = false;
+
+                // Skip sending mouse events
+                return;
+            } else {
+                this._viewportDragging = false;
+
+                // If we actually performed a drag then we are done
+                // here and should not send any mouse events
+                if (this._viewportHasMoved) {
+                    return;
+                }
+
+                // Otherwise we treat this as a mouse click event.
+                // Send the button down event here, as the button up
+                // event is sent at the end of this function.
+                this._sendMouse(x, y, bmask);
+            }
+        }
+
+        // Flush waiting move event first
+        if (this._mouseMoveTimer !== null) {
+            clearTimeout(this._mouseMoveTimer);
+            this._mouseMoveTimer = null;
+            this._sendMouse(x, y, this._mouseButtonMask);
+        }
+
+        if (down) {
+            this._mouseButtonMask |= bmask;
+        } else {
+            this._mouseButtonMask &= ~bmask;
+        }
+
+        this._sendMouse(x, y, this._mouseButtonMask);
+    }
+
+    _handleMouseMove(x, y) {
+        if (this._viewportDragging) {
+            const deltaX = this._viewportDragPos.x - x;
+            const deltaY = this._viewportDragPos.y - y;
+
+            if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold ||
+                                           Math.abs(deltaY) > dragThreshold)) {
+                this._viewportHasMoved = true;
+
+                this._viewportDragPos = {'x': x, 'y': y};
+                this._display.viewportChangePos(deltaX, deltaY);
+            }
+
+            // Skip sending mouse events
+            return;
+        }
+
+        this._mousePos = { 'x': x, 'y': y };
+
+        // Limit many mouse move events to one every MOUSE_MOVE_DELAY ms
+        if (this._mouseMoveTimer == null) {
+
+            const timeSinceLastMove = Date.now() - this._mouseLastMoveTime;
+            if (timeSinceLastMove > MOUSE_MOVE_DELAY) {
+                this._sendMouse(x, y, this._mouseButtonMask);
+                this._mouseLastMoveTime = Date.now();
+            } else {
+                // Too soon since the latest move, wait the remaining time
+                this._mouseMoveTimer = setTimeout(() => {
+                    this._handleDelayedMouseMove();
+                }, MOUSE_MOVE_DELAY - timeSinceLastMove);
+            }
+        }
+    }
+
+    _handleDelayedMouseMove() {
+        this._mouseMoveTimer = null;
+        this._sendMouse(this._mousePos.x, this._mousePos.y,
+                        this._mouseButtonMask);
+        this._mouseLastMoveTime = Date.now();
+    }
+
+    _sendMouse(x, y, mask) {
+        if (this._rfbConnectionState !== 'connected') { return; }
+        if (this._viewOnly) { return; } // View only, skip mouse events
+
+        RFB.messages.pointerEvent(this._sock, this._display.absX(x),
+                                  this._display.absY(y), mask);
+    }
+
+    _handleWheel(ev) {
+        if (this._rfbConnectionState !== 'connected') { return; }
+        if (this._viewOnly) { return; } // View only, skip mouse events
+
+        ev.stopPropagation();
+        ev.preventDefault();
+
+        let pos = clientToElement(ev.clientX, ev.clientY,
+                                  this._canvas);
+
+        let dX = ev.deltaX;
+        let dY = ev.deltaY;
+
+        // Pixel units unless it's non-zero.
+        // Note that if deltamode is line or page won't matter since we aren't
+        // sending the mouse wheel delta to the server anyway.
+        // The difference between pixel and line can be important however since
+        // we have a threshold that can be smaller than the line height.
+        if (ev.deltaMode !== 0) {
+            dX *= WHEEL_LINE_HEIGHT;
+            dY *= WHEEL_LINE_HEIGHT;
+        }
+
+        // Mouse wheel events are sent in steps over VNC. This means that the VNC
+        // protocol can't handle a wheel event with specific distance or speed.
+        // Therefor, if we get a lot of small mouse wheel events we combine them.
+        this._accumulatedWheelDeltaX += dX;
+        this._accumulatedWheelDeltaY += dY;
+
+        // Generate a mouse wheel step event when the accumulated delta
+        // for one of the axes is large enough.
+        if (Math.abs(this._accumulatedWheelDeltaX) >= WHEEL_STEP) {
+            if (this._accumulatedWheelDeltaX < 0) {
+                this._handleMouseButton(pos.x, pos.y, true, 1 << 5);
+                this._handleMouseButton(pos.x, pos.y, false, 1 << 5);
+            } else if (this._accumulatedWheelDeltaX > 0) {
+                this._handleMouseButton(pos.x, pos.y, true, 1 << 6);
+                this._handleMouseButton(pos.x, pos.y, false, 1 << 6);
+            }
+
+            this._accumulatedWheelDeltaX = 0;
+        }
+        if (Math.abs(this._accumulatedWheelDeltaY) >= WHEEL_STEP) {
+            if (this._accumulatedWheelDeltaY < 0) {
+                this._handleMouseButton(pos.x, pos.y, true, 1 << 3);
+                this._handleMouseButton(pos.x, pos.y, false, 1 << 3);
+            } else if (this._accumulatedWheelDeltaY > 0) {
+                this._handleMouseButton(pos.x, pos.y, true, 1 << 4);
+                this._handleMouseButton(pos.x, pos.y, false, 1 << 4);
+            }
+
+            this._accumulatedWheelDeltaY = 0;
+        }
+    }
+
+    _fakeMouseMove(ev, elementX, elementY) {
+        this._handleMouseMove(elementX, elementY);
+        this._cursor.move(ev.detail.clientX, ev.detail.clientY);
+    }
+
+    _handleTapEvent(ev, bmask) {
+        let pos = clientToElement(ev.detail.clientX, ev.detail.clientY,
+                                  this._canvas);
+
+        // If the user quickly taps multiple times we assume they meant to
+        // hit the same spot, so slightly adjust coordinates
+
+        if ((this._gestureLastTapTime !== null) &&
+            ((Date.now() - this._gestureLastTapTime) < DOUBLE_TAP_TIMEOUT) &&
+            (this._gestureFirstDoubleTapEv.detail.type === ev.detail.type)) {
+            let dx = this._gestureFirstDoubleTapEv.detail.clientX - ev.detail.clientX;
+            let dy = this._gestureFirstDoubleTapEv.detail.clientY - ev.detail.clientY;
+            let distance = Math.hypot(dx, dy);
+
+            if (distance < DOUBLE_TAP_THRESHOLD) {
+                pos = clientToElement(this._gestureFirstDoubleTapEv.detail.clientX,
+                                      this._gestureFirstDoubleTapEv.detail.clientY,
+                                      this._canvas);
+            } else {
+                this._gestureFirstDoubleTapEv = ev;
+            }
+        } else {
+            this._gestureFirstDoubleTapEv = ev;
+        }
+        this._gestureLastTapTime = Date.now();
+
+        this._fakeMouseMove(this._gestureFirstDoubleTapEv, pos.x, pos.y);
+        this._handleMouseButton(pos.x, pos.y, true, bmask);
+        this._handleMouseButton(pos.x, pos.y, false, bmask);
+    }
+
+    _handleGesture(ev) {
+        let magnitude;
+
+        let pos = clientToElement(ev.detail.clientX, ev.detail.clientY,
+                                  this._canvas);
+        switch (ev.type) {
+            case 'gesturestart':
+                switch (ev.detail.type) {
+                    case 'onetap':
+                        this._handleTapEvent(ev, 0x1);
+                        break;
+                    case 'twotap':
+                        this._handleTapEvent(ev, 0x4);
+                        break;
+                    case 'threetap':
+                        this._handleTapEvent(ev, 0x2);
+                        break;
+                    case 'drag':
+                        this._fakeMouseMove(ev, pos.x, pos.y);
+                        this._handleMouseButton(pos.x, pos.y, true, 0x1);
+                        break;
+                    case 'longpress':
+                        this._fakeMouseMove(ev, pos.x, pos.y);
+                        this._handleMouseButton(pos.x, pos.y, true, 0x4);
+                        break;
+
+                    case 'twodrag':
+                        this._gestureLastMagnitudeX = ev.detail.magnitudeX;
+                        this._gestureLastMagnitudeY = ev.detail.magnitudeY;
+                        this._fakeMouseMove(ev, pos.x, pos.y);
+                        break;
+                    case 'pinch':
+                        this._gestureLastMagnitudeX = Math.hypot(ev.detail.magnitudeX,
+                                                                 ev.detail.magnitudeY);
+                        this._fakeMouseMove(ev, pos.x, pos.y);
+                        break;
+                }
+                break;
+
+            case 'gesturemove':
+                switch (ev.detail.type) {
+                    case 'onetap':
+                    case 'twotap':
+                    case 'threetap':
+                        break;
+                    case 'drag':
+                    case 'longpress':
+                        this._fakeMouseMove(ev, pos.x, pos.y);
+                        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.
+                        this._fakeMouseMove(ev, pos.x, pos.y);
+                        while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) > GESTURE_SCRLSENS) {
+                            this._handleMouseButton(pos.x, pos.y, true, 0x8);
+                            this._handleMouseButton(pos.x, pos.y, false, 0x8);
+                            this._gestureLastMagnitudeY += GESTURE_SCRLSENS;
+                        }
+                        while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) < -GESTURE_SCRLSENS) {
+                            this._handleMouseButton(pos.x, pos.y, true, 0x10);
+                            this._handleMouseButton(pos.x, pos.y, false, 0x10);
+                            this._gestureLastMagnitudeY -= GESTURE_SCRLSENS;
+                        }
+                        while ((ev.detail.magnitudeX - this._gestureLastMagnitudeX) > GESTURE_SCRLSENS) {
+                            this._handleMouseButton(pos.x, pos.y, true, 0x20);
+                            this._handleMouseButton(pos.x, pos.y, false, 0x20);
+                            this._gestureLastMagnitudeX += GESTURE_SCRLSENS;
+                        }
+                        while ((ev.detail.magnitudeX - this._gestureLastMagnitudeX) < -GESTURE_SCRLSENS) {
+                            this._handleMouseButton(pos.x, pos.y, true, 0x40);
+                            this._handleMouseButton(pos.x, pos.y, false, 0x40);
+                            this._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.
+                        this._fakeMouseMove(ev, pos.x, pos.y);
+                        magnitude = Math.hypot(ev.detail.magnitudeX, ev.detail.magnitudeY);
+                        if (Math.abs(magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) {
+                            this._handleKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
+                            while ((magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) {
+                                this._handleMouseButton(pos.x, pos.y, true, 0x8);
+                                this._handleMouseButton(pos.x, pos.y, false, 0x8);
+                                this._gestureLastMagnitudeX += GESTURE_ZOOMSENS;
+                            }
+                            while ((magnitude -  this._gestureLastMagnitudeX) < -GESTURE_ZOOMSENS) {
+                                this._handleMouseButton(pos.x, pos.y, true, 0x10);
+                                this._handleMouseButton(pos.x, pos.y, false, 0x10);
+                                this._gestureLastMagnitudeX -= GESTURE_ZOOMSENS;
+                            }
+                        }
+                        this._handleKeyEvent(KeyTable.XK_Control_L, "ControlLeft", false);
+                        break;
+                }
+                break;
+
+            case 'gestureend':
+                switch (ev.detail.type) {
+                    case 'onetap':
+                    case 'twotap':
+                    case 'threetap':
+                    case 'pinch':
+                    case 'twodrag':
+                        break;
+                    case 'drag':
+                        this._fakeMouseMove(ev, pos.x, pos.y);
+                        this._handleMouseButton(pos.x, pos.y, false, 0x1);
+                        break;
+                    case 'longpress':
+                        this._fakeMouseMove(ev, pos.x, pos.y);
+                        this._handleMouseButton(pos.x, pos.y, false, 0x4);
+                        break;
+                }
+                break;
+        }
+    }
+
+    // Message Handlers
+
+    _negotiateProtocolVersion() {
+        if (this._sock.rQwait("version", 12)) {
+            return false;
+        }
+
+        const sversion = this._sock.rQshiftStr(12).substr(4, 7);
+        Log.Info("Server ProtocolVersion: " + sversion);
+        let isRepeater = 0;
+        switch (sversion) {
+            case "000.000":  // UltraVNC repeater
+                isRepeater = 1;
+                break;
+            case "003.003":
+            case "003.006":  // UltraVNC
+            case "003.889":  // Apple Remote Desktop
+                this._rfbVersion = 3.3;
+                break;
+            case "003.007":
+                this._rfbVersion = 3.7;
+                break;
+            case "003.008":
+            case "004.000":  // Intel AMT KVM
+            case "004.001":  // RealVNC 4.6
+            case "005.000":  // RealVNC 5.3
+                this._rfbVersion = 3.8;
+                break;
+            default:
+                return this._fail("Invalid server version " + sversion);
+        }
+
+        if (isRepeater) {
+            let repeaterID = "ID:" + this._repeaterID;
+            while (repeaterID.length < 250) {
+                repeaterID += "\0";
+            }
+            this._sock.sendString(repeaterID);
+            return true;
+        }
+
+        if (this._rfbVersion > this._rfbMaxVersion) {
+            this._rfbVersion = this._rfbMaxVersion;
+        }
+
+        const cversion = "00" + parseInt(this._rfbVersion, 10) +
+                       ".00" + ((this._rfbVersion * 10) % 10);
+        this._sock.sendString("RFB " + cversion + "\n");
+        Log.Debug('Sent ProtocolVersion: ' + cversion);
+
+        this._rfbInitState = 'Security';
+    }
+
+    _negotiateSecurity() {
+        if (this._rfbVersion >= 3.7) {
+            // Server sends supported list, client decides
+            const numTypes = this._sock.rQshift8();
+            if (this._sock.rQwait("security type", numTypes, 1)) { return false; }
+
+            if (numTypes === 0) {
+                this._rfbInitState = "SecurityReason";
+                this._securityContext = "no security types";
+                this._securityStatus = 1;
+                return this._initMsg();
+            }
+
+            const types = this._sock.rQshiftBytes(numTypes);
+            Log.Debug("Server security types: " + types);
+
+            // Look for each auth in preferred order
+            if (types.includes(1)) {
+                this._rfbAuthScheme = 1; // None
+            } else if (types.includes(22)) {
+                this._rfbAuthScheme = 22; // XVP
+            } else if (types.includes(16)) {
+                this._rfbAuthScheme = 16; // Tight
+            } else if (types.includes(2)) {
+                this._rfbAuthScheme = 2; // VNC Auth
+            } else if (types.includes(19)) {
+                this._rfbAuthScheme = 19; // VeNCrypt Auth
+            } else {
+                return this._fail("Unsupported security types (types: " + types + ")");
+            }
+
+            this._sock.send([this._rfbAuthScheme]);
+        } else {
+            // Server decides
+            if (this._sock.rQwait("security scheme", 4)) { return false; }
+            this._rfbAuthScheme = this._sock.rQshift32();
+
+            if (this._rfbAuthScheme == 0) {
+                this._rfbInitState = "SecurityReason";
+                this._securityContext = "authentication scheme";
+                this._securityStatus = 1;
+                return this._initMsg();
+            }
+        }
+
+        this._rfbInitState = 'Authentication';
+        Log.Debug('Authenticating using scheme: ' + this._rfbAuthScheme);
+
+        return this._initMsg(); // jump to authentication
+    }
+
+    _handleSecurityReason() {
+        if (this._sock.rQwait("reason length", 4)) {
+            return false;
+        }
+        const strlen = this._sock.rQshift32();
+        let reason = "";
+
+        if (strlen > 0) {
+            if (this._sock.rQwait("reason", strlen, 4)) { return false; }
+            reason = this._sock.rQshiftStr(strlen);
+        }
+
+        if (reason !== "") {
+            this.dispatchEvent(new CustomEvent(
+                "securityfailure",
+                { detail: { status: this._securityStatus,
+                            reason: reason } }));
+
+            return this._fail("Security negotiation failed on " +
+                              this._securityContext +
+                              " (reason: " + reason + ")");
+        } else {
+            this.dispatchEvent(new CustomEvent(
+                "securityfailure",
+                { detail: { status: this._securityStatus } }));
+
+            return this._fail("Security negotiation failed on " +
+                              this._securityContext);
+        }
+    }
+
+    // authentication
+    _negotiateXvpAuth() {
+        if (this._rfbCredentials.username === undefined ||
+            this._rfbCredentials.password === undefined ||
+            this._rfbCredentials.target === undefined) {
+            this.dispatchEvent(new CustomEvent(
+                "credentialsrequired",
+                { detail: { types: ["username", "password", "target"] } }));
+            return false;
+        }
+
+        const xvpAuthStr = String.fromCharCode(this._rfbCredentials.username.length) +
+                           String.fromCharCode(this._rfbCredentials.target.length) +
+                           this._rfbCredentials.username +
+                           this._rfbCredentials.target;
+        this._sock.sendString(xvpAuthStr);
+        this._rfbAuthScheme = 2;
+        return this._negotiateAuthentication();
+    }
+
+    // VeNCrypt authentication, currently only supports version 0.2 and only Plain subtype
+    _negotiateVeNCryptAuth() {
+
+        // waiting for VeNCrypt version
+        if (this._rfbVeNCryptState == 0) {
+            if (this._sock.rQwait("vencrypt version", 2)) { return false; }
+
+            const major = this._sock.rQshift8();
+            const minor = this._sock.rQshift8();
+
+            if (!(major == 0 && minor == 2)) {
+                return this._fail("Unsupported VeNCrypt version " + major + "." + minor);
+            }
+
+            this._sock.send([0, 2]);
+            this._rfbVeNCryptState = 1;
+        }
+
+        // waiting for ACK
+        if (this._rfbVeNCryptState == 1) {
+            if (this._sock.rQwait("vencrypt ack", 1)) { return false; }
+
+            const res = this._sock.rQshift8();
+
+            if (res != 0) {
+                return this._fail("VeNCrypt failure " + res);
+            }
+
+            this._rfbVeNCryptState = 2;
+        }
+        // must fall through here (i.e. no "else if"), beacause we may have already received
+        // the subtypes length and won't be called again
+
+        if (this._rfbVeNCryptState == 2) { // waiting for subtypes length
+            if (this._sock.rQwait("vencrypt subtypes length", 1)) { return false; }
+
+            const subtypesLength = this._sock.rQshift8();
+            if (subtypesLength < 1) {
+                return this._fail("VeNCrypt subtypes empty");
+            }
+
+            this._rfbVeNCryptSubtypesLength = subtypesLength;
+            this._rfbVeNCryptState = 3;
+        }
+
+        // waiting for subtypes list
+        if (this._rfbVeNCryptState == 3) {
+            if (this._sock.rQwait("vencrypt subtypes", 4 * this._rfbVeNCryptSubtypesLength)) { return false; }
+
+            const subtypes = [];
+            for (let i = 0; i < this._rfbVeNCryptSubtypesLength; i++) {
+                subtypes.push(this._sock.rQshift32());
+            }
+
+            // 256 = Plain subtype
+            if (subtypes.indexOf(256) != -1) {
+                // 0x100 = 256
+                this._sock.send([0, 0, 1, 0]);
+                this._rfbVeNCryptState = 4;
+            } else {
+                return this._fail("VeNCrypt Plain subtype not offered by server");
+            }
+        }
+
+        // negotiated Plain subtype, server waits for password
+        if (this._rfbVeNCryptState == 4) {
+            if (this._rfbCredentials.username === undefined ||
+                this._rfbCredentials.password === undefined) {
+                this.dispatchEvent(new CustomEvent(
+                    "credentialsrequired",
+                    { detail: { types: ["username", "password"] } }));
+                return false;
+            }
+
+            const user = encodeUTF8(this._rfbCredentials.username);
+            const pass = encodeUTF8(this._rfbCredentials.password);
+
+            this._sock.send([
+                (user.length >> 24) & 0xFF,
+                (user.length >> 16) & 0xFF,
+                (user.length >> 8) & 0xFF,
+                user.length & 0xFF
+            ]);
+            this._sock.send([
+                (pass.length >> 24) & 0xFF,
+                (pass.length >> 16) & 0xFF,
+                (pass.length >> 8) & 0xFF,
+                pass.length & 0xFF
+            ]);
+            this._sock.sendString(user);
+            this._sock.sendString(pass);
+
+            this._rfbInitState = "SecurityResult";
+            return true;
+        }
+    }
+
+    _negotiateStdVNCAuth() {
+        if (this._sock.rQwait("auth challenge", 16)) { return false; }
+
+        if (this._rfbCredentials.password === undefined) {
+            this.dispatchEvent(new CustomEvent(
+                "credentialsrequired",
+                { detail: { types: ["password"] } }));
+            return false;
+        }
+
+        // TODO(directxman12): make genDES not require an Array
+        const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16));
+        const response = RFB.genDES(this._rfbCredentials.password, challenge);
+        this._sock.send(response);
+        this._rfbInitState = "SecurityResult";
+        return true;
+    }
+
+    _negotiateTightUnixAuth() {
+        if (this._rfbCredentials.username === undefined ||
+            this._rfbCredentials.password === undefined) {
+            this.dispatchEvent(new CustomEvent(
+                "credentialsrequired",
+                { detail: { types: ["username", "password"] } }));
+            return false;
+        }
+
+        this._sock.send([0, 0, 0, this._rfbCredentials.username.length]);
+        this._sock.send([0, 0, 0, this._rfbCredentials.password.length]);
+        this._sock.sendString(this._rfbCredentials.username);
+        this._sock.sendString(this._rfbCredentials.password);
+        this._rfbInitState = "SecurityResult";
+        return true;
+    }
+
+    _negotiateTightTunnels(numTunnels) {
+        const clientSupportedTunnelTypes = {
+            0: { vendor: 'TGHT', signature: 'NOTUNNEL' }
+        };
+        const serverSupportedTunnelTypes = {};
+        // receive tunnel capabilities
+        for (let i = 0; i < numTunnels; i++) {
+            const capCode = this._sock.rQshift32();
+            const capVendor = this._sock.rQshiftStr(4);
+            const capSignature = this._sock.rQshiftStr(8);
+            serverSupportedTunnelTypes[capCode] = { vendor: capVendor, signature: capSignature };
+        }
+
+        Log.Debug("Server Tight tunnel types: " + serverSupportedTunnelTypes);
+
+        // Siemens touch panels have a VNC server that supports NOTUNNEL,
+        // but forgets to advertise it. Try to detect such servers by
+        // looking for their custom tunnel type.
+        if (serverSupportedTunnelTypes[1] &&
+            (serverSupportedTunnelTypes[1].vendor === "SICR") &&
+            (serverSupportedTunnelTypes[1].signature === "SCHANNEL")) {
+            Log.Debug("Detected Siemens server. Assuming NOTUNNEL support.");
+            serverSupportedTunnelTypes[0] = { vendor: 'TGHT', signature: 'NOTUNNEL' };
+        }
+
+        // choose the notunnel type
+        if (serverSupportedTunnelTypes[0]) {
+            if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor ||
+                serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) {
+                return this._fail("Client's tunnel type had the incorrect " +
+                                  "vendor or signature");
+            }
+            Log.Debug("Selected tunnel type: " + clientSupportedTunnelTypes[0]);
+            this._sock.send([0, 0, 0, 0]);  // use NOTUNNEL
+            return false; // wait until we receive the sub auth count to continue
+        } else {
+            return this._fail("Server wanted tunnels, but doesn't support " +
+                              "the notunnel type");
+        }
+    }
+
+    _negotiateTightAuth() {
+        if (!this._rfbTightVNC) {  // first pass, do the tunnel negotiation
+            if (this._sock.rQwait("num tunnels", 4)) { return false; }
+            const numTunnels = this._sock.rQshift32();
+            if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; }
+
+            this._rfbTightVNC = true;
+
+            if (numTunnels > 0) {
+                this._negotiateTightTunnels(numTunnels);
+                return false;  // wait until we receive the sub auth to continue
+            }
+        }
+
+        // second pass, do the sub-auth negotiation
+        if (this._sock.rQwait("sub auth count", 4)) { return false; }
+        const subAuthCount = this._sock.rQshift32();
+        if (subAuthCount === 0) {  // empty sub-auth list received means 'no auth' subtype selected
+            this._rfbInitState = 'SecurityResult';
+            return true;
+        }
+
+        if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; }
+
+        const clientSupportedTypes = {
+            'STDVNOAUTH__': 1,
+            'STDVVNCAUTH_': 2,
+            'TGHTULGNAUTH': 129
+        };
+
+        const serverSupportedTypes = [];
+
+        for (let i = 0; i < subAuthCount; i++) {
+            this._sock.rQshift32(); // capNum
+            const capabilities = this._sock.rQshiftStr(12);
+            serverSupportedTypes.push(capabilities);
+        }
+
+        Log.Debug("Server Tight authentication types: " + serverSupportedTypes);
+
+        for (let authType in clientSupportedTypes) {
+            if (serverSupportedTypes.indexOf(authType) != -1) {
+                this._sock.send([0, 0, 0, clientSupportedTypes[authType]]);
+                Log.Debug("Selected authentication type: " + authType);
+
+                switch (authType) {
+                    case 'STDVNOAUTH__':  // no auth
+                        this._rfbInitState = 'SecurityResult';
+                        return true;
+                    case 'STDVVNCAUTH_': // VNC auth
+                        this._rfbAuthScheme = 2;
+                        return this._initMsg();
+                    case 'TGHTULGNAUTH': // UNIX auth
+                        this._rfbAuthScheme = 129;
+                        return this._initMsg();
+                    default:
+                        return this._fail("Unsupported tiny auth scheme " +
+                                          "(scheme: " + authType + ")");
+                }
+            }
+        }
+
+        return this._fail("No supported sub-auth types!");
+    }
+
+    _negotiateAuthentication() {
+        switch (this._rfbAuthScheme) {
+            case 1:  // no auth
+                if (this._rfbVersion >= 3.8) {
+                    this._rfbInitState = 'SecurityResult';
+                    return true;
+                }
+                this._rfbInitState = 'ClientInitialisation';
+                return this._initMsg();
+
+            case 22:  // XVP auth
+                return this._negotiateXvpAuth();
+
+            case 2:  // VNC authentication
+                return this._negotiateStdVNCAuth();
+
+            case 16:  // TightVNC Security Type
+                return this._negotiateTightAuth();
+
+            case 19:  // VeNCrypt Security Type
+                return this._negotiateVeNCryptAuth();
+
+            case 129:  // TightVNC UNIX Security Type
+                return this._negotiateTightUnixAuth();
+
+            default:
+                return this._fail("Unsupported auth scheme (scheme: " +
+                                  this._rfbAuthScheme + ")");
+        }
+    }
+
+    _handleSecurityResult() {
+        if (this._sock.rQwait('VNC auth response ', 4)) { return false; }
+
+        const status = this._sock.rQshift32();
+
+        if (status === 0) { // OK
+            this._rfbInitState = 'ClientInitialisation';
+            Log.Debug('Authentication OK');
+            return this._initMsg();
+        } else {
+            if (this._rfbVersion >= 3.8) {
+                this._rfbInitState = "SecurityReason";
+                this._securityContext = "security result";
+                this._securityStatus = status;
+                return this._initMsg();
+            } else {
+                this.dispatchEvent(new CustomEvent(
+                    "securityfailure",
+                    { detail: { status: status } }));
+
+                return this._fail("Security handshake failed");
+            }
+        }
+    }
+
+    _negotiateServerInit() {
+        if (this._sock.rQwait("server initialization", 24)) { return false; }
+
+        /* Screen size */
+        const width = this._sock.rQshift16();
+        const height = this._sock.rQshift16();
+
+        /* PIXEL_FORMAT */
+        const bpp         = this._sock.rQshift8();
+        const depth       = this._sock.rQshift8();
+        const bigEndian  = this._sock.rQshift8();
+        const trueColor  = this._sock.rQshift8();
+
+        const redMax     = this._sock.rQshift16();
+        const greenMax   = this._sock.rQshift16();
+        const blueMax    = this._sock.rQshift16();
+        const redShift   = this._sock.rQshift8();
+        const greenShift = this._sock.rQshift8();
+        const blueShift  = this._sock.rQshift8();
+        this._sock.rQskipBytes(3);  // padding
+
+        // NB(directxman12): we don't want to call any callbacks or print messages until
+        //                   *after* we're past the point where we could backtrack
+
+        /* Connection name/title */
+        const nameLength = this._sock.rQshift32();
+        if (this._sock.rQwait('server init name', nameLength, 24)) { return false; }
+        let name = this._sock.rQshiftStr(nameLength);
+        name = decodeUTF8(name, true);
+
+        if (this._rfbTightVNC) {
+            if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + nameLength)) { return false; }
+            // In TightVNC mode, ServerInit message is extended
+            const numServerMessages = this._sock.rQshift16();
+            const numClientMessages = this._sock.rQshift16();
+            const numEncodings = this._sock.rQshift16();
+            this._sock.rQskipBytes(2);  // padding
+
+            const totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16;
+            if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + nameLength)) { return false; }
+
+            // we don't actually do anything with the capability information that TIGHT sends,
+            // so we just skip the all of this.
+
+            // TIGHT server message capabilities
+            this._sock.rQskipBytes(16 * numServerMessages);
+
+            // TIGHT client message capabilities
+            this._sock.rQskipBytes(16 * numClientMessages);
+
+            // TIGHT encoding capabilities
+            this._sock.rQskipBytes(16 * numEncodings);
+        }
+
+        // NB(directxman12): these are down here so that we don't run them multiple times
+        //                   if we backtrack
+        Log.Info("Screen: " + width + "x" + height +
+                  ", bpp: " + bpp + ", depth: " + depth +
+                  ", bigEndian: " + bigEndian +
+                  ", trueColor: " + trueColor +
+                  ", redMax: " + redMax +
+                  ", greenMax: " + greenMax +
+                  ", blueMax: " + blueMax +
+                  ", redShift: " + redShift +
+                  ", greenShift: " + greenShift +
+                  ", blueShift: " + blueShift);
+
+        // we're past the point where we could backtrack, so it's safe to call this
+        this._setDesktopName(name);
+        this._resize(width, height);
+
+        if (!this._viewOnly) { this._keyboard.grab(); }
+
+        this._fbDepth = 24;
+
+        if (this._fbName === "Intel(r) AMT KVM") {
+            Log.Warn("Intel AMT KVM only supports 8/16 bit depths. Using low color mode.");
+            this._fbDepth = 8;
+        }
+
+        RFB.messages.pixelFormat(this._sock, this._fbDepth, true);
+        this._sendEncodings();
+        RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fbWidth, this._fbHeight);
+
+        this._updateConnectionState('connected');
+        return true;
+    }
+
+    _sendEncodings() {
+        const encs = [];
+
+        // In preference order
+        encs.push(encodings.encodingCopyRect);
+        // Only supported with full depth support
+        if (this._fbDepth == 24) {
+            encs.push(encodings.encodingTight);
+            encs.push(encodings.encodingTightPNG);
+            encs.push(encodings.encodingHextile);
+            encs.push(encodings.encodingRRE);
+        }
+        encs.push(encodings.encodingRaw);
+
+        // Psuedo-encoding settings
+        encs.push(encodings.pseudoEncodingQualityLevel0 + this._qualityLevel);
+        encs.push(encodings.pseudoEncodingCompressLevel0 + this._compressionLevel);
+
+        encs.push(encodings.pseudoEncodingDesktopSize);
+        encs.push(encodings.pseudoEncodingLastRect);
+        encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
+        encs.push(encodings.pseudoEncodingExtendedDesktopSize);
+        encs.push(encodings.pseudoEncodingXvp);
+        encs.push(encodings.pseudoEncodingFence);
+        encs.push(encodings.pseudoEncodingContinuousUpdates);
+        encs.push(encodings.pseudoEncodingDesktopName);
+        encs.push(encodings.pseudoEncodingExtendedClipboard);
+
+        if (this._fbDepth == 24) {
+            encs.push(encodings.pseudoEncodingVMwareCursor);
+            encs.push(encodings.pseudoEncodingCursor);
+        }
+
+        RFB.messages.clientEncodings(this._sock, encs);
+    }
+
+    /* RFB protocol initialization states:
+     *   ProtocolVersion
+     *   Security
+     *   Authentication
+     *   SecurityResult
+     *   ClientInitialization - not triggered by server message
+     *   ServerInitialization
+     */
+    _initMsg() {
+        switch (this._rfbInitState) {
+            case 'ProtocolVersion':
+                return this._negotiateProtocolVersion();
+
+            case 'Security':
+                return this._negotiateSecurity();
+
+            case 'Authentication':
+                return this._negotiateAuthentication();
+
+            case 'SecurityResult':
+                return this._handleSecurityResult();
+
+            case 'SecurityReason':
+                return this._handleSecurityReason();
+
+            case 'ClientInitialisation':
+                this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation
+                this._rfbInitState = 'ServerInitialisation';
+                return true;
+
+            case 'ServerInitialisation':
+                return this._negotiateServerInit();
+
+            default:
+                return this._fail("Unknown init state (state: " +
+                                  this._rfbInitState + ")");
+        }
+    }
+
+    _handleSetColourMapMsg() {
+        Log.Debug("SetColorMapEntries");
+
+        return this._fail("Unexpected SetColorMapEntries message");
+    }
+
+    _handleServerCutText() {
+        Log.Debug("ServerCutText");
+
+        if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; }
+
+        this._sock.rQskipBytes(3);  // Padding
+
+        let length = this._sock.rQshift32();
+        length = toSigned32bit(length);
+
+        if (this._sock.rQwait("ServerCutText content", Math.abs(length), 8)) { return false; }
+
+        if (length >= 0) {
+            //Standard msg
+            const text = this._sock.rQshiftStr(length);
+            if (this._viewOnly) {
+                return true;
+            }
+
+            this.dispatchEvent(new CustomEvent(
+                "clipboard",
+                { detail: { text: text } }));
+
+        } else {
+            //Extended msg.
+            length = Math.abs(length);
+            const flags = this._sock.rQshift32();
+            let formats = flags & 0x0000FFFF;
+            let actions = flags & 0xFF000000;
+
+            let isCaps = (!!(actions & extendedClipboardActionCaps));
+            if (isCaps) {
+                this._clipboardServerCapabilitiesFormats = {};
+                this._clipboardServerCapabilitiesActions = {};
+
+                // Update our server capabilities for Formats
+                for (let i = 0; i <= 15; i++) {
+                    let index = 1 << i;
+
+                    // Check if format flag is set.
+                    if ((formats & index)) {
+                        this._clipboardServerCapabilitiesFormats[index] = true;
+                        // We don't send unsolicited clipboard, so we
+                        // ignore the size
+                        this._sock.rQshift32();
+                    }
+                }
+
+                // Update our server capabilities for Actions
+                for (let i = 24; i <= 31; i++) {
+                    let index = 1 << i;
+                    this._clipboardServerCapabilitiesActions[index] = !!(actions & index);
+                }
+
+                /*  Caps handling done, send caps with the clients
+                    capabilities set as a response */
+                let clientActions = [
+                    extendedClipboardActionCaps,
+                    extendedClipboardActionRequest,
+                    extendedClipboardActionPeek,
+                    extendedClipboardActionNotify,
+                    extendedClipboardActionProvide
+                ];
+                RFB.messages.extendedClipboardCaps(this._sock, clientActions, {extendedClipboardFormatText: 0});
+
+            } else if (actions === extendedClipboardActionRequest) {
+                if (this._viewOnly) {
+                    return true;
+                }
+
+                // Check if server has told us it can handle Provide and there is clipboard data to send.
+                if (this._clipboardText != null &&
+                    this._clipboardServerCapabilitiesActions[extendedClipboardActionProvide]) {
+
+                    if (formats & extendedClipboardFormatText) {
+                        RFB.messages.extendedClipboardProvide(this._sock, [extendedClipboardFormatText], [this._clipboardText]);
+                    }
+                }
+
+            } else if (actions === extendedClipboardActionPeek) {
+                if (this._viewOnly) {
+                    return true;
+                }
+
+                if (this._clipboardServerCapabilitiesActions[extendedClipboardActionNotify]) {
+
+                    if (this._clipboardText != null) {
+                        RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]);
+                    } else {
+                        RFB.messages.extendedClipboardNotify(this._sock, []);
+                    }
+                }
+
+            } else if (actions === extendedClipboardActionNotify) {
+                if (this._viewOnly) {
+                    return true;
+                }
+
+                if (this._clipboardServerCapabilitiesActions[extendedClipboardActionRequest]) {
+
+                    if (formats & extendedClipboardFormatText) {
+                        RFB.messages.extendedClipboardRequest(this._sock, [extendedClipboardFormatText]);
+                    }
+                }
+
+            } else if (actions === extendedClipboardActionProvide) {
+                if (this._viewOnly) {
+                    return true;
+                }
+
+                if (!(formats & extendedClipboardFormatText)) {
+                    return true;
+                }
+                // Ignore what we had in our clipboard client side.
+                this._clipboardText = null;
+
+                // FIXME: Should probably verify that this data was actually requested
+                let zlibStream = this._sock.rQshiftBytes(length - 4);
+                let streamInflator = new Inflator();
+                let textData = null;
+
+                streamInflator.setInput(zlibStream);
+                for (let i = 0; i <= 15; i++) {
+                    let format = 1 << i;
+
+                    if (formats & format) {
+
+                        let size = 0x00;
+                        let sizeArray = streamInflator.inflate(4);
+
+                        size |= (sizeArray[0] << 24);
+                        size |= (sizeArray[1] << 16);
+                        size |= (sizeArray[2] << 8);
+                        size |= (sizeArray[3]);
+                        let chunk = streamInflator.inflate(size);
+
+                        if (format === extendedClipboardFormatText) {
+                            textData = chunk;
+                        }
+                    }
+                }
+                streamInflator.setInput(null);
+
+                if (textData !== null) {
+                    let tmpText = "";
+                    for (let i = 0; i < textData.length; i++) {
+                        tmpText += String.fromCharCode(textData[i]);
+                    }
+                    textData = tmpText;
+
+                    textData = decodeUTF8(textData);
+                    if ((textData.length > 0) && "\0" === textData.charAt(textData.length - 1)) {
+                        textData = textData.slice(0, -1);
+                    }
+
+                    textData = textData.replace("\r\n", "\n");
+
+                    this.dispatchEvent(new CustomEvent(
+                        "clipboard",
+                        { detail: { text: textData } }));
+                }
+            } else {
+                return this._fail("Unexpected action in extended clipboard message: " + actions);
+            }
+        }
+        return true;
+    }
+
+    _handleServerFenceMsg() {
+        if (this._sock.rQwait("ServerFence header", 8, 1)) { return false; }
+        this._sock.rQskipBytes(3); // Padding
+        let flags = this._sock.rQshift32();
+        let length = this._sock.rQshift8();
+
+        if (this._sock.rQwait("ServerFence payload", length, 9)) { return false; }
+
+        if (length > 64) {
+            Log.Warn("Bad payload length (" + length + ") in fence response");
+            length = 64;
+        }
+
+        const payload = this._sock.rQshiftStr(length);
+
+        this._supportsFence = true;
+
+        /*
+         * Fence flags
+         *
+         *  (1<<0)  - BlockBefore
+         *  (1<<1)  - BlockAfter
+         *  (1<<2)  - SyncNext
+         *  (1<<31) - Request
+         */
+
+        if (!(flags & (1<<31))) {
+            return this._fail("Unexpected fence response");
+        }
+
+        // Filter out unsupported flags
+        // FIXME: support syncNext
+        flags &= (1<<0) | (1<<1);
+
+        // BlockBefore and BlockAfter are automatically handled by
+        // the fact that we process each incoming message
+        // synchronuosly.
+        RFB.messages.clientFence(this._sock, flags, payload);
+
+        return true;
+    }
+
+    _handleXvpMsg() {
+        if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; }
+        this._sock.rQskipBytes(1);  // Padding
+        const xvpVer = this._sock.rQshift8();
+        const xvpMsg = this._sock.rQshift8();
+
+        switch (xvpMsg) {
+            case 0:  // XVP_FAIL
+                Log.Error("XVP Operation Failed");
+                break;
+            case 1:  // XVP_INIT
+                this._rfbXvpVer = xvpVer;
+                Log.Info("XVP extensions enabled (version " + this._rfbXvpVer + ")");
+                this._setCapability("power", true);
+                break;
+            default:
+                this._fail("Illegal server XVP message (msg: " + xvpMsg + ")");
+                break;
+        }
+
+        return true;
+    }
+
+    _normalMsg() {
+        let msgType;
+        if (this._FBU.rects > 0) {
+            msgType = 0;
+        } else {
+            msgType = this._sock.rQshift8();
+        }
+
+        let first, ret;
+        switch (msgType) {
+            case 0:  // FramebufferUpdate
+                ret = this._framebufferUpdate();
+                if (ret && !this._enabledContinuousUpdates) {
+                    RFB.messages.fbUpdateRequest(this._sock, true, 0, 0,
+                                                 this._fbWidth, this._fbHeight);
+                }
+                return ret;
+
+            case 1:  // SetColorMapEntries
+                return this._handleSetColourMapMsg();
+
+            case 2:  // Bell
+                Log.Debug("Bell");
+                this.dispatchEvent(new CustomEvent(
+                    "bell",
+                    { detail: {} }));
+                return true;
+
+            case 3:  // ServerCutText
+                return this._handleServerCutText();
+
+            case 150: // EndOfContinuousUpdates
+                first = !this._supportsContinuousUpdates;
+                this._supportsContinuousUpdates = true;
+                this._enabledContinuousUpdates = false;
+                if (first) {
+                    this._enabledContinuousUpdates = true;
+                    this._updateContinuousUpdates();
+                    Log.Info("Enabling continuous updates.");
+                } else {
+                    // FIXME: We need to send a framebufferupdaterequest here
+                    // if we add support for turning off continuous updates
+                }
+                return true;
+
+            case 248: // ServerFence
+                return this._handleServerFenceMsg();
+
+            case 250:  // XVP
+                return this._handleXvpMsg();
+
+            default:
+                this._fail("Unexpected server message (type " + msgType + ")");
+                Log.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30));
+                return true;
+        }
+    }
+
+    _onFlush() {
+        this._flushing = false;
+        // Resume processing
+        if (this._sock.rQlen > 0) {
+            this._handleMessage();
+        }
+    }
+
+    _framebufferUpdate() {
+        if (this._FBU.rects === 0) {
+            if (this._sock.rQwait("FBU header", 3, 1)) { return false; }
+            this._sock.rQskipBytes(1);  // Padding
+            this._FBU.rects = this._sock.rQshift16();
+
+            // Make sure the previous frame is fully rendered first
+            // to avoid building up an excessive queue
+            if (this._display.pending()) {
+                this._flushing = true;
+                this._display.flush();
+                return false;
+            }
+        }
+
+        while (this._FBU.rects > 0) {
+            if (this._FBU.encoding === null) {
+                if (this._sock.rQwait("rect header", 12)) { return false; }
+                /* New FramebufferUpdate */
+
+                const hdr = this._sock.rQshiftBytes(12);
+                this._FBU.x        = (hdr[0] << 8) + hdr[1];
+                this._FBU.y        = (hdr[2] << 8) + hdr[3];
+                this._FBU.width    = (hdr[4] << 8) + hdr[5];
+                this._FBU.height   = (hdr[6] << 8) + hdr[7];
+                this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) +
+                                              (hdr[10] << 8) + hdr[11], 10);
+            }
+
+            if (!this._handleRect()) {
+                return false;
+            }
+
+            this._FBU.rects--;
+            this._FBU.encoding = null;
+        }
+
+        this._display.flip();
+
+        return true;  // We finished this FBU
+    }
+
+    _handleRect() {
+        switch (this._FBU.encoding) {
+            case encodings.pseudoEncodingLastRect:
+                this._FBU.rects = 1; // Will be decreased when we return
+                return true;
+
+            case encodings.pseudoEncodingVMwareCursor:
+                return this._handleVMwareCursor();
+
+            case encodings.pseudoEncodingCursor:
+                return this._handleCursor();
+
+            case encodings.pseudoEncodingQEMUExtendedKeyEvent:
+                this._qemuExtKeyEventSupported = true;
+                return true;
+
+            case encodings.pseudoEncodingDesktopName:
+                return this._handleDesktopName();
+
+            case encodings.pseudoEncodingDesktopSize:
+                this._resize(this._FBU.width, this._FBU.height);
+                return true;
+
+            case encodings.pseudoEncodingExtendedDesktopSize:
+                return this._handleExtendedDesktopSize();
+
+            default:
+                return this._handleDataRect();
+        }
+    }
+
+    _handleVMwareCursor() {
+        const hotx = this._FBU.x;  // hotspot-x
+        const hoty = this._FBU.y;  // hotspot-y
+        const w = this._FBU.width;
+        const h = this._FBU.height;
+        if (this._sock.rQwait("VMware cursor encoding", 1)) {
+            return false;
+        }
+
+        const cursorType = this._sock.rQshift8();
+
+        this._sock.rQshift8(); //Padding
+
+        let rgba;
+        const bytesPerPixel = 4;
+
+        //Classic cursor
+        if (cursorType == 0) {
+            //Used to filter away unimportant bits.
+            //OR is used for correct conversion in js.
+            const PIXEL_MASK = 0xffffff00 | 0;
+            rgba = new Array(w * h * bytesPerPixel);
+
+            if (this._sock.rQwait("VMware cursor classic encoding",
+                                  (w * h * bytesPerPixel) * 2, 2)) {
+                return false;
+            }
+
+            let andMask = new Array(w * h);
+            for (let pixel = 0; pixel < (w * h); pixel++) {
+                andMask[pixel] = this._sock.rQshift32();
+            }
+
+            let xorMask = new Array(w * h);
+            for (let pixel = 0; pixel < (w * h); pixel++) {
+                xorMask[pixel] = this._sock.rQshift32();
+            }
+
+            for (let pixel = 0; pixel < (w * h); pixel++) {
+                if (andMask[pixel] == 0) {
+                    //Fully opaque pixel
+                    let bgr = xorMask[pixel];
+                    let r   = bgr >> 8  & 0xff;
+                    let g   = bgr >> 16 & 0xff;
+                    let b   = bgr >> 24 & 0xff;
+
+                    rgba[(pixel * bytesPerPixel)     ] = r;    //r
+                    rgba[(pixel * bytesPerPixel) + 1 ] = g;    //g
+                    rgba[(pixel * bytesPerPixel) + 2 ] = b;    //b
+                    rgba[(pixel * bytesPerPixel) + 3 ] = 0xff; //a
+
+                } else if ((andMask[pixel] & PIXEL_MASK) ==
+                           PIXEL_MASK) {
+                    //Only screen value matters, no mouse colouring
+                    if (xorMask[pixel] == 0) {
+                        //Transparent pixel
+                        rgba[(pixel * bytesPerPixel)     ] = 0x00;
+                        rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
+                        rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
+                        rgba[(pixel * bytesPerPixel) + 3 ] = 0x00;
+
+                    } else if ((xorMask[pixel] & PIXEL_MASK) ==
+                               PIXEL_MASK) {
+                        //Inverted pixel, not supported in browsers.
+                        //Fully opaque instead.
+                        rgba[(pixel * bytesPerPixel)     ] = 0x00;
+                        rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
+                        rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
+                        rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
+
+                    } else {
+                        //Unhandled xorMask
+                        rgba[(pixel * bytesPerPixel)     ] = 0x00;
+                        rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
+                        rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
+                        rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
+                    }
+
+                } else {
+                    //Unhandled andMask
+                    rgba[(pixel * bytesPerPixel)     ] = 0x00;
+                    rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
+                    rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
+                    rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
+                }
+            }
+
+        //Alpha cursor.
+        } else if (cursorType == 1) {
+            if (this._sock.rQwait("VMware cursor alpha encoding",
+                                  (w * h * 4), 2)) {
+                return false;
+            }
+
+            rgba = new Array(w * h * bytesPerPixel);
+
+            for (let pixel = 0; pixel < (w * h); pixel++) {
+                let data = this._sock.rQshift32();
+
+                rgba[(pixel * 4)     ] = data >> 24 & 0xff; //r
+                rgba[(pixel * 4) + 1 ] = data >> 16 & 0xff; //g
+                rgba[(pixel * 4) + 2 ] = data >> 8 & 0xff;  //b
+                rgba[(pixel * 4) + 3 ] = data & 0xff;       //a
+            }
+
+        } else {
+            Log.Warn("The given cursor type is not supported: "
+                      + cursorType + " given.");
+            return false;
+        }
+
+        this._updateCursor(rgba, hotx, hoty, w, h);
+
+        return true;
+    }
+
+    _handleCursor() {
+        const hotx = this._FBU.x;  // hotspot-x
+        const hoty = this._FBU.y;  // hotspot-y
+        const w = this._FBU.width;
+        const h = this._FBU.height;
+
+        const pixelslength = w * h * 4;
+        const masklength = Math.ceil(w / 8) * h;
+
+        let bytes = pixelslength + masklength;
+        if (this._sock.rQwait("cursor encoding", bytes)) {
+            return false;
+        }
+
+        // Decode from BGRX pixels + bit mask to RGBA
+        const pixels = this._sock.rQshiftBytes(pixelslength);
+        const mask = this._sock.rQshiftBytes(masklength);
+        let rgba = new Uint8Array(w * h * 4);
+
+        let pixIdx = 0;
+        for (let y = 0; y < h; y++) {
+            for (let x = 0; x < w; x++) {
+                let maskIdx = y * Math.ceil(w / 8) + Math.floor(x / 8);
+                let alpha = (mask[maskIdx] << (x % 8)) & 0x80 ? 255 : 0;
+                rgba[pixIdx    ] = pixels[pixIdx + 2];
+                rgba[pixIdx + 1] = pixels[pixIdx + 1];
+                rgba[pixIdx + 2] = pixels[pixIdx];
+                rgba[pixIdx + 3] = alpha;
+                pixIdx += 4;
+            }
+        }
+
+        this._updateCursor(rgba, hotx, hoty, w, h);
+
+        return true;
+    }
+
+    _handleDesktopName() {
+        if (this._sock.rQwait("DesktopName", 4)) {
+            return false;
+        }
+
+        let length = this._sock.rQshift32();
+
+        if (this._sock.rQwait("DesktopName", length, 4)) {
+            return false;
+        }
+
+        let name = this._sock.rQshiftStr(length);
+        name = decodeUTF8(name, true);
+
+        this._setDesktopName(name);
+
+        return true;
+    }
+
+    _handleExtendedDesktopSize() {
+        if (this._sock.rQwait("ExtendedDesktopSize", 4)) {
+            return false;
+        }
+
+        const numberOfScreens = this._sock.rQpeek8();
+
+        let bytes = 4 + (numberOfScreens * 16);
+        if (this._sock.rQwait("ExtendedDesktopSize", bytes)) {
+            return false;
+        }
+
+        const firstUpdate = !this._supportsSetDesktopSize;
+        this._supportsSetDesktopSize = true;
+
+        // Normally we only apply the current resize mode after a
+        // window resize event. However there is no such trigger on the
+        // initial connect. And we don't know if the server supports
+        // resizing until we've gotten here.
+        if (firstUpdate) {
+            this._requestRemoteResize();
+        }
+
+        this._sock.rQskipBytes(1);  // number-of-screens
+        this._sock.rQskipBytes(3);  // padding
+
+        for (let i = 0; i < numberOfScreens; i += 1) {
+            // Save the id and flags of the first screen
+            if (i === 0) {
+                this._screenID = this._sock.rQshiftBytes(4);    // id
+                this._sock.rQskipBytes(2);                       // x-position
+                this._sock.rQskipBytes(2);                       // y-position
+                this._sock.rQskipBytes(2);                       // width
+                this._sock.rQskipBytes(2);                       // height
+                this._screenFlags = this._sock.rQshiftBytes(4); // flags
+            } else {
+                this._sock.rQskipBytes(16);
+            }
+        }
+
+        /*
+         * The x-position indicates the reason for the change:
+         *
+         *  0 - server resized on its own
+         *  1 - this client requested the resize
+         *  2 - another client requested the resize
+         */
+
+        // We need to handle errors when we requested the resize.
+        if (this._FBU.x === 1 && this._FBU.y !== 0) {
+            let msg = "";
+            // The y-position indicates the status code from the server
+            switch (this._FBU.y) {
+                case 1:
+                    msg = "Resize is administratively prohibited";
+                    break;
+                case 2:
+                    msg = "Out of resources";
+                    break;
+                case 3:
+                    msg = "Invalid screen layout";
+                    break;
+                default:
+                    msg = "Unknown reason";
+                    break;
+            }
+            Log.Warn("Server did not accept the resize request: "
+                     + msg);
+        } else {
+            this._resize(this._FBU.width, this._FBU.height);
+        }
+
+        return true;
+    }
+
+    _handleDataRect() {
+        let decoder = this._decoders[this._FBU.encoding];
+        if (!decoder) {
+            this._fail("Unsupported encoding (encoding: " +
+                       this._FBU.encoding + ")");
+            return false;
+        }
+
+        try {
+            return decoder.decodeRect(this._FBU.x, this._FBU.y,
+                                      this._FBU.width, this._FBU.height,
+                                      this._sock, this._display,
+                                      this._fbDepth);
+        } catch (err) {
+            this._fail("Error decoding rect: " + err);
+            return false;
+        }
+    }
+
+    _updateContinuousUpdates() {
+        if (!this._enabledContinuousUpdates) { return; }
+
+        RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0,
+                                             this._fbWidth, this._fbHeight);
+    }
+
+    _resize(width, height) {
+        this._fbWidth = width;
+        this._fbHeight = height;
+
+        this._display.resize(this._fbWidth, this._fbHeight);
+
+        // Adjust the visible viewport based on the new dimensions
+        this._updateClip();
+        this._updateScale();
+
+        this._updateContinuousUpdates();
+    }
+
+    _xvpOp(ver, op) {
+        if (this._rfbXvpVer < ver) { return; }
+        Log.Info("Sending XVP operation " + op + " (version " + ver + ")");
+        RFB.messages.xvpOp(this._sock, ver, op);
+    }
+
+    _updateCursor(rgba, hotx, hoty, w, h) {
+        this._cursorImage = {
+            rgbaPixels: rgba,
+            hotx: hotx, hoty: hoty, w: w, h: h,
+        };
+        this._refreshCursor();
+    }
+
+    _shouldShowDotCursor() {
+        // Called when this._cursorImage is updated
+        if (!this._showDotCursor) {
+            // User does not want to see the dot, so...
+            return false;
+        }
+
+        // The dot should not be shown if the cursor is already visible,
+        // i.e. contains at least one not-fully-transparent pixel.
+        // So iterate through all alpha bytes in rgba and stop at the
+        // first non-zero.
+        for (let i = 3; i < this._cursorImage.rgbaPixels.length; i += 4) {
+            if (this._cursorImage.rgbaPixels[i]) {
+                return false;
+            }
+        }
+
+        // At this point, we know that the cursor is fully transparent, and
+        // the user wants to see the dot instead of this.
+        return true;
+    }
+
+    _refreshCursor() {
+        if (this._rfbConnectionState !== "connecting" &&
+            this._rfbConnectionState !== "connected") {
+            return;
+        }
+        const image = this._shouldShowDotCursor() ? RFB.cursors.dot : this._cursorImage;
+        this._cursor.change(image.rgbaPixels,
+                            image.hotx, image.hoty,
+                            image.w, image.h
+        );
+    }
+
+    static genDES(password, challenge) {
+        const passwordChars = password.split('').map(c => c.charCodeAt(0));
+        return (new DES(passwordChars)).encrypt(challenge);
+    }
+}
+
+// Class Methods
+RFB.messages = {
+    keyEvent(sock, keysym, down) {
+        const buff = sock._sQ;
+        const offset = sock._sQlen;
+
+        buff[offset] = 4;  // msg-type
+        buff[offset + 1] = down;
+
+        buff[offset + 2] = 0;
+        buff[offset + 3] = 0;
+
+        buff[offset + 4] = (keysym >> 24);
+        buff[offset + 5] = (keysym >> 16);
+        buff[offset + 6] = (keysym >> 8);
+        buff[offset + 7] = keysym;
+
+        sock._sQlen += 8;
+        sock.flush();
+    },
+
+    QEMUExtendedKeyEvent(sock, keysym, down, keycode) {
+        function getRFBkeycode(xtScanCode) {
+            const upperByte = (keycode >> 8);
+            const lowerByte = (keycode & 0x00ff);
+            if (upperByte === 0xe0 && lowerByte < 0x7f) {
+                return lowerByte | 0x80;
+            }
+            return xtScanCode;
+        }
+
+        const buff = sock._sQ;
+        const offset = sock._sQlen;
+
+        buff[offset] = 255; // msg-type
+        buff[offset + 1] = 0; // sub msg-type
+
+        buff[offset + 2] = (down >> 8);
+        buff[offset + 3] = down;
+
+        buff[offset + 4] = (keysym >> 24);
+        buff[offset + 5] = (keysym >> 16);
+        buff[offset + 6] = (keysym >> 8);
+        buff[offset + 7] = keysym;
+
+        const RFBkeycode = getRFBkeycode(keycode);
+
+        buff[offset + 8] = (RFBkeycode >> 24);
+        buff[offset + 9] = (RFBkeycode >> 16);
+        buff[offset + 10] = (RFBkeycode >> 8);
+        buff[offset + 11] = RFBkeycode;
+
+        sock._sQlen += 12;
+        sock.flush();
+    },
+
+    pointerEvent(sock, x, y, mask) {
+        const buff = sock._sQ;
+        const offset = sock._sQlen;
+
+        buff[offset] = 5; // msg-type
+
+        buff[offset + 1] = mask;
+
+        buff[offset + 2] = x >> 8;
+        buff[offset + 3] = x;
+
+        buff[offset + 4] = y >> 8;
+        buff[offset + 5] = y;
+
+        sock._sQlen += 6;
+        sock.flush();
+    },
+
+    // Used to build Notify and Request data.
+    _buildExtendedClipboardFlags(actions, formats) {
+        let data = new Uint8Array(4);
+        let formatFlag = 0x00000000;
+        let actionFlag = 0x00000000;
+
+        for (let i = 0; i < actions.length; i++) {
+            actionFlag |= actions[i];
+        }
+
+        for (let i = 0; i < formats.length; i++) {
+            formatFlag |= formats[i];
+        }
+
+        data[0] = actionFlag >> 24; // Actions
+        data[1] = 0x00;             // Reserved
+        data[2] = 0x00;             // Reserved
+        data[3] = formatFlag;       // Formats
+
+        return data;
+    },
+
+    extendedClipboardProvide(sock, formats, inData) {
+        // Deflate incomming data and their sizes
+        let deflator = new Deflator();
+        let dataToDeflate = [];
+
+        for (let i = 0; i < formats.length; i++) {
+            // We only support the format Text at this time
+            if (formats[i] != extendedClipboardFormatText) {
+                throw new Error("Unsupported extended clipboard format for Provide message.");
+            }
+
+            // Change lone \r or \n into \r\n as defined in rfbproto
+            inData[i] = inData[i].replace(/\r\n|\r|\n/gm, "\r\n");
+
+            // Check if it already has \0
+            let text = encodeUTF8(inData[i] + "\0");
+
+            dataToDeflate.push( (text.length >> 24) & 0xFF,
+                                (text.length >> 16) & 0xFF,
+                                (text.length >>  8) & 0xFF,
+                                (text.length & 0xFF));
+
+            for (let j = 0; j < text.length; j++) {
+                dataToDeflate.push(text.charCodeAt(j));
+            }
+        }
+
+        let deflatedData = deflator.deflate(new Uint8Array(dataToDeflate));
+
+        // Build data  to send
+        let data = new Uint8Array(4 + deflatedData.length);
+        data.set(RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionProvide],
+                                                           formats));
+        data.set(deflatedData, 4);
+
+        RFB.messages.clientCutText(sock, data, true);
+    },
+
+    extendedClipboardNotify(sock, formats) {
+        let flags = RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionNotify],
+                                                              formats);
+        RFB.messages.clientCutText(sock, flags, true);
+    },
+
+    extendedClipboardRequest(sock, formats) {
+        let flags = RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionRequest],
+                                                              formats);
+        RFB.messages.clientCutText(sock, flags, true);
+    },
+
+    extendedClipboardCaps(sock, actions, formats) {
+        let formatKeys = Object.keys(formats);
+        let data  = new Uint8Array(4 + (4 * formatKeys.length));
+
+        formatKeys.map(x => parseInt(x));
+        formatKeys.sort((a, b) =>  a - b);
+
+        data.set(RFB.messages._buildExtendedClipboardFlags(actions, []));
+
+        let loopOffset = 4;
+        for (let i = 0; i < formatKeys.length; i++) {
+            data[loopOffset]     = formats[formatKeys[i]] >> 24;
+            data[loopOffset + 1] = formats[formatKeys[i]] >> 16;
+            data[loopOffset + 2] = formats[formatKeys[i]] >> 8;
+            data[loopOffset + 3] = formats[formatKeys[i]] >> 0;
+
+            loopOffset += 4;
+            data[3] |= (1 << formatKeys[i]); // Update our format flags
+        }
+
+        RFB.messages.clientCutText(sock, data, true);
+    },
+
+    clientCutText(sock, data, extended = false) {
+        const buff = sock._sQ;
+        const offset = sock._sQlen;
+
+        buff[offset] = 6; // msg-type
+
+        buff[offset + 1] = 0; // padding
+        buff[offset + 2] = 0; // padding
+        buff[offset + 3] = 0; // padding
+
+        let length;
+        if (extended) {
+            length = toUnsigned32bit(-data.length);
+        } else {
+            length = data.length;
+        }
+
+        buff[offset + 4] = length >> 24;
+        buff[offset + 5] = length >> 16;
+        buff[offset + 6] = length >> 8;
+        buff[offset + 7] = length;
+
+        sock._sQlen += 8;
+
+        // We have to keep track of from where in the data we begin creating the
+        // buffer for the flush in the next iteration.
+        let dataOffset = 0;
+
+        let remaining = data.length;
+        while (remaining > 0) {
+
+            let flushSize = Math.min(remaining, (sock._sQbufferSize - sock._sQlen));
+            for (let i = 0; i < flushSize; i++) {
+                buff[sock._sQlen + i] = data[dataOffset + i];
+            }
+
+            sock._sQlen += flushSize;
+            sock.flush();
+
+            remaining -= flushSize;
+            dataOffset += flushSize;
+        }
+
+    },
+
+    setDesktopSize(sock, width, height, id, flags) {
+        const buff = sock._sQ;
+        const offset = sock._sQlen;
+
+        buff[offset] = 251;              // msg-type
+        buff[offset + 1] = 0;            // padding
+        buff[offset + 2] = width >> 8;   // width
+        buff[offset + 3] = width;
+        buff[offset + 4] = height >> 8;  // height
+        buff[offset + 5] = height;
+
+        buff[offset + 6] = 1;            // number-of-screens
+        buff[offset + 7] = 0;            // padding
+
+        // screen array
+        buff[offset + 8] = id >> 24;     // id
+        buff[offset + 9] = id >> 16;
+        buff[offset + 10] = id >> 8;
+        buff[offset + 11] = id;
+        buff[offset + 12] = 0;           // x-position
+        buff[offset + 13] = 0;
+        buff[offset + 14] = 0;           // y-position
+        buff[offset + 15] = 0;
+        buff[offset + 16] = width >> 8;  // width
+        buff[offset + 17] = width;
+        buff[offset + 18] = height >> 8; // height
+        buff[offset + 19] = height;
+        buff[offset + 20] = flags >> 24; // flags
+        buff[offset + 21] = flags >> 16;
+        buff[offset + 22] = flags >> 8;
+        buff[offset + 23] = flags;
+
+        sock._sQlen += 24;
+        sock.flush();
+    },
+
+    clientFence(sock, flags, payload) {
+        const buff = sock._sQ;
+        const offset = sock._sQlen;
+
+        buff[offset] = 248; // msg-type
+
+        buff[offset + 1] = 0; // padding
+        buff[offset + 2] = 0; // padding
+        buff[offset + 3] = 0; // padding
+
+        buff[offset + 4] = flags >> 24; // flags
+        buff[offset + 5] = flags >> 16;
+        buff[offset + 6] = flags >> 8;
+        buff[offset + 7] = flags;
+
+        const n = payload.length;
+
+        buff[offset + 8] = n; // length
+
+        for (let i = 0; i < n; i++) {
+            buff[offset + 9 + i] = payload.charCodeAt(i);
+        }
+
+        sock._sQlen += 9 + n;
+        sock.flush();
+    },
+
+    enableContinuousUpdates(sock, enable, x, y, width, height) {
+        const buff = sock._sQ;
+        const offset = sock._sQlen;
+
+        buff[offset] = 150;             // msg-type
+        buff[offset + 1] = enable;      // enable-flag
+
+        buff[offset + 2] = x >> 8;      // x
+        buff[offset + 3] = x;
+        buff[offset + 4] = y >> 8;      // y
+        buff[offset + 5] = y;
+        buff[offset + 6] = width >> 8;  // width
+        buff[offset + 7] = width;
+        buff[offset + 8] = height >> 8; // height
+        buff[offset + 9] = height;
+
+        sock._sQlen += 10;
+        sock.flush();
+    },
+
+    pixelFormat(sock, depth, trueColor) {
+        const buff = sock._sQ;
+        const offset = sock._sQlen;
+
+        let bpp;
+
+        if (depth > 16) {
+            bpp = 32;
+        } else if (depth > 8) {
+            bpp = 16;
+        } else {
+            bpp = 8;
+        }
+
+        const bits = Math.floor(depth/3);
+
+        buff[offset] = 0;  // msg-type
+
+        buff[offset + 1] = 0; // padding
+        buff[offset + 2] = 0; // padding
+        buff[offset + 3] = 0; // padding
+
+        buff[offset + 4] = bpp;                 // bits-per-pixel
+        buff[offset + 5] = depth;               // depth
+        buff[offset + 6] = 0;                   // little-endian
+        buff[offset + 7] = trueColor ? 1 : 0;  // true-color
+
+        buff[offset + 8] = 0;    // red-max
+        buff[offset + 9] = (1 << bits) - 1;  // red-max
+
+        buff[offset + 10] = 0;   // green-max
+        buff[offset + 11] = (1 << bits) - 1; // green-max
+
+        buff[offset + 12] = 0;   // blue-max
+        buff[offset + 13] = (1 << bits) - 1; // blue-max
+
+        buff[offset + 14] = bits * 0; // red-shift
+        buff[offset + 15] = bits * 1; // green-shift
+        buff[offset + 16] = bits * 2; // blue-shift
+
+        buff[offset + 17] = 0;   // padding
+        buff[offset + 18] = 0;   // padding
+        buff[offset + 19] = 0;   // padding
+
+        sock._sQlen += 20;
+        sock.flush();
+    },
+
+    clientEncodings(sock, encodings) {
+        const buff = sock._sQ;
+        const offset = sock._sQlen;
+
+        buff[offset] = 2; // msg-type
+        buff[offset + 1] = 0; // padding
+
+        buff[offset + 2] = encodings.length >> 8;
+        buff[offset + 3] = encodings.length;
+
+        let j = offset + 4;
+        for (let i = 0; i < encodings.length; i++) {
+            const enc = encodings[i];
+            buff[j] = enc >> 24;
+            buff[j + 1] = enc >> 16;
+            buff[j + 2] = enc >> 8;
+            buff[j + 3] = enc;
+
+            j += 4;
+        }
+
+        sock._sQlen += j - offset;
+        sock.flush();
+    },
+
+    fbUpdateRequest(sock, incremental, x, y, w, h) {
+        const buff = sock._sQ;
+        const offset = sock._sQlen;
+
+        if (typeof(x) === "undefined") { x = 0; }
+        if (typeof(y) === "undefined") { y = 0; }
+
+        buff[offset] = 3;  // msg-type
+        buff[offset + 1] = incremental ? 1 : 0;
+
+        buff[offset + 2] = (x >> 8) & 0xFF;
+        buff[offset + 3] = x & 0xFF;
+
+        buff[offset + 4] = (y >> 8) & 0xFF;
+        buff[offset + 5] = y & 0xFF;
+
+        buff[offset + 6] = (w >> 8) & 0xFF;
+        buff[offset + 7] = w & 0xFF;
+
+        buff[offset + 8] = (h >> 8) & 0xFF;
+        buff[offset + 9] = h & 0xFF;
+
+        sock._sQlen += 10;
+        sock.flush();
+    },
+
+    xvpOp(sock, ver, op) {
+        const buff = sock._sQ;
+        const offset = sock._sQlen;
+
+        buff[offset] = 250; // msg-type
+        buff[offset + 1] = 0; // padding
+
+        buff[offset + 2] = ver;
+        buff[offset + 3] = op;
+
+        sock._sQlen += 4;
+        sock.flush();
+    }
+};
+
+RFB.cursors = {
+    none: {
+        rgbaPixels: new Uint8Array(),
+        w: 0, h: 0,
+        hotx: 0, hoty: 0,
+    },
+
+    dot: {
+        /* eslint-disable indent */
+        rgbaPixels: new Uint8Array([
+            255, 255, 255, 255,   0,   0,   0, 255, 255, 255, 255, 255,
+              0,   0,   0, 255,   0,   0,   0,   0,   0,   0,  0,  255,
+            255, 255, 255, 255,   0,   0,   0, 255, 255, 255, 255, 255,
+        ]),
+        /* eslint-enable indent */
+        w: 3, h: 3,
+        hotx: 1, hoty: 1,
+    }
+};
diff --git a/app/src/main/assets/novnc/core/util/browser.js b/app/src/main/assets/novnc/core/util/browser.js
new file mode 100644
index 00000000..24b5e960
--- /dev/null
+++ b/app/src/main/assets/novnc/core/util/browser.js
@@ -0,0 +1,103 @@
+/*
+ * 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
+ */
+
+import * as Log from './logging.js';
+
+// Touch detection
+export let isTouchDevice = ('ontouchstart' in document.documentElement) ||
+                                 // requried for Chrome debugger
+                                 (document.ontouchstart !== undefined) ||
+                                 // required for MS Surface
+                                 (navigator.maxTouchPoints > 0) ||
+                                 (navigator.msMaxTouchPoints > 0);
+window.addEventListener('touchstart', function onFirstTouch() {
+    isTouchDevice = true;
+    window.removeEventListener('touchstart', onFirstTouch, false);
+}, false);
+
+
+// The goal is to find a certain physical width, the devicePixelRatio
+// brings us a bit closer but is not optimal.
+export let dragThreshold = 10 * (window.devicePixelRatio || 1);
+
+let _supportsCursorURIs = false;
+
+try {
+    const target = document.createElement('canvas');
+    target.style.cursor = 'url("data:image/x-icon;base64,AAACAAEACAgAAAIAAgA4AQAAFgAAACgAAAAIAAAAEAAAAAEAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAD/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAA==") 2 2, default';
+
+    if (target.style.cursor.indexOf("url") === 0) {
+        Log.Info("Data URI scheme cursor supported");
+        _supportsCursorURIs = true;
+    } else {
+        Log.Warn("Data URI scheme cursor not supported");
+    }
+} catch (exc) {
+    Log.Error("Data URI scheme cursor test exception: " + exc);
+}
+
+export const supportsCursorURIs = _supportsCursorURIs;
+
+let _hasScrollbarGutter = true;
+try {
+    // Create invisible container
+    const container = document.createElement('div');
+    container.style.visibility = 'hidden';
+    container.style.overflow = 'scroll'; // forcing scrollbars
+    document.body.appendChild(container);
+
+    // Create a div and place it in the container
+    const child = document.createElement('div');
+    container.appendChild(child);
+
+    // Calculate the difference between the container's full width
+    // and the child's width - the difference is the scrollbars
+    const scrollbarWidth = (container.offsetWidth - child.offsetWidth);
+
+    // Clean up
+    container.parentNode.removeChild(container);
+
+    _hasScrollbarGutter = scrollbarWidth != 0;
+} catch (exc) {
+    Log.Error("Scrollbar test exception: " + exc);
+}
+export const hasScrollbarGutter = _hasScrollbarGutter;
+
+/*
+ * The functions for detection of platforms and browsers below are exported
+ * but the use of these should be minimized as much as possible.
+ *
+ * It's better to use feature detection than platform 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 isFirefox() {
+    return navigator && !!(/firefox/i).exec(navigator.userAgent);
+}
+
diff --git a/app/src/main/assets/novnc/core/util/cursor.js b/app/src/main/assets/novnc/core/util/cursor.js
new file mode 100644
index 00000000..12bcceda
--- /dev/null
+++ b/app/src/main/assets/novnc/core/util/cursor.js
@@ -0,0 +1,243 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2019 The noVNC Authors
+ * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
+ */
+
+import { supportsCursorURIs, isTouchDevice } from './browser.js';
+
+const useFallback = !supportsCursorURIs || isTouchDevice;
+
+export default class Cursor {
+    constructor() {
+        this._target = null;
+
+        this._canvas = document.createElement('canvas');
+
+        if (useFallback) {
+            this._canvas.style.position = 'fixed';
+            this._canvas.style.zIndex = '65535';
+            this._canvas.style.pointerEvents = 'none';
+            // Can't use "display" because of Firefox bug #1445997
+            this._canvas.style.visibility = 'hidden';
+        }
+
+        this._position = { x: 0, y: 0 };
+        this._hotSpot = { x: 0, y: 0 };
+
+        this._eventHandlers = {
+            'mouseover': this._handleMouseOver.bind(this),
+            'mouseleave': this._handleMouseLeave.bind(this),
+            'mousemove': this._handleMouseMove.bind(this),
+            'mouseup': this._handleMouseUp.bind(this),
+        };
+    }
+
+    attach(target) {
+        if (this._target) {
+            this.detach();
+        }
+
+        this._target = target;
+
+        if (useFallback) {
+            document.body.appendChild(this._canvas);
+
+            const options = { capture: true, passive: true };
+            this._target.addEventListener('mouseover', this._eventHandlers.mouseover, options);
+            this._target.addEventListener('mouseleave', this._eventHandlers.mouseleave, options);
+            this._target.addEventListener('mousemove', this._eventHandlers.mousemove, options);
+            this._target.addEventListener('mouseup', this._eventHandlers.mouseup, options);
+        }
+
+        this.clear();
+    }
+
+    detach() {
+        if (!this._target) {
+            return;
+        }
+
+        if (useFallback) {
+            const options = { capture: true, passive: true };
+            this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options);
+            this._target.removeEventListener('mouseleave', this._eventHandlers.mouseleave, options);
+            this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options);
+            this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options);
+
+            document.body.removeChild(this._canvas);
+        }
+
+        this._target = null;
+    }
+
+    change(rgba, hotx, hoty, w, h) {
+        if ((w === 0) || (h === 0)) {
+            this.clear();
+            return;
+        }
+
+        this._position.x = this._position.x + this._hotSpot.x - hotx;
+        this._position.y = this._position.y + this._hotSpot.y - hoty;
+        this._hotSpot.x = hotx;
+        this._hotSpot.y = hoty;
+
+        let ctx = this._canvas.getContext('2d');
+
+        this._canvas.width = w;
+        this._canvas.height = h;
+
+        let img = new ImageData(new Uint8ClampedArray(rgba), w, h);
+        ctx.clearRect(0, 0, w, h);
+        ctx.putImageData(img, 0, 0);
+
+        if (useFallback) {
+            this._updatePosition();
+        } else {
+            let url = this._canvas.toDataURL();
+            this._target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default';
+        }
+    }
+
+    clear() {
+        this._target.style.cursor = 'none';
+        this._canvas.width = 0;
+        this._canvas.height = 0;
+        this._position.x = this._position.x + this._hotSpot.x;
+        this._position.y = this._position.y + this._hotSpot.y;
+        this._hotSpot.x = 0;
+        this._hotSpot.y = 0;
+    }
+
+    // Mouse events might be emulated, this allows
+    // moving the cursor in such cases
+    move(clientX, clientY) {
+        if (!useFallback) {
+            return;
+        }
+        // clientX/clientY are relative the _visual viewport_,
+        // but our position is relative the _layout viewport_,
+        // so try to compensate when we can
+        if (window.visualViewport) {
+            this._position.x = clientX + window.visualViewport.offsetLeft;
+            this._position.y = clientY + window.visualViewport.offsetTop;
+        } else {
+            this._position.x = clientX;
+            this._position.y = clientY;
+        }
+        this._updatePosition();
+        let target = document.elementFromPoint(clientX, clientY);
+        this._updateVisibility(target);
+    }
+
+    _handleMouseOver(event) {
+        // This event could be because we're entering the target, or
+        // moving around amongst its sub elements. Let the move handler
+        // sort things out.
+        this._handleMouseMove(event);
+    }
+
+    _handleMouseLeave(event) {
+        // Check if we should show the cursor on the element we are leaving to
+        this._updateVisibility(event.relatedTarget);
+    }
+
+    _handleMouseMove(event) {
+        this._updateVisibility(event.target);
+
+        this._position.x = event.clientX - this._hotSpot.x;
+        this._position.y = event.clientY - this._hotSpot.y;
+
+        this._updatePosition();
+    }
+
+    _handleMouseUp(event) {
+        // We might get this event because of a drag operation that
+        // moved outside of the target. Check what's under the cursor
+        // now and adjust visibility based on that.
+        let target = document.elementFromPoint(event.clientX, event.clientY);
+        this._updateVisibility(target);
+
+        // Captures end with a mouseup but we can't know the event order of
+        // mouseup vs releaseCapture.
+        //
+        // In the cases when releaseCapture comes first, the code above is
+        // enough.
+        //
+        // In the cases when the mouseup comes first, we need wait for the
+        // browser to flush all events and then check again if the cursor
+        // should be visible.
+        if (this._captureIsActive()) {
+            window.setTimeout(() => {
+                // We might have detached at this point
+                if (!this._target) {
+                    return;
+                }
+                // Refresh the target from elementFromPoint since queued events
+                // might have altered the DOM
+                target = document.elementFromPoint(event.clientX,
+                                                   event.clientY);
+                this._updateVisibility(target);
+            }, 0);
+        }
+    }
+
+    _showCursor() {
+        if (this._canvas.style.visibility === 'hidden') {
+            this._canvas.style.visibility = '';
+        }
+    }
+
+    _hideCursor() {
+        if (this._canvas.style.visibility !== 'hidden') {
+            this._canvas.style.visibility = 'hidden';
+        }
+    }
+
+    // Should we currently display the cursor?
+    // (i.e. are we over the target, or a child of the target without a
+    // different cursor set)
+    _shouldShowCursor(target) {
+        if (!target) {
+            return false;
+        }
+        // Easy case
+        if (target === this._target) {
+            return true;
+        }
+        // Other part of the DOM?
+        if (!this._target.contains(target)) {
+            return false;
+        }
+        // Has the child its own cursor?
+        // FIXME: How can we tell that a sub element has an
+        //        explicit "cursor: none;"?
+        if (window.getComputedStyle(target).cursor !== 'none') {
+            return false;
+        }
+        return true;
+    }
+
+    _updateVisibility(target) {
+        // When the cursor target has capture we want to show the cursor.
+        // So, if a capture is active - look at the captured element instead.
+        if (this._captureIsActive()) {
+            target = document.captureElement;
+        }
+        if (this._shouldShowCursor(target)) {
+            this._showCursor();
+        } else {
+            this._hideCursor();
+        }
+    }
+
+    _updatePosition() {
+        this._canvas.style.left = this._position.x + "px";
+        this._canvas.style.top = this._position.y + "px";
+    }
+
+    _captureIsActive() {
+        return document.captureElement &&
+            document.documentElement.contains(document.captureElement);
+    }
+}
diff --git a/app/src/main/assets/novnc/core/util/element.js b/app/src/main/assets/novnc/core/util/element.js
new file mode 100644
index 00000000..466a7453
--- /dev/null
+++ b/app/src/main/assets/novnc/core/util/element.js
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+/*
+ * HTML element utility functions
+ */
+
+export function clientToElement(x, y, elem) {
+    const bounds = elem.getBoundingClientRect();
+    let pos = { x: 0, y: 0 };
+    // Clip to target bounds
+    if (x < bounds.left) {
+        pos.x = 0;
+    } else if (x >= bounds.right) {
+        pos.x = bounds.width - 1;
+    } else {
+        pos.x = x - bounds.left;
+    }
+    if (y < bounds.top) {
+        pos.y = 0;
+    } else if (y >= bounds.bottom) {
+        pos.y = bounds.height - 1;
+    } else {
+        pos.y = y - bounds.top;
+    }
+    return pos;
+}
diff --git a/app/src/main/assets/novnc/core/util/events.js b/app/src/main/assets/novnc/core/util/events.js
new file mode 100644
index 00000000..eb09fe1e
--- /dev/null
+++ b/app/src/main/assets/novnc/core/util/events.js
@@ -0,0 +1,138 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2018 The noVNC Authors
+ * Licensed under MPL 2.0 (see LICENSE.txt)
+ *
+ * See README.md for usage and integration instructions.
+ */
+
+/*
+ * Cross-browser event and position routines
+ */
+
+export function getPointerEvent(e) {
+    return e.changedTouches ? e.changedTouches[0] : e.touches ? e.touches[0] : e;
+}
+
+export function stopEvent(e) {
+    e.stopPropagation();
+    e.preventDefault();
+}
+
+// Emulate Element.setCapture() when not supported
+let _captureRecursion = false;
+let _elementForUnflushedEvents = null;
+document.captureElement = null;
+function _captureProxy(e) {
+    // Recursion protection as we'll see our own event
+    if (_captureRecursion) return;
+
+    // Clone the event as we cannot dispatch an already dispatched event
+    const newEv = new e.constructor(e.type, e);
+
+    _captureRecursion = true;
+    if (document.captureElement) {
+        document.captureElement.dispatchEvent(newEv);
+    } else {
+        _elementForUnflushedEvents.dispatchEvent(newEv);
+    }
+    _captureRecursion = false;
+
+    // Avoid double events
+    e.stopPropagation();
+
+    // Respect the wishes of the redirected event handlers
+    if (newEv.defaultPrevented) {
+        e.preventDefault();
+    }
+
+    // Implicitly release the capture on button release
+    if (e.type === "mouseup") {
+        releaseCapture();
+    }
+}
+
+// Follow cursor style of target element
+function _capturedElemChanged() {
+    const proxyElem = document.getElementById("noVNC_mouse_capture_elem");
+    proxyElem.style.cursor = window.getComputedStyle(document.captureElement).cursor;
+}
+
+const _captureObserver = new MutationObserver(_capturedElemChanged);
+
+export function setCapture(target) {
+    if (target.setCapture) {
+
+        target.setCapture();
+        document.captureElement = target;
+    } else {
+        // Release any existing capture in case this method is
+        // called multiple times without coordination
+        releaseCapture();
+
+        let proxyElem = document.getElementById("noVNC_mouse_capture_elem");
+
+        if (proxyElem === null) {
+            proxyElem = document.createElement("div");
+            proxyElem.id = "noVNC_mouse_capture_elem";
+            proxyElem.style.position = "fixed";
+            proxyElem.style.top = "0px";
+            proxyElem.style.left = "0px";
+            proxyElem.style.width = "100%";
+            proxyElem.style.height = "100%";
+            proxyElem.style.zIndex = 10000;
+            proxyElem.style.display = "none";
+            document.body.appendChild(proxyElem);
+
+            // This is to make sure callers don't get confused by having
+            // our blocking element as the target
+            proxyElem.addEventListener('contextmenu', _captureProxy);
+
+            proxyElem.addEventListener('mousemove', _captureProxy);
+            proxyElem.addEventListener('mouseup', _captureProxy);
+        }
+
+        document.captureElement = target;
+
+        // Track cursor and get initial cursor
+        _captureObserver.observe(target, {attributes: true});
+        _capturedElemChanged();
+
+        proxyElem.style.display = "";
+
+        // We listen to events on window in order to keep tracking if it
+        // happens to leave the viewport
+        window.addEventListener('mousemove', _captureProxy);
+        window.addEventListener('mouseup', _captureProxy);
+    }
+}
+
+export function releaseCapture() {
+    if (document.releaseCapture) {
+
+        document.releaseCapture();
+        document.captureElement = null;
+
+    } else {
+        if (!document.captureElement) {
+            return;
+        }
+
+        // There might be events already queued. The event proxy needs
+        // access to the captured element for these queued events.
+        // E.g. contextmenu (right-click) in Microsoft Edge
+        //
+        // Before removing the capturedElem pointer we save it to a
+        // temporary variable that the unflushed events can use.
+        _elementForUnflushedEvents = document.captureElement;
+        document.captureElement = null;
+
+        _captureObserver.disconnect();
+
+        const proxyElem = document.getElementById("noVNC_mouse_capture_elem");
+        proxyElem.style.display = "none";
+
+        window.removeEventListener('mousemove', _captureProxy);
+        window.removeEventListener('mouseup', _captureProxy);
+    }
+}
diff --git a/app/src/main/assets/novnc/core/util/eventtarget.js b/app/src/main/assets/novnc/core/util/eventtarget.js
new file mode 100644
index 00000000..a21aa549
--- /dev/null
+++ b/app/src/main/assets/novnc/core/util/eventtarget.js
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+export default class EventTargetMixin {
+    constructor() {
+        this._listeners = new Map();
+    }
+
+    addEventListener(type, callback) {
+        if (!this._listeners.has(type)) {
+            this._listeners.set(type, new Set());
+        }
+        this._listeners.get(type).add(callback);
+    }
+
+    removeEventListener(type, callback) {
+        if (this._listeners.has(type)) {
+            this._listeners.get(type).delete(callback);
+        }
+    }
+
+    dispatchEvent(event) {
+        if (!this._listeners.has(event.type)) {
+            return true;
+        }
+        this._listeners.get(event.type)
+            .forEach(callback => callback.call(this, event));
+        return !event.defaultPrevented;
+    }
+}
diff --git a/app/src/main/assets/novnc/core/util/int.js b/app/src/main/assets/novnc/core/util/int.js
new file mode 100644
index 00000000..001f40f2
--- /dev/null
+++ b/app/src/main/assets/novnc/core/util/int.js
@@ -0,0 +1,15 @@
+/*
+ * 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.
+ */
+
+export function toUnsigned32bit(toConvert) {
+    return toConvert >>> 0;
+}
+
+export function toSigned32bit(toConvert) {
+    return toConvert | 0;
+}
diff --git a/app/src/main/assets/novnc/core/util/logging.js b/app/src/main/assets/novnc/core/util/logging.js
new file mode 100644
index 00000000..fe449e93
--- /dev/null
+++ b/app/src/main/assets/novnc/core/util/logging.js
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+
+/*
+ * Logging/debug routines
+ */
+
+let _logLevel = 'warn';
+
+let Debug = () => {};
+let Info = () => {};
+let Warn = () => {};
+let Error = () => {};
+
+export function initLogging(level) {
+    if (typeof level === 'undefined') {
+        level = _logLevel;
+    } else {
+        _logLevel = level;
+    }
+
+    Debug = Info = Warn = Error = () => {};
+
+    if (typeof window.console !== "undefined") {
+        /* eslint-disable no-console, no-fallthrough */
+        switch (level) {
+            case 'debug':
+                Debug = console.debug.bind(window.console);
+            case 'info':
+                Info  = console.info.bind(window.console);
+            case 'warn':
+                Warn  = console.warn.bind(window.console);
+            case 'error':
+                Error = console.error.bind(window.console);
+            case 'none':
+                break;
+            default:
+                throw new window.Error("invalid logging type '" + level + "'");
+        }
+        /* eslint-enable no-console, no-fallthrough */
+    }
+}
+
+export function getLogging() {
+    return _logLevel;
+}
+
+export { Debug, Info, Warn, Error };
+
+// Initialize logging level
+initLogging();
diff --git a/app/src/main/assets/novnc/core/util/strings.js b/app/src/main/assets/novnc/core/util/strings.js
new file mode 100644
index 00000000..3dd4b29f
--- /dev/null
+++ b/app/src/main/assets/novnc/core/util/strings.js
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+// Decode from UTF-8
+export function decodeUTF8(utf8string, allowLatin1=false) {
+    try {
+        return decodeURIComponent(escape(utf8string));
+    } catch (e) {
+        if (e instanceof URIError) {
+            if (allowLatin1) {
+                // If we allow Latin1 we can ignore any decoding fails
+                // and in these cases return the original string
+                return utf8string;
+            }
+        }
+        throw e;
+    }
+}
+
+// Encode to UTF-8
+export function encodeUTF8(DOMString) {
+    return unescape(encodeURIComponent(DOMString));
+}
diff --git a/app/src/main/assets/novnc/core/websock.js b/app/src/main/assets/novnc/core/websock.js
new file mode 100644
index 00000000..37b33fcc
--- /dev/null
+++ b/app/src/main/assets/novnc/core/websock.js
@@ -0,0 +1,353 @@
+/*
+ * Websock: high-performance buffering wrapper
+ * Copyright (C) 2019 The noVNC Authors
+ * Licensed under MPL 2.0 (see LICENSE.txt)
+ *
+ * Websock is similar to the standard WebSocket / RTCDataChannel object
+ * but with extra buffer handling.
+ *
+ * Websock has built-in receive queue buffering; the message event
+ * does not contain actual data but is simply a notification that
+ * there is new data available. Several rQ* methods are available to
+ * read binary data off of the receive queue.
+ */
+
+import * as Log from './util/logging.js';
+
+// this has performance issues in some versions Chromium, and
+// doesn't gain a tremendous amount of performance increase in Firefox
+// at the moment.  It may be valuable to turn it on in the future.
+const MAX_RQ_GROW_SIZE = 40 * 1024 * 1024;  // 40 MiB
+
+// Constants pulled from RTCDataChannelState enum
+// https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/readyState#RTCDataChannelState_enum
+const DataChannel = {
+    CONNECTING: "connecting",
+    OPEN: "open",
+    CLOSING: "closing",
+    CLOSED: "closed"
+};
+
+const ReadyStates = {
+    CONNECTING: [WebSocket.CONNECTING, DataChannel.CONNECTING],
+    OPEN: [WebSocket.OPEN, DataChannel.OPEN],
+    CLOSING: [WebSocket.CLOSING, DataChannel.CLOSING],
+    CLOSED: [WebSocket.CLOSED, DataChannel.CLOSED],
+};
+
+// Properties a raw channel must have, WebSocket and RTCDataChannel are two examples
+const rawChannelProps = [
+    "send",
+    "close",
+    "binaryType",
+    "onerror",
+    "onmessage",
+    "onopen",
+    "protocol",
+    "readyState",
+];
+
+export default class Websock {
+    constructor() {
+        this._websocket = null;  // WebSocket or RTCDataChannel object
+
+        this._rQi = 0;           // Receive queue index
+        this._rQlen = 0;         // Next write position in the receive queue
+        this._rQbufferSize = 1024 * 1024 * 4; // Receive queue buffer size (4 MiB)
+        // called in init: this._rQ = new Uint8Array(this._rQbufferSize);
+        this._rQ = null; // Receive queue
+
+        this._sQbufferSize = 1024 * 10;  // 10 KiB
+        // called in init: this._sQ = new Uint8Array(this._sQbufferSize);
+        this._sQlen = 0;
+        this._sQ = null;  // Send queue
+
+        this._eventHandlers = {
+            message: () => {},
+            open: () => {},
+            close: () => {},
+            error: () => {}
+        };
+    }
+
+    // Getters and Setters
+
+    get readyState() {
+        let subState;
+
+        if (this._websocket === null) {
+            return "unused";
+        }
+
+        subState = this._websocket.readyState;
+
+        if (ReadyStates.CONNECTING.includes(subState)) {
+            return "connecting";
+        } else if (ReadyStates.OPEN.includes(subState)) {
+            return "open";
+        } else if (ReadyStates.CLOSING.includes(subState)) {
+            return "closing";
+        } else if (ReadyStates.CLOSED.includes(subState)) {
+            return "closed";
+        }
+
+        return "unknown";
+    }
+
+    get sQ() {
+        return this._sQ;
+    }
+
+    get rQ() {
+        return this._rQ;
+    }
+
+    get rQi() {
+        return this._rQi;
+    }
+
+    set rQi(val) {
+        this._rQi = val;
+    }
+
+    // Receive Queue
+    get rQlen() {
+        return this._rQlen - this._rQi;
+    }
+
+    rQpeek8() {
+        return this._rQ[this._rQi];
+    }
+
+    rQskipBytes(bytes) {
+        this._rQi += bytes;
+    }
+
+    rQshift8() {
+        return this._rQshift(1);
+    }
+
+    rQshift16() {
+        return this._rQshift(2);
+    }
+
+    rQshift32() {
+        return this._rQshift(4);
+    }
+
+    // TODO(directxman12): test performance with these vs a DataView
+    _rQshift(bytes) {
+        let res = 0;
+        for (let byte = bytes - 1; byte >= 0; byte--) {
+            res += this._rQ[this._rQi++] << (byte * 8);
+        }
+        return res;
+    }
+
+    rQshiftStr(len) {
+        if (typeof(len) === 'undefined') { len = this.rQlen; }
+        let str = "";
+        // Handle large arrays in steps to avoid long strings on the stack
+        for (let i = 0; i < len; i += 4096) {
+            let part = this.rQshiftBytes(Math.min(4096, len - i));
+            str += String.fromCharCode.apply(null, part);
+        }
+        return str;
+    }
+
+    rQshiftBytes(len) {
+        if (typeof(len) === 'undefined') { len = this.rQlen; }
+        this._rQi += len;
+        return new Uint8Array(this._rQ.buffer, this._rQi - len, len);
+    }
+
+    rQshiftTo(target, len) {
+        if (len === undefined) { len = this.rQlen; }
+        // TODO: make this just use set with views when using a ArrayBuffer to store the rQ
+        target.set(new Uint8Array(this._rQ.buffer, this._rQi, len));
+        this._rQi += len;
+    }
+
+    rQslice(start, end = this.rQlen) {
+        return new Uint8Array(this._rQ.buffer, this._rQi + start, end - start);
+    }
+
+    // Check to see if we must wait for 'num' bytes (default to FBU.bytes)
+    // to be available in the receive queue. Return true if we need to
+    // wait (and possibly print a debug message), otherwise false.
+    rQwait(msg, num, goback) {
+        if (this.rQlen < num) {
+            if (goback) {
+                if (this._rQi < goback) {
+                    throw new Error("rQwait cannot backup " + goback + " bytes");
+                }
+                this._rQi -= goback;
+            }
+            return true; // true means need more data
+        }
+        return false;
+    }
+
+    // Send Queue
+
+    flush() {
+        if (this._sQlen > 0 && this.readyState === 'open') {
+            this._websocket.send(this._encodeMessage());
+            this._sQlen = 0;
+        }
+    }
+
+    send(arr) {
+        this._sQ.set(arr, this._sQlen);
+        this._sQlen += arr.length;
+        this.flush();
+    }
+
+    sendString(str) {
+        this.send(str.split('').map(chr => chr.charCodeAt(0)));
+    }
+
+    // Event Handlers
+    off(evt) {
+        this._eventHandlers[evt] = () => {};
+    }
+
+    on(evt, handler) {
+        this._eventHandlers[evt] = handler;
+    }
+
+    _allocateBuffers() {
+        this._rQ = new Uint8Array(this._rQbufferSize);
+        this._sQ = new Uint8Array(this._sQbufferSize);
+    }
+
+    init() {
+        this._allocateBuffers();
+        this._rQi = 0;
+        this._websocket = null;
+    }
+
+    open(uri, protocols) {
+        this.attach(new WebSocket(uri, protocols));
+    }
+
+    attach(rawChannel) {
+        this.init();
+
+        // Must get object and class methods to be compatible with the tests.
+        const channelProps = [...Object.keys(rawChannel), ...Object.getOwnPropertyNames(Object.getPrototypeOf(rawChannel))];
+        for (let i = 0; i < rawChannelProps.length; i++) {
+            const prop = rawChannelProps[i];
+            if (channelProps.indexOf(prop) < 0) {
+                throw new Error('Raw channel missing property: ' + prop);
+            }
+        }
+
+        this._websocket = rawChannel;
+        this._websocket.binaryType = "arraybuffer";
+        this._websocket.onmessage = this._recvMessage.bind(this);
+
+        this._websocket.onopen = () => {
+            Log.Debug('>> WebSock.onopen');
+            if (this._websocket.protocol) {
+                Log.Info("Server choose sub-protocol: " + this._websocket.protocol);
+            }
+
+            this._eventHandlers.open();
+            Log.Debug("<< WebSock.onopen");
+        };
+
+        this._websocket.onclose = (e) => {
+            Log.Debug(">> WebSock.onclose");
+            this._eventHandlers.close(e);
+            Log.Debug("<< WebSock.onclose");
+        };
+
+        this._websocket.onerror = (e) => {
+            Log.Debug(">> WebSock.onerror: " + e);
+            this._eventHandlers.error(e);
+            Log.Debug("<< WebSock.onerror: " + e);
+        };
+    }
+
+    close() {
+        if (this._websocket) {
+            if (this.readyState === 'connecting' ||
+                this.readyState === 'open') {
+                Log.Info("Closing WebSocket connection");
+                this._websocket.close();
+            }
+
+            this._websocket.onmessage = () => {};
+        }
+    }
+
+    // private methods
+    _encodeMessage() {
+        // Put in a binary arraybuffer
+        // according to the spec, you can send ArrayBufferViews with the send method
+        return new Uint8Array(this._sQ.buffer, 0, this._sQlen);
+    }
+
+    // We want to move all the unread data to the start of the queue,
+    // e.g. compacting.
+    // The function also expands the receive que if needed, and for
+    // performance reasons we combine these two actions to avoid
+    // unneccessary copying.
+    _expandCompactRQ(minFit) {
+        // if we're using less than 1/8th of the buffer even with the incoming bytes, compact in place
+        // instead of resizing
+        const requiredBufferSize =  (this._rQlen - this._rQi + minFit) * 8;
+        const resizeNeeded = this._rQbufferSize < requiredBufferSize;
+
+        if (resizeNeeded) {
+            // Make sure we always *at least* double the buffer size, and have at least space for 8x
+            // the current amount of data
+            this._rQbufferSize = Math.max(this._rQbufferSize * 2, requiredBufferSize);
+        }
+
+        // we don't want to grow unboundedly
+        if (this._rQbufferSize > MAX_RQ_GROW_SIZE) {
+            this._rQbufferSize = MAX_RQ_GROW_SIZE;
+            if (this._rQbufferSize - this.rQlen < minFit) {
+                throw new Error("Receive Queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit");
+            }
+        }
+
+        if (resizeNeeded) {
+            const oldRQbuffer = this._rQ.buffer;
+            this._rQ = new Uint8Array(this._rQbufferSize);
+            this._rQ.set(new Uint8Array(oldRQbuffer, this._rQi, this._rQlen - this._rQi));
+        } else {
+            this._rQ.copyWithin(0, this._rQi, this._rQlen);
+        }
+
+        this._rQlen = this._rQlen - this._rQi;
+        this._rQi = 0;
+    }
+
+    // push arraybuffer values onto the end of the receive que
+    _DecodeMessage(data) {
+        const u8 = new Uint8Array(data);
+        if (u8.length > this._rQbufferSize - this._rQlen) {
+            this._expandCompactRQ(u8.length);
+        }
+        this._rQ.set(u8, this._rQlen);
+        this._rQlen += u8.length;
+    }
+
+    _recvMessage(e) {
+        this._DecodeMessage(e.data);
+        if (this.rQlen > 0) {
+            this._eventHandlers.message();
+            if (this._rQlen == this._rQi) {
+                // All data has now been processed, this means we
+                // can reset the receive queue.
+                this._rQlen = 0;
+                this._rQi = 0;
+            }
+        } else {
+            Log.Debug("Ignoring empty message");
+        }
+    }
+}
diff --git a/app/src/main/assets/novnc/karma.conf.js b/app/src/main/assets/novnc/karma.conf.js
new file mode 100644
index 00000000..1ea17475
--- /dev/null
+++ b/app/src/main/assets/novnc/karma.conf.js
@@ -0,0 +1,85 @@
+// Karma configuration
+
+// The Safari launcher is broken, so construct our own
+function SafariBrowser(id, baseBrowserDecorator, args) {
+  baseBrowserDecorator(this);
+
+  this._start = function(url) {
+    this._execCommand('/usr/bin/open', ['-W', '-n', '-a', 'Safari', url]);
+  }
+}
+
+SafariBrowser.prototype = {
+  name: 'Safari'
+}
+
+module.exports = (config) => {
+  let browsers = [];
+
+  if (process.env.TEST_BROWSER_NAME) {
+    browsers = process.env.TEST_BROWSER_NAME.split(',');
+  }
+
+  const my_conf = {
+
+    // base path that will be used to resolve all patterns (eg. files, exclude)
+    basePath: '',
+
+    // frameworks to use
+    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+    frameworks: ['mocha', 'sinon-chai'],
+
+    // list of files / patterns to load in the browser (loaded in order)
+    files: [
+      { pattern: 'app/localization.js', included: false, type: 'module' },
+      { pattern: 'app/webutil.js', included: false, type: 'module' },
+      { pattern: 'core/**/*.js', included: false, type: 'module' },
+      { pattern: 'vendor/pako/**/*.js', included: false, type: 'module' },
+      { pattern: 'tests/test.*.js', type: 'module' },
+      { pattern: 'tests/fake.*.js', included: false, type: 'module' },
+      { pattern: 'tests/assertions.js', type: 'module' },
+    ],
+
+    client: {
+      mocha: {
+        // replace Karma debug page with mocha display
+        'reporter': 'html',
+        'ui': 'bdd'
+      }
+    },
+
+    // list of files to exclude
+    exclude: [
+    ],
+
+    plugins: [
+      'karma-*',
+      '@chiragrupani/karma-chromium-edge-launcher',
+      { 'launcher:Safari': [ 'type', SafariBrowser ] },
+    ],
+
+    // start these browsers
+    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
+    browsers: browsers,
+
+    // test results reporter to use
+    // possible values: 'dots', 'progress'
+    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+    reporters: ['mocha'],
+
+
+    // level of logging
+    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+    logLevel: config.LOG_INFO,
+
+
+    // enable / disable watching file and executing tests whenever any file changes
+    autoWatch: false,
+
+    // Continuous Integration mode
+    // if true, Karma captures browsers, runs the tests and exits
+    singleRun: true,
+  };
+
+  config.set(my_conf);
+};
diff --git a/app/src/main/assets/novnc/package.json b/app/src/main/assets/novnc/package.json
new file mode 100644
index 00000000..b227673b
--- /dev/null
+++ b/app/src/main/assets/novnc/package.json
@@ -0,0 +1,82 @@
+{
+  "name": "@novnc/novnc",
+  "version": "1.3.0",
+  "description": "An HTML5 VNC client",
+  "browser": "lib/rfb",
+  "directories": {
+    "lib": "lib",
+    "doc": "docs",
+    "test": "tests"
+  },
+  "files": [
+    "lib",
+    "AUTHORS",
+    "VERSION",
+    "docs/API.md",
+    "docs/LIBRARY.md",
+    "docs/LICENSE*",
+    "core",
+    "vendor/pako"
+  ],
+  "scripts": {
+    "lint": "eslint app core po/po2js po/xgettext-html tests utils",
+    "test": "karma start karma.conf.js",
+    "prepublish": "node ./utils/use_require.js --clean"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/novnc/noVNC.git"
+  },
+  "author": "Joel Martin <github@martintribe.org> (https://github.com/kanaka)",
+  "contributors": [
+    "Samuel Mannehed <samuel@cendio.se> (https://github.com/samhed)",
+    "Pierre Ossman <ossman@cendio.se> (https://github.com/CendioOssman)"
+  ],
+  "license": "MPL-2.0",
+  "bugs": {
+    "url": "https://github.com/novnc/noVNC/issues"
+  },
+  "homepage": "https://github.com/novnc/noVNC",
+  "devDependencies": {
+    "@babel/core": "*",
+    "@babel/plugin-syntax-dynamic-import": "*",
+    "@babel/plugin-transform-modules-commonjs": "*",
+    "@babel/preset-env": "*",
+    "@babel/cli": "*",
+    "babel-plugin-import-redirect": "*",
+    "browserify": "*",
+    "babelify": "*",
+    "core-js": "*",
+    "chai": "*",
+    "commander": "*",
+    "es-module-loader": "*",
+    "eslint": "*",
+    "fs-extra": "*",
+    "jsdom": "*",
+    "karma": "*",
+    "karma-mocha": "*",
+    "karma-chrome-launcher": "*",
+    "@chiragrupani/karma-chromium-edge-launcher": "*",
+    "karma-firefox-launcher": "*",
+    "karma-ie-launcher": "*",
+    "karma-mocha-reporter": "*",
+    "karma-safari-launcher": "*",
+    "karma-script-launcher": "*",
+    "karma-sinon-chai": "*",
+    "mocha": "*",
+    "node-getopt": "*",
+    "po2json": "*",
+    "requirejs": "*",
+    "rollup": "*",
+    "rollup-plugin-node-resolve": "*",
+    "sinon": "*",
+    "sinon-chai": "*"
+  },
+  "dependencies": {},
+  "keywords": [
+    "vnc",
+    "rfb",
+    "novnc",
+    "websockify"
+  ]
+}
diff --git a/app/src/main/assets/novnc/po/.eslintrc b/app/src/main/assets/novnc/po/.eslintrc
new file mode 100644
index 00000000..a0157e2a
--- /dev/null
+++ b/app/src/main/assets/novnc/po/.eslintrc
@@ -0,0 +1,5 @@
+{
+    "env": {
+        "node": true,
+    },
+}
diff --git a/app/src/main/assets/novnc/po/Makefile b/app/src/main/assets/novnc/po/Makefile
new file mode 100644
index 00000000..1513b38e
--- /dev/null
+++ b/app/src/main/assets/novnc/po/Makefile
@@ -0,0 +1,36 @@
+all:
+.PHONY: update-po update-js update-pot
+.PHONY: FORCE
+
+LINGUAS := cs de el es fr ja ko nl pl pt_BR ru sv tr zh_CN zh_TW
+
+VERSION := $(shell grep '"version"' ../package.json | cut -d '"' -f 4)
+
+POFILES := $(addsuffix .po,$(LINGUAS))
+JSONFILES := $(addprefix ../app/locale/,$(addsuffix .json,$(LINGUAS)))
+
+update-po: $(POFILES)
+update-js: $(JSONFILES)
+
+%.po: FORCE
+	msgmerge --update --lang=$* $@ noVNC.pot
+../app/locale/%.json: FORCE
+	./po2js $*.po $@
+
+update-pot:
+	xgettext --output=noVNC.js.pot \
+		--copyright-holder="The noVNC Authors" \
+		--package-name="noVNC" \
+		--package-version="$(VERSION)" \
+		--msgid-bugs-address="novnc@googlegroups.com" \
+		--add-comments=TRANSLATORS: \
+		--from-code=UTF-8 \
+		--sort-by-file \
+		../app/*.js \
+		../core/*.js \
+		../core/input/*.js
+	./xgettext-html --output=noVNC.html.pot \
+		../vnc.html
+	msgcat --output-file=noVNC.pot \
+		--sort-by-file noVNC.js.pot noVNC.html.pot
+	rm -f noVNC.js.pot noVNC.html.pot
diff --git a/app/src/main/assets/novnc/po/cs.po b/app/src/main/assets/novnc/po/cs.po
new file mode 100644
index 00000000..2b1efd8d
--- /dev/null
+++ b/app/src/main/assets/novnc/po/cs.po
@@ -0,0 +1,294 @@
+# Czech translations for noVNC package.
+# Copyright (C) 2018 The noVNC Authors
+# This file is distributed under the same license as the noVNC package.
+# Petr <petr@kle.cz>, 2018.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: noVNC 1.0.0-testing.2\n"
+"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
+"POT-Creation-Date: 2018-10-19 12:00+0200\n"
+"PO-Revision-Date: 2018-10-19 12:00+0200\n"
+"Last-Translator: Petr <petr@kle.cz>\n"
+"Language-Team: Czech\n"
+"Language: cs\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: ../app/ui.js:389
+msgid "Connecting..."
+msgstr "Připojení..."
+
+#: ../app/ui.js:396
+msgid "Disconnecting..."
+msgstr "Odpojení..."
+
+#: ../app/ui.js:402
+msgid "Reconnecting..."
+msgstr "Obnova připojení..."
+
+#: ../app/ui.js:407
+msgid "Internal error"
+msgstr "Vnitřní chyba"
+
+#: ../app/ui.js:997
+msgid "Must set host"
+msgstr "Hostitel musí být nastavení"
+
+#: ../app/ui.js:1079
+msgid "Connected (encrypted) to "
+msgstr "Připojení (šifrované) k "
+
+#: ../app/ui.js:1081
+msgid "Connected (unencrypted) to "
+msgstr "Připojení (nešifrované) k "
+
+#: ../app/ui.js:1104
+msgid "Something went wrong, connection is closed"
+msgstr "Něco se pokazilo, odpojeno"
+
+#: ../app/ui.js:1107
+msgid "Failed to connect to server"
+msgstr "Chyba připojení k serveru"
+
+#: ../app/ui.js:1117
+msgid "Disconnected"
+msgstr "Odpojeno"
+
+#: ../app/ui.js:1130
+msgid "New connection has been rejected with reason: "
+msgstr "Nové připojení bylo odmítnuto s odůvodněním: "
+
+#: ../app/ui.js:1133
+msgid "New connection has been rejected"
+msgstr "Nové připojení bylo odmítnuto"
+
+#: ../app/ui.js:1153
+msgid "Password is required"
+msgstr "Je vyžadováno heslo"
+
+#: ../vnc.html:84
+msgid "noVNC encountered an error:"
+msgstr "noVNC narazilo na chybu:"
+
+#: ../vnc.html:94
+msgid "Hide/Show the control bar"
+msgstr "Skrýt/zobrazit ovládací panel"
+
+#: ../vnc.html:101
+msgid "Move/Drag Viewport"
+msgstr "Přesunout/přetáhnout výřez"
+
+#: ../vnc.html:101
+msgid "viewport drag"
+msgstr "přesun výřezu"
+
+#: ../vnc.html:107 ../vnc.html:110 ../vnc.html:113 ../vnc.html:116
+msgid "Active Mouse Button"
+msgstr "Aktivní tlačítka myši"
+
+#: ../vnc.html:107
+msgid "No mousebutton"
+msgstr "Žádné"
+
+#: ../vnc.html:110
+msgid "Left mousebutton"
+msgstr "Levé tlačítko myši"
+
+#: ../vnc.html:113
+msgid "Middle mousebutton"
+msgstr "Prostřední tlačítko myši"
+
+#: ../vnc.html:116
+msgid "Right mousebutton"
+msgstr "Pravé tlačítko myši"
+
+#: ../vnc.html:119
+msgid "Keyboard"
+msgstr "Klávesnice"
+
+#: ../vnc.html:119
+msgid "Show Keyboard"
+msgstr "Zobrazit klávesnici"
+
+#: ../vnc.html:126
+msgid "Extra keys"
+msgstr "Extra klávesy"
+
+#: ../vnc.html:126
+msgid "Show Extra Keys"
+msgstr "Zobrazit extra klávesy"
+
+#: ../vnc.html:131
+msgid "Ctrl"
+msgstr "Ctrl"
+
+#: ../vnc.html:131
+msgid "Toggle Ctrl"
+msgstr "Přepnout Ctrl"
+
+#: ../vnc.html:134
+msgid "Alt"
+msgstr "Alt"
+
+#: ../vnc.html:134
+msgid "Toggle Alt"
+msgstr "Přepnout Alt"
+
+#: ../vnc.html:137
+msgid "Send Tab"
+msgstr "Odeslat tabulátor"
+
+#: ../vnc.html:137
+msgid "Tab"
+msgstr "Tab"
+
+#: ../vnc.html:140
+msgid "Esc"
+msgstr "Esc"
+
+#: ../vnc.html:140
+msgid "Send Escape"
+msgstr "Odeslat Esc"
+
+#: ../vnc.html:143
+msgid "Ctrl+Alt+Del"
+msgstr "Ctrl+Alt+Del"
+
+#: ../vnc.html:143
+msgid "Send Ctrl-Alt-Del"
+msgstr "Poslat Ctrl-Alt-Del"
+
+#: ../vnc.html:151
+msgid "Shutdown/Reboot"
+msgstr "Vypnutí/Restart"
+
+#: ../vnc.html:151
+msgid "Shutdown/Reboot..."
+msgstr "Vypnutí/Restart..."
+
+#: ../vnc.html:157
+msgid "Power"
+msgstr "Napájení"
+
+#: ../vnc.html:159
+msgid "Shutdown"
+msgstr "Vypnout"
+
+#: ../vnc.html:160
+msgid "Reboot"
+msgstr "Restart"
+
+#: ../vnc.html:161
+msgid "Reset"
+msgstr "Reset"
+
+#: ../vnc.html:166 ../vnc.html:172
+msgid "Clipboard"
+msgstr "Schránka"
+
+#: ../vnc.html:176
+msgid "Clear"
+msgstr "Vymazat"
+
+#: ../vnc.html:182
+msgid "Fullscreen"
+msgstr "Celá obrazovka"
+
+#: ../vnc.html:187 ../vnc.html:194
+msgid "Settings"
+msgstr "Nastavení"
+
+#: ../vnc.html:197
+msgid "Shared Mode"
+msgstr "Sdílený režim"
+
+#: ../vnc.html:200
+msgid "View Only"
+msgstr "Pouze prohlížení"
+
+#: ../vnc.html:204
+msgid "Clip to Window"
+msgstr "Přizpůsobit oknu"
+
+#: ../vnc.html:207
+msgid "Scaling Mode:"
+msgstr "Přizpůsobení velikosti"
+
+#: ../vnc.html:209
+msgid "None"
+msgstr "Žádné"
+
+#: ../vnc.html:210
+msgid "Local Scaling"
+msgstr "Místní"
+
+#: ../vnc.html:211
+msgid "Remote Resizing"
+msgstr "Vzdálené"
+
+#: ../vnc.html:216
+msgid "Advanced"
+msgstr "Pokročilé"
+
+#: ../vnc.html:219
+msgid "Repeater ID:"
+msgstr "ID opakovače"
+
+#: ../vnc.html:223
+msgid "WebSocket"
+msgstr "WebSocket"
+
+#: ../vnc.html:226
+msgid "Encrypt"
+msgstr "Šifrování:"
+
+#: ../vnc.html:229
+msgid "Host:"
+msgstr "Hostitel:"
+
+#: ../vnc.html:233
+msgid "Port:"
+msgstr "Port:"
+
+#: ../vnc.html:237
+msgid "Path:"
+msgstr "Cesta"
+
+#: ../vnc.html:244
+msgid "Automatic Reconnect"
+msgstr "Automatická obnova připojení"
+
+#: ../vnc.html:247
+msgid "Reconnect Delay (ms):"
+msgstr "Zpoždění připojení (ms)"
+
+#: ../vnc.html:252
+msgid "Show Dot when No Cursor"
+msgstr "Tečka místo chybějícího kurzoru myši"
+
+#: ../vnc.html:257
+msgid "Logging:"
+msgstr "Logování:"
+
+#: ../vnc.html:269
+msgid "Disconnect"
+msgstr "Odpojit"
+
+#: ../vnc.html:288
+msgid "Connect"
+msgstr "Připojit"
+
+#: ../vnc.html:298
+msgid "Password:"
+msgstr "Heslo"
+
+#: ../vnc.html:302
+msgid "Send Password"
+msgstr "Odeslat heslo"
+
+#: ../vnc.html:312
+msgid "Cancel"
+msgstr "Zrušit"
diff --git a/app/src/main/assets/novnc/po/de.po b/app/src/main/assets/novnc/po/de.po
new file mode 100644
index 00000000..0c3fa0d4
--- /dev/null
+++ b/app/src/main/assets/novnc/po/de.po
@@ -0,0 +1,303 @@
+# German translations for noVNC package
+# German translation for noVNC.
+# Copyright (C) 2018 The noVNC Authors
+# This file is distributed under the same license as the noVNC package.
+# Loek Janssen <loekjanssen@gmail.com>, 2016.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: noVNC 0.6.1\n"
+"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
+"POT-Creation-Date: 2017-11-24 07:16+0000\n"
+"PO-Revision-Date: 2017-11-24 08:20+0100\n"
+"Last-Translator: Dominik Csapak <d.csapak@proxmox.com>\n"
+"Language-Team: none\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Poedit 1.8.11\n"
+
+#: ../app/ui.js:404
+msgid "Connecting..."
+msgstr "Verbinden..."
+
+#: ../app/ui.js:411
+msgid "Disconnecting..."
+msgstr "Verbindung trennen..."
+
+#: ../app/ui.js:417
+msgid "Reconnecting..."
+msgstr "Verbindung wiederherstellen..."
+
+#: ../app/ui.js:422
+msgid "Internal error"
+msgstr "Interner Fehler"
+
+#: ../app/ui.js:1019
+msgid "Must set host"
+msgstr "Richten Sie den Server ein"
+
+#: ../app/ui.js:1099
+msgid "Connected (encrypted) to "
+msgstr "Verbunden mit (verschlüsselt) "
+
+#: ../app/ui.js:1101
+msgid "Connected (unencrypted) to "
+msgstr "Verbunden mit (unverschlüsselt) "
+
+#: ../app/ui.js:1119
+msgid "Something went wrong, connection is closed"
+msgstr "Etwas lief schief, Verbindung wurde getrennt"
+
+#: ../app/ui.js:1129
+msgid "Disconnected"
+msgstr "Verbindung zum Server getrennt"
+
+#: ../app/ui.js:1142
+msgid "New connection has been rejected with reason: "
+msgstr "Verbindung wurde aus folgendem Grund abgelehnt: "
+
+#: ../app/ui.js:1145
+msgid "New connection has been rejected"
+msgstr "Verbindung wurde abgelehnt"
+
+#: ../app/ui.js:1166
+msgid "Password is required"
+msgstr "Passwort ist erforderlich"
+
+#: ../vnc.html:89
+msgid "noVNC encountered an error:"
+msgstr "Ein Fehler ist aufgetreten:"
+
+#: ../vnc.html:99
+msgid "Hide/Show the control bar"
+msgstr "Kontrollleiste verstecken/anzeigen"
+
+#: ../vnc.html:106
+msgid "Move/Drag Viewport"
+msgstr "Ansichtsfenster verschieben/ziehen"
+
+#: ../vnc.html:106
+msgid "viewport drag"
+msgstr "Ansichtsfenster ziehen"
+
+#: ../vnc.html:112 ../vnc.html:115 ../vnc.html:118 ../vnc.html:121
+msgid "Active Mouse Button"
+msgstr "Aktive Maustaste"
+
+#: ../vnc.html:112
+msgid "No mousebutton"
+msgstr "Keine Maustaste"
+
+#: ../vnc.html:115
+msgid "Left mousebutton"
+msgstr "Linke Maustaste"
+
+#: ../vnc.html:118
+msgid "Middle mousebutton"
+msgstr "Mittlere Maustaste"
+
+#: ../vnc.html:121
+msgid "Right mousebutton"
+msgstr "Rechte Maustaste"
+
+#: ../vnc.html:124
+msgid "Keyboard"
+msgstr "Tastatur"
+
+#: ../vnc.html:124
+msgid "Show Keyboard"
+msgstr "Tastatur anzeigen"
+
+#: ../vnc.html:131
+msgid "Extra keys"
+msgstr "Zusatztasten"
+
+#: ../vnc.html:131
+msgid "Show Extra Keys"
+msgstr "Zusatztasten anzeigen"
+
+#: ../vnc.html:136
+msgid "Ctrl"
+msgstr "Strg"
+
+#: ../vnc.html:136
+msgid "Toggle Ctrl"
+msgstr "Strg umschalten"
+
+#: ../vnc.html:139
+msgid "Alt"
+msgstr "Alt"
+
+#: ../vnc.html:139
+msgid "Toggle Alt"
+msgstr "Alt umschalten"
+
+#: ../vnc.html:142
+msgid "Send Tab"
+msgstr "Tab senden"
+
+#: ../vnc.html:142
+msgid "Tab"
+msgstr "Tab"
+
+#: ../vnc.html:145
+msgid "Esc"
+msgstr "Esc"
+
+#: ../vnc.html:145
+msgid "Send Escape"
+msgstr "Escape senden"
+
+#: ../vnc.html:148
+msgid "Ctrl+Alt+Del"
+msgstr "Strg+Alt+Entf"
+
+#: ../vnc.html:148
+msgid "Send Ctrl-Alt-Del"
+msgstr "Strg+Alt+Entf senden"
+
+#: ../vnc.html:156
+msgid "Shutdown/Reboot"
+msgstr "Herunterfahren/Neustarten"
+
+#: ../vnc.html:156
+msgid "Shutdown/Reboot..."
+msgstr "Herunterfahren/Neustarten..."
+
+#: ../vnc.html:162
+msgid "Power"
+msgstr "Energie"
+
+#: ../vnc.html:164
+msgid "Shutdown"
+msgstr "Herunterfahren"
+
+#: ../vnc.html:165
+msgid "Reboot"
+msgstr "Neustarten"
+
+#: ../vnc.html:166
+msgid "Reset"
+msgstr "Zurücksetzen"
+
+#: ../vnc.html:171 ../vnc.html:177
+msgid "Clipboard"
+msgstr "Zwischenablage"
+
+#: ../vnc.html:181
+msgid "Clear"
+msgstr "Löschen"
+
+#: ../vnc.html:187
+msgid "Fullscreen"
+msgstr "Vollbild"
+
+#: ../vnc.html:192 ../vnc.html:199
+msgid "Settings"
+msgstr "Einstellungen"
+
+#: ../vnc.html:202
+msgid "Shared Mode"
+msgstr "Geteilter Modus"
+
+#: ../vnc.html:205
+msgid "View Only"
+msgstr "Nur betrachten"
+
+#: ../vnc.html:209
+msgid "Clip to Window"
+msgstr "Auf Fenster begrenzen"
+
+#: ../vnc.html:212
+msgid "Scaling Mode:"
+msgstr "Skalierungsmodus:"
+
+#: ../vnc.html:214
+msgid "None"
+msgstr "Keiner"
+
+#: ../vnc.html:215
+msgid "Local Scaling"
+msgstr "Lokales skalieren"
+
+#: ../vnc.html:216
+msgid "Remote Resizing"
+msgstr "Serverseitiges skalieren"
+
+#: ../vnc.html:221
+msgid "Advanced"
+msgstr "Erweitert"
+
+#: ../vnc.html:224
+msgid "Repeater ID:"
+msgstr "Repeater ID:"
+
+#: ../vnc.html:228
+msgid "WebSocket"
+msgstr "WebSocket"
+
+#: ../vnc.html:231
+msgid "Encrypt"
+msgstr "Verschlüsselt"
+
+#: ../vnc.html:234
+msgid "Host:"
+msgstr "Server:"
+
+#: ../vnc.html:238
+msgid "Port:"
+msgstr "Port:"
+
+#: ../vnc.html:242
+msgid "Path:"
+msgstr "Pfad:"
+
+#: ../vnc.html:249
+msgid "Automatic Reconnect"
+msgstr "Automatisch wiederverbinden"
+
+#: ../vnc.html:252
+msgid "Reconnect Delay (ms):"
+msgstr "Wiederverbindungsverzögerung (ms):"
+
+#: ../vnc.html:258
+msgid "Logging:"
+msgstr "Protokollierung:"
+
+#: ../vnc.html:270
+msgid "Disconnect"
+msgstr "Verbindung trennen"
+
+#: ../vnc.html:289
+msgid "Connect"
+msgstr "Verbinden"
+
+#: ../vnc.html:299
+msgid "Password:"
+msgstr "Passwort:"
+
+#: ../vnc.html:313
+msgid "Cancel"
+msgstr "Abbrechen"
+
+#: ../vnc.html:329
+msgid "Canvas not supported."
+msgstr "Canvas nicht unterstützt."
+
+#~ msgid "Disconnect timeout"
+#~ msgstr "Zeitüberschreitung beim Trennen"
+
+#~ msgid "Local Downscaling"
+#~ msgstr "Lokales herunterskalieren"
+
+#~ msgid "Local Cursor"
+#~ msgstr "Lokaler Mauszeiger"
+
+#~ msgid "Forcing clipping mode since scrollbars aren't supported by IE in fullscreen"
+#~ msgstr "'Clipping-Modus' aktiviert, Scrollbalken in 'IE-Vollbildmodus' werden nicht unterstützt"
+
+#~ msgid "True Color"
+#~ msgstr "True Color"
diff --git a/app/src/main/assets/novnc/po/el.po b/app/src/main/assets/novnc/po/el.po
new file mode 100644
index 00000000..5213ae54
--- /dev/null
+++ b/app/src/main/assets/novnc/po/el.po
@@ -0,0 +1,323 @@
+# Greek translations for noVNC package.
+# Copyright (C) 2018 The noVNC Authors
+# This file is distributed under the same license as the noVNC package.
+# Giannis Kosmas <kosmasgiannis@gmail.com>, 2016.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: noVNC 0.6.1\n"
+"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
+"POT-Creation-Date: 2017-11-17 21:40+0200\n"
+"PO-Revision-Date: 2017-10-11 16:16+0200\n"
+"Last-Translator: Giannis Kosmas <kosmasgiannis@gmail.com>\n"
+"Language-Team: none\n"
+"Language: el\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../app/ui.js:404
+msgid "Connecting..."
+msgstr "Συνδέεται..."
+
+#: ../app/ui.js:411
+msgid "Disconnecting..."
+msgstr "Aποσυνδέεται..."
+
+#: ../app/ui.js:417
+msgid "Reconnecting..."
+msgstr "Επανασυνδέεται..."
+
+#: ../app/ui.js:422
+msgid "Internal error"
+msgstr "Εσωτερικό σφάλμα"
+
+#: ../app/ui.js:1019
+msgid "Must set host"
+msgstr "Πρέπει να οριστεί ο διακομιστής"
+
+#: ../app/ui.js:1099
+msgid "Connected (encrypted) to "
+msgstr "Συνδέθηκε (κρυπτογραφημένα) με το "
+
+#: ../app/ui.js:1101
+msgid "Connected (unencrypted) to "
+msgstr "Συνδέθηκε (μη κρυπτογραφημένα) με το "
+
+#: ../app/ui.js:1119
+msgid "Something went wrong, connection is closed"
+msgstr "Κάτι πήγε στραβά, η σύνδεση διακόπηκε"
+
+#: ../app/ui.js:1129
+msgid "Disconnected"
+msgstr "Αποσυνδέθηκε"
+
+#: ../app/ui.js:1142
+msgid "New connection has been rejected with reason: "
+msgstr "Η νέα σύνδεση απορρίφθηκε διότι: "
+
+#: ../app/ui.js:1145
+msgid "New connection has been rejected"
+msgstr "Η νέα σύνδεση απορρίφθηκε "
+
+#: ../app/ui.js:1166
+msgid "Password is required"
+msgstr "Απαιτείται ο κωδικός πρόσβασης"
+
+#: ../vnc.html:89
+msgid "noVNC encountered an error:"
+msgstr "το noVNC αντιμετώπισε ένα σφάλμα:"
+
+#: ../vnc.html:99
+msgid "Hide/Show the control bar"
+msgstr "Απόκρυψη/Εμφάνιση γραμμής ελέγχου"
+
+#: ../vnc.html:106
+msgid "Move/Drag Viewport"
+msgstr "Μετακίνηση/Σύρσιμο Θεατού πεδίου"
+
+#: ../vnc.html:106
+msgid "viewport drag"
+msgstr "σύρσιμο θεατού πεδίου"
+
+#: ../vnc.html:112 ../vnc.html:115 ../vnc.html:118 ../vnc.html:121
+msgid "Active Mouse Button"
+msgstr "Ενεργό Πλήκτρο Ποντικιού"
+
+#: ../vnc.html:112
+msgid "No mousebutton"
+msgstr "Χωρίς Πλήκτρο Ποντικιού"
+
+#: ../vnc.html:115
+msgid "Left mousebutton"
+msgstr "Αριστερό Πλήκτρο Ποντικιού"
+
+#: ../vnc.html:118
+msgid "Middle mousebutton"
+msgstr "Μεσαίο Πλήκτρο Ποντικιού"
+
+#: ../vnc.html:121
+msgid "Right mousebutton"
+msgstr "Δεξί Πλήκτρο Ποντικιού"
+
+#: ../vnc.html:124
+msgid "Keyboard"
+msgstr "Πληκτρολόγιο"
+
+#: ../vnc.html:124
+msgid "Show Keyboard"
+msgstr "Εμφάνιση Πληκτρολογίου"
+
+#: ../vnc.html:131
+msgid "Extra keys"
+msgstr "Επιπλέον πλήκτρα"
+
+#: ../vnc.html:131
+msgid "Show Extra Keys"
+msgstr "Εμφάνιση Επιπλέον Πλήκτρων"
+
+#: ../vnc.html:136
+msgid "Ctrl"
+msgstr "Ctrl"
+
+#: ../vnc.html:136
+msgid "Toggle Ctrl"
+msgstr "Εναλλαγή Ctrl"
+
+#: ../vnc.html:139
+msgid "Alt"
+msgstr "Alt"
+
+#: ../vnc.html:139
+msgid "Toggle Alt"
+msgstr "Εναλλαγή Alt"
+
+#: ../vnc.html:142
+msgid "Send Tab"
+msgstr "Αποστολή Tab"
+
+#: ../vnc.html:142
+msgid "Tab"
+msgstr "Tab"
+
+#: ../vnc.html:145
+msgid "Esc"
+msgstr "Esc"
+
+#: ../vnc.html:145
+msgid "Send Escape"
+msgstr "Αποστολή Escape"
+
+#: ../vnc.html:148
+msgid "Ctrl+Alt+Del"
+msgstr "Ctrl+Alt+Del"
+
+#: ../vnc.html:148
+msgid "Send Ctrl-Alt-Del"
+msgstr "Αποστολή Ctrl-Alt-Del"
+
+#: ../vnc.html:156
+msgid "Shutdown/Reboot"
+msgstr "Κλείσιμο/Επανεκκίνηση"
+
+#: ../vnc.html:156
+msgid "Shutdown/Reboot..."
+msgstr "Κλείσιμο/Επανεκκίνηση..."
+
+#: ../vnc.html:162
+msgid "Power"
+msgstr "Απενεργοποίηση"
+
+#: ../vnc.html:164
+msgid "Shutdown"
+msgstr "Κλείσιμο"
+
+#: ../vnc.html:165
+msgid "Reboot"
+msgstr "Επανεκκίνηση"
+
+#: ../vnc.html:166
+msgid "Reset"
+msgstr "Επαναφορά"
+
+#: ../vnc.html:171 ../vnc.html:177
+msgid "Clipboard"
+msgstr "Πρόχειρο"
+
+#: ../vnc.html:181
+msgid "Clear"
+msgstr "Καθάρισμα"
+
+#: ../vnc.html:187
+msgid "Fullscreen"
+msgstr "Πλήρης Οθόνη"
+
+#: ../vnc.html:192 ../vnc.html:199
+msgid "Settings"
+msgstr "Ρυθμίσεις"
+
+#: ../vnc.html:202
+msgid "Shared Mode"
+msgstr "Κοινόχρηστη Λειτουργία"
+
+#: ../vnc.html:205
+msgid "View Only"
+msgstr "Μόνο Θέαση"
+
+#: ../vnc.html:209
+msgid "Clip to Window"
+msgstr "Αποκοπή στο όριο του Παράθυρου"
+
+#: ../vnc.html:212
+msgid "Scaling Mode:"
+msgstr "Λειτουργία Κλιμάκωσης:"
+
+#: ../vnc.html:214
+msgid "None"
+msgstr "Καμία"
+
+#: ../vnc.html:215
+msgid "Local Scaling"
+msgstr "Τοπική Κλιμάκωση"
+
+#: ../vnc.html:216
+msgid "Remote Resizing"
+msgstr "Απομακρυσμένη Αλλαγή μεγέθους"
+
+#: ../vnc.html:221
+msgid "Advanced"
+msgstr "Για προχωρημένους"
+
+#: ../vnc.html:224
+msgid "Repeater ID:"
+msgstr "Repeater ID:"
+
+#: ../vnc.html:228
+msgid "WebSocket"
+msgstr "WebSocket"
+
+#: ../vnc.html:231
+msgid "Encrypt"
+msgstr "Κρυπτογράφηση"
+
+#: ../vnc.html:234
+msgid "Host:"
+msgstr "Όνομα διακομιστή:"
+
+#: ../vnc.html:238
+msgid "Port:"
+msgstr "Πόρτα διακομιστή:"
+
+#: ../vnc.html:242
+msgid "Path:"
+msgstr "Διαδρομή:"
+
+#: ../vnc.html:249
+msgid "Automatic Reconnect"
+msgstr "Αυτόματη επανασύνδεση"
+
+#: ../vnc.html:252
+msgid "Reconnect Delay (ms):"
+msgstr "Καθυστέρηση επανασύνδεσης (ms):"
+
+#: ../vnc.html:258
+msgid "Logging:"
+msgstr "Καταγραφή:"
+
+#: ../vnc.html:270
+msgid "Disconnect"
+msgstr "Αποσύνδεση"
+
+#: ../vnc.html:289
+msgid "Connect"
+msgstr "Σύνδεση"
+
+#: ../vnc.html:299
+msgid "Password:"
+msgstr "Κωδικός Πρόσβασης:"
+
+#: ../vnc.html:313
+msgid "Cancel"
+msgstr "Ακύρωση"
+
+#: ../vnc.html:329
+msgid "Canvas not supported."
+msgstr "Δεν υποστηρίζεται το στοιχείο Canvas"
+
+#~ msgid "Disconnect timeout"
+#~ msgstr "Παρέλευση χρονικού ορίου αποσύνδεσης"
+
+#~ msgid "Local Downscaling"
+#~ msgstr "Τοπική Συρρίκνωση"
+
+#~ msgid "Local Cursor"
+#~ msgstr "Τοπικός Δρομέας"
+
+#~ msgid ""
+#~ "Forcing clipping mode since scrollbars aren't supported by IE in "
+#~ "fullscreen"
+#~ msgstr ""
+#~ "Εφαρμογή λειτουργίας αποκοπής αφού δεν υποστηρίζονται οι λωρίδες κύλισης "
+#~ "σε πλήρη οθόνη στον IE"
+
+#~ msgid "True Color"
+#~ msgstr "Πραγματικά Χρώματα"
+
+#~ msgid "Style:"
+#~ msgstr "Στυλ:"
+
+#~ msgid "default"
+#~ msgstr "προεπιλεγμένο"
+
+#~ msgid "Apply"
+#~ msgstr "Εφαρμογή"
+
+#~ msgid "Connection"
+#~ msgstr "Σύνδεση"
+
+#~ msgid "Token:"
+#~ msgstr "Διακριτικό:"
+
+#~ msgid "Send Password"
+#~ msgstr "Αποστολή Κωδικού Πρόσβασης"
diff --git a/app/src/main/assets/novnc/po/es.po b/app/src/main/assets/novnc/po/es.po
new file mode 100644
index 00000000..1230402f
--- /dev/null
+++ b/app/src/main/assets/novnc/po/es.po
@@ -0,0 +1,284 @@
+# Spanish translations for noVNC package
+# Traducciones al español para el paquete noVNC.
+# Copyright (C) 2018 The noVNC Authors
+# This file is distributed under the same license as the noVNC package.
+# Juanjo Diaz <juanjo.diazmo@gmail.com>, 2018.
+# Adrian Scillato <ascillato@gmail.com>, 2021.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: noVNC 1.0.0-testing.2\n"
+"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
+"POT-Creation-Date: 2017-10-06 10:07+0200\n"
+"PO-Revision-Date: 2021-04-23 12:00-0300\n"
+"Last-Translator: Adrian Scillato <ascillato@gmail.com>\n"
+"Language-Team: Spanish\n"
+"Language: es\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../app/ui.js:430
+msgid "Connecting..."
+msgstr "Conectando..."
+
+#: ../app/ui.js:438
+msgid "Connected (encrypted) to "
+msgstr "Conectado (con encriptación) a"
+
+#: ../app/ui.js:440
+msgid "Connected (unencrypted) to "
+msgstr "Conectado (sin encriptación) a"
+
+#: ../app/ui.js:446
+msgid "Disconnecting..."
+msgstr "Desconectando..."
+
+#: ../app/ui.js:450
+msgid "Disconnected"
+msgstr "Desconectado"
+
+#: ../app/ui.js:1052 ../core/rfb.js:248
+msgid "Must set host"
+msgstr "Se debe configurar el host"
+
+#: ../app/ui.js:1101
+msgid "Reconnecting..."
+msgstr "Reconectando..."
+
+#: ../app/ui.js:1140
+msgid "Password is required"
+msgstr "La contraseña es obligatoria"
+
+#: ../core/rfb.js:548
+msgid "Disconnect timeout"
+msgstr "Tiempo de desconexión agotado"
+
+#: ../vnc.html:89
+msgid "noVNC encountered an error:"
+msgstr "noVNC ha encontrado un error:"
+
+#: ../vnc.html:99
+msgid "Hide/Show the control bar"
+msgstr "Ocultar/Mostrar la barra de control"
+
+#: ../vnc.html:106
+msgid "Move/Drag Viewport"
+msgstr "Mover/Arrastrar la ventana"
+
+#: ../vnc.html:106
+msgid "viewport drag"
+msgstr "Arrastrar la ventana"
+
+#: ../vnc.html:112 ../vnc.html:115 ../vnc.html:118 ../vnc.html:121
+msgid "Active Mouse Button"
+msgstr "Botón activo del ratón"
+
+#: ../vnc.html:112
+msgid "No mousebutton"
+msgstr "Ningún botón del ratón"
+
+#: ../vnc.html:115
+msgid "Left mousebutton"
+msgstr "Botón izquierdo del ratón"
+
+#: ../vnc.html:118
+msgid "Middle mousebutton"
+msgstr "Botón central del ratón"
+
+#: ../vnc.html:121
+msgid "Right mousebutton"
+msgstr "Botón derecho del ratón"
+
+#: ../vnc.html:124
+msgid "Keyboard"
+msgstr "Teclado"
+
+#: ../vnc.html:124
+msgid "Show Keyboard"
+msgstr "Mostrar teclado"
+
+#: ../vnc.html:131
+msgid "Extra keys"
+msgstr "Teclas adicionales"
+
+#: ../vnc.html:131
+msgid "Show Extra Keys"
+msgstr "Mostrar Teclas Adicionales"
+
+#: ../vnc.html:136
+msgid "Ctrl"
+msgstr "Ctrl"
+
+#: ../vnc.html:136
+msgid "Toggle Ctrl"
+msgstr "Pulsar/Soltar Ctrl"
+
+#: ../vnc.html:139
+msgid "Alt"
+msgstr "Alt"
+
+#: ../vnc.html:139
+msgid "Toggle Alt"
+msgstr "Pulsar/Soltar Alt"
+
+#: ../vnc.html:142
+msgid "Send Tab"
+msgstr "Enviar Tabulación"
+
+#: ../vnc.html:142
+msgid "Tab"
+msgstr "Tabulación"
+
+#: ../vnc.html:145
+msgid "Esc"
+msgstr "Esc"
+
+#: ../vnc.html:145
+msgid "Send Escape"
+msgstr "Enviar Escape"
+
+#: ../vnc.html:148
+msgid "Ctrl+Alt+Del"
+msgstr "Ctrl+Alt+Del"
+
+#: ../vnc.html:148
+msgid "Send Ctrl-Alt-Del"
+msgstr "Enviar Ctrl+Alt+Del"
+
+#: ../vnc.html:156
+msgid "Shutdown/Reboot"
+msgstr "Apagar/Reiniciar"
+
+#: ../vnc.html:156
+msgid "Shutdown/Reboot..."
+msgstr "Apagar/Reiniciar..."
+
+#: ../vnc.html:162
+msgid "Power"
+msgstr "Encender"
+
+#: ../vnc.html:164
+msgid "Shutdown"
+msgstr "Apagar"
+
+#: ../vnc.html:165
+msgid "Reboot"
+msgstr "Reiniciar"
+
+#: ../vnc.html:166
+msgid "Reset"
+msgstr "Restablecer"
+
+#: ../vnc.html:171 ../vnc.html:177
+msgid "Clipboard"
+msgstr "Portapapeles"
+
+#: ../vnc.html:181
+msgid "Clear"
+msgstr "Vaciar"
+
+#: ../vnc.html:187
+msgid "Fullscreen"
+msgstr "Pantalla Completa"
+
+#: ../vnc.html:192 ../vnc.html:199
+msgid "Settings"
+msgstr "Configuraciones"
+
+#: ../vnc.html:200
+msgid "Encrypt"
+msgstr "Encriptar"
+
+#: ../vnc.html:202
+msgid "Shared Mode"
+msgstr "Modo Compartido"
+
+#: ../vnc.html:205
+msgid "View Only"
+msgstr "Solo visualización"
+
+#: ../vnc.html:209
+msgid "Clip to Window"
+msgstr "Recortar al tamaño de la ventana"
+
+#: ../vnc.html:212
+msgid "Scaling Mode:"
+msgstr "Modo de escalado:"
+
+#: ../vnc.html:214
+msgid "None"
+msgstr "Ninguno"
+
+#: ../vnc.html:215
+msgid "Local Scaling"
+msgstr "Escalado Local"
+
+#: ../vnc.html:216
+msgid "Local Downscaling"
+msgstr "Reducción de escala local"
+
+#: ../vnc.html:217
+msgid "Remote Resizing"
+msgstr "Cambio de tamaño remoto"
+
+#: ../vnc.html:222
+msgid "Advanced"
+msgstr "Avanzado"
+
+#: ../vnc.html:225
+msgid "Local Cursor"
+msgstr "Cursor Local"
+
+#: ../vnc.html:229
+msgid "Repeater ID:"
+msgstr "ID del Repetidor:"
+
+#: ../vnc.html:233
+msgid "WebSocket"
+msgstr "WebSocket"
+
+#: ../vnc.html:239
+msgid "Host:"
+msgstr "Host:"
+
+#: ../vnc.html:243
+msgid "Port:"
+msgstr "Puerto:"
+
+#: ../vnc.html:247
+msgid "Path:"
+msgstr "Ruta:"
+
+#: ../vnc.html:254
+msgid "Automatic Reconnect"
+msgstr "Reconexión automática"
+
+#: ../vnc.html:257
+msgid "Reconnect Delay (ms):"
+msgstr "Retraso en la reconexión (ms):"
+
+#: ../vnc.html:263
+msgid "Logging:"
+msgstr "Registrando:"
+
+#: ../vnc.html:275
+msgid "Disconnect"
+msgstr "Desconectar"
+
+#: ../vnc.html:294
+msgid "Connect"
+msgstr "Conectar"
+
+#: ../vnc.html:304
+msgid "Password:"
+msgstr "Contraseña:"
+
+#: ../vnc.html:318
+msgid "Cancel"
+msgstr "Cancelar"
+
+#: ../vnc.html:334
+msgid "Canvas not supported."
+msgstr "Canvas no soportado."
diff --git a/app/src/main/assets/novnc/po/fr.po b/app/src/main/assets/novnc/po/fr.po
new file mode 100644
index 00000000..6082881f
--- /dev/null
+++ b/app/src/main/assets/novnc/po/fr.po
@@ -0,0 +1,299 @@
+# French translations for noVNC package
+# Traductions françaises du paquet noVNC.
+# Copyright (C) 2021 The noVNC Authors
+# This file is distributed under the same license as the noVNC package.
+# Jose <jose.matsuda@canada.ca>, 2021.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: noVNC 1.2.0\n"
+"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
+"POT-Creation-Date: 2020-07-03 16:11+0200\n"
+"PO-Revision-Date: 2021-05-05 20:19-0400\n"
+"Last-Translator: Jose <jose.matsuda@canada.ca>\n"
+"Language-Team: French\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: ../app/ui.js:394
+msgid "Connecting..."
+msgstr "En cours de connexion..."
+
+#: ../app/ui.js:401
+msgid "Disconnecting..."
+msgstr "Déconnexion en cours..."
+
+#: ../app/ui.js:407
+msgid "Reconnecting..."
+msgstr "Reconnexion en cours..."
+
+#: ../app/ui.js:412
+msgid "Internal error"
+msgstr "Erreur interne"
+
+#: ../app/ui.js:1008
+msgid "Must set host"
+msgstr "Doit définir l'hôte"
+
+#: ../app/ui.js:1090
+msgid "Connected (encrypted) to "
+msgstr "Connecté (crypté) à "
+
+#: ../app/ui.js:1092
+msgid "Connected (unencrypted) to "
+msgstr "Connecté (non crypté) à "
+
+#: ../app/ui.js:1115
+msgid "Something went wrong, connection is closed"
+msgstr "Quelque chose est arrivé, la connexion est fermée"
+
+#: ../app/ui.js:1118
+msgid "Failed to connect to server"
+msgstr "Échec de connexion au serveur"
+
+#: ../app/ui.js:1128
+msgid "Disconnected"
+msgstr "Déconnecté"
+
+#: ../app/ui.js:1143
+msgid "New connection has been rejected with reason: "
+msgstr "Une nouvelle connexion a été rejetée avec raison: "
+
+#: ../app/ui.js:1146
+msgid "New connection has been rejected"
+msgstr "Une nouvelle connexion a été rejetée"
+
+#: ../app/ui.js:1181
+msgid "Credentials are required"
+msgstr "Les identifiants sont requis"
+
+#: ../vnc.html:74
+msgid "noVNC encountered an error:"
+msgstr "noVNC a rencontré une erreur:"
+
+#: ../vnc.html:84
+msgid "Hide/Show the control bar"
+msgstr "Masquer/Afficher la barre de contrôle"
+
+#: ../vnc.html:91
+msgid "Drag"
+msgstr "Faire glisser"
+
+#: ../vnc.html:91
+msgid "Move/Drag Viewport"
+msgstr "Déplacer/faire glisser Viewport"
+
+#: ../vnc.html:97
+msgid "Keyboard"
+msgstr "Clavier"
+
+#: ../vnc.html:97
+msgid "Show Keyboard"
+msgstr "Afficher le clavier"
+
+#: ../vnc.html:102
+msgid "Extra keys"
+msgstr "Touches supplémentaires"
+
+#: ../vnc.html:102
+msgid "Show Extra Keys"
+msgstr "Afficher les touches supplémentaires"
+
+#: ../vnc.html:107
+msgid "Ctrl"
+msgstr "Ctrl"
+
+#: ../vnc.html:107
+msgid "Toggle Ctrl"
+msgstr "Basculer Ctrl"
+
+#: ../vnc.html:110
+msgid "Alt"
+msgstr "Alt"
+
+#: ../vnc.html:110
+msgid "Toggle Alt"
+msgstr "Basculer Alt"
+
+#: ../vnc.html:113
+msgid "Toggle Windows"
+msgstr "Basculer Windows"
+
+#: ../vnc.html:113
+msgid "Windows"
+msgstr "Windows"
+
+#: ../vnc.html:116
+msgid "Send Tab"
+msgstr "Envoyer l'onglet"
+
+#: ../vnc.html:116
+msgid "Tab"
+msgstr "l'onglet"
+
+#: ../vnc.html:119
+msgid "Esc"
+msgstr "Esc"
+
+#: ../vnc.html:119
+msgid "Send Escape"
+msgstr "Envoyer Escape"
+
+#: ../vnc.html:122
+msgid "Ctrl+Alt+Del"
+msgstr "Ctrl+Alt+Del"
+
+#: ../vnc.html:122
+msgid "Send Ctrl-Alt-Del"
+msgstr "Envoyer Ctrl-Alt-Del"
+
+#: ../vnc.html:129
+msgid "Shutdown/Reboot"
+msgstr "Arrêter/Redémarrer"
+
+#: ../vnc.html:129
+msgid "Shutdown/Reboot..."
+msgstr "Arrêter/Redémarrer..."
+
+#: ../vnc.html:135
+msgid "Power"
+msgstr "Alimentation"
+
+#: ../vnc.html:137
+msgid "Shutdown"
+msgstr "Arrêter"
+
+#: ../vnc.html:138
+msgid "Reboot"
+msgstr "Redémarrer"
+
+#: ../vnc.html:139
+msgid "Reset"
+msgstr "Réinitialiser"
+
+#: ../vnc.html:144 ../vnc.html:150
+msgid "Clipboard"
+msgstr "Presse-papiers"
+
+#: ../vnc.html:154
+msgid "Clear"
+msgstr "Effacer"
+
+#: ../vnc.html:160
+msgid "Fullscreen"
+msgstr "Plein écran"
+
+#: ../vnc.html:165 ../vnc.html:172
+msgid "Settings"
+msgstr "Paramètres"
+
+#: ../vnc.html:175
+msgid "Shared Mode"
+msgstr "Mode partagé"
+
+#: ../vnc.html:178
+msgid "View Only"
+msgstr "Afficher uniquement"
+
+#: ../vnc.html:182
+msgid "Clip to Window"
+msgstr "Clip à fenêtre"
+
+#: ../vnc.html:185
+msgid "Scaling Mode:"
+msgstr "Mode mise à l'échelle:"
+
+#: ../vnc.html:187
+msgid "None"
+msgstr "Aucun"
+
+#: ../vnc.html:188
+msgid "Local Scaling"
+msgstr "Mise à l'échelle locale"
+
+#: ../vnc.html:189
+msgid "Remote Resizing"
+msgstr "Redimensionnement à distance"
+
+#: ../vnc.html:194
+msgid "Advanced"
+msgstr "Avancé"
+
+#: ../vnc.html:197
+msgid "Quality:"
+msgstr "Qualité:"
+
+#: ../vnc.html:201
+msgid "Compression level:"
+msgstr "Niveau de compression:"
+
+#: ../vnc.html:206
+msgid "Repeater ID:"
+msgstr "ID Répéteur:"
+
+#: ../vnc.html:210
+msgid "WebSocket"
+msgstr "WebSocket"
+
+#: ../vnc.html:213
+msgid "Encrypt"
+msgstr "Crypter"
+
+#: ../vnc.html:216
+msgid "Host:"
+msgstr "Hôte:"
+
+#: ../vnc.html:220
+msgid "Port:"
+msgstr "Port:"
+
+#: ../vnc.html:224
+msgid "Path:"
+msgstr "Chemin:"
+
+#: ../vnc.html:231
+msgid "Automatic Reconnect"
+msgstr "Reconnecter automatiquemen"
+
+#: ../vnc.html:234
+msgid "Reconnect Delay (ms):"
+msgstr "Délai de reconnexion (ms):"
+
+#: ../vnc.html:239
+msgid "Show Dot when No Cursor"
+msgstr "Afficher le point lorsqu'il n'y a pas de curseur"
+
+#: ../vnc.html:244
+msgid "Logging:"
+msgstr "Se connecter:"
+
+#: ../vnc.html:253
+msgid "Version:"
+msgstr "Version:"
+
+#: ../vnc.html:261
+msgid "Disconnect"
+msgstr "Déconnecter"
+
+#: ../vnc.html:280
+msgid "Connect"
+msgstr "Connecter"
+
+#: ../vnc.html:290
+msgid "Username:"
+msgstr "Nom d'utilisateur:"
+
+#: ../vnc.html:294
+msgid "Password:"
+msgstr "Mot de passe:"
+
+#: ../vnc.html:298
+msgid "Send Credentials"
+msgstr "Envoyer les identifiants"
+
+#: ../vnc.html:308
+msgid "Cancel"
+msgstr "Annuler"
diff --git a/app/src/main/assets/novnc/po/ja.po b/app/src/main/assets/novnc/po/ja.po
new file mode 100644
index 00000000..a9b3dcd1
--- /dev/null
+++ b/app/src/main/assets/novnc/po/ja.po
@@ -0,0 +1,324 @@
+# Japanese translations for noVNC package
+# noVNC パッケージに対する日訳
+# Copyright (C) 2019 The noVNC Authors
+# This file is distributed under the same license as the noVNC package.
+# nnn1590 <nnn1590@nnn1590.org>, 2019-2020.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: noVNC 1.1.0\n"
+"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
+"POT-Creation-Date: 2020-07-03 16:11+0200\n"
+"PO-Revision-Date: 2021-01-15 12:37+0900\n"
+"Last-Translator: nnn1590 <nnn1590@nnn1590.org>\n"
+"Language-Team: Japanese\n"
+"Language: ja\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Generator: Poedit 2.3\n"
+
+#: ../app/ui.js:394
+msgid "Connecting..."
+msgstr "接続しています..."
+
+#: ../app/ui.js:401
+msgid "Disconnecting..."
+msgstr "切断しています..."
+
+#: ../app/ui.js:407
+msgid "Reconnecting..."
+msgstr "再接続しています..."
+
+#: ../app/ui.js:412
+msgid "Internal error"
+msgstr "内部エラー"
+
+#: ../app/ui.js:1008
+msgid "Must set host"
+msgstr "ホストを設定する必要があります"
+
+#: ../app/ui.js:1090
+msgid "Connected (encrypted) to "
+msgstr "接続しました (暗号化済み): "
+
+#: ../app/ui.js:1092
+msgid "Connected (unencrypted) to "
+msgstr "接続しました (暗号化されていません): "
+
+#: ../app/ui.js:1115
+msgid "Something went wrong, connection is closed"
+msgstr "何らかの問題で、接続が閉じられました"
+
+#: ../app/ui.js:1118
+msgid "Failed to connect to server"
+msgstr "サーバーへの接続に失敗しました"
+
+#: ../app/ui.js:1128
+msgid "Disconnected"
+msgstr "切断しました"
+
+#: ../app/ui.js:1143
+msgid "New connection has been rejected with reason: "
+msgstr "新規接続は次の理由で拒否されました: "
+
+#: ../app/ui.js:1146
+msgid "New connection has been rejected"
+msgstr "新規接続は拒否されました"
+
+#: ../app/ui.js:1181
+msgid "Credentials are required"
+msgstr "資格情報が必要です"
+
+#: ../vnc.html:74
+msgid "noVNC encountered an error:"
+msgstr "noVNC でエラーが発生しました:"
+
+#: ../vnc.html:84
+msgid "Hide/Show the control bar"
+msgstr "コントロールバーを隠す/表示する"
+
+#: ../vnc.html:91
+msgid "Drag"
+msgstr "ドラッグ"
+
+#: ../vnc.html:91
+msgid "Move/Drag Viewport"
+msgstr "ビューポートを移動/ドラッグ"
+
+#: ../vnc.html:97
+msgid "Keyboard"
+msgstr "キーボード"
+
+#: ../vnc.html:97
+msgid "Show Keyboard"
+msgstr "キーボードを表示"
+
+#: ../vnc.html:102
+msgid "Extra keys"
+msgstr "追加キー"
+
+#: ../vnc.html:102
+msgid "Show Extra Keys"
+msgstr "追加キーを表示"
+
+#: ../vnc.html:107
+msgid "Ctrl"
+msgstr "Ctrl"
+
+#: ../vnc.html:107
+msgid "Toggle Ctrl"
+msgstr "Ctrl キーを切り替え"
+
+#: ../vnc.html:110
+msgid "Alt"
+msgstr "Alt"
+
+#: ../vnc.html:110
+msgid "Toggle Alt"
+msgstr "Alt キーを切り替え"
+
+#: ../vnc.html:113
+msgid "Toggle Windows"
+msgstr "Windows キーを切り替え"
+
+#: ../vnc.html:113
+msgid "Windows"
+msgstr "Windows"
+
+#: ../vnc.html:116
+msgid "Send Tab"
+msgstr "Tab キーを送信"
+
+#: ../vnc.html:116
+msgid "Tab"
+msgstr "Tab"
+
+#: ../vnc.html:119
+msgid "Esc"
+msgstr "Esc"
+
+#: ../vnc.html:119
+msgid "Send Escape"
+msgstr "Escape キーを送信"
+
+#: ../vnc.html:122
+msgid "Ctrl+Alt+Del"
+msgstr "Ctrl+Alt+Del"
+
+#: ../vnc.html:122
+msgid "Send Ctrl-Alt-Del"
+msgstr "Ctrl-Alt-Del を送信"
+
+#: ../vnc.html:129
+msgid "Shutdown/Reboot"
+msgstr "シャットダウン/再起動"
+
+#: ../vnc.html:129
+msgid "Shutdown/Reboot..."
+msgstr "シャットダウン/再起動..."
+
+#: ../vnc.html:135
+msgid "Power"
+msgstr "電源"
+
+#: ../vnc.html:137
+msgid "Shutdown"
+msgstr "シャットダウン"
+
+#: ../vnc.html:138
+msgid "Reboot"
+msgstr "再起動"
+
+#: ../vnc.html:139
+msgid "Reset"
+msgstr "リセット"
+
+#: ../vnc.html:144 ../vnc.html:150
+msgid "Clipboard"
+msgstr "クリップボード"
+
+#: ../vnc.html:154
+msgid "Clear"
+msgstr "クリア"
+
+#: ../vnc.html:160
+msgid "Fullscreen"
+msgstr "全画面表示"
+
+#: ../vnc.html:165 ../vnc.html:172
+msgid "Settings"
+msgstr "設定"
+
+#: ../vnc.html:175
+msgid "Shared Mode"
+msgstr "共有モード"
+
+#: ../vnc.html:178
+msgid "View Only"
+msgstr "表示のみ"
+
+#: ../vnc.html:182
+msgid "Clip to Window"
+msgstr "ウィンドウにクリップ"
+
+#: ../vnc.html:185
+msgid "Scaling Mode:"
+msgstr "スケーリングモード:"
+
+#: ../vnc.html:187
+msgid "None"
+msgstr "なし"
+
+#: ../vnc.html:188
+msgid "Local Scaling"
+msgstr "ローカルスケーリング"
+
+#: ../vnc.html:189
+msgid "Remote Resizing"
+msgstr "リモートでリサイズ"
+
+#: ../vnc.html:194
+msgid "Advanced"
+msgstr "高度"
+
+#: ../vnc.html:197
+msgid "Quality:"
+msgstr "品質:"
+
+#: ../vnc.html:201
+msgid "Compression level:"
+msgstr "圧縮レベル:"
+
+#: ../vnc.html:206
+msgid "Repeater ID:"
+msgstr "リピーター ID:"
+
+#: ../vnc.html:210
+msgid "WebSocket"
+msgstr "WebSocket"
+
+#: ../vnc.html:213
+msgid "Encrypt"
+msgstr "暗号化"
+
+#: ../vnc.html:216
+msgid "Host:"
+msgstr "ホスト:"
+
+#: ../vnc.html:220
+msgid "Port:"
+msgstr "ポート:"
+
+#: ../vnc.html:224
+msgid "Path:"
+msgstr "パス:"
+
+#: ../vnc.html:231
+msgid "Automatic Reconnect"
+msgstr "自動再接続"
+
+#: ../vnc.html:234
+msgid "Reconnect Delay (ms):"
+msgstr "再接続する遅延 (ミリ秒):"
+
+#: ../vnc.html:239
+msgid "Show Dot when No Cursor"
+msgstr "カーソルがないときにドットを表示"
+
+#: ../vnc.html:244
+msgid "Logging:"
+msgstr "ロギング:"
+
+#: ../vnc.html:253
+msgid "Version:"
+msgstr "バージョン:"
+
+#: ../vnc.html:261
+msgid "Disconnect"
+msgstr "切断"
+
+#: ../vnc.html:280
+msgid "Connect"
+msgstr "接続"
+
+#: ../vnc.html:290
+msgid "Username:"
+msgstr "ユーザー名:"
+
+#: ../vnc.html:294
+msgid "Password:"
+msgstr "パスワード:"
+
+#: ../vnc.html:298
+msgid "Send Credentials"
+msgstr "資格情報を送信"
+
+#: ../vnc.html:308
+msgid "Cancel"
+msgstr "キャンセル"
+
+#~ msgid "Password is required"
+#~ msgstr "パスワードが必要です"
+
+#~ msgid "viewport drag"
+#~ msgstr "ビューポートをドラッグ"
+
+#~ msgid "Active Mouse Button"
+#~ msgstr "アクティブなマウスボタン"
+
+#~ msgid "No mousebutton"
+#~ msgstr "マウスボタンなし"
+
+#~ msgid "Left mousebutton"
+#~ msgstr "左マウスボタン"
+
+#~ msgid "Middle mousebutton"
+#~ msgstr "中マウスボタン"
+
+#~ msgid "Right mousebutton"
+#~ msgstr "右マウスボタン"
+
+#~ msgid "Send Password"
+#~ msgstr "パスワードを送信"
diff --git a/app/src/main/assets/novnc/po/ko.po b/app/src/main/assets/novnc/po/ko.po
new file mode 100644
index 00000000..87ae1069
--- /dev/null
+++ b/app/src/main/assets/novnc/po/ko.po
@@ -0,0 +1,290 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2018 The noVNC Authors
+# This file is distributed under the same license as the noVNC package.
+# Baw Appie <pp121324@gmail.com>, 2018.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: noVNC 1.0.0-testing.2\n"
+"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
+"POT-Creation-Date: 2018-01-31 16:29+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: Baw Appie <pp121324@gmail.com>\n"
+"Language-Team: Korean\n"
+"Language: ko\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: ../app/ui.js:395
+msgid "Connecting..."
+msgstr "연결중..."
+
+#: ../app/ui.js:402
+msgid "Disconnecting..."
+msgstr "연결 해제중..."
+
+#: ../app/ui.js:408
+msgid "Reconnecting..."
+msgstr "재연결중..."
+
+#: ../app/ui.js:413
+msgid "Internal error"
+msgstr "내부 오류"
+
+#: ../app/ui.js:1002
+msgid "Must set host"
+msgstr "호스트는 설정되어야 합니다."
+
+#: ../app/ui.js:1083
+msgid "Connected (encrypted) to "
+msgstr "다음과 (암호화되어) 연결되었습니다:"
+
+#: ../app/ui.js:1085
+msgid "Connected (unencrypted) to "
+msgstr "다음과 (암호화 없이) 연결되었습니다:"
+
+#: ../app/ui.js:1108
+msgid "Something went wrong, connection is closed"
+msgstr "무언가 잘못되었습니다, 연결이 닫혔습니다."
+
+#: ../app/ui.js:1111
+msgid "Failed to connect to server"
+msgstr "서버에 연결하지 못했습니다."
+
+#: ../app/ui.js:1121
+msgid "Disconnected"
+msgstr "연결이 해제되었습니다."
+
+#: ../app/ui.js:1134
+msgid "New connection has been rejected with reason: "
+msgstr "새 연결이 다음 이유로 거부되었습니다:"
+
+#: ../app/ui.js:1137
+msgid "New connection has been rejected"
+msgstr "새 연결이 거부되었습니다."
+
+#: ../app/ui.js:1158
+msgid "Password is required"
+msgstr "비밀번호가 필요합니다."
+
+#: ../vnc.html:91
+msgid "noVNC encountered an error:"
+msgstr "noVNC에 오류가 발생했습니다:"
+
+#: ../vnc.html:101
+msgid "Hide/Show the control bar"
+msgstr "컨트롤 바 숨기기/보이기"
+
+#: ../vnc.html:108
+msgid "Move/Drag Viewport"
+msgstr "움직이기/드래그 뷰포트"
+
+#: ../vnc.html:108
+msgid "viewport drag"
+msgstr "뷰포트 드래그"
+
+#: ../vnc.html:114 ../vnc.html:117 ../vnc.html:120 ../vnc.html:123
+msgid "Active Mouse Button"
+msgstr "마우스 버튼 활성화"
+
+#: ../vnc.html:114
+msgid "No mousebutton"
+msgstr "마우스 버튼 없음"
+
+#: ../vnc.html:117
+msgid "Left mousebutton"
+msgstr "왼쪽 마우스 버튼"
+
+#: ../vnc.html:120
+msgid "Middle mousebutton"
+msgstr "중간 마우스 버튼"
+
+#: ../vnc.html:123
+msgid "Right mousebutton"
+msgstr "오른쪽 마우스 버튼"
+
+#: ../vnc.html:126
+msgid "Keyboard"
+msgstr "키보드"
+
+#: ../vnc.html:126
+msgid "Show Keyboard"
+msgstr "키보드 보이기"
+
+#: ../vnc.html:133
+msgid "Extra keys"
+msgstr "기타 키들"
+
+#: ../vnc.html:133
+msgid "Show Extra Keys"
+msgstr "기타 키들 보이기"
+
+#: ../vnc.html:138
+msgid "Ctrl"
+msgstr "Ctrl"
+
+#: ../vnc.html:138
+msgid "Toggle Ctrl"
+msgstr "Ctrl 켜기/끄기"
+
+#: ../vnc.html:141
+msgid "Alt"
+msgstr "Alt"
+
+#: ../vnc.html:141
+msgid "Toggle Alt"
+msgstr "Alt 켜기/끄기"
+
+#: ../vnc.html:144
+msgid "Send Tab"
+msgstr "Tab 보내기"
+
+#: ../vnc.html:144
+msgid "Tab"
+msgstr "Tab"
+
+#: ../vnc.html:147
+msgid "Esc"
+msgstr "Esc"
+
+#: ../vnc.html:147
+msgid "Send Escape"
+msgstr "Esc 보내기"
+
+#: ../vnc.html:150
+msgid "Ctrl+Alt+Del"
+msgstr "Ctrl+Alt+Del"
+
+#: ../vnc.html:150
+msgid "Send Ctrl-Alt-Del"
+msgstr "Ctrl+Alt+Del 보내기"
+
+#: ../vnc.html:158
+msgid "Shutdown/Reboot"
+msgstr "셧다운/리붓"
+
+#: ../vnc.html:158
+msgid "Shutdown/Reboot..."
+msgstr "셧다운/리붓..."
+
+#: ../vnc.html:164
+msgid "Power"
+msgstr "전원"
+
+#: ../vnc.html:166
+msgid "Shutdown"
+msgstr "셧다운"
+
+#: ../vnc.html:167
+msgid "Reboot"
+msgstr "리붓"
+
+#: ../vnc.html:168
+msgid "Reset"
+msgstr "리셋"
+
+#: ../vnc.html:173 ../vnc.html:179
+msgid "Clipboard"
+msgstr "클립보드"
+
+#: ../vnc.html:183
+msgid "Clear"
+msgstr "지우기"
+
+#: ../vnc.html:189
+msgid "Fullscreen"
+msgstr "전체화면"
+
+#: ../vnc.html:194 ../vnc.html:201
+msgid "Settings"
+msgstr "설정"
+
+#: ../vnc.html:204
+msgid "Shared Mode"
+msgstr "공유 모드"
+
+#: ../vnc.html:207
+msgid "View Only"
+msgstr "보기 전용"
+
+#: ../vnc.html:211
+msgid "Clip to Window"
+msgstr "창에 클립"
+
+#: ../vnc.html:214
+msgid "Scaling Mode:"
+msgstr "스케일링 모드:"
+
+#: ../vnc.html:216
+msgid "None"
+msgstr "없음"
+
+#: ../vnc.html:217
+msgid "Local Scaling"
+msgstr "로컬 스케일링"
+
+#: ../vnc.html:218
+msgid "Remote Resizing"
+msgstr "원격 크기 조절"
+
+#: ../vnc.html:223
+msgid "Advanced"
+msgstr "고급"
+
+#: ../vnc.html:226
+msgid "Repeater ID:"
+msgstr "중계 ID"
+
+#: ../vnc.html:230
+msgid "WebSocket"
+msgstr "웹소켓"
+
+#: ../vnc.html:233
+msgid "Encrypt"
+msgstr "암호화"
+
+#: ../vnc.html:236
+msgid "Host:"
+msgstr "호스트:"
+
+#: ../vnc.html:240
+msgid "Port:"
+msgstr "포트:"
+
+#: ../vnc.html:244
+msgid "Path:"
+msgstr "위치:"
+
+#: ../vnc.html:251
+msgid "Automatic Reconnect"
+msgstr "자동 재연결"
+
+#: ../vnc.html:254
+msgid "Reconnect Delay (ms):"
+msgstr "재연결 지연 시간 (ms)"
+
+#: ../vnc.html:260
+msgid "Logging:"
+msgstr "로깅"
+
+#: ../vnc.html:272
+msgid "Disconnect"
+msgstr "연결 해제"
+
+#: ../vnc.html:291
+msgid "Connect"
+msgstr "연결"
+
+#: ../vnc.html:301
+msgid "Password:"
+msgstr "비밀번호:"
+
+#: ../vnc.html:305
+msgid "Send Password"
+msgstr "비밀번호 전송"
+
+#: ../vnc.html:315
+msgid "Cancel"
+msgstr "취소"
diff --git a/app/src/main/assets/novnc/po/nl.po b/app/src/main/assets/novnc/po/nl.po
new file mode 100644
index 00000000..343204a9
--- /dev/null
+++ b/app/src/main/assets/novnc/po/nl.po
@@ -0,0 +1,322 @@
+# Dutch translations for noVNC package
+# Nederlandse vertalingen voor het pakket noVNC.
+# Copyright (C) 2018 The noVNC Authors
+# This file is distributed under the same license as the noVNC package.
+# Loek Janssen <loekjanssen@gmail.com>, 2016.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: noVNC 1.1.0\n"
+"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
+"POT-Creation-Date: 2019-04-09 11:06+0100\n"
+"PO-Revision-Date: 2019-04-09 17:17+0100\n"
+"Last-Translator: Arend Lapere <arend.lapere@gmail.com>\n"
+"Language-Team: none\n"
+"Language: nl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../app/ui.js:383
+msgid "Connecting..."
+msgstr "Verbinden..."
+
+#: ../app/ui.js:390
+msgid "Disconnecting..."
+msgstr "Verbinding verbreken..."
+
+#: ../app/ui.js:396
+msgid "Reconnecting..."
+msgstr "Opnieuw verbinding maken..."
+
+#: ../app/ui.js:401
+msgid "Internal error"
+msgstr "Interne fout"
+
+#: ../app/ui.js:991
+msgid "Must set host"
+msgstr "Host moeten worden ingesteld"
+
+#: ../app/ui.js:1073
+msgid "Connected (encrypted) to "
+msgstr "Verbonden (versleuteld) met "
+
+#: ../app/ui.js:1075
+msgid "Connected (unencrypted) to "
+msgstr "Verbonden (onversleuteld) met "
+
+#: ../app/ui.js:1098
+msgid "Something went wrong, connection is closed"
+msgstr "Er iets fout gelopen, verbinding werd verbroken"
+
+#: ../app/ui.js:1101
+msgid "Failed to connect to server"
+msgstr "Verbinding maken met server is mislukt"
+
+#: ../app/ui.js:1111
+msgid "Disconnected"
+msgstr "Verbinding verbroken"
+
+#: ../app/ui.js:1124
+msgid "New connection has been rejected with reason: "
+msgstr "Nieuwe verbinding is geweigerd omwille van de volgende reden: "
+
+#: ../app/ui.js:1127
+msgid "New connection has been rejected"
+msgstr "Nieuwe verbinding is geweigerd"
+
+#: ../app/ui.js:1147
+msgid "Password is required"
+msgstr "Wachtwoord is vereist"
+
+#: ../vnc.html:80
+msgid "noVNC encountered an error:"
+msgstr "noVNC heeft een fout bemerkt:"
+
+#: ../vnc.html:90
+msgid "Hide/Show the control bar"
+msgstr "Verberg/Toon de bedieningsbalk"
+
+#: ../vnc.html:97
+msgid "Move/Drag Viewport"
+msgstr "Verplaats/Versleep Kijkvenster"
+
+#: ../vnc.html:97
+msgid "viewport drag"
+msgstr "kijkvenster slepen"
+
+#: ../vnc.html:103 ../vnc.html:106 ../vnc.html:109 ../vnc.html:112
+msgid "Active Mouse Button"
+msgstr "Actieve Muisknop"
+
+#: ../vnc.html:103
+msgid "No mousebutton"
+msgstr "Geen muisknop"
+
+#: ../vnc.html:106
+msgid "Left mousebutton"
+msgstr "Linker muisknop"
+
+#: ../vnc.html:109
+msgid "Middle mousebutton"
+msgstr "Middelste muisknop"
+
+#: ../vnc.html:112
+msgid "Right mousebutton"
+msgstr "Rechter muisknop"
+
+#: ../vnc.html:115
+msgid "Keyboard"
+msgstr "Toetsenbord"
+
+#: ../vnc.html:115
+msgid "Show Keyboard"
+msgstr "Toon Toetsenbord"
+
+#: ../vnc.html:121
+msgid "Extra keys"
+msgstr "Extra toetsen"
+
+#: ../vnc.html:121
+msgid "Show Extra Keys"
+msgstr "Toon Extra Toetsen"
+
+#: ../vnc.html:126
+msgid "Ctrl"
+msgstr "Ctrl"
+
+#: ../vnc.html:126
+msgid "Toggle Ctrl"
+msgstr "Ctrl omschakelen"
+
+#: ../vnc.html:129
+msgid "Alt"
+msgstr "Alt"
+
+#: ../vnc.html:129
+msgid "Toggle Alt"
+msgstr "Alt omschakelen"
+
+#: ../vnc.html:132
+msgid "Toggle Windows"
+msgstr "Windows omschakelen"
+
+#: ../vnc.html:132
+msgid "Windows"
+msgstr "Windows"
+
+#: ../vnc.html:135
+msgid "Send Tab"
+msgstr "Tab Sturen"
+
+#: ../vnc.html:135
+msgid "Tab"
+msgstr "Tab"
+
+#: ../vnc.html:138
+msgid "Esc"
+msgstr "Esc"
+
+#: ../vnc.html:138
+msgid "Send Escape"
+msgstr "Escape Sturen"
+
+#: ../vnc.html:141
+msgid "Ctrl+Alt+Del"
+msgstr "Ctrl-Alt-Del"
+
+#: ../vnc.html:141
+msgid "Send Ctrl-Alt-Del"
+msgstr "Ctrl-Alt-Del Sturen"
+
+#: ../vnc.html:149
+msgid "Shutdown/Reboot"
+msgstr "Uitschakelen/Herstarten"
+
+#: ../vnc.html:149
+msgid "Shutdown/Reboot..."
+msgstr "Uitschakelen/Herstarten..."
+
+#: ../vnc.html:155
+msgid "Power"
+msgstr "Systeem"
+
+#: ../vnc.html:157
+msgid "Shutdown"
+msgstr "Uitschakelen"
+
+#: ../vnc.html:158
+msgid "Reboot"
+msgstr "Herstarten"
+
+#: ../vnc.html:159
+msgid "Reset"
+msgstr "Resetten"
+
+#: ../vnc.html:164 ../vnc.html:170
+msgid "Clipboard"
+msgstr "Klembord"
+
+#: ../vnc.html:174
+msgid "Clear"
+msgstr "Wissen"
+
+#: ../vnc.html:180
+msgid "Fullscreen"
+msgstr "Volledig Scherm"
+
+#: ../vnc.html:185 ../vnc.html:192
+msgid "Settings"
+msgstr "Instellingen"
+
+#: ../vnc.html:195
+msgid "Shared Mode"
+msgstr "Gedeelde Modus"
+
+#: ../vnc.html:198
+msgid "View Only"
+msgstr "Alleen Kijken"
+
+#: ../vnc.html:202
+msgid "Clip to Window"
+msgstr "Randen buiten venster afsnijden"
+
+#: ../vnc.html:205
+msgid "Scaling Mode:"
+msgstr "Schaalmodus:"
+
+#: ../vnc.html:207
+msgid "None"
+msgstr "Geen"
+
+#: ../vnc.html:208
+msgid "Local Scaling"
+msgstr "Lokaal Schalen"
+
+#: ../vnc.html:209
+msgid "Remote Resizing"
+msgstr "Op Afstand Formaat Wijzigen"
+
+#: ../vnc.html:214
+msgid "Advanced"
+msgstr "Geavanceerd"
+
+#: ../vnc.html:217
+msgid "Repeater ID:"
+msgstr "Repeater ID:"
+
+#: ../vnc.html:221
+msgid "WebSocket"
+msgstr "WebSocket"
+
+#: ../vnc.html:224
+msgid "Encrypt"
+msgstr "Versleutelen"
+
+#: ../vnc.html:227
+msgid "Host:"
+msgstr "Host:"
+
+#: ../vnc.html:231
+msgid "Port:"
+msgstr "Poort:"
+
+#: ../vnc.html:235
+msgid "Path:"
+msgstr "Pad:"
+
+#: ../vnc.html:242
+msgid "Automatic Reconnect"
+msgstr "Automatisch Opnieuw Verbinden"
+
+#: ../vnc.html:245
+msgid "Reconnect Delay (ms):"
+msgstr "Vertraging voor Opnieuw Verbinden (ms):"
+
+#: ../vnc.html:250
+msgid "Show Dot when No Cursor"
+msgstr "Geef stip weer indien geen cursor"
+
+#: ../vnc.html:255
+msgid "Logging:"
+msgstr "Logmeldingen:"
+
+#: ../vnc.html:267
+msgid "Disconnect"
+msgstr "Verbinding verbreken"
+
+#: ../vnc.html:286
+msgid "Connect"
+msgstr "Verbinden"
+
+#: ../vnc.html:296
+msgid "Password:"
+msgstr "Wachtwoord:"
+
+#: ../vnc.html:300
+msgid "Send Password"
+msgstr "Verzend Wachtwoord:"
+
+#: ../vnc.html:310
+msgid "Cancel"
+msgstr "Annuleren"
+
+#~ msgid "Disconnect timeout"
+#~ msgstr "Timeout tijdens verbreken van verbinding"
+
+#~ msgid "Local Downscaling"
+#~ msgstr "Lokaal Neerschalen"
+
+#~ msgid "Local Cursor"
+#~ msgstr "Lokale Cursor"
+
+#~ msgid "Canvas not supported."
+#~ msgstr "Canvas wordt niet ondersteund."
+
+#~ msgid ""
+#~ "Forcing clipping mode since scrollbars aren't supported by IE in "
+#~ "fullscreen"
+#~ msgstr ""
+#~ "''Clipping mode' ingeschakeld, omdat schuifbalken in volledige-scherm-"
+#~ "modus in IE niet worden ondersteund"
diff --git a/app/src/main/assets/novnc/po/noVNC.pot b/app/src/main/assets/novnc/po/noVNC.pot
new file mode 100644
index 00000000..d8f19fcd
--- /dev/null
+++ b/app/src/main/assets/novnc/po/noVNC.pot
@@ -0,0 +1,298 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR The noVNC Authors
+# This file is distributed under the same license as the noVNC package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: noVNC 1.3.0\n"
+"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
+"POT-Creation-Date: 2021-08-27 16:03+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: ../app/ui.js:400
+msgid "Connecting..."
+msgstr ""
+
+#: ../app/ui.js:407
+msgid "Disconnecting..."
+msgstr ""
+
+#: ../app/ui.js:413
+msgid "Reconnecting..."
+msgstr ""
+
+#: ../app/ui.js:418
+msgid "Internal error"
+msgstr ""
+
+#: ../app/ui.js:1009
+msgid "Must set host"
+msgstr ""
+
+#: ../app/ui.js:1091
+msgid "Connected (encrypted) to "
+msgstr ""
+
+#: ../app/ui.js:1093
+msgid "Connected (unencrypted) to "
+msgstr ""
+
+#: ../app/ui.js:1116
+msgid "Something went wrong, connection is closed"
+msgstr ""
+
+#: ../app/ui.js:1119
+msgid "Failed to connect to server"
+msgstr ""
+
+#: ../app/ui.js:1129
+msgid "Disconnected"
+msgstr ""
+
+#: ../app/ui.js:1144
+msgid "New connection has been rejected with reason: "
+msgstr ""
+
+#: ../app/ui.js:1147
+msgid "New connection has been rejected"
+msgstr ""
+
+#: ../app/ui.js:1182
+msgid "Credentials are required"
+msgstr ""
+
+#: ../vnc.html:61
+msgid "noVNC encountered an error:"
+msgstr ""
+
+#: ../vnc.html:71
+msgid "Hide/Show the control bar"
+msgstr ""
+
+#: ../vnc.html:78
+msgid "Drag"
+msgstr ""
+
+#: ../vnc.html:78
+msgid "Move/Drag Viewport"
+msgstr ""
+
+#: ../vnc.html:84
+msgid "Keyboard"
+msgstr ""
+
+#: ../vnc.html:84
+msgid "Show Keyboard"
+msgstr ""
+
+#: ../vnc.html:89
+msgid "Extra keys"
+msgstr ""
+
+#: ../vnc.html:89
+msgid "Show Extra Keys"
+msgstr ""
+
+#: ../vnc.html:94
+msgid "Ctrl"
+msgstr ""
+
+#: ../vnc.html:94
+msgid "Toggle Ctrl"
+msgstr ""
+
+#: ../vnc.html:97
+msgid "Alt"
+msgstr ""
+
+#: ../vnc.html:97
+msgid "Toggle Alt"
+msgstr ""
+
+#: ../vnc.html:100
+msgid "Toggle Windows"
+msgstr ""
+
+#: ../vnc.html:100
+msgid "Windows"
+msgstr ""
+
+#: ../vnc.html:103
+msgid "Send Tab"
+msgstr ""
+
+#: ../vnc.html:103
+msgid "Tab"
+msgstr ""
+
+#: ../vnc.html:106
+msgid "Esc"
+msgstr ""
+
+#: ../vnc.html:106
+msgid "Send Escape"
+msgstr ""
+
+#: ../vnc.html:109
+msgid "Ctrl+Alt+Del"
+msgstr ""
+
+#: ../vnc.html:109
+msgid "Send Ctrl-Alt-Del"
+msgstr ""
+
+#: ../vnc.html:116
+msgid "Shutdown/Reboot"
+msgstr ""
+
+#: ../vnc.html:116
+msgid "Shutdown/Reboot..."
+msgstr ""
+
+#: ../vnc.html:122
+msgid "Power"
+msgstr ""
+
+#: ../vnc.html:124
+msgid "Shutdown"
+msgstr ""
+
+#: ../vnc.html:125
+msgid "Reboot"
+msgstr ""
+
+#: ../vnc.html:126
+msgid "Reset"
+msgstr ""
+
+#: ../vnc.html:131 ../vnc.html:137
+msgid "Clipboard"
+msgstr ""
+
+#: ../vnc.html:141
+msgid "Clear"
+msgstr ""
+
+#: ../vnc.html:147
+msgid "Fullscreen"
+msgstr ""
+
+#: ../vnc.html:152 ../vnc.html:159
+msgid "Settings"
+msgstr ""
+
+#: ../vnc.html:162
+msgid "Shared Mode"
+msgstr ""
+
+#: ../vnc.html:165
+msgid "View Only"
+msgstr ""
+
+#: ../vnc.html:169
+msgid "Clip to Window"
+msgstr ""
+
+#: ../vnc.html:172
+msgid "Scaling Mode:"
+msgstr ""
+
+#: ../vnc.html:174
+msgid "None"
+msgstr ""
+
+#: ../vnc.html:175
+msgid "Local Scaling"
+msgstr ""
+
+#: ../vnc.html:176
+msgid "Remote Resizing"
+msgstr ""
+
+#: ../vnc.html:181
+msgid "Advanced"
+msgstr ""
+
+#: ../vnc.html:184
+msgid "Quality:"
+msgstr ""
+
+#: ../vnc.html:188
+msgid "Compression level:"
+msgstr ""
+
+#: ../vnc.html:193
+msgid "Repeater ID:"
+msgstr ""
+
+#: ../vnc.html:197
+msgid "WebSocket"
+msgstr ""
+
+#: ../vnc.html:200
+msgid "Encrypt"
+msgstr ""
+
+#: ../vnc.html:203
+msgid "Host:"
+msgstr ""
+
+#: ../vnc.html:207
+msgid "Port:"
+msgstr ""
+
+#: ../vnc.html:211
+msgid "Path:"
+msgstr ""
+
+#: ../vnc.html:218
+msgid "Automatic Reconnect"
+msgstr ""
+
+#: ../vnc.html:221
+msgid "Reconnect Delay (ms):"
+msgstr ""
+
+#: ../vnc.html:226
+msgid "Show Dot when No Cursor"
+msgstr ""
+
+#: ../vnc.html:231
+msgid "Logging:"
+msgstr ""
+
+#: ../vnc.html:240
+msgid "Version:"
+msgstr ""
+
+#: ../vnc.html:248
+msgid "Disconnect"
+msgstr ""
+
+#: ../vnc.html:267
+msgid "Connect"
+msgstr ""
+
+#: ../vnc.html:277
+msgid "Username:"
+msgstr ""
+
+#: ../vnc.html:281
+msgid "Password:"
+msgstr ""
+
+#: ../vnc.html:285
+msgid "Send Credentials"
+msgstr ""
+
+#: ../vnc.html:295
+msgid "Cancel"
+msgstr ""
diff --git a/app/src/main/assets/novnc/po/pl.po b/app/src/main/assets/novnc/po/pl.po
new file mode 100644
index 00000000..5acfdc4f
--- /dev/null
+++ b/app/src/main/assets/novnc/po/pl.po
@@ -0,0 +1,325 @@
+# Polish translations for noVNC package.
+# Copyright (C) 2018 The noVNC Authors
+# This file is distributed under the same license as the noVNC package.
+# Mariusz Jamro <mariusz.jamro@gmail.com>, 2017.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: noVNC 0.6.1\n"
+"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
+"POT-Creation-Date: 2017-11-21 19:53+0100\n"
+"PO-Revision-Date: 2017-11-21 19:54+0100\n"
+"Last-Translator: Mariusz Jamro <mariusz.jamro@gmail.com>\n"
+"Language-Team: Polish\n"
+"Language: pl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
+"|| n%100>=20) ? 1 : 2);\n"
+"X-Generator: Poedit 2.0.1\n"
+
+#: ../app/ui.js:404
+msgid "Connecting..."
+msgstr "Łączenie..."
+
+#: ../app/ui.js:411
+msgid "Disconnecting..."
+msgstr "Rozłączanie..."
+
+#: ../app/ui.js:417
+msgid "Reconnecting..."
+msgstr "Łączenie..."
+
+#: ../app/ui.js:422
+msgid "Internal error"
+msgstr "Błąd wewnętrzny"
+
+#: ../app/ui.js:1019
+msgid "Must set host"
+msgstr "Host i port są wymagane"
+
+#: ../app/ui.js:1099
+msgid "Connected (encrypted) to "
+msgstr "Połączenie (szyfrowane) z "
+
+#: ../app/ui.js:1101
+msgid "Connected (unencrypted) to "
+msgstr "Połączenie (nieszyfrowane) z "
+
+#: ../app/ui.js:1119
+msgid "Something went wrong, connection is closed"
+msgstr "Coś poszło źle, połączenie zostało zamknięte"
+
+#: ../app/ui.js:1129
+msgid "Disconnected"
+msgstr "Rozłączony"
+
+#: ../app/ui.js:1142
+msgid "New connection has been rejected with reason: "
+msgstr "Nowe połączenie zostało odrzucone z powodu: "
+
+#: ../app/ui.js:1145
+msgid "New connection has been rejected"
+msgstr "Nowe połączenie zostało odrzucone"
+
+#: ../app/ui.js:1166
+msgid "Password is required"
+msgstr "Hasło jest wymagane"
+
+#: ../vnc.html:89
+msgid "noVNC encountered an error:"
+msgstr "noVNC napotkało błąd:"
+
+#: ../vnc.html:99
+msgid "Hide/Show the control bar"
+msgstr "Pokaż/Ukryj pasek ustawień"
+
+#: ../vnc.html:106
+msgid "Move/Drag Viewport"
+msgstr "Ruszaj/Przeciągaj Viewport"
+
+#: ../vnc.html:106
+msgid "viewport drag"
+msgstr "przeciągnij viewport"
+
+#: ../vnc.html:112 ../vnc.html:115 ../vnc.html:118 ../vnc.html:121
+msgid "Active Mouse Button"
+msgstr "Aktywny Przycisk Myszy"
+
+#: ../vnc.html:112
+msgid "No mousebutton"
+msgstr "Brak przycisku myszy"
+
+#: ../vnc.html:115
+msgid "Left mousebutton"
+msgstr "Lewy przycisk myszy"
+
+#: ../vnc.html:118
+msgid "Middle mousebutton"
+msgstr "Środkowy przycisk myszy"
+
+#: ../vnc.html:121
+msgid "Right mousebutton"
+msgstr "Prawy przycisk myszy"
+
+#: ../vnc.html:124
+msgid "Keyboard"
+msgstr "Klawiatura"
+
+#: ../vnc.html:124
+msgid "Show Keyboard"
+msgstr "Pokaż klawiaturę"
+
+#: ../vnc.html:131
+msgid "Extra keys"
+msgstr "Przyciski dodatkowe"
+
+#: ../vnc.html:131
+msgid "Show Extra Keys"
+msgstr "Pokaż przyciski dodatkowe"
+
+#: ../vnc.html:136
+msgid "Ctrl"
+msgstr "Ctrl"
+
+#: ../vnc.html:136
+msgid "Toggle Ctrl"
+msgstr "Przełącz Ctrl"
+
+#: ../vnc.html:139
+msgid "Alt"
+msgstr "Alt"
+
+#: ../vnc.html:139
+msgid "Toggle Alt"
+msgstr "Przełącz Alt"
+
+#: ../vnc.html:142
+msgid "Send Tab"
+msgstr "Wyślij Tab"
+
+#: ../vnc.html:142
+msgid "Tab"
+msgstr "Tab"
+
+#: ../vnc.html:145
+msgid "Esc"
+msgstr "Esc"
+
+#: ../vnc.html:145
+msgid "Send Escape"
+msgstr "Wyślij Escape"
+
+#: ../vnc.html:148
+msgid "Ctrl+Alt+Del"
+msgstr "Ctrl+Alt+Del"
+
+#: ../vnc.html:148
+msgid "Send Ctrl-Alt-Del"
+msgstr "Wyślij Ctrl-Alt-Del"
+
+#: ../vnc.html:156
+msgid "Shutdown/Reboot"
+msgstr "Wyłącz/Uruchom ponownie"
+
+#: ../vnc.html:156
+msgid "Shutdown/Reboot..."
+msgstr "Wyłącz/Uruchom ponownie..."
+
+#: ../vnc.html:162
+msgid "Power"
+msgstr "Włączony"
+
+#: ../vnc.html:164
+msgid "Shutdown"
+msgstr "Wyłącz"
+
+#: ../vnc.html:165
+msgid "Reboot"
+msgstr "Uruchom ponownie"
+
+#: ../vnc.html:166
+msgid "Reset"
+msgstr "Resetuj"
+
+#: ../vnc.html:171 ../vnc.html:177
+msgid "Clipboard"
+msgstr "Schowek"
+
+#: ../vnc.html:181
+msgid "Clear"
+msgstr "Wyczyść"
+
+#: ../vnc.html:187
+msgid "Fullscreen"
+msgstr "Pełny ekran"
+
+#: ../vnc.html:192 ../vnc.html:199
+msgid "Settings"
+msgstr "Ustawienia"
+
+#: ../vnc.html:202
+msgid "Shared Mode"
+msgstr "Tryb Współdzielenia"
+
+#: ../vnc.html:205
+msgid "View Only"
+msgstr "Tylko Podgląd"
+
+#: ../vnc.html:209
+msgid "Clip to Window"
+msgstr "Przytnij do Okna"
+
+#: ../vnc.html:212
+msgid "Scaling Mode:"
+msgstr "Tryb Skalowania:"
+
+#: ../vnc.html:214
+msgid "None"
+msgstr "Brak"
+
+#: ../vnc.html:215
+msgid "Local Scaling"
+msgstr "Skalowanie lokalne"
+
+#: ../vnc.html:216
+msgid "Remote Resizing"
+msgstr "Skalowanie zdalne"
+
+#: ../vnc.html:221
+msgid "Advanced"
+msgstr "Zaawansowane"
+
+#: ../vnc.html:224
+msgid "Repeater ID:"
+msgstr "ID Repeatera:"
+
+#: ../vnc.html:228
+msgid "WebSocket"
+msgstr "WebSocket"
+
+#: ../vnc.html:231
+msgid "Encrypt"
+msgstr "Szyfrowanie"
+
+#: ../vnc.html:234
+msgid "Host:"
+msgstr "Host:"
+
+#: ../vnc.html:238
+msgid "Port:"
+msgstr "Port:"
+
+#: ../vnc.html:242
+msgid "Path:"
+msgstr "Ścieżka:"
+
+#: ../vnc.html:249
+msgid "Automatic Reconnect"
+msgstr "Automatycznie wznawiaj połączenie"
+
+#: ../vnc.html:252
+msgid "Reconnect Delay (ms):"
+msgstr "Opóźnienie wznawiania (ms):"
+
+#: ../vnc.html:258
+msgid "Logging:"
+msgstr "Poziom logowania:"
+
+#: ../vnc.html:270
+msgid "Disconnect"
+msgstr "Rozłącz"
+
+#: ../vnc.html:289
+msgid "Connect"
+msgstr "Połącz"
+
+#: ../vnc.html:299
+msgid "Password:"
+msgstr "Hasło:"
+
+#: ../vnc.html:313
+msgid "Cancel"
+msgstr "Anuluj"
+
+#: ../vnc.html:329
+msgid "Canvas not supported."
+msgstr "Element Canvas nie jest wspierany."
+
+#~ msgid "Disconnect timeout"
+#~ msgstr "Timeout rozłączenia"
+
+#~ msgid "Local Downscaling"
+#~ msgstr "Downscaling lokalny"
+
+#~ msgid "Local Cursor"
+#~ msgstr "Lokalny kursor"
+
+#~ msgid ""
+#~ "Forcing clipping mode since scrollbars aren't supported by IE in "
+#~ "fullscreen"
+#~ msgstr ""
+#~ "Wymuszam clipping mode ponieważ paski przewijania nie są wspierane przez "
+#~ "IE w trybie pełnoekranowym"
+
+#~ msgid "True Color"
+#~ msgstr "True Color"
+
+#~ msgid "Style:"
+#~ msgstr "Styl:"
+
+#~ msgid "default"
+#~ msgstr "domyślny"
+
+#~ msgid "Apply"
+#~ msgstr "Zapisz"
+
+#~ msgid "Connection"
+#~ msgstr "Połączenie"
+
+#~ msgid "Token:"
+#~ msgstr "Token:"
+
+#~ msgid "Send Password"
+#~ msgstr "Wyślij Hasło"
diff --git a/app/src/main/assets/novnc/po/po2js b/app/src/main/assets/novnc/po/po2js
new file mode 100755
index 00000000..fc6e8810
--- /dev/null
+++ b/app/src/main/assets/novnc/po/po2js
@@ -0,0 +1,43 @@
+#!/usr/bin/env node
+/*
+ * ps2js: gettext .po to noVNC .js converter
+ * Copyright (C) 2018 The noVNC Authors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+const getopt = require('node-getopt');
+const fs = require('fs');
+const po2json = require("po2json");
+
+const opt = getopt.create([
+    ['h', 'help', 'display this help'],
+]).bindHelp().parseSystem();
+
+if (opt.argv.length != 2) {
+    console.error("Incorrect number of arguments given");
+    process.exit(1);
+}
+
+const data = po2json.parseFileSync(opt.argv[0]);
+
+const bodyPart = Object.keys(data).filter(msgid => msgid !== "").map((msgid) => {
+    if (msgid === "") return;
+    const msgstr = data[msgid][1];
+    return "    " + JSON.stringify(msgid) + ": " + JSON.stringify(msgstr);
+}).join(",\n");
+
+const output = "{\n" + bodyPart + "\n}";
+
+fs.writeFileSync(opt.argv[1], output);
diff --git a/app/src/main/assets/novnc/po/pt_BR.po b/app/src/main/assets/novnc/po/pt_BR.po
new file mode 100644
index 00000000..77951aef
--- /dev/null
+++ b/app/src/main/assets/novnc/po/pt_BR.po
@@ -0,0 +1,299 @@
+# Portuguese translations for noVNC package.
+# Copyright (C) 2021 The noVNC Authors
+# This file is distributed under the same license as the noVNC package.
+#  <liddack@outlook.com>, 2021.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: noVNC 1.2.0\n"
+"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
+"POT-Creation-Date: 2021-03-15 21:55-0300\n"
+"PO-Revision-Date: 2021-03-15 22:09-0300\n"
+"Last-Translator: <liddack@outlook.com>\n"
+"Language-Team: Brazilian Portuguese\n"
+"Language: pt_BR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+"X-Generator: Poedit 2.4.1\n"
+
+#: ../app/ui.js:400
+msgid "Connecting..."
+msgstr "Conectando..."
+
+#: ../app/ui.js:407
+msgid "Disconnecting..."
+msgstr "Desconectando..."
+
+#: ../app/ui.js:413
+msgid "Reconnecting..."
+msgstr "Reconectando..."
+
+#: ../app/ui.js:418
+msgid "Internal error"
+msgstr "Erro interno"
+
+#: ../app/ui.js:1009
+msgid "Must set host"
+msgstr "É necessário definir o host"
+
+#: ../app/ui.js:1091
+msgid "Connected (encrypted) to "
+msgstr "Conectado (com criptografia) a "
+
+#: ../app/ui.js:1093
+msgid "Connected (unencrypted) to "
+msgstr "Conectado (sem criptografia) a "
+
+#: ../app/ui.js:1116
+msgid "Something went wrong, connection is closed"
+msgstr "Algo deu errado. A conexão foi encerrada."
+
+#: ../app/ui.js:1119
+msgid "Failed to connect to server"
+msgstr "Falha ao conectar-se ao servidor"
+
+#: ../app/ui.js:1129
+msgid "Disconnected"
+msgstr "Desconectado"
+
+#: ../app/ui.js:1144
+msgid "New connection has been rejected with reason: "
+msgstr "A nova conexão foi rejeitada pelo motivo: "
+
+#: ../app/ui.js:1147
+msgid "New connection has been rejected"
+msgstr "A nova conexão foi rejeitada"
+
+#: ../app/ui.js:1182
+msgid "Credentials are required"
+msgstr "Credenciais são obrigatórias"
+
+#: ../vnc.html:61
+msgid "noVNC encountered an error:"
+msgstr "O noVNC encontrou um erro:"
+
+#: ../vnc.html:71
+msgid "Hide/Show the control bar"
+msgstr "Esconder/mostrar a barra de controles"
+
+#: ../vnc.html:78
+msgid "Drag"
+msgstr "Arrastar"
+
+#: ../vnc.html:78
+msgid "Move/Drag Viewport"
+msgstr "Mover/arrastar a janela"
+
+#: ../vnc.html:84
+msgid "Keyboard"
+msgstr "Teclado"
+
+#: ../vnc.html:84
+msgid "Show Keyboard"
+msgstr "Mostrar teclado"
+
+#: ../vnc.html:89
+msgid "Extra keys"
+msgstr "Teclas adicionais"
+
+#: ../vnc.html:89
+msgid "Show Extra Keys"
+msgstr "Mostar teclas adicionais"
+
+#: ../vnc.html:94
+msgid "Ctrl"
+msgstr "Ctrl"
+
+#: ../vnc.html:94
+msgid "Toggle Ctrl"
+msgstr "Pressionar/soltar Ctrl"
+
+#: ../vnc.html:97
+msgid "Alt"
+msgstr "Alt"
+
+#: ../vnc.html:97
+msgid "Toggle Alt"
+msgstr "Pressionar/soltar Alt"
+
+#: ../vnc.html:100
+msgid "Toggle Windows"
+msgstr "Pressionar/soltar Windows"
+
+#: ../vnc.html:100
+msgid "Windows"
+msgstr "Windows"
+
+#: ../vnc.html:103
+msgid "Send Tab"
+msgstr "Enviar Tab"
+
+#: ../vnc.html:103
+msgid "Tab"
+msgstr "Tab"
+
+#: ../vnc.html:106
+msgid "Esc"
+msgstr "Esc"
+
+#: ../vnc.html:106
+msgid "Send Escape"
+msgstr "Enviar Esc"
+
+#: ../vnc.html:109
+msgid "Ctrl+Alt+Del"
+msgstr "Ctrl+Alt+Del"
+
+#: ../vnc.html:109
+msgid "Send Ctrl-Alt-Del"
+msgstr "Enviar Ctrl-Alt-Del"
+
+#: ../vnc.html:116
+msgid "Shutdown/Reboot"
+msgstr "Desligar/reiniciar"
+
+#: ../vnc.html:116
+msgid "Shutdown/Reboot..."
+msgstr "Desligar/reiniciar..."
+
+#: ../vnc.html:122
+msgid "Power"
+msgstr "Ligar"
+
+#: ../vnc.html:124
+msgid "Shutdown"
+msgstr "Desligar"
+
+#: ../vnc.html:125
+msgid "Reboot"
+msgstr "Reiniciar"
+
+#: ../vnc.html:126
+msgid "Reset"
+msgstr "Reiniciar (forçado)"
+
+#: ../vnc.html:131 ../vnc.html:137
+msgid "Clipboard"
+msgstr "Área de transferência"
+
+#: ../vnc.html:141
+msgid "Clear"
+msgstr "Limpar"
+
+#: ../vnc.html:147
+msgid "Fullscreen"
+msgstr "Tela cheia"
+
+#: ../vnc.html:152 ../vnc.html:159
+msgid "Settings"
+msgstr "Configurações"
+
+#: ../vnc.html:162
+msgid "Shared Mode"
+msgstr "Modo compartilhado"
+
+#: ../vnc.html:165
+msgid "View Only"
+msgstr "Apenas visualizar"
+
+#: ../vnc.html:169
+msgid "Clip to Window"
+msgstr "Recortar à janela"
+
+#: ../vnc.html:172
+msgid "Scaling Mode:"
+msgstr "Modo de dimensionamento:"
+
+#: ../vnc.html:174
+msgid "None"
+msgstr "Nenhum"
+
+#: ../vnc.html:175
+msgid "Local Scaling"
+msgstr "Local"
+
+#: ../vnc.html:176
+msgid "Remote Resizing"
+msgstr "Remoto"
+
+#: ../vnc.html:181
+msgid "Advanced"
+msgstr "Avançado"
+
+#: ../vnc.html:184
+msgid "Quality:"
+msgstr "Qualidade:"
+
+#: ../vnc.html:188
+msgid "Compression level:"
+msgstr "Nível de compressão:"
+
+#: ../vnc.html:193
+msgid "Repeater ID:"
+msgstr "ID do repetidor:"
+
+#: ../vnc.html:197
+msgid "WebSocket"
+msgstr "WebSocket"
+
+#: ../vnc.html:200
+msgid "Encrypt"
+msgstr "Criptografar"
+
+#: ../vnc.html:203
+msgid "Host:"
+msgstr "Host:"
+
+#: ../vnc.html:207
+msgid "Port:"
+msgstr "Porta:"
+
+#: ../vnc.html:211
+msgid "Path:"
+msgstr "Caminho:"
+
+#: ../vnc.html:218
+msgid "Automatic Reconnect"
+msgstr "Reconexão automática"
+
+#: ../vnc.html:221
+msgid "Reconnect Delay (ms):"
+msgstr "Atraso da reconexão (ms)"
+
+#: ../vnc.html:226
+msgid "Show Dot when No Cursor"
+msgstr "Mostrar ponto quando não há cursor"
+
+#: ../vnc.html:231
+msgid "Logging:"
+msgstr "Registros:"
+
+#: ../vnc.html:240
+msgid "Version:"
+msgstr "Versão:"
+
+#: ../vnc.html:248
+msgid "Disconnect"
+msgstr "Desconectar"
+
+#: ../vnc.html:267
+msgid "Connect"
+msgstr "Conectar"
+
+#: ../vnc.html:277
+msgid "Username:"
+msgstr "Nome de usuário:"
+
+#: ../vnc.html:281
+msgid "Password:"
+msgstr "Senha:"
+
+#: ../vnc.html:285
+msgid "Send Credentials"
+msgstr "Enviar credenciais"
+
+#: ../vnc.html:295
+msgid "Cancel"
+msgstr "Cancelar"
diff --git a/app/src/main/assets/novnc/po/ru.po b/app/src/main/assets/novnc/po/ru.po
new file mode 100644
index 00000000..5a81bb06
--- /dev/null
+++ b/app/src/main/assets/novnc/po/ru.po
@@ -0,0 +1,302 @@
+# Russian translations for noVNC package
+# Русский перевод для пакета noVNC.
+# Copyright (C) 2019 Dmitriy Shweew
+# This file is distributed under the same license as the noVNC package.
+# Dmitriy Shweew <shweew@it-advisor.ru>, 2019.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: noVNC 1.3.0\n"
+"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
+"POT-Creation-Date: 2021-08-27 16:03+0200\n"
+"PO-Revision-Date: 2021-09-09 10:29+0400\n"
+"Last-Translator: Nia Remez <nia.remez@cendio.com>\n"
+"Language-Team: Russian\n"
+"Language: ru\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
+"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"X-Generator: Poedit 2.2.1\n"
+"X-Poedit-Flags-xgettext: --add-comments\n"
+
+#: ../app/ui.js:400
+msgid "Connecting..."
+msgstr "Подключение..."
+
+#: ../app/ui.js:407
+msgid "Disconnecting..."
+msgstr "Отключение..."
+
+#: ../app/ui.js:413
+msgid "Reconnecting..."
+msgstr "Переподключение..."
+
+#: ../app/ui.js:418
+msgid "Internal error"
+msgstr "Внутренняя ошибка"
+
+#: ../app/ui.js:1009
+msgid "Must set host"
+msgstr "Задайте имя сервера или IP"
+
+#: ../app/ui.js:1091
+msgid "Connected (encrypted) to "
+msgstr "Подключено (с шифрованием) к "
+
+#: ../app/ui.js:1093
+msgid "Connected (unencrypted) to "
+msgstr "Подключено (без шифрования) к "
+
+#: ../app/ui.js:1116
+msgid "Something went wrong, connection is closed"
+msgstr "Что-то пошло не так, подключение разорвано"
+
+#: ../app/ui.js:1119
+msgid "Failed to connect to server"
+msgstr "Ошибка подключения к серверу"
+
+#: ../app/ui.js:1129
+msgid "Disconnected"
+msgstr "Отключено"
+
+#: ../app/ui.js:1144
+msgid "New connection has been rejected with reason: "
+msgstr "Новое соединение отклонено по причине: "
+
+#: ../app/ui.js:1147
+msgid "New connection has been rejected"
+msgstr "Новое соединение отклонено"
+
+#: ../app/ui.js:1182
+msgid "Credentials are required"
+msgstr "Требуются учетные данные"
+
+#: ../vnc.html:61
+msgid "noVNC encountered an error:"
+msgstr "Ошибка noVNC: "
+
+#: ../vnc.html:71
+msgid "Hide/Show the control bar"
+msgstr "Скрыть/Показать контрольную панель"
+
+#: ../vnc.html:78
+msgid "Drag"
+msgstr "Переместить"
+
+#: ../vnc.html:78
+msgid "Move/Drag Viewport"
+msgstr "Переместить окно"
+
+#: ../vnc.html:84
+msgid "Keyboard"
+msgstr "Клавиатура"
+
+#: ../vnc.html:84
+msgid "Show Keyboard"
+msgstr "Показать клавиатуру"
+
+#: ../vnc.html:89
+msgid "Extra keys"
+msgstr "Дополнительные Кнопки"
+
+#: ../vnc.html:89
+msgid "Show Extra Keys"
+msgstr "Показать Дополнительные Кнопки"
+
+#: ../vnc.html:94
+msgid "Ctrl"
+msgstr "Ctrl"
+
+#: ../vnc.html:94
+msgid "Toggle Ctrl"
+msgstr "Переключение нажатия Ctrl"
+
+#: ../vnc.html:97
+msgid "Alt"
+msgstr "Alt"
+
+#: ../vnc.html:97
+msgid "Toggle Alt"
+msgstr "Переключение нажатия Alt"
+
+#: ../vnc.html:100
+msgid "Toggle Windows"
+msgstr "Переключение вкладок"
+
+#: ../vnc.html:100
+msgid "Windows"
+msgstr "Вкладка"
+
+#: ../vnc.html:103
+msgid "Send Tab"
+msgstr "Передать нажатие Tab"
+
+#: ../vnc.html:103
+msgid "Tab"
+msgstr "Tab"
+
+#: ../vnc.html:106
+msgid "Esc"
+msgstr "Esc"
+
+#: ../vnc.html:106
+msgid "Send Escape"
+msgstr "Передать нажатие Escape"
+
+#: ../vnc.html:109
+msgid "Ctrl+Alt+Del"
+msgstr "Ctrl+Alt+Del"
+
+#: ../vnc.html:109
+msgid "Send Ctrl-Alt-Del"
+msgstr "Передать нажатие Ctrl-Alt-Del"
+
+#: ../vnc.html:116
+msgid "Shutdown/Reboot"
+msgstr "Выключить/Перезагрузить"
+
+#: ../vnc.html:116
+msgid "Shutdown/Reboot..."
+msgstr "Выключить/Перезагрузить..."
+
+#: ../vnc.html:122
+msgid "Power"
+msgstr "Питание"
+
+#: ../vnc.html:124
+msgid "Shutdown"
+msgstr "Выключить"
+
+#: ../vnc.html:125
+msgid "Reboot"
+msgstr "Перезагрузить"
+
+#: ../vnc.html:126
+msgid "Reset"
+msgstr "Сброс"
+
+#: ../vnc.html:131 ../vnc.html:137
+msgid "Clipboard"
+msgstr "Буфер обмена"
+
+#: ../vnc.html:141
+msgid "Clear"
+msgstr "Очистить"
+
+#: ../vnc.html:147
+msgid "Fullscreen"
+msgstr "Во весь экран"
+
+#: ../vnc.html:152 ../vnc.html:159
+msgid "Settings"
+msgstr "Настройки"
+
+#: ../vnc.html:162
+msgid "Shared Mode"
+msgstr "Общий режим"
+
+#: ../vnc.html:165
+msgid "View Only"
+msgstr "Только Просмотр"
+
+#: ../vnc.html:169
+msgid "Clip to Window"
+msgstr "В окно"
+
+#: ../vnc.html:172
+msgid "Scaling Mode:"
+msgstr "Масштаб:"
+
+#: ../vnc.html:174
+msgid "None"
+msgstr "Нет"
+
+#: ../vnc.html:175
+msgid "Local Scaling"
+msgstr "Локльный масштаб"
+
+#: ../vnc.html:176
+msgid "Remote Resizing"
+msgstr "Удаленная перенастройка размера"
+
+#: ../vnc.html:181
+msgid "Advanced"
+msgstr "Дополнительно"
+
+#: ../vnc.html:184
+msgid "Quality:"
+msgstr "Качество"
+
+#: ../vnc.html:188
+msgid "Compression level:"
+msgstr "Уровень Сжатия"
+
+#: ../vnc.html:193
+msgid "Repeater ID:"
+msgstr "Идентификатор ID:"
+
+#: ../vnc.html:197
+msgid "WebSocket"
+msgstr "WebSocket"
+
+#: ../vnc.html:200
+msgid "Encrypt"
+msgstr "Шифрование"
+
+#: ../vnc.html:203
+msgid "Host:"
+msgstr "Сервер:"
+
+#: ../vnc.html:207
+msgid "Port:"
+msgstr "Порт:"
+
+#: ../vnc.html:211
+msgid "Path:"
+msgstr "Путь:"
+
+#: ../vnc.html:218
+msgid "Automatic Reconnect"
+msgstr "Автоматическое переподключение"
+
+#: ../vnc.html:221
+msgid "Reconnect Delay (ms):"
+msgstr "Задержка переподключения (мс):"
+
+#: ../vnc.html:226
+msgid "Show Dot when No Cursor"
+msgstr "Показать точку вместо курсора"
+
+#: ../vnc.html:231
+msgid "Logging:"
+msgstr "Лог:"
+
+#: ../vnc.html:240
+msgid "Version:"
+msgstr "Версия"
+
+#: ../vnc.html:248
+msgid "Disconnect"
+msgstr "Отключение"
+
+#: ../vnc.html:267
+msgid "Connect"
+msgstr "Подключение"
+
+#: ../vnc.html:277
+msgid "Username:"
+msgstr "Имя Пользователя"
+
+#: ../vnc.html:281
+msgid "Password:"
+msgstr "Пароль:"
+
+#: ../vnc.html:285
+msgid "Send Credentials"
+msgstr "Передача Учетных Данных"
+
+#: ../vnc.html:295
+msgid "Cancel"
+msgstr "Выход"
diff --git a/app/src/main/assets/novnc/po/sv.po b/app/src/main/assets/novnc/po/sv.po
new file mode 100644
index 00000000..0f0e90b5
--- /dev/null
+++ b/app/src/main/assets/novnc/po/sv.po
@@ -0,0 +1,300 @@
+# Swedish translations for noVNC package
+# Svenska översättningar för paketet noVNC.
+# Copyright (C) 2020 The noVNC Authors
+# This file is distributed under the same license as the noVNC package.
+# Samuel Mannehed <samuel@cendio.se>, 2020.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: noVNC 1.3.0\n"
+"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
+"POT-Creation-Date: 2021-08-27 16:03+0200\n"
+"PO-Revision-Date: 2021-08-27 16:18+0200\n"
+"Last-Translator: Samuel Mannehed <samuel@cendio.se>\n"
+"Language-Team: none\n"
+"Language: sv\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Poedit 2.0.3\n"
+
+#: ../app/ui.js:400
+msgid "Connecting..."
+msgstr "Ansluter..."
+
+#: ../app/ui.js:407
+msgid "Disconnecting..."
+msgstr "Kopplar ner..."
+
+#: ../app/ui.js:413
+msgid "Reconnecting..."
+msgstr "Återansluter..."
+
+#: ../app/ui.js:418
+msgid "Internal error"
+msgstr "Internt fel"
+
+#: ../app/ui.js:1009
+msgid "Must set host"
+msgstr "Du måste specifiera en värd"
+
+#: ../app/ui.js:1091
+msgid "Connected (encrypted) to "
+msgstr "Ansluten (krypterat) till "
+
+#: ../app/ui.js:1093
+msgid "Connected (unencrypted) to "
+msgstr "Ansluten (okrypterat) till "
+
+#: ../app/ui.js:1116
+msgid "Something went wrong, connection is closed"
+msgstr "Något gick fel, anslutningen avslutades"
+
+#: ../app/ui.js:1119
+msgid "Failed to connect to server"
+msgstr "Misslyckades att ansluta till servern"
+
+#: ../app/ui.js:1129
+msgid "Disconnected"
+msgstr "Frånkopplad"
+
+#: ../app/ui.js:1144
+msgid "New connection has been rejected with reason: "
+msgstr "Ny anslutning har blivit nekad med följande skäl: "
+
+#: ../app/ui.js:1147
+msgid "New connection has been rejected"
+msgstr "Ny anslutning har blivit nekad"
+
+#: ../app/ui.js:1182
+msgid "Credentials are required"
+msgstr "Användaruppgifter krävs"
+
+#: ../vnc.html:61
+msgid "noVNC encountered an error:"
+msgstr "noVNC stötte på ett problem:"
+
+#: ../vnc.html:71
+msgid "Hide/Show the control bar"
+msgstr "Göm/Visa kontrollbaren"
+
+#: ../vnc.html:78
+msgid "Drag"
+msgstr "Dra"
+
+#: ../vnc.html:78
+msgid "Move/Drag Viewport"
+msgstr "Flytta/Dra Vyn"
+
+#: ../vnc.html:84
+msgid "Keyboard"
+msgstr "Tangentbord"
+
+#: ../vnc.html:84
+msgid "Show Keyboard"
+msgstr "Visa Tangentbord"
+
+#: ../vnc.html:89
+msgid "Extra keys"
+msgstr "Extraknappar"
+
+#: ../vnc.html:89
+msgid "Show Extra Keys"
+msgstr "Visa Extraknappar"
+
+#: ../vnc.html:94
+msgid "Ctrl"
+msgstr "Ctrl"
+
+#: ../vnc.html:94
+msgid "Toggle Ctrl"
+msgstr "Växla Ctrl"
+
+#: ../vnc.html:97
+msgid "Alt"
+msgstr "Alt"
+
+#: ../vnc.html:97
+msgid "Toggle Alt"
+msgstr "Växla Alt"
+
+#: ../vnc.html:100
+msgid "Toggle Windows"
+msgstr "Växla Windows"
+
+#: ../vnc.html:100
+msgid "Windows"
+msgstr "Windows"
+
+#: ../vnc.html:103
+msgid "Send Tab"
+msgstr "Skicka Tab"
+
+#: ../vnc.html:103
+msgid "Tab"
+msgstr "Tab"
+
+#: ../vnc.html:106
+msgid "Esc"
+msgstr "Esc"
+
+#: ../vnc.html:106
+msgid "Send Escape"
+msgstr "Skicka Escape"
+
+#: ../vnc.html:109
+msgid "Ctrl+Alt+Del"
+msgstr "Ctrl+Alt+Del"
+
+#: ../vnc.html:109
+msgid "Send Ctrl-Alt-Del"
+msgstr "Skicka Ctrl-Alt-Del"
+
+#: ../vnc.html:116
+msgid "Shutdown/Reboot"
+msgstr "Stäng av/Boota om"
+
+#: ../vnc.html:116
+msgid "Shutdown/Reboot..."
+msgstr "Stäng av/Boota om..."
+
+#: ../vnc.html:122
+msgid "Power"
+msgstr "Ström"
+
+#: ../vnc.html:124
+msgid "Shutdown"
+msgstr "Stäng av"
+
+#: ../vnc.html:125
+msgid "Reboot"
+msgstr "Boota om"
+
+#: ../vnc.html:126
+msgid "Reset"
+msgstr "Återställ"
+
+#: ../vnc.html:131 ../vnc.html:137
+msgid "Clipboard"
+msgstr "Urklipp"
+
+#: ../vnc.html:141
+msgid "Clear"
+msgstr "Rensa"
+
+#: ../vnc.html:147
+msgid "Fullscreen"
+msgstr "Fullskärm"
+
+#: ../vnc.html:152 ../vnc.html:159
+msgid "Settings"
+msgstr "Inställningar"
+
+#: ../vnc.html:162
+msgid "Shared Mode"
+msgstr "Delat Läge"
+
+#: ../vnc.html:165
+msgid "View Only"
+msgstr "Endast Visning"
+
+#: ../vnc.html:169
+msgid "Clip to Window"
+msgstr "Begränsa till Fönster"
+
+#: ../vnc.html:172
+msgid "Scaling Mode:"
+msgstr "Skalningsläge:"
+
+#: ../vnc.html:174
+msgid "None"
+msgstr "Ingen"
+
+#: ../vnc.html:175
+msgid "Local Scaling"
+msgstr "Lokal Skalning"
+
+#: ../vnc.html:176
+msgid "Remote Resizing"
+msgstr "Ändra Storlek"
+
+#: ../vnc.html:181
+msgid "Advanced"
+msgstr "Avancerat"
+
+#: ../vnc.html:184
+msgid "Quality:"
+msgstr "Kvalitet:"
+
+#: ../vnc.html:188
+msgid "Compression level:"
+msgstr "Kompressionsnivå:"
+
+#: ../vnc.html:193
+msgid "Repeater ID:"
+msgstr "Repeater-ID:"
+
+#: ../vnc.html:197
+msgid "WebSocket"
+msgstr "WebSocket"
+
+#: ../vnc.html:200
+msgid "Encrypt"
+msgstr "Kryptera"
+
+#: ../vnc.html:203
+msgid "Host:"
+msgstr "Värd:"
+
+#: ../vnc.html:207
+msgid "Port:"
+msgstr "Port:"
+
+#: ../vnc.html:211
+msgid "Path:"
+msgstr "Sökväg:"
+
+#: ../vnc.html:218
+msgid "Automatic Reconnect"
+msgstr "Automatisk Återanslutning"
+
+#: ../vnc.html:221
+msgid "Reconnect Delay (ms):"
+msgstr "Fördröjning (ms):"
+
+#: ../vnc.html:226
+msgid "Show Dot when No Cursor"
+msgstr "Visa prick när ingen muspekare finns"
+
+#: ../vnc.html:231
+msgid "Logging:"
+msgstr "Loggning:"
+
+#: ../vnc.html:240
+msgid "Version:"
+msgstr "Version:"
+
+#: ../vnc.html:248
+msgid "Disconnect"
+msgstr "Koppla från"
+
+#: ../vnc.html:267
+msgid "Connect"
+msgstr "Anslut"
+
+#: ../vnc.html:277
+msgid "Username:"
+msgstr "Användarnamn:"
+
+#: ../vnc.html:281
+msgid "Password:"
+msgstr "Lösenord:"
+
+#: ../vnc.html:285
+msgid "Send Credentials"
+msgstr "Skicka Användaruppgifter"
+
+#: ../vnc.html:295
+msgid "Cancel"
+msgstr "Avbryt"
diff --git a/app/src/main/assets/novnc/po/tr.po b/app/src/main/assets/novnc/po/tr.po
new file mode 100644
index 00000000..8b5c1813
--- /dev/null
+++ b/app/src/main/assets/novnc/po/tr.po
@@ -0,0 +1,288 @@
+# Turkish translations for noVNC package
+# Turkish translation for noVNC.
+# Copyright (C) 2018 The noVNC Authors
+# This file is distributed under the same license as the noVNC package.
+# Ömer ÇAKMAK <farukomercakmak@gmail.com>, 2018.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: noVNC 0.6.1\n"
+"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
+"POT-Creation-Date: 2017-11-24 07:16+0000\n"
+"PO-Revision-Date: 2018-01-05 19:07+0300\n"
+"Last-Translator: Ömer ÇAKMAK <farukomercakmak@gmail.com>\n"
+"Language-Team: Türkçe <gnome-turk@gnome.org>\n"
+"Language: tr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Generator: Gtranslator 2.91.7\n"
+
+#: ../app/ui.js:404
+msgid "Connecting..."
+msgstr "Bağlanıyor..."
+
+#: ../app/ui.js:411
+msgid "Disconnecting..."
+msgstr "Bağlantı kesiliyor..."
+
+#: ../app/ui.js:417
+msgid "Reconnecting..."
+msgstr "Yeniden bağlantı kuruluyor..."
+
+#: ../app/ui.js:422
+msgid "Internal error"
+msgstr "İç hata"
+
+#: ../app/ui.js:1019
+msgid "Must set host"
+msgstr "Sunucuyu kur"
+
+#: ../app/ui.js:1099
+msgid "Connected (encrypted) to "
+msgstr "Bağlı (şifrelenmiş)"
+
+#: ../app/ui.js:1101
+msgid "Connected (unencrypted) to "
+msgstr "Bağlandı (şifrelenmemiş)"
+
+#: ../app/ui.js:1119
+msgid "Something went wrong, connection is closed"
+msgstr "Bir şeyler ters gitti, bağlantı kesildi"
+
+#: ../app/ui.js:1129
+msgid "Disconnected"
+msgstr "Bağlantı kesildi"
+
+#: ../app/ui.js:1142
+msgid "New connection has been rejected with reason: "
+msgstr "Bağlantı aşağıdaki nedenlerden dolayı reddedildi: "
+
+#: ../app/ui.js:1145
+msgid "New connection has been rejected"
+msgstr "Bağlantı reddedildi"
+
+#: ../app/ui.js:1166
+msgid "Password is required"
+msgstr "Şifre gerekli"
+
+#: ../vnc.html:89
+msgid "noVNC encountered an error:"
+msgstr "Bir hata oluştu:"
+
+#: ../vnc.html:99
+msgid "Hide/Show the control bar"
+msgstr "Denetim masasını Gizle/Göster"
+
+#: ../vnc.html:106
+msgid "Move/Drag Viewport"
+msgstr "Görünümü Taşı/Sürükle"
+
+#: ../vnc.html:106
+msgid "viewport drag"
+msgstr "Görüntü penceresini sürükle"
+
+#: ../vnc.html:112 ../vnc.html:115 ../vnc.html:118 ../vnc.html:121
+msgid "Active Mouse Button"
+msgstr "Aktif Fare Düğmesi"
+
+#: ../vnc.html:112
+msgid "No mousebutton"
+msgstr "Fare düğmesi yok"
+
+#: ../vnc.html:115
+msgid "Left mousebutton"
+msgstr "Farenin sol düğmesi"
+
+#: ../vnc.html:118
+msgid "Middle mousebutton"
+msgstr "Farenin orta düğmesi"
+
+#: ../vnc.html:121
+msgid "Right mousebutton"
+msgstr "Farenin sağ düğmesi"
+
+#: ../vnc.html:124
+msgid "Keyboard"
+msgstr "Klavye"
+
+#: ../vnc.html:124
+msgid "Show Keyboard"
+msgstr "Klavye Düzenini Göster"
+
+#: ../vnc.html:131
+msgid "Extra keys"
+msgstr "Ekstra tuşlar"
+
+#: ../vnc.html:131
+msgid "Show Extra Keys"
+msgstr "Ekstra tuşları göster"
+
+#: ../vnc.html:136
+msgid "Ctrl"
+msgstr "Ctrl"
+
+#: ../vnc.html:136
+msgid "Toggle Ctrl"
+msgstr "Ctrl Değiştir "
+
+#: ../vnc.html:139
+msgid "Alt"
+msgstr "Alt"
+
+#: ../vnc.html:139
+msgid "Toggle Alt"
+msgstr "Alt Değiştir"
+
+#: ../vnc.html:142
+msgid "Send Tab"
+msgstr "Sekme Gönder"
+
+#: ../vnc.html:142
+msgid "Tab"
+msgstr "Sekme"
+
+#: ../vnc.html:145
+msgid "Esc"
+msgstr "Esc"
+
+#: ../vnc.html:145
+msgid "Send Escape"
+msgstr "Boşluk Gönder"
+
+#: ../vnc.html:148
+msgid "Ctrl+Alt+Del"
+msgstr "Ctrl + Alt + Del"
+
+#: ../vnc.html:148
+msgid "Send Ctrl-Alt-Del"
+msgstr "Ctrl-Alt-Del Gönder"
+
+#: ../vnc.html:156
+msgid "Shutdown/Reboot"
+msgstr "Kapat/Yeniden Başlat"
+
+#: ../vnc.html:156
+msgid "Shutdown/Reboot..."
+msgstr "Kapat/Yeniden Başlat..."
+
+#: ../vnc.html:162
+msgid "Power"
+msgstr "Güç"
+
+#: ../vnc.html:164
+msgid "Shutdown"
+msgstr "Kapat"
+
+#: ../vnc.html:165
+msgid "Reboot"
+msgstr "Yeniden Başlat"
+
+#: ../vnc.html:166
+msgid "Reset"
+msgstr "Sıfırla"
+
+#: ../vnc.html:171 ../vnc.html:177
+msgid "Clipboard"
+msgstr "Pano"
+
+#: ../vnc.html:181
+msgid "Clear"
+msgstr "Temizle"
+
+#: ../vnc.html:187
+msgid "Fullscreen"
+msgstr "Tam Ekran"
+
+#: ../vnc.html:192 ../vnc.html:199
+msgid "Settings"
+msgstr "Ayarlar"
+
+#: ../vnc.html:202
+msgid "Shared Mode"
+msgstr "Paylaşım Modu"
+
+#: ../vnc.html:205
+msgid "View Only"
+msgstr "Sadece Görüntüle"
+
+#: ../vnc.html:209
+msgid "Clip to Window"
+msgstr "Pencereye Tıkla"
+
+#: ../vnc.html:212
+msgid "Scaling Mode:"
+msgstr "Ölçekleme Modu:"
+
+#: ../vnc.html:214
+msgid "None"
+msgstr "Bilinmeyen"
+
+#: ../vnc.html:215
+msgid "Local Scaling"
+msgstr "Yerel Ölçeklendirme"
+
+#: ../vnc.html:216
+msgid "Remote Resizing"
+msgstr "Uzaktan Yeniden Boyutlandırma"
+
+#: ../vnc.html:221
+msgid "Advanced"
+msgstr "Gelişmiş"
+
+#: ../vnc.html:224
+msgid "Repeater ID:"
+msgstr "Tekralayıcı ID:"
+
+#: ../vnc.html:228
+msgid "WebSocket"
+msgstr "WebSocket"
+
+#: ../vnc.html:231
+msgid "Encrypt"
+msgstr "Şifrele"
+
+#: ../vnc.html:234
+msgid "Host:"
+msgstr "Ana makine:"
+
+#: ../vnc.html:238
+msgid "Port:"
+msgstr "Port:"
+
+#: ../vnc.html:242
+msgid "Path:"
+msgstr "Yol:"
+
+#: ../vnc.html:249
+msgid "Automatic Reconnect"
+msgstr "Otomatik Yeniden Bağlan"
+
+#: ../vnc.html:252
+msgid "Reconnect Delay (ms):"
+msgstr "Yeniden Bağlanma Süreci (ms):"
+
+#: ../vnc.html:258
+msgid "Logging:"
+msgstr "Giriş yapılıyor:"
+
+#: ../vnc.html:270
+msgid "Disconnect"
+msgstr "Bağlantıyı Kes"
+
+#: ../vnc.html:289
+msgid "Connect"
+msgstr "Bağlan"
+
+#: ../vnc.html:299
+msgid "Password:"
+msgstr "Parola:"
+
+#: ../vnc.html:313
+msgid "Cancel"
+msgstr "Vazgeç"
+
+#: ../vnc.html:329
+msgid "Canvas not supported."
+msgstr "Tuval desteklenmiyor."
diff --git a/app/src/main/assets/novnc/po/xgettext-html b/app/src/main/assets/novnc/po/xgettext-html
new file mode 100755
index 00000000..bb30d3bc
--- /dev/null
+++ b/app/src/main/assets/novnc/po/xgettext-html
@@ -0,0 +1,115 @@
+#!/usr/bin/env node
+/*
+ * xgettext-html: HTML gettext parser
+ * Copyright (C) 2018 The noVNC Authors
+ * Licensed under MPL 2.0 (see LICENSE.txt)
+ */
+
+const getopt = require('node-getopt');
+const jsdom = require("jsdom");
+const fs = require("fs");
+
+const opt = getopt.create([
+    ['o', 'output=FILE', 'write output to specified file'],
+    ['h', 'help', 'display this help'],
+]).bindHelp().parseSystem();
+
+const strings = {};
+
+function addString(str, location) {
+    if (str.length == 0) {
+        return;
+    }
+
+    if (strings[str] === undefined) {
+        strings[str] = {};
+    }
+    strings[str][location] = null;
+}
+
+// See https://html.spec.whatwg.org/multipage/dom.html#attr-translate
+function process(elem, locator, enabled) {
+    function isAnyOf(searchElement, items) {
+        return items.indexOf(searchElement) !== -1;
+    }
+
+    if (elem.hasAttribute("translate")) {
+        if (isAnyOf(elem.getAttribute("translate"), ["", "yes"])) {
+            enabled = true;
+        } else if (isAnyOf(elem.getAttribute("translate"), ["no"])) {
+            enabled = false;
+        }
+    }
+
+    if (enabled) {
+        if (elem.hasAttribute("abbr") &&
+            elem.tagName === "TH") {
+            addString(elem.getAttribute("abbr"), locator(elem));
+        }
+        if (elem.hasAttribute("alt") &&
+            isAnyOf(elem.tagName, ["AREA", "IMG", "INPUT"])) {
+            addString(elem.getAttribute("alt"), locator(elem));
+        }
+        if (elem.hasAttribute("download") &&
+            isAnyOf(elem.tagName, ["A", "AREA"])) {
+            addString(elem.getAttribute("download"), locator(elem));
+        }
+        if (elem.hasAttribute("label") &&
+            isAnyOf(elem.tagName, ["MENUITEM", "MENU", "OPTGROUP",
+                                   "OPTION", "TRACK"])) {
+            addString(elem.getAttribute("label"), locator(elem));
+        }
+        if (elem.hasAttribute("placeholder") &&
+            isAnyOf(elem.tagName in ["INPUT", "TEXTAREA"])) {
+            addString(elem.getAttribute("placeholder"), locator(elem));
+        }
+        if (elem.hasAttribute("title")) {
+            addString(elem.getAttribute("title"), locator(elem));
+        }
+        if (elem.hasAttribute("value") &&
+            elem.tagName === "INPUT" &&
+            isAnyOf(elem.getAttribute("type"), ["reset", "button", "submit"])) {
+            addString(elem.getAttribute("value"), locator(elem));
+        }
+    }
+
+    for (let i = 0; i < elem.childNodes.length; i++) {
+        let node = elem.childNodes[i];
+        if (node.nodeType === node.ELEMENT_NODE) {
+            process(node, locator, enabled);
+        } else if (node.nodeType === node.TEXT_NODE && enabled) {
+            addString(node.data.trim(), locator(node));
+        }
+    }
+}
+
+for (let i = 0; i < opt.argv.length; i++) {
+    const fn = opt.argv[i];
+    const file = fs.readFileSync(fn, "utf8");
+    const dom = new jsdom.JSDOM(file, { includeNodeLocations: true });
+    const body = dom.window.document.body;
+
+    let locator = (elem) => {
+        const offset = dom.nodeLocation(elem).startOffset;
+        const line = file.slice(0, offset).split("\n").length;
+        return fn + ":" + line;
+    };
+
+    process(body, locator, true);
+}
+
+let output = "";
+
+for (let str in strings) {
+    output += "#:";
+    for (location in strings[str]) {
+        output += " " + location;
+    }
+    output += "\n";
+
+    output += "msgid " + JSON.stringify(str) + "\n";
+    output += "msgstr \"\"\n";
+    output += "\n";
+}
+
+fs.writeFileSync(opt.options.output, output);
diff --git a/app/src/main/assets/novnc/po/zh_CN.po b/app/src/main/assets/novnc/po/zh_CN.po
new file mode 100644
index 00000000..ede9d441
--- /dev/null
+++ b/app/src/main/assets/novnc/po/zh_CN.po
@@ -0,0 +1,284 @@
+# Simplified Chinese translations for noVNC package.
+# Copyright (C) 2020 The noVNC Authors
+# This file is distributed under the same license as the noVNC package.
+# Peter Dave Hello <hsu@peterdavehello.org>, 2018.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: noVNC 1.1.0\n"
+"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
+"POT-Creation-Date: 2018-01-10 00:53+0800\n"
+"PO-Revision-Date: 2020-01-02 13:19+0800\n"
+"Last-Translator: CUI Wei <ghostplant@qq.com>\n"
+"Language: zh_CN\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: ../app/ui.js:395
+msgid "Connecting..."
+msgstr "连接中..."
+
+#: ../app/ui.js:402
+msgid "Disconnecting..."
+msgstr "正在断开连接..."
+
+#: ../app/ui.js:408
+msgid "Reconnecting..."
+msgstr "重新连接中..."
+
+#: ../app/ui.js:413
+msgid "Internal error"
+msgstr "内部错误"
+
+#: ../app/ui.js:1015
+msgid "Must set host"
+msgstr "请提供主机名"
+
+#: ../app/ui.js:1097
+msgid "Connected (encrypted) to "
+msgstr "已连接到(加密)"
+
+#: ../app/ui.js:1099
+msgid "Connected (unencrypted) to "
+msgstr "已连接到(未加密)"
+
+#: ../app/ui.js:1120
+msgid "Something went wrong, connection is closed"
+msgstr "发生错误,连接已关闭"
+
+#: ../app/ui.js:1123
+msgid "Failed to connect to server"
+msgstr "无法连接到服务器"
+
+#: ../app/ui.js:1133
+msgid "Disconnected"
+msgstr "已断开连接"
+
+#: ../app/ui.js:1146
+msgid "New connection has been rejected with reason: "
+msgstr "连接被拒绝,原因:"
+
+#: ../app/ui.js:1149
+msgid "New connection has been rejected"
+msgstr "连接被拒绝"
+
+#: ../app/ui.js:1170
+msgid "Password is required"
+msgstr "请提供密码"
+
+#: ../vnc.html:89
+msgid "noVNC encountered an error:"
+msgstr "noVNC 遇到一个错误:"
+
+#: ../vnc.html:99
+msgid "Hide/Show the control bar"
+msgstr "显示/隐藏控制栏"
+
+#: ../vnc.html:106
+msgid "Move/Drag Viewport"
+msgstr "拖放显示范围"
+
+#: ../vnc.html:106
+msgid "viewport drag"
+msgstr "显示范围拖放"
+
+#: ../vnc.html:112 ../vnc.html:115 ../vnc.html:118 ../vnc.html:121
+msgid "Active Mouse Button"
+msgstr "启动鼠标按鍵"
+
+#: ../vnc.html:112
+msgid "No mousebutton"
+msgstr "禁用鼠标按鍵"
+
+#: ../vnc.html:115
+msgid "Left mousebutton"
+msgstr "鼠标左鍵"
+
+#: ../vnc.html:118
+msgid "Middle mousebutton"
+msgstr "鼠标中鍵"
+
+#: ../vnc.html:121
+msgid "Right mousebutton"
+msgstr "鼠标右鍵"
+
+#: ../vnc.html:124
+msgid "Keyboard"
+msgstr "键盘"
+
+#: ../vnc.html:124
+msgid "Show Keyboard"
+msgstr "显示键盘"
+
+#: ../vnc.html:131
+msgid "Extra keys"
+msgstr "额外按键"
+
+#: ../vnc.html:131
+msgid "Show Extra Keys"
+msgstr "显示额外按键"
+
+#: ../vnc.html:136
+msgid "Ctrl"
+msgstr "Ctrl"
+
+#: ../vnc.html:136
+msgid "Toggle Ctrl"
+msgstr "切换 Ctrl"
+
+#: ../vnc.html:139
+msgid "Alt"
+msgstr "Alt"
+
+#: ../vnc.html:139
+msgid "Toggle Alt"
+msgstr "切换 Alt"
+
+#: ../vnc.html:142
+msgid "Send Tab"
+msgstr "发送 Tab 键"
+
+#: ../vnc.html:142
+msgid "Tab"
+msgstr "Tab"
+
+#: ../vnc.html:145
+msgid "Esc"
+msgstr "Esc"
+
+#: ../vnc.html:145
+msgid "Send Escape"
+msgstr "发送 Escape 键"
+
+#: ../vnc.html:148
+msgid "Ctrl+Alt+Del"
+msgstr "Ctrl-Alt-Del"
+
+#: ../vnc.html:148
+msgid "Send Ctrl-Alt-Del"
+msgstr "发送 Ctrl-Alt-Del 键"
+
+#: ../vnc.html:156
+msgid "Shutdown/Reboot"
+msgstr "关机/重新启动"
+
+#: ../vnc.html:156
+msgid "Shutdown/Reboot..."
+msgstr "关机/重新启动..."
+
+#: ../vnc.html:162
+msgid "Power"
+msgstr "电源"
+
+#: ../vnc.html:164
+msgid "Shutdown"
+msgstr "关机"
+
+#: ../vnc.html:165
+msgid "Reboot"
+msgstr "重新启动"
+
+#: ../vnc.html:166
+msgid "Reset"
+msgstr "重置"
+
+#: ../vnc.html:171 ../vnc.html:177
+msgid "Clipboard"
+msgstr "剪贴板"
+
+#: ../vnc.html:181
+msgid "Clear"
+msgstr "清除"
+
+#: ../vnc.html:187
+msgid "Fullscreen"
+msgstr "全屏"
+
+#: ../vnc.html:192 ../vnc.html:199
+msgid "Settings"
+msgstr "设置"
+
+#: ../vnc.html:202
+msgid "Shared Mode"
+msgstr "分享模式"
+
+#: ../vnc.html:205
+msgid "View Only"
+msgstr "仅查看"
+
+#: ../vnc.html:209
+msgid "Clip to Window"
+msgstr "限制/裁切窗口大小"
+
+#: ../vnc.html:212
+msgid "Scaling Mode:"
+msgstr "缩放模式:"
+
+#: ../vnc.html:214
+msgid "None"
+msgstr "无"
+
+#: ../vnc.html:215
+msgid "Local Scaling"
+msgstr "本地缩放"
+
+#: ../vnc.html:216
+msgid "Remote Resizing"
+msgstr "远程调整大小"
+
+#: ../vnc.html:221
+msgid "Advanced"
+msgstr "高级"
+
+#: ../vnc.html:224
+msgid "Repeater ID:"
+msgstr "中继站 ID"
+
+#: ../vnc.html:228
+msgid "WebSocket"
+msgstr "WebSocket"
+
+#: ../vnc.html:231
+msgid "Encrypt"
+msgstr "加密"
+
+#: ../vnc.html:234
+msgid "Host:"
+msgstr "主机:"
+
+#: ../vnc.html:238
+msgid "Port:"
+msgstr "端口:"
+
+#: ../vnc.html:242
+msgid "Path:"
+msgstr "路径:"
+
+#: ../vnc.html:249
+msgid "Automatic Reconnect"
+msgstr "自动重新连接"
+
+#: ../vnc.html:252
+msgid "Reconnect Delay (ms):"
+msgstr "重新连接间隔 (ms):"
+
+#: ../vnc.html:258
+msgid "Logging:"
+msgstr "日志级别:"
+
+#: ../vnc.html:270
+msgid "Disconnect"
+msgstr "中断连接"
+
+#: ../vnc.html:289
+msgid "Connect"
+msgstr "连接"
+
+#: ../vnc.html:299
+msgid "Password:"
+msgstr "密码:"
+
+#: ../vnc.html:313
+msgid "Cancel"
+msgstr "取消"
diff --git a/app/src/main/assets/novnc/po/zh_TW.po b/app/src/main/assets/novnc/po/zh_TW.po
new file mode 100644
index 00000000..9ddf550c
--- /dev/null
+++ b/app/src/main/assets/novnc/po/zh_TW.po
@@ -0,0 +1,285 @@
+# Traditional Chinese translations for noVNC package.
+# Copyright (C) 2018 The noVNC Authors
+# This file is distributed under the same license as the noVNC package.
+# Peter Dave Hello <hsu@peterdavehello.org>, 2018.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: noVNC 1.0.0-testing.2\n"
+"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
+"POT-Creation-Date: 2018-01-10 00:53+0800\n"
+"PO-Revision-Date: 2018-01-10 01:33+0800\n"
+"Last-Translator: Peter Dave Hello <hsu@peterdavehello.org>\n"
+"Language-Team: Peter Dave Hello <hsu@peterdavehello.org>\n"
+"Language: zh\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: ../app/ui.js:395
+msgid "Connecting..."
+msgstr "連線中..."
+
+#: ../app/ui.js:402
+msgid "Disconnecting..."
+msgstr "正在中斷連線..."
+
+#: ../app/ui.js:408
+msgid "Reconnecting..."
+msgstr "重新連線中..."
+
+#: ../app/ui.js:413
+msgid "Internal error"
+msgstr "內部錯誤"
+
+#: ../app/ui.js:1015
+msgid "Must set host"
+msgstr "請提供主機資訊"
+
+#: ../app/ui.js:1097
+msgid "Connected (encrypted) to "
+msgstr "已加密連線到"
+
+#: ../app/ui.js:1099
+msgid "Connected (unencrypted) to "
+msgstr "未加密連線到"
+
+#: ../app/ui.js:1120
+msgid "Something went wrong, connection is closed"
+msgstr "發生錯誤,連線已關閉"
+
+#: ../app/ui.js:1123
+msgid "Failed to connect to server"
+msgstr "無法連線到伺服器"
+
+#: ../app/ui.js:1133
+msgid "Disconnected"
+msgstr "連線已中斷"
+
+#: ../app/ui.js:1146
+msgid "New connection has been rejected with reason: "
+msgstr "連線被拒絕,原因:"
+
+#: ../app/ui.js:1149
+msgid "New connection has been rejected"
+msgstr "連線被拒絕"
+
+#: ../app/ui.js:1170
+msgid "Password is required"
+msgstr "請提供密碼"
+
+#: ../vnc.html:89
+msgid "noVNC encountered an error:"
+msgstr "noVNC 遇到一個錯誤:"
+
+#: ../vnc.html:99
+msgid "Hide/Show the control bar"
+msgstr "顯示/隱藏控制列"
+
+#: ../vnc.html:106
+msgid "Move/Drag Viewport"
+msgstr "拖放顯示範圍"
+
+#: ../vnc.html:106
+msgid "viewport drag"
+msgstr "顯示範圍拖放"
+
+#: ../vnc.html:112 ../vnc.html:115 ../vnc.html:118 ../vnc.html:121
+msgid "Active Mouse Button"
+msgstr "啟用滑鼠按鍵"
+
+#: ../vnc.html:112
+msgid "No mousebutton"
+msgstr "無滑鼠按鍵"
+
+#: ../vnc.html:115
+msgid "Left mousebutton"
+msgstr "滑鼠左鍵"
+
+#: ../vnc.html:118
+msgid "Middle mousebutton"
+msgstr "滑鼠中鍵"
+
+#: ../vnc.html:121
+msgid "Right mousebutton"
+msgstr "滑鼠右鍵"
+
+#: ../vnc.html:124
+msgid "Keyboard"
+msgstr "鍵盤"
+
+#: ../vnc.html:124
+msgid "Show Keyboard"
+msgstr "顯示鍵盤"
+
+#: ../vnc.html:131
+msgid "Extra keys"
+msgstr "額外按鍵"
+
+#: ../vnc.html:131
+msgid "Show Extra Keys"
+msgstr "顯示額外按鍵"
+
+#: ../vnc.html:136
+msgid "Ctrl"
+msgstr "Ctrl"
+
+#: ../vnc.html:136
+msgid "Toggle Ctrl"
+msgstr "切換 Ctrl"
+
+#: ../vnc.html:139
+msgid "Alt"
+msgstr "Alt"
+
+#: ../vnc.html:139
+msgid "Toggle Alt"
+msgstr "切換 Alt"
+
+#: ../vnc.html:142
+msgid "Send Tab"
+msgstr "送出 Tab 鍵"
+
+#: ../vnc.html:142
+msgid "Tab"
+msgstr "Tab"
+
+#: ../vnc.html:145
+msgid "Esc"
+msgstr "Esc"
+
+#: ../vnc.html:145
+msgid "Send Escape"
+msgstr "送出 Escape 鍵"
+
+#: ../vnc.html:148
+msgid "Ctrl+Alt+Del"
+msgstr "Ctrl-Alt-Del"
+
+#: ../vnc.html:148
+msgid "Send Ctrl-Alt-Del"
+msgstr "送出 Ctrl-Alt-Del 快捷鍵"
+
+#: ../vnc.html:156
+msgid "Shutdown/Reboot"
+msgstr "關機/重新啟動"
+
+#: ../vnc.html:156
+msgid "Shutdown/Reboot..."
+msgstr "關機/重新啟動..."
+
+#: ../vnc.html:162
+msgid "Power"
+msgstr "電源"
+
+#: ../vnc.html:164
+msgid "Shutdown"
+msgstr "關機"
+
+#: ../vnc.html:165
+msgid "Reboot"
+msgstr "重新啟動"
+
+#: ../vnc.html:166
+msgid "Reset"
+msgstr "重設"
+
+#: ../vnc.html:171 ../vnc.html:177
+msgid "Clipboard"
+msgstr "剪貼簿"
+
+#: ../vnc.html:181
+msgid "Clear"
+msgstr "清除"
+
+#: ../vnc.html:187
+msgid "Fullscreen"
+msgstr "全螢幕"
+
+#: ../vnc.html:192 ../vnc.html:199
+msgid "Settings"
+msgstr "設定"
+
+#: ../vnc.html:202
+msgid "Shared Mode"
+msgstr "分享模式"
+
+#: ../vnc.html:205
+msgid "View Only"
+msgstr "僅檢視"
+
+#: ../vnc.html:209
+msgid "Clip to Window"
+msgstr "限制/裁切視窗大小"
+
+#: ../vnc.html:212
+msgid "Scaling Mode:"
+msgstr "縮放模式:"
+
+#: ../vnc.html:214
+msgid "None"
+msgstr "無"
+
+#: ../vnc.html:215
+msgid "Local Scaling"
+msgstr "本機縮放"
+
+#: ../vnc.html:216
+msgid "Remote Resizing"
+msgstr "遠端調整大小"
+
+#: ../vnc.html:221
+msgid "Advanced"
+msgstr "進階"
+
+#: ../vnc.html:224
+msgid "Repeater ID:"
+msgstr "中繼站 ID"
+
+#: ../vnc.html:228
+msgid "WebSocket"
+msgstr "WebSocket"
+
+#: ../vnc.html:231
+msgid "Encrypt"
+msgstr "加密"
+
+#: ../vnc.html:234
+msgid "Host:"
+msgstr "主機:"
+
+#: ../vnc.html:238
+msgid "Port:"
+msgstr "連接埠:"
+
+#: ../vnc.html:242
+msgid "Path:"
+msgstr "路徑:"
+
+#: ../vnc.html:249
+msgid "Automatic Reconnect"
+msgstr "自動重新連線"
+
+#: ../vnc.html:252
+msgid "Reconnect Delay (ms):"
+msgstr "重新連線間隔 (ms):"
+
+#: ../vnc.html:258
+msgid "Logging:"
+msgstr "日誌級別:"
+
+#: ../vnc.html:270
+msgid "Disconnect"
+msgstr "中斷連線"
+
+#: ../vnc.html:289
+msgid "Connect"
+msgstr "連線"
+
+#: ../vnc.html:299
+msgid "Password:"
+msgstr "密碼:"
+
+#: ../vnc.html:313
+msgid "Cancel"
+msgstr "取消"
diff --git a/app/src/main/assets/novnc/snap/hooks/configure b/app/src/main/assets/novnc/snap/hooks/configure
new file mode 100644
index 00000000..ff4f8047
--- /dev/null
+++ b/app/src/main/assets/novnc/snap/hooks/configure
@@ -0,0 +1,3 @@
+#!/bin/sh -e
+
+snapctl restart novnc.novncsvc
diff --git a/app/src/main/assets/novnc/snap/local/svc_wrapper.sh b/app/src/main/assets/novnc/snap/local/svc_wrapper.sh
new file mode 100755
index 00000000..77db5394
--- /dev/null
+++ b/app/src/main/assets/novnc/snap/local/svc_wrapper.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+# `snapctl get services` returns a JSON array, example:
+#{
+#"n6801": {
+#   "listen": 6801,
+#   "vnc": "localhost:5901"
+#},
+#"n6802": {
+#    "listen": 6802,
+#   "vnc": "localhost:5902"
+#}
+#}
+snapctl get services | jq -c '.[]' | while read service; do # for each service the user sepcified..
+    # get the important data for the service (listen port, VNC host:port)
+    listen_port="$(echo $service | jq --raw-output '.listen')"
+    vnc_host_port="$(echo $service | jq --raw-output '.vnc')" # --raw-output removes any quotation marks from the output
+    
+    # check whether those values are valid
+    expr "$listen_port" : '^[0-9]\+$' > /dev/null
+    listen_port_valid=$?
+    if [ ! $listen_port_valid ] || [ -z "$vnc_host_port" ]; then
+        # invalid values mean the service is disabled, do nothing except for printing a message (logged in /var/log/system or systemd journal)
+        echo "novnc: not starting service ${service} with listen_port ${listen_port} and vnc_host_port ${vnc_host_port}"
+    else
+        # start (and fork with '&') the service using the specified listen port and VNC host:port
+        $SNAP/novnc_proxy --listen $listen_port --vnc $vnc_host_port &
+    fi
+done
diff --git a/app/src/main/assets/novnc/snap/snapcraft.yaml b/app/src/main/assets/novnc/snap/snapcraft.yaml
new file mode 100644
index 00000000..ffba501e
--- /dev/null
+++ b/app/src/main/assets/novnc/snap/snapcraft.yaml
@@ -0,0 +1,55 @@
+name: novnc
+base: core18 # the base snap is the execution environment for this snap
+version: '@VERSION@'
+summary: Open Source VNC client using HTML5 (WebSockets, Canvas)
+description: |
+  Open Source VNC client using HTML5 (WebSockets, Canvas).
+  noVNC is both a VNC client JavaScript library as well as an
+  application built on top of that library. noVNC runs well in any
+  modern browser including mobile browsers (iOS and Android).
+
+grade: stable
+confinement: strict
+
+parts:
+    novnc:
+        source: .
+        plugin: dump
+        organize:
+            utils/novnc_proxy: /
+        stage:
+            - vnc.html
+            - app
+            - core/**/*.js
+            - vendor/**/*.js
+            - novnc_proxy
+        stage-packages:
+            - bash
+
+    svc-script:
+        source: snap/local
+        plugin: dump
+        stage:
+            - svc_wrapper.sh
+        stage-packages:
+            - bash
+            - jq
+
+    websockify:
+        source: https://github.com/novnc/websockify/archive/v0.9.0.tar.gz
+        plugin: python
+        stage-packages:
+            - python3-numpy
+
+hooks:
+    configure:
+        plugs: [network, network-bind]
+
+apps:
+    novnc:
+        command: ./novnc_proxy
+        plugs: [network, network-bind]
+    novncsvc:
+        command: ./svc_wrapper.sh
+        daemon: forking
+        plugs: [network, network-bind]
diff --git a/app/src/main/assets/novnc/utils/.eslintrc b/app/src/main/assets/novnc/utils/.eslintrc
new file mode 100644
index 00000000..b7dc129f
--- /dev/null
+++ b/app/src/main/assets/novnc/utils/.eslintrc
@@ -0,0 +1,8 @@
+{
+  "env": {
+    "node": true
+  },
+  "rules": {
+  	"no-console": 0
+  }
+}
\ No newline at end of file
diff --git a/app/src/main/assets/novnc/utils/README.md b/app/src/main/assets/novnc/utils/README.md
new file mode 100644
index 00000000..05d75c9e
--- /dev/null
+++ b/app/src/main/assets/novnc/utils/README.md
@@ -0,0 +1,14 @@
+## WebSockets Proxy/Bridge
+
+Websockify has been forked out into its own project. `novnc_proxy` will
+automatically download it here if it is not already present and not
+installed as system-wide.
+
+For more detailed description and usage information please refer to
+the [websockify README](https://github.com/novnc/websockify/blob/master/README.md).
+
+The other versions of websockify (C, Node.js) and the associated test
+programs have been moved to
+[websockify](https://github.com/novnc/websockify).  Websockify was
+formerly named wsproxy.
+
diff --git a/app/src/main/assets/novnc/utils/b64-to-binary.pl b/app/src/main/assets/novnc/utils/b64-to-binary.pl
new file mode 100755
index 00000000..280e28c9
--- /dev/null
+++ b/app/src/main/assets/novnc/utils/b64-to-binary.pl
@@ -0,0 +1,17 @@
+#!/usr/bin/env perl
+use MIME::Base64;
+
+for (<>) {
+    unless (/^'([{}])(\d+)\1(.+?)',$/) {
+        print;
+        next;
+    }
+
+    my ($dir, $amt, $b64) = ($1, $2, $3);
+
+    my $decoded = MIME::Base64::decode($b64) or die "Could not base64-decode line `$_`";
+
+    my $decoded_escaped = join "", map { "\\x$_" } unpack("(H2)*", $decoded);
+
+    print "'${dir}${amt}${dir}${decoded_escaped}',\n";
+}
diff --git a/app/src/main/assets/novnc/utils/genkeysymdef.js b/app/src/main/assets/novnc/utils/genkeysymdef.js
new file mode 100755
index 00000000..f539a0b2
--- /dev/null
+++ b/app/src/main/assets/novnc/utils/genkeysymdef.js
@@ -0,0 +1,127 @@
+#!/usr/bin/env node
+/*
+ * genkeysymdef: X11 keysymdef.h to JavaScript converter
+ * Copyright (C) 2018 The noVNC Authors
+ * Licensed under MPL 2.0 (see LICENSE.txt)
+ */
+
+"use strict";
+
+const fs = require('fs');
+
+let showHelp = process.argv.length === 2;
+let filename;
+
+for (let i = 2; i < process.argv.length; ++i) {
+    switch (process.argv[i]) {
+        case "--help":
+        case "-h":
+            showHelp = true;
+            break;
+        case "--file":
+        case "-f":
+        default:
+            filename = process.argv[i];
+    }
+}
+
+if (!filename) {
+    showHelp = true;
+    console.log("Error: No filename specified\n");
+}
+
+if (showHelp) {
+    console.log("Parses a *nix keysymdef.h to generate Unicode code point mappings");
+    console.log("Usage: node parse.js [options] filename:");
+    console.log("  -h [ --help ]                 Produce this help message");
+    console.log("  filename                      The keysymdef.h file to parse");
+    process.exit(0);
+}
+
+const buf = fs.readFileSync(filename);
+const str = buf.toString('utf8');
+
+const re = /^#define XK_([a-zA-Z_0-9]+)\s+0x([0-9a-fA-F]+)\s*(\/\*\s*(.*)\s*\*\/)?\s*$/m;
+
+const arr = str.split('\n');
+
+const codepoints = {};
+
+for (let i = 0; i < arr.length; ++i) {
+    const result = re.exec(arr[i]);
+    if (result) {
+        const keyname = result[1];
+        const keysym = parseInt(result[2], 16);
+        const remainder = result[3];
+
+        const unicodeRes = /U\+([0-9a-fA-F]+)/.exec(remainder);
+        if (unicodeRes) {
+            const unicode = parseInt(unicodeRes[1], 16);
+            // The first entry is the preferred one
+            if (!codepoints[unicode]) {
+                codepoints[unicode] = { keysym: keysym, name: keyname };
+            }
+        }
+    }
+}
+
+let out =
+"/*\n" +
+" * Mapping from Unicode codepoints to X11/RFB keysyms\n" +
+" *\n" +
+" * This file was automatically generated from keysymdef.h\n" +
+" * DO NOT EDIT!\n" +
+" */\n" +
+"\n" +
+"/* Functions at the bottom */\n" +
+"\n" +
+"const codepoints = {\n";
+
+function toHex(num) {
+    let s = num.toString(16);
+    if (s.length < 4) {
+        s = ("0000" + s).slice(-4);
+    }
+    return "0x" + s;
+}
+
+for (let codepoint in codepoints) {
+    codepoint = parseInt(codepoint);
+
+    // Latin-1?
+    if ((codepoint >= 0x20) && (codepoint <= 0xff)) {
+        continue;
+    }
+
+    // Handled by the general Unicode mapping?
+    if ((codepoint | 0x01000000) === codepoints[codepoint].keysym) {
+        continue;
+    }
+
+    out += "    " + toHex(codepoint) + ": " +
+           toHex(codepoints[codepoint].keysym) +
+           ", // XK_" + codepoints[codepoint].name + "\n";
+}
+
+out +=
+"};\n" +
+"\n" +
+"export default {\n" +
+"    lookup(u) {\n" +
+"        // Latin-1 is one-to-one mapping\n" +
+"        if ((u >= 0x20) && (u <= 0xff)) {\n" +
+"            return u;\n" +
+"        }\n" +
+"\n" +
+"        // Lookup table (fairly random)\n" +
+"        const keysym = codepoints[u];\n" +
+"        if (keysym !== undefined) {\n" +
+"            return keysym;\n" +
+"        }\n" +
+"\n" +
+"        // General mapping as final fallback\n" +
+"        return 0x01000000 | u;\n" +
+"    },\n" +
+"};";
+
+console.log(out);
diff --git a/app/src/main/assets/novnc/utils/novnc_proxy b/app/src/main/assets/novnc/utils/novnc_proxy
new file mode 100755
index 00000000..0900f7e3
--- /dev/null
+++ b/app/src/main/assets/novnc/utils/novnc_proxy
@@ -0,0 +1,198 @@
+#!/usr/bin/env bash
+
+# Copyright (C) 2018 The noVNC Authors
+# Licensed under MPL 2.0 or any later version (see LICENSE.txt)
+
+usage() {
+    if [ "$*" ]; then
+        echo "$*"
+        echo
+    fi
+    echo "Usage: ${NAME} [--listen PORT] [--vnc VNC_HOST:PORT] [--cert CERT] [--ssl-only]"
+    echo
+    echo "Starts the WebSockets proxy and a mini-webserver and "
+    echo "provides a cut-and-paste URL to go to."
+    echo
+    echo "    --listen PORT         Port for proxy/webserver to listen on"
+    echo "                          Default: 6080"
+    echo "    --vnc VNC_HOST:PORT   VNC server host:port proxy target"
+    echo "                          Default: localhost:5900"
+    echo "    --cert CERT           Path to combined cert/key file, or just"
+    echo "                          the cert file if used with --key"
+    echo "                          Default: self.pem"
+    echo "    --key KEY             Path to key file, when not combined with cert"
+    echo "    --web WEB             Path to web files (e.g. vnc.html)"
+    echo "                          Default: ./"
+    echo "    --ssl-only            Disable non-https connections."
+    echo "                                    "
+    echo "    --record FILE         Record traffic to FILE.session.js"
+    echo "                                    "
+    echo "    --syslog SERVER       Can be local socket such as /dev/log, or a UDP host:port pair."
+    echo "                                    "
+    echo "    --heartbeat SEC       send a ping to the client every SEC seconds"
+    echo "    --timeout SEC         after SEC seconds exit when not connected"
+    echo "    --idle-timeout SEC    server exits after SEC seconds if there are no"
+    echo "                          active connections"
+    echo "                                    "
+    exit 2
+}
+
+NAME="$(basename $0)"
+REAL_NAME="$(readlink -f $0)"
+HERE="$(cd "$(dirname "$REAL_NAME")" && pwd)"
+PORT="6080"
+VNC_DEST="localhost:5900"
+CERT=""
+KEY=""
+WEB=""
+proxy_pid=""
+SSLONLY=""
+RECORD_ARG=""
+SYSLOG_ARG=""
+HEARTBEAT_ARG=""
+IDLETIMEOUT_ARG=""
+TIMEOUT_ARG=""
+
+die() {
+    echo "$*"
+    exit 1
+}
+
+cleanup() {
+    trap - TERM QUIT INT EXIT
+    trap "true" CHLD   # Ignore cleanup messages
+    echo
+    if [ -n "${proxy_pid}" ]; then
+        echo "Terminating WebSockets proxy (${proxy_pid})"
+        kill ${proxy_pid}
+    fi
+}
+
+# Process Arguments
+
+# Arguments that only apply to chrooter itself
+while [ "$*" ]; do
+    param=$1; shift; OPTARG=$1
+    case $param in
+    --listen)  PORT="${OPTARG}"; shift            ;;
+    --vnc)     VNC_DEST="${OPTARG}"; shift        ;;
+    --cert)    CERT="${OPTARG}"; shift            ;;
+    --key)     KEY="${OPTARG}"; shift             ;;
+    --web)     WEB="${OPTARG}"; shift            ;;
+    --ssl-only) SSLONLY="--ssl-only"             ;;
+    --record) RECORD_ARG="--record ${OPTARG}"; shift ;;
+    --syslog) SYSLOG_ARG="--syslog ${OPTARG}"; shift ;;
+    --heartbeat) HEARTBEAT_ARG="--heartbeat ${OPTARG}"; shift ;;
+    --idle-timeout) IDLETIMEOUT_ARG="--idle-timeout ${OPTARG}"; shift ;;
+    --timeout) TIMEOUT_ARG="--timeout ${OPTARG}"; shift ;;
+    -h|--help) usage                              ;;
+    -*) usage "Unknown chrooter option: ${param}" ;;
+    *) break                                      ;;
+    esac
+done
+
+# Sanity checks
+if bash -c "exec 7<>/dev/tcp/localhost/${PORT}" &> /dev/null; then
+    exec 7<&-
+    exec 7>&-
+    die "Port ${PORT} in use. Try --listen PORT"
+else
+    exec 7<&-
+    exec 7>&-
+fi
+
+trap "cleanup" TERM QUIT INT EXIT
+
+# Find vnc.html
+if [ -n "${WEB}" ]; then
+    if [ ! -e "${WEB}/vnc.html" ]; then
+        die "Could not find ${WEB}/vnc.html"
+    fi
+elif [ -e "$(pwd)/vnc.html" ]; then
+    WEB=$(pwd)
+elif [ -e "${HERE}/../vnc.html" ]; then
+    WEB=${HERE}/../
+elif [ -e "${HERE}/vnc.html" ]; then
+    WEB=${HERE}
+elif [ -e "${HERE}/../share/novnc/vnc.html" ]; then
+    WEB=${HERE}/../share/novnc/
+else
+    die "Could not find vnc.html"
+fi
+
+# Find self.pem
+if [ -n "${CERT}" ]; then
+    if [ ! -e "${CERT}" ]; then
+        die "Could not find ${CERT}"
+    fi
+elif [ -e "$(pwd)/self.pem" ]; then
+    CERT="$(pwd)/self.pem"
+elif [ -e "${HERE}/../self.pem" ]; then
+    CERT="${HERE}/../self.pem"
+elif [ -e "${HERE}/self.pem" ]; then
+    CERT="${HERE}/self.pem"
+else
+    echo "Warning: could not find self.pem"
+fi
+
+# Check key file
+if [ -n "${KEY}" ]; then
+    if [ ! -e "${KEY}" ]; then
+        die "Could not find ${KEY}"
+    fi
+fi
+
+# try to find websockify (prefer local, try global, then download local)
+if [[ -d ${HERE}/websockify ]]; then
+    WEBSOCKIFY=${HERE}/websockify/run
+
+    if [[ ! -x $WEBSOCKIFY ]]; then
+        echo "The path ${HERE}/websockify exists, but $WEBSOCKIFY either does not exist or is not executable."
+        echo "If you intended to use an installed websockify package, please remove ${HERE}/websockify."
+        exit 1
+    fi
+
+    echo "Using local websockify at $WEBSOCKIFY"
+else
+    WEBSOCKIFY_FROMSYSTEM=$(which websockify 2>/dev/null)
+    WEBSOCKIFY_FROMSNAP=${HERE}/../usr/bin/python2-websockify
+    [ -f $WEBSOCKIFY_FROMSYSTEM ] && WEBSOCKIFY=$WEBSOCKIFY_FROMSYSTEM
+    [ -f $WEBSOCKIFY_FROMSNAP ] && WEBSOCKIFY=$WEBSOCKIFY_FROMSNAP
+
+    if [ ! -f "$WEBSOCKIFY" ]; then
+        echo "No installed websockify, attempting to clone websockify..."
+        WEBSOCKIFY=${HERE}/websockify/run
+        git clone https://github.com/novnc/websockify ${HERE}/websockify
+
+        if [[ ! -e $WEBSOCKIFY ]]; then
+            echo "Unable to locate ${HERE}/websockify/run after downloading"
+            exit 1
+        fi
+
+        echo "Using local websockify at $WEBSOCKIFY"
+    else
+        echo "Using installed websockify at $WEBSOCKIFY"
+    fi
+fi
+
+echo "Starting webserver and WebSockets proxy on port ${PORT}"
+#${HERE}/websockify --web ${WEB} ${CERT:+--cert ${CERT}} ${PORT} ${VNC_DEST} &
+${WEBSOCKIFY} ${SYSLOG_ARG} ${SSLONLY} --web ${WEB} ${CERT:+--cert ${CERT}} ${KEY:+--key ${KEY}} ${PORT} ${VNC_DEST} ${HEARTBEAT_ARG} ${IDLETIMEOUT_ARG} ${RECORD_ARG} ${TIMEOUT_ARG} &
+proxy_pid="$!"
+sleep 1
+if ! ps -p ${proxy_pid} >/dev/null; then
+    proxy_pid=
+    echo "Failed to start WebSockets proxy"
+    exit 1
+fi
+
+echo -e "\n\nNavigate to this URL:\n"
+if [ "x$SSLONLY" == "x" ]; then
+    echo -e "    http://$(hostname):${PORT}/vnc.html?host=$(hostname)&port=${PORT}\n"
+else
+    echo -e "    https://$(hostname):${PORT}/vnc.html?host=$(hostname)&port=${PORT}\n"
+fi
+
+echo -e "Press Ctrl-C to exit\n\n"
+
+wait ${proxy_pid}
diff --git a/app/src/main/assets/novnc/utils/u2x11 b/app/src/main/assets/novnc/utils/u2x11
new file mode 100755
index 00000000..fd3e4ba8
--- /dev/null
+++ b/app/src/main/assets/novnc/utils/u2x11
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+#
+# Convert "U+..." commented entries in /usr/include/X11/keysymdef.h
+# into JavaScript for use by noVNC.  Note this is likely to produce
+# a few duplicate properties with clashing values, that will need
+# resolving manually.
+#
+# Colin Dean <colin@xvpsource.org>
+#
+
+regex="^#define[ \t]+XK_[A-Za-z0-9_]+[ \t]+0x([0-9a-fA-F]+)[ \t]+\/\*[ \t]+U\+([0-9a-fA-F]+)[ \t]+[^*]+.[ \t]+\*\/[ \t]*$"
+echo "unicodeTable = {"
+while read line; do
+    if echo "${line}" | egrep -qs "${regex}"; then
+
+        x11=$(echo "${line}" | sed -r "s/${regex}/\1/")
+        vnc=$(echo "${line}" | sed -r "s/${regex}/\2/")
+	
+	if echo "${vnc}" | egrep -qs "^00[2-9A-F][0-9A-F]$"; then
+	    : # skip ISO Latin-1 (U+0020 to U+00FF) as 1-to-1 mapping
+	else
+	    # note 1-to-1 is possible (e.g. for Euro symbol, U+20AC)
+	    echo "    0x${vnc} : 0x${x11},"
+	fi
+    fi
+done < /usr/include/X11/keysymdef.h | uniq
+echo "};"
+
diff --git a/app/src/main/assets/novnc/utils/use_require.js b/app/src/main/assets/novnc/utils/use_require.js
new file mode 100755
index 00000000..aeba49d9
--- /dev/null
+++ b/app/src/main/assets/novnc/utils/use_require.js
@@ -0,0 +1,140 @@
+#!/usr/bin/env node
+
+const path = require('path');
+const program = require('commander');
+const fs = require('fs');
+const fse = require('fs-extra');
+const babel = require('@babel/core');
+
+program
+    .option('-m, --with-source-maps [type]', 'output source maps when not generating a bundled app (type may be empty for external source maps, inline for inline source maps, or both) ')
+    .option('--clean', 'clear the lib folder before building')
+    .parse(process.argv);
+
+// the various important paths
+const paths = {
+    main: path.resolve(__dirname, '..'),
+    core: path.resolve(__dirname, '..', 'core'),
+    vendor: path.resolve(__dirname, '..', 'vendor'),
+    libDirBase: path.resolve(__dirname, '..', 'lib'),
+};
+
+// util.promisify requires Node.js 8.x, so we have our own
+function promisify(original) {
+    return function promiseWrap() {
+        const args = Array.prototype.slice.call(arguments);
+        return new Promise((resolve, reject) => {
+            original.apply(this, args.concat((err, value) => {
+                if (err) return reject(err);
+                resolve(value);
+            }));
+        });
+    };
+}
+
+const writeFile = promisify(fs.writeFile);
+
+const readdir = promisify(fs.readdir);
+const lstat = promisify(fs.lstat);
+
+const ensureDir = promisify(fse.ensureDir);
+
+const babelTransformFile = promisify(babel.transformFile);
+
+// walkDir *recursively* walks directories trees,
+// calling the callback for all normal files found.
+function walkDir(basePath, cb, filter) {
+    return readdir(basePath)
+        .then((files) => {
+            const paths = files.map(filename => path.join(basePath, filename));
+            return Promise.all(paths.map(filepath => lstat(filepath)
+                .then((stats) => {
+                    if (filter !== undefined && !filter(filepath, stats)) return;
+
+                    if (stats.isSymbolicLink()) return;
+                    if (stats.isFile()) return cb(filepath);
+                    if (stats.isDirectory()) return walkDir(filepath, cb, filter);
+                })));
+        });
+}
+
+function makeLibFiles(sourceMaps) {
+    // NB: we need to make a copy of babelOpts, since babel sets some defaults on it
+    const babelOpts = () => ({
+        plugins: [],
+        presets: [
+            [ '@babel/preset-env',
+              { modules: 'commonjs' } ]
+        ],
+        ast: false,
+        sourceMaps: sourceMaps,
+    });
+
+    fse.ensureDirSync(paths.libDirBase);
+
+    const outFiles = [];
+
+    const handleDir = (vendorRewrite, inPathBase, filename) => Promise.resolve()
+        .then(() => {
+            const outPath = path.join(paths.libDirBase, path.relative(inPathBase, filename));
+
+            if (path.extname(filename) !== '.js') {
+                return;  // skip non-javascript files
+            }
+            return Promise.resolve()
+                .then(() => ensureDir(path.dirname(outPath)))
+                .then(() => {
+                    const opts = babelOpts();
+            // Adjust for the fact that we move the core files relative
+            // to the vendor directory
+                    if (vendorRewrite) {
+                        opts.plugins.push(["import-redirect",
+                                           {"root": paths.libDirBase,
+                                            "redirect": { "vendor/(.+)": "./vendor/$1"}}]);
+                    }
+
+                    return babelTransformFile(filename, opts)
+                        .then((res) => {
+                            console.log(`Writing ${outPath}`);
+                            const {map} = res;
+                            let {code} = res;
+                            if (sourceMaps === true) {
+                    // append URL for external source map
+                                code += `\n//# sourceMappingURL=${path.basename(outPath)}.map\n`;
+                            }
+                            outFiles.push(`${outPath}`);
+                            return writeFile(outPath, code)
+                                .then(() => {
+                                    if (sourceMaps === true || sourceMaps === 'both') {
+                                        console.log(`  and ${outPath}.map`);
+                                        outFiles.push(`${outPath}.map`);
+                                        return writeFile(`${outPath}.map`, JSON.stringify(map));
+                                    }
+                                });
+                        });
+                });
+        });
+
+    Promise.resolve()
+        .then(() => {
+            const handler = handleDir.bind(null, false, paths.main);
+            return walkDir(paths.vendor, handler);
+        })
+        .then(() => {
+            const handler = handleDir.bind(null, true, paths.core);
+            return walkDir(paths.core, handler);
+        })
+        .catch((err) => {
+            console.error(`Failure converting modules: ${err}`);
+            process.exit(1);
+        });
+}
+
+let options = program.opts();
+
+if (options.clean) {
+    console.log(`Removing ${paths.libDirBase}`);
+    fse.removeSync(paths.libDirBase);
+}
+
+makeLibFiles(options.withSourceMaps);
diff --git a/app/src/main/assets/novnc/utils/validate b/app/src/main/assets/novnc/utils/validate
new file mode 100755
index 00000000..a6b5507d
--- /dev/null
+++ b/app/src/main/assets/novnc/utils/validate
@@ -0,0 +1,45 @@
+#!/bin/bash
+
+set -e
+
+RET=0
+
+OUT=`mktemp`
+
+for fn in "$@"; do
+	echo "Validating $fn..."
+	echo
+
+	case $fn in
+		*.html)
+			type="text/html"
+			;;
+		*.css)
+			type="text/css"
+			;;
+		*)
+			echo "Unknown format!"
+			echo
+			RET=1
+			continue
+			;;
+	esac
+
+	curl --silent \
+		--header "Content-Type: ${type}; charset=utf-8" \
+		--data-binary @${fn} \
+		https://validator.w3.org/nu/?out=text > $OUT
+	cat $OUT
+	echo
+
+	# We don't fail the check for warnings as some warnings are
+	# not relevant for us, and we don't currently have a way to
+	# ignore just those
+	if grep -q -s -E "^Error:" $OUT; then
+		RET=1
+	fi
+done
+
+rm $OUT
+
+exit $RET
diff --git a/app/src/main/assets/novnc/vendor/pako/LICENSE b/app/src/main/assets/novnc/vendor/pako/LICENSE
new file mode 100644
index 00000000..d082ae32
--- /dev/null
+++ b/app/src/main/assets/novnc/vendor/pako/LICENSE
@@ -0,0 +1,21 @@
+(The MIT License)
+
+Copyright (C) 2014-2016 by Vitaly Puzrin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/app/src/main/assets/novnc/vendor/pako/README.md b/app/src/main/assets/novnc/vendor/pako/README.md
new file mode 100644
index 00000000..755df643
--- /dev/null
+++ b/app/src/main/assets/novnc/vendor/pako/README.md
@@ -0,0 +1,6 @@
+This is an ES6-modules-compatible version of
+https://github.com/nodeca/pako, based on pako version 1.0.3.
+
+It's more-or-less a direct translation of the original, with unused parts
+removed, and the dynamic support for non-typed arrays removed (since ES6
+modules don't work well with dynamic exports).
diff --git a/app/src/main/assets/novnc/vendor/pako/lib/utils/common.js b/app/src/main/assets/novnc/vendor/pako/lib/utils/common.js
new file mode 100644
index 00000000..576fd59d
--- /dev/null
+++ b/app/src/main/assets/novnc/vendor/pako/lib/utils/common.js
@@ -0,0 +1,45 @@
+// reduce buffer size, avoiding mem copy
+export function shrinkBuf (buf, size) {
+  if (buf.length === size) { return buf; }
+  if (buf.subarray) { return buf.subarray(0, size); }
+  buf.length = size;
+  return buf;
+};
+
+
+export function arraySet (dest, src, src_offs, len, dest_offs) {
+  if (src.subarray && dest.subarray) {
+    dest.set(src.subarray(src_offs, src_offs + len), dest_offs);
+    return;
+  }
+  // Fallback to ordinary array
+  for (var i = 0; i < len; i++) {
+    dest[dest_offs + i] = src[src_offs + i];
+  }
+}
+
+// Join array of chunks to single array.
+export function flattenChunks (chunks) {
+  var i, l, len, pos, chunk, result;
+
+  // calculate data length
+  len = 0;
+  for (i = 0, l = chunks.length; i < l; i++) {
+    len += chunks[i].length;
+  }
+
+  // join chunks
+  result = new Uint8Array(len);
+  pos = 0;
+  for (i = 0, l = chunks.length; i < l; i++) {
+    chunk = chunks[i];
+    result.set(chunk, pos);
+    pos += chunk.length;
+  }
+
+  return result;
+}
+
+export var Buf8  = Uint8Array;
+export var Buf16 = Uint16Array;
+export var Buf32 = Int32Array;
diff --git a/app/src/main/assets/novnc/vendor/pako/lib/zlib/adler32.js b/app/src/main/assets/novnc/vendor/pako/lib/zlib/adler32.js
new file mode 100644
index 00000000..058a534a
--- /dev/null
+++ b/app/src/main/assets/novnc/vendor/pako/lib/zlib/adler32.js
@@ -0,0 +1,27 @@
+// Note: adler32 takes 12% for level 0 and 2% for level 6.
+// It doesn't worth to make additional optimizationa as in original.
+// Small size is preferable.
+
+export default function adler32(adler, buf, len, pos) {
+  var s1 = (adler & 0xffff) |0,
+      s2 = ((adler >>> 16) & 0xffff) |0,
+      n = 0;
+
+  while (len !== 0) {
+    // Set limit ~ twice less than 5552, to keep
+    // s2 in 31-bits, because we force signed ints.
+    // in other case %= will fail.
+    n = len > 2000 ? 2000 : len;
+    len -= n;
+
+    do {
+      s1 = (s1 + buf[pos++]) |0;
+      s2 = (s2 + s1) |0;
+    } while (--n);
+
+    s1 %= 65521;
+    s2 %= 65521;
+  }
+
+  return (s1 | (s2 << 16)) |0;
+}
diff --git a/app/src/main/assets/novnc/vendor/pako/lib/zlib/constants.js b/app/src/main/assets/novnc/vendor/pako/lib/zlib/constants.js
new file mode 100644
index 00000000..7d80502d
--- /dev/null
+++ b/app/src/main/assets/novnc/vendor/pako/lib/zlib/constants.js
@@ -0,0 +1,47 @@
+export default {
+
+  /* Allowed flush values; see deflate() and inflate() below for details */
+  Z_NO_FLUSH:         0,
+  Z_PARTIAL_FLUSH:    1,
+  Z_SYNC_FLUSH:       2,
+  Z_FULL_FLUSH:       3,
+  Z_FINISH:           4,
+  Z_BLOCK:            5,
+  Z_TREES:            6,
+
+  /* Return codes for the compression/decompression functions. Negative values
+  * are errors, positive values are used for special but normal events.
+  */
+  Z_OK:               0,
+  Z_STREAM_END:       1,
+  Z_NEED_DICT:        2,
+  Z_ERRNO:           -1,
+  Z_STREAM_ERROR:    -2,
+  Z_DATA_ERROR:      -3,
+  //Z_MEM_ERROR:     -4,
+  Z_BUF_ERROR:       -5,
+  //Z_VERSION_ERROR: -6,
+
+  /* compression levels */
+  Z_NO_COMPRESSION:         0,
+  Z_BEST_SPEED:             1,
+  Z_BEST_COMPRESSION:       9,
+  Z_DEFAULT_COMPRESSION:   -1,
+
+
+  Z_FILTERED:               1,
+  Z_HUFFMAN_ONLY:           2,
+  Z_RLE:                    3,
+  Z_FIXED:                  4,
+  Z_DEFAULT_STRATEGY:       0,
+
+  /* Possible values of the data_type field (though see inflate()) */
+  Z_BINARY:                 0,
+  Z_TEXT:                   1,
+  //Z_ASCII:                1, // = Z_TEXT (deprecated)
+  Z_UNKNOWN:                2,
+
+  /* The deflate compression method */
+  Z_DEFLATED:               8
+  //Z_NULL:                 null // Use -1 or null inline, depending on var type
+};
diff --git a/app/src/main/assets/novnc/vendor/pako/lib/zlib/crc32.js b/app/src/main/assets/novnc/vendor/pako/lib/zlib/crc32.js
new file mode 100644
index 00000000..611ffb29
--- /dev/null
+++ b/app/src/main/assets/novnc/vendor/pako/lib/zlib/crc32.js
@@ -0,0 +1,36 @@
+// Note: we can't get significant speed boost here.
+// So write code to minimize size - no pregenerated tables
+// and array tools dependencies.
+
+
+// Use ordinary array, since untyped makes no boost here
+export default function makeTable() {
+  var c, table = [];
+
+  for (var n = 0; n < 256; n++) {
+    c = n;
+    for (var k = 0; k < 8; k++) {
+      c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
+    }
+    table[n] = c;
+  }
+
+  return table;
+}
+
+// Create table on load. Just 255 signed longs. Not a problem.
+var crcTable = makeTable();
+
+
+function crc32(crc, buf, len, pos) {
+  var t = crcTable,
+      end = pos + len;
+
+  crc ^= -1;
+
+  for (var i = pos; i < end; i++) {
+    crc = (crc >>> 8) ^ t[(crc ^ buf[i]) & 0xFF];
+  }
+
+  return (crc ^ (-1)); // >>> 0;
+}
diff --git a/app/src/main/assets/novnc/vendor/pako/lib/zlib/deflate.js b/app/src/main/assets/novnc/vendor/pako/lib/zlib/deflate.js
new file mode 100644
index 00000000..c3a5ba49
--- /dev/null
+++ b/app/src/main/assets/novnc/vendor/pako/lib/zlib/deflate.js
@@ -0,0 +1,1846 @@
+import * as utils from "../utils/common.js";
+import * as trees from "./trees.js";
+import adler32 from "./adler32.js";
+import crc32 from "./crc32.js";
+import msg from "./messages.js";
+
+/* Public constants ==========================================================*/
+/* ===========================================================================*/
+
+
+/* Allowed flush values; see deflate() and inflate() below for details */
+export const Z_NO_FLUSH      = 0;
+export const Z_PARTIAL_FLUSH = 1;
+//export const Z_SYNC_FLUSH    = 2;
+export const Z_FULL_FLUSH    = 3;
+export const Z_FINISH        = 4;
+export const Z_BLOCK         = 5;
+//export const Z_TREES         = 6;
+
+
+/* Return codes for the compression/decompression functions. Negative values
+ * are errors, positive values are used for special but normal events.
+ */
+export const Z_OK            = 0;
+export const Z_STREAM_END    = 1;
+//export const Z_NEED_DICT     = 2;
+//export const Z_ERRNO         = -1;
+export const Z_STREAM_ERROR  = -2;
+export const Z_DATA_ERROR    = -3;
+//export const Z_MEM_ERROR     = -4;
+export const Z_BUF_ERROR     = -5;
+//export const Z_VERSION_ERROR = -6;
+
+
+/* compression levels */
+//export const Z_NO_COMPRESSION      = 0;
+//export const Z_BEST_SPEED          = 1;
+//export const Z_BEST_COMPRESSION    = 9;
+export const Z_DEFAULT_COMPRESSION = -1;
+
+
+export const Z_FILTERED            = 1;
+export const Z_HUFFMAN_ONLY        = 2;
+export const Z_RLE                 = 3;
+export const Z_FIXED               = 4;
+export const Z_DEFAULT_STRATEGY    = 0;
+
+/* Possible values of the data_type field (though see inflate()) */
+//export const Z_BINARY              = 0;
+//export const Z_TEXT                = 1;
+//export const Z_ASCII               = 1; // = Z_TEXT
+export const Z_UNKNOWN             = 2;
+
+
+/* The deflate compression method */
+export const Z_DEFLATED  = 8;
+
+/*============================================================================*/
+
+
+var MAX_MEM_LEVEL = 9;
+/* Maximum value for memLevel in deflateInit2 */
+var MAX_WBITS = 15;
+/* 32K LZ77 window */
+var DEF_MEM_LEVEL = 8;
+
+
+var LENGTH_CODES  = 29;
+/* number of length codes, not counting the special END_BLOCK code */
+var LITERALS      = 256;
+/* number of literal bytes 0..255 */
+var L_CODES       = LITERALS + 1 + LENGTH_CODES;
+/* number of Literal or Length codes, including the END_BLOCK code */
+var D_CODES       = 30;
+/* number of distance codes */
+var BL_CODES      = 19;
+/* number of codes used to transfer the bit lengths */
+var HEAP_SIZE     = 2 * L_CODES + 1;
+/* maximum heap size */
+var MAX_BITS  = 15;
+/* All codes must not exceed MAX_BITS bits */
+
+var MIN_MATCH = 3;
+var MAX_MATCH = 258;
+var MIN_LOOKAHEAD = (MAX_MATCH + MIN_MATCH + 1);
+
+var PRESET_DICT = 0x20;
+
+var INIT_STATE = 42;
+var EXTRA_STATE = 69;
+var NAME_STATE = 73;
+var COMMENT_STATE = 91;
+var HCRC_STATE = 103;
+var BUSY_STATE = 113;
+var FINISH_STATE = 666;
+
+var BS_NEED_MORE      = 1; /* block not completed, need more input or more output */
+var BS_BLOCK_DONE     = 2; /* block flush performed */
+var BS_FINISH_STARTED = 3; /* finish started, need only more output at next deflate */
+var BS_FINISH_DONE    = 4; /* finish done, accept no more input or output */
+
+var OS_CODE = 0x03; // Unix :) . Don't detect, use this default.
+
+function err(strm, errorCode) {
+  strm.msg = msg[errorCode];
+  return errorCode;
+}
+
+function rank(f) {
+  return ((f) << 1) - ((f) > 4 ? 9 : 0);
+}
+
+function zero(buf) { var len = buf.length; while (--len >= 0) { buf[len] = 0; } }
+
+
+/* =========================================================================
+ * Flush as much pending output as possible. All deflate() output goes
+ * through this function so some applications may wish to modify it
+ * to avoid allocating a large strm->output buffer and copying into it.
+ * (See also read_buf()).
+ */
+function flush_pending(strm) {
+  var s = strm.state;
+
+  //_tr_flush_bits(s);
+  var len = s.pending;
+  if (len > strm.avail_out) {
+    len = strm.avail_out;
+  }
+  if (len === 0) { return; }
+
+  utils.arraySet(strm.output, s.pending_buf, s.pending_out, len, strm.next_out);
+  strm.next_out += len;
+  s.pending_out += len;
+  strm.total_out += len;
+  strm.avail_out -= len;
+  s.pending -= len;
+  if (s.pending === 0) {
+    s.pending_out = 0;
+  }
+}
+
+
+function flush_block_only(s, last) {
+  trees._tr_flush_block(s, (s.block_start >= 0 ? s.block_start : -1), s.strstart - s.block_start, last);
+  s.block_start = s.strstart;
+  flush_pending(s.strm);
+}
+
+
+function put_byte(s, b) {
+  s.pending_buf[s.pending++] = b;
+}
+
+
+/* =========================================================================
+ * Put a short in the pending buffer. The 16-bit value is put in MSB order.
+ * IN assertion: the stream state is correct and there is enough room in
+ * pending_buf.
+ */
+function putShortMSB(s, b) {
+//  put_byte(s, (Byte)(b >> 8));
+//  put_byte(s, (Byte)(b & 0xff));
+  s.pending_buf[s.pending++] = (b >>> 8) & 0xff;
+  s.pending_buf[s.pending++] = b & 0xff;
+}
+
+
+/* ===========================================================================
+ * Read a new buffer from the current input stream, update the adler32
+ * and total number of bytes read.  All deflate() input goes through
+ * this function so some applications may wish to modify it to avoid
+ * allocating a large strm->input buffer and copying from it.
+ * (See also flush_pending()).
+ */
+function read_buf(strm, buf, start, size) {
+  var len = strm.avail_in;
+
+  if (len > size) { len = size; }
+  if (len === 0) { return 0; }
+
+  strm.avail_in -= len;
+
+  // zmemcpy(buf, strm->next_in, len);
+  utils.arraySet(buf, strm.input, strm.next_in, len, start);
+  if (strm.state.wrap === 1) {
+    strm.adler = adler32(strm.adler, buf, len, start);
+  }
+
+  else if (strm.state.wrap === 2) {
+    strm.adler = crc32(strm.adler, buf, len, start);
+  }
+
+  strm.next_in += len;
+  strm.total_in += len;
+
+  return len;
+}
+
+
+/* ===========================================================================
+ * Set match_start to the longest match starting at the given string and
+ * return its length. Matches shorter or equal to prev_length are discarded,
+ * in which case the result is equal to prev_length and match_start is
+ * garbage.
+ * IN assertions: cur_match is the head of the hash chain for the current
+ *   string (strstart) and its distance is <= MAX_DIST, and prev_length >= 1
+ * OUT assertion: the match length is not greater than s->lookahead.
+ */
+function longest_match(s, cur_match) {
+  var chain_length = s.max_chain_length;      /* max hash chain length */
+  var scan = s.strstart; /* current string */
+  var match;                       /* matched string */
+  var len;                           /* length of current match */
+  var best_len = s.prev_length;              /* best match length so far */
+  var nice_match = s.nice_match;             /* stop if match long enough */
+  var limit = (s.strstart > (s.w_size - MIN_LOOKAHEAD)) ?
+      s.strstart - (s.w_size - MIN_LOOKAHEAD) : 0/*NIL*/;
+
+  var _win = s.window; // shortcut
+
+  var wmask = s.w_mask;
+  var prev  = s.prev;
+
+  /* Stop when cur_match becomes <= limit. To simplify the code,
+   * we prevent matches with the string of window index 0.
+   */
+
+  var strend = s.strstart + MAX_MATCH;
+  var scan_end1  = _win[scan + best_len - 1];
+  var scan_end   = _win[scan + best_len];
+
+  /* The code is optimized for HASH_BITS >= 8 and MAX_MATCH-2 multiple of 16.
+   * It is easy to get rid of this optimization if necessary.
+   */
+  // Assert(s->hash_bits >= 8 && MAX_MATCH == 258, "Code too clever");
+
+  /* Do not waste too much time if we already have a good match: */
+  if (s.prev_length >= s.good_match) {
+    chain_length >>= 2;
+  }
+  /* Do not look for matches beyond the end of the input. This is necessary
+   * to make deflate deterministic.
+   */
+  if (nice_match > s.lookahead) { nice_match = s.lookahead; }
+
+  // Assert((ulg)s->strstart <= s->window_size-MIN_LOOKAHEAD, "need lookahead");
+
+  do {
+    // Assert(cur_match < s->strstart, "no future");
+    match = cur_match;
+
+    /* Skip to next match if the match length cannot increase
+     * or if the match length is less than 2.  Note that the checks below
+     * for insufficient lookahead only occur occasionally for performance
+     * reasons.  Therefore uninitialized memory will be accessed, and
+     * conditional jumps will be made that depend on those values.
+     * However the length of the match is limited to the lookahead, so
+     * the output of deflate is not affected by the uninitialized values.
+     */
+
+    if (_win[match + best_len]     !== scan_end  ||
+        _win[match + best_len - 1] !== scan_end1 ||
+        _win[match]                !== _win[scan] ||
+        _win[++match]              !== _win[scan + 1]) {
+      continue;
+    }
+
+    /* The check at best_len-1 can be removed because it will be made
+     * again later. (This heuristic is not always a win.)
+     * It is not necessary to compare scan[2] and match[2] since they
+     * are always equal when the other bytes match, given that
+     * the hash keys are equal and that HASH_BITS >= 8.
+     */
+    scan += 2;
+    match++;
+    // Assert(*scan == *match, "match[2]?");
+
+    /* We check for insufficient lookahead only every 8th comparison;
+     * the 256th check will be made at strstart+258.
+     */
+    do {
+      // Do nothing
+    } while (_win[++scan] === _win[++match] && _win[++scan] === _win[++match] &&
+             _win[++scan] === _win[++match] && _win[++scan] === _win[++match] &&
+             _win[++scan] === _win[++match] && _win[++scan] === _win[++match] &&
+             _win[++scan] === _win[++match] && _win[++scan] === _win[++match] &&
+             scan < strend);
+
+    // Assert(scan <= s->window+(unsigned)(s->window_size-1), "wild scan");
+
+    len = MAX_MATCH - (strend - scan);
+    scan = strend - MAX_MATCH;
+
+    if (len > best_len) {
+      s.match_start = cur_match;
+      best_len = len;
+      if (len >= nice_match) {
+        break;
+      }
+      scan_end1  = _win[scan + best_len - 1];
+      scan_end   = _win[scan + best_len];
+    }
+  } while ((cur_match = prev[cur_match & wmask]) > limit && --chain_length !== 0);
+
+  if (best_len <= s.lookahead) {
+    return best_len;
+  }
+  return s.lookahead;
+}
+
+
+/* ===========================================================================
+ * Fill the window when the lookahead becomes insufficient.
+ * Updates strstart and lookahead.
+ *
+ * IN assertion: lookahead < MIN_LOOKAHEAD
+ * OUT assertions: strstart <= window_size-MIN_LOOKAHEAD
+ *    At least one byte has been read, or avail_in == 0; reads are
+ *    performed for at least two bytes (required for the zip translate_eol
+ *    option -- not supported here).
+ */
+function fill_window(s) {
+  var _w_size = s.w_size;
+  var p, n, m, more, str;
+
+  //Assert(s->lookahead < MIN_LOOKAHEAD, "already enough lookahead");
+
+  do {
+    more = s.window_size - s.lookahead - s.strstart;
+
+    // JS ints have 32 bit, block below not needed
+    /* Deal with !@#$% 64K limit: */
+    //if (sizeof(int) <= 2) {
+    //    if (more == 0 && s->strstart == 0 && s->lookahead == 0) {
+    //        more = wsize;
+    //
+    //  } else if (more == (unsigned)(-1)) {
+    //        /* Very unlikely, but possible on 16 bit machine if
+    //         * strstart == 0 && lookahead == 1 (input done a byte at time)
+    //         */
+    //        more--;
+    //    }
+    //}
+
+
+    /* If the window is almost full and there is insufficient lookahead,
+     * move the upper half to the lower one to make room in the upper half.
+     */
+    if (s.strstart >= _w_size + (_w_size - MIN_LOOKAHEAD)) {
+
+      utils.arraySet(s.window, s.window, _w_size, _w_size, 0);
+      s.match_start -= _w_size;
+      s.strstart -= _w_size;
+      /* we now have strstart >= MAX_DIST */
+      s.block_start -= _w_size;
+
+      /* Slide the hash table (could be avoided with 32 bit values
+       at the expense of memory usage). We slide even when level == 0
+       to keep the hash table consistent if we switch back to level > 0
+       later. (Using level 0 permanently is not an optimal usage of
+       zlib, so we don't care about this pathological case.)
+       */
+
+      n = s.hash_size;
+      p = n;
+      do {
+        m = s.head[--p];
+        s.head[p] = (m >= _w_size ? m - _w_size : 0);
+      } while (--n);
+
+      n = _w_size;
+      p = n;
+      do {
+        m = s.prev[--p];
+        s.prev[p] = (m >= _w_size ? m - _w_size : 0);
+        /* If n is not on any hash chain, prev[n] is garbage but
+         * its value will never be used.
+         */
+      } while (--n);
+
+      more += _w_size;
+    }
+    if (s.strm.avail_in === 0) {
+      break;
+    }
+
+    /* If there was no sliding:
+     *    strstart <= WSIZE+MAX_DIST-1 && lookahead <= MIN_LOOKAHEAD - 1 &&
+     *    more == window_size - lookahead - strstart
+     * => more >= window_size - (MIN_LOOKAHEAD-1 + WSIZE + MAX_DIST-1)
+     * => more >= window_size - 2*WSIZE + 2
+     * In the BIG_MEM or MMAP case (not yet supported),
+     *   window_size == input_size + MIN_LOOKAHEAD  &&
+     *   strstart + s->lookahead <= input_size => more >= MIN_LOOKAHEAD.
+     * Otherwise, window_size == 2*WSIZE so more >= 2.
+     * If there was sliding, more >= WSIZE. So in all cases, more >= 2.
+     */
+    //Assert(more >= 2, "more < 2");
+    n = read_buf(s.strm, s.window, s.strstart + s.lookahead, more);
+    s.lookahead += n;
+
+    /* Initialize the hash value now that we have some input: */
+    if (s.lookahead + s.insert >= MIN_MATCH) {
+      str = s.strstart - s.insert;
+      s.ins_h = s.window[str];
+
+      /* UPDATE_HASH(s, s->ins_h, s->window[str + 1]); */
+      s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[str + 1]) & s.hash_mask;
+//#if MIN_MATCH != 3
+//        Call update_hash() MIN_MATCH-3 more times
+//#endif
+      while (s.insert) {
+        /* UPDATE_HASH(s, s->ins_h, s->window[str + MIN_MATCH-1]); */
+        s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[str + MIN_MATCH - 1]) & s.hash_mask;
+
+        s.prev[str & s.w_mask] = s.head[s.ins_h];
+        s.head[s.ins_h] = str;
+        str++;
+        s.insert--;
+        if (s.lookahead + s.insert < MIN_MATCH) {
+          break;
+        }
+      }
+    }
+    /* If the whole input has less than MIN_MATCH bytes, ins_h is garbage,
+     * but this is not important since only literal bytes will be emitted.
+     */
+
+  } while (s.lookahead < MIN_LOOKAHEAD && s.strm.avail_in !== 0);
+
+  /* If the WIN_INIT bytes after the end of the current data have never been
+   * written, then zero those bytes in order to avoid memory check reports of
+   * the use of uninitialized (or uninitialised as Julian writes) bytes by
+   * the longest match routines.  Update the high water mark for the next
+   * time through here.  WIN_INIT is set to MAX_MATCH since the longest match
+   * routines allow scanning to strstart + MAX_MATCH, ignoring lookahead.
+   */
+//  if (s.high_water < s.window_size) {
+//    var curr = s.strstart + s.lookahead;
+//    var init = 0;
+//
+//    if (s.high_water < curr) {
+//      /* Previous high water mark below current data -- zero WIN_INIT
+//       * bytes or up to end of window, whichever is less.
+//       */
+//      init = s.window_size - curr;
+//      if (init > WIN_INIT)
+//        init = WIN_INIT;
+//      zmemzero(s->window + curr, (unsigned)init);
+//      s->high_water = curr + init;
+//    }
+//    else if (s->high_water < (ulg)curr + WIN_INIT) {
+//      /* High water mark at or above current data, but below current data
+//       * plus WIN_INIT -- zero out to current data plus WIN_INIT, or up
+//       * to end of window, whichever is less.
+//       */
+//      init = (ulg)curr + WIN_INIT - s->high_water;
+//      if (init > s->window_size - s->high_water)
+//        init = s->window_size - s->high_water;
+//      zmemzero(s->window + s->high_water, (unsigned)init);
+//      s->high_water += init;
+//    }
+//  }
+//
+//  Assert((ulg)s->strstart <= s->window_size - MIN_LOOKAHEAD,
+//    "not enough room for search");
+}
+
+/* ===========================================================================
+ * Copy without compression as much as possible from the input stream, return
+ * the current block state.
+ * This function does not insert new strings in the dictionary since
+ * uncompressible data is probably not useful. This function is used
+ * only for the level=0 compression option.
+ * NOTE: this function should be optimized to avoid extra copying from
+ * window to pending_buf.
+ */
+function deflate_stored(s, flush) {
+  /* Stored blocks are limited to 0xffff bytes, pending_buf is limited
+   * to pending_buf_size, and each stored block has a 5 byte header:
+   */
+  var max_block_size = 0xffff;
+
+  if (max_block_size > s.pending_buf_size - 5) {
+    max_block_size = s.pending_buf_size - 5;
+  }
+
+  /* Copy as much as possible from input to output: */
+  for (;;) {
+    /* Fill the window as much as possible: */
+    if (s.lookahead <= 1) {
+
+      //Assert(s->strstart < s->w_size+MAX_DIST(s) ||
+      //  s->block_start >= (long)s->w_size, "slide too late");
+//      if (!(s.strstart < s.w_size + (s.w_size - MIN_LOOKAHEAD) ||
+//        s.block_start >= s.w_size)) {
+//        throw  new Error("slide too late");
+//      }
+
+      fill_window(s);
+      if (s.lookahead === 0 && flush === Z_NO_FLUSH) {
+        return BS_NEED_MORE;
+      }
+
+      if (s.lookahead === 0) {
+        break;
+      }
+      /* flush the current block */
+    }
+    //Assert(s->block_start >= 0L, "block gone");
+//    if (s.block_start < 0) throw new Error("block gone");
+
+    s.strstart += s.lookahead;
+    s.lookahead = 0;
+
+    /* Emit a stored block if pending_buf will be full: */
+    var max_start = s.block_start + max_block_size;
+
+    if (s.strstart === 0 || s.strstart >= max_start) {
+      /* strstart == 0 is possible when wraparound on 16-bit machine */
+      s.lookahead = s.strstart - max_start;
+      s.strstart = max_start;
+      /*** FLUSH_BLOCK(s, 0); ***/
+      flush_block_only(s, false);
+      if (s.strm.avail_out === 0) {
+        return BS_NEED_MORE;
+      }
+      /***/
+
+
+    }
+    /* Flush if we may have to slide, otherwise block_start may become
+     * negative and the data will be gone:
+     */
+    if (s.strstart - s.block_start >= (s.w_size - MIN_LOOKAHEAD)) {
+      /*** FLUSH_BLOCK(s, 0); ***/
+      flush_block_only(s, false);
+      if (s.strm.avail_out === 0) {
+        return BS_NEED_MORE;
+      }
+      /***/
+    }
+  }
+
+  s.insert = 0;
+
+  if (flush === Z_FINISH) {
+    /*** FLUSH_BLOCK(s, 1); ***/
+    flush_block_only(s, true);
+    if (s.strm.avail_out === 0) {
+      return BS_FINISH_STARTED;
+    }
+    /***/
+    return BS_FINISH_DONE;
+  }
+
+  if (s.strstart > s.block_start) {
+    /*** FLUSH_BLOCK(s, 0); ***/
+    flush_block_only(s, false);
+    if (s.strm.avail_out === 0) {
+      return BS_NEED_MORE;
+    }
+    /***/
+  }
+
+  return BS_NEED_MORE;
+}
+
+/* ===========================================================================
+ * Compress as much as possible from the input stream, return the current
+ * block state.
+ * This function does not perform lazy evaluation of matches and inserts
+ * new strings in the dictionary only for unmatched strings or for short
+ * matches. It is used only for the fast compression options.
+ */
+function deflate_fast(s, flush) {
+  var hash_head;        /* head of the hash chain */
+  var bflush;           /* set if current block must be flushed */
+
+  for (;;) {
+    /* Make sure that we always have enough lookahead, except
+     * at the end of the input file. We need MAX_MATCH bytes
+     * for the next match, plus MIN_MATCH bytes to insert the
+     * string following the next match.
+     */
+    if (s.lookahead < MIN_LOOKAHEAD) {
+      fill_window(s);
+      if (s.lookahead < MIN_LOOKAHEAD && flush === Z_NO_FLUSH) {
+        return BS_NEED_MORE;
+      }
+      if (s.lookahead === 0) {
+        break; /* flush the current block */
+      }
+    }
+
+    /* Insert the string window[strstart .. strstart+2] in the
+     * dictionary, and set hash_head to the head of the hash chain:
+     */
+    hash_head = 0/*NIL*/;
+    if (s.lookahead >= MIN_MATCH) {
+      /*** INSERT_STRING(s, s.strstart, hash_head); ***/
+      s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[s.strstart + MIN_MATCH - 1]) & s.hash_mask;
+      hash_head = s.prev[s.strstart & s.w_mask] = s.head[s.ins_h];
+      s.head[s.ins_h] = s.strstart;
+      /***/
+    }
+
+    /* Find the longest match, discarding those <= prev_length.
+     * At this point we have always match_length < MIN_MATCH
+     */
+    if (hash_head !== 0/*NIL*/ && ((s.strstart - hash_head) <= (s.w_size - MIN_LOOKAHEAD))) {
+      /* To simplify the code, we prevent matches with the string
+       * of window index 0 (in particular we have to avoid a match
+       * of the string with itself at the start of the input file).
+       */
+      s.match_length = longest_match(s, hash_head);
+      /* longest_match() sets match_start */
+    }
+    if (s.match_length >= MIN_MATCH) {
+      // check_match(s, s.strstart, s.match_start, s.match_length); // for debug only
+
+      /*** _tr_tally_dist(s, s.strstart - s.match_start,
+                     s.match_length - MIN_MATCH, bflush); ***/
+      bflush = trees._tr_tally(s, s.strstart - s.match_start, s.match_length - MIN_MATCH);
+
+      s.lookahead -= s.match_length;
+
+      /* Insert new strings in the hash table only if the match length
+       * is not too large. This saves time but degrades compression.
+       */
+      if (s.match_length <= s.max_lazy_match/*max_insert_length*/ && s.lookahead >= MIN_MATCH) {
+        s.match_length--; /* string at strstart already in table */
+        do {
+          s.strstart++;
+          /*** INSERT_STRING(s, s.strstart, hash_head); ***/
+          s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[s.strstart + MIN_MATCH - 1]) & s.hash_mask;
+          hash_head = s.prev[s.strstart & s.w_mask] = s.head[s.ins_h];
+          s.head[s.ins_h] = s.strstart;
+          /***/
+          /* strstart never exceeds WSIZE-MAX_MATCH, so there are
+           * always MIN_MATCH bytes ahead.
+           */
+        } while (--s.match_length !== 0);
+        s.strstart++;
+      } else
+      {
+        s.strstart += s.match_length;
+        s.match_length = 0;
+        s.ins_h = s.window[s.strstart];
+        /* UPDATE_HASH(s, s.ins_h, s.window[s.strstart+1]); */
+        s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[s.strstart + 1]) & s.hash_mask;
+
+//#if MIN_MATCH != 3
+//                Call UPDATE_HASH() MIN_MATCH-3 more times
+//#endif
+        /* If lookahead < MIN_MATCH, ins_h is garbage, but it does not
+         * matter since it will be recomputed at next deflate call.
+         */
+      }
+    } else {
+      /* No match, output a literal byte */
+      //Tracevv((stderr,"%c", s.window[s.strstart]));
+      /*** _tr_tally_lit(s, s.window[s.strstart], bflush); ***/
+      bflush = trees._tr_tally(s, 0, s.window[s.strstart]);
+
+      s.lookahead--;
+      s.strstart++;
+    }
+    if (bflush) {
+      /*** FLUSH_BLOCK(s, 0); ***/
+      flush_block_only(s, false);
+      if (s.strm.avail_out === 0) {
+        return BS_NEED_MORE;
+      }
+      /***/
+    }
+  }
+  s.insert = ((s.strstart < (MIN_MATCH - 1)) ? s.strstart : MIN_MATCH - 1);
+  if (flush === Z_FINISH) {
+    /*** FLUSH_BLOCK(s, 1); ***/
+    flush_block_only(s, true);
+    if (s.strm.avail_out === 0) {
+      return BS_FINISH_STARTED;
+    }
+    /***/
+    return BS_FINISH_DONE;
+  }
+  if (s.last_lit) {
+    /*** FLUSH_BLOCK(s, 0); ***/
+    flush_block_only(s, false);
+    if (s.strm.avail_out === 0) {
+      return BS_NEED_MORE;
+    }
+    /***/
+  }
+  return BS_BLOCK_DONE;
+}
+
+/* ===========================================================================
+ * Same as above, but achieves better compression. We use a lazy
+ * evaluation for matches: a match is finally adopted only if there is
+ * no better match at the next window position.
+ */
+function deflate_slow(s, flush) {
+  var hash_head;          /* head of hash chain */
+  var bflush;              /* set if current block must be flushed */
+
+  var max_insert;
+
+  /* Process the input block. */
+  for (;;) {
+    /* Make sure that we always have enough lookahead, except
+     * at the end of the input file. We need MAX_MATCH bytes
+     * for the next match, plus MIN_MATCH bytes to insert the
+     * string following the next match.
+     */
+    if (s.lookahead < MIN_LOOKAHEAD) {
+      fill_window(s);
+      if (s.lookahead < MIN_LOOKAHEAD && flush === Z_NO_FLUSH) {
+        return BS_NEED_MORE;
+      }
+      if (s.lookahead === 0) { break; } /* flush the current block */
+    }
+
+    /* Insert the string window[strstart .. strstart+2] in the
+     * dictionary, and set hash_head to the head of the hash chain:
+     */
+    hash_head = 0/*NIL*/;
+    if (s.lookahead >= MIN_MATCH) {
+      /*** INSERT_STRING(s, s.strstart, hash_head); ***/
+      s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[s.strstart + MIN_MATCH - 1]) & s.hash_mask;
+      hash_head = s.prev[s.strstart & s.w_mask] = s.head[s.ins_h];
+      s.head[s.ins_h] = s.strstart;
+      /***/
+    }
+
+    /* Find the longest match, discarding those <= prev_length.
+     */
+    s.prev_length = s.match_length;
+    s.prev_match = s.match_start;
+    s.match_length = MIN_MATCH - 1;
+
+    if (hash_head !== 0/*NIL*/ && s.prev_length < s.max_lazy_match &&
+        s.strstart - hash_head <= (s.w_size - MIN_LOOKAHEAD)/*MAX_DIST(s)*/) {
+      /* To simplify the code, we prevent matches with the string
+       * of window index 0 (in particular we have to avoid a match
+       * of the string with itself at the start of the input file).
+       */
+      s.match_length = longest_match(s, hash_head);
+      /* longest_match() sets match_start */
+
+      if (s.match_length <= 5 &&
+         (s.strategy === Z_FILTERED || (s.match_length === MIN_MATCH && s.strstart - s.match_start > 4096/*TOO_FAR*/))) {
+
+        /* If prev_match is also MIN_MATCH, match_start is garbage
+         * but we will ignore the current match anyway.
+         */
+        s.match_length = MIN_MATCH - 1;
+      }
+    }
+    /* If there was a match at the previous step and the current
+     * match is not better, output the previous match:
+     */
+    if (s.prev_length >= MIN_MATCH && s.match_length <= s.prev_length) {
+      max_insert = s.strstart + s.lookahead - MIN_MATCH;
+      /* Do not insert strings in hash table beyond this. */
+
+      //check_match(s, s.strstart-1, s.prev_match, s.prev_length);
+
+      /***_tr_tally_dist(s, s.strstart - 1 - s.prev_match,
+                     s.prev_length - MIN_MATCH, bflush);***/
+      bflush = trees._tr_tally(s, s.strstart - 1 - s.prev_match, s.prev_length - MIN_MATCH);
+      /* Insert in hash table all strings up to the end of the match.
+       * strstart-1 and strstart are already inserted. If there is not
+       * enough lookahead, the last two strings are not inserted in
+       * the hash table.
+       */
+      s.lookahead -= s.prev_length - 1;
+      s.prev_length -= 2;
+      do {
+        if (++s.strstart <= max_insert) {
+          /*** INSERT_STRING(s, s.strstart, hash_head); ***/
+          s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[s.strstart + MIN_MATCH - 1]) & s.hash_mask;
+          hash_head = s.prev[s.strstart & s.w_mask] = s.head[s.ins_h];
+          s.head[s.ins_h] = s.strstart;
+          /***/
+        }
+      } while (--s.prev_length !== 0);
+      s.match_available = 0;
+      s.match_length = MIN_MATCH - 1;
+      s.strstart++;
+
+      if (bflush) {
+        /*** FLUSH_BLOCK(s, 0); ***/
+        flush_block_only(s, false);
+        if (s.strm.avail_out === 0) {
+          return BS_NEED_MORE;
+        }
+        /***/
+      }
+
+    } else if (s.match_available) {
+      /* If there was no match at the previous position, output a
+       * single literal. If there was a match but the current match
+       * is longer, truncate the previous match to a single literal.
+       */
+      //Tracevv((stderr,"%c", s->window[s->strstart-1]));
+      /*** _tr_tally_lit(s, s.window[s.strstart-1], bflush); ***/
+      bflush = trees._tr_tally(s, 0, s.window[s.strstart - 1]);
+
+      if (bflush) {
+        /*** FLUSH_BLOCK_ONLY(s, 0) ***/
+        flush_block_only(s, false);
+        /***/
+      }
+      s.strstart++;
+      s.lookahead--;
+      if (s.strm.avail_out === 0) {
+        return BS_NEED_MORE;
+      }
+    } else {
+      /* There is no previous match to compare with, wait for
+       * the next step to decide.
+       */
+      s.match_available = 1;
+      s.strstart++;
+      s.lookahead--;
+    }
+  }
+  //Assert (flush != Z_NO_FLUSH, "no flush?");
+  if (s.match_available) {
+    //Tracevv((stderr,"%c", s->window[s->strstart-1]));
+    /*** _tr_tally_lit(s, s.window[s.strstart-1], bflush); ***/
+    bflush = trees._tr_tally(s, 0, s.window[s.strstart - 1]);
+
+    s.match_available = 0;
+  }
+  s.insert = s.strstart < MIN_MATCH - 1 ? s.strstart : MIN_MATCH - 1;
+  if (flush === Z_FINISH) {
+    /*** FLUSH_BLOCK(s, 1); ***/
+    flush_block_only(s, true);
+    if (s.strm.avail_out === 0) {
+      return BS_FINISH_STARTED;
+    }
+    /***/
+    return BS_FINISH_DONE;
+  }
+  if (s.last_lit) {
+    /*** FLUSH_BLOCK(s, 0); ***/
+    flush_block_only(s, false);
+    if (s.strm.avail_out === 0) {
+      return BS_NEED_MORE;
+    }
+    /***/
+  }
+
+  return BS_BLOCK_DONE;
+}
+
+
+/* ===========================================================================
+ * For Z_RLE, simply look for runs of bytes, generate matches only of distance
+ * one.  Do not maintain a hash table.  (It will be regenerated if this run of
+ * deflate switches away from Z_RLE.)
+ */
+function deflate_rle(s, flush) {
+  var bflush;            /* set if current block must be flushed */
+  var prev;              /* byte at distance one to match */
+  var scan, strend;      /* scan goes up to strend for length of run */
+
+  var _win = s.window;
+
+  for (;;) {
+    /* Make sure that we always have enough lookahead, except
+     * at the end of the input file. We need MAX_MATCH bytes
+     * for the longest run, plus one for the unrolled loop.
+     */
+    if (s.lookahead <= MAX_MATCH) {
+      fill_window(s);
+      if (s.lookahead <= MAX_MATCH && flush === Z_NO_FLUSH) {
+        return BS_NEED_MORE;
+      }
+      if (s.lookahead === 0) { break; } /* flush the current block */
+    }
+
+    /* See how many times the previous byte repeats */
+    s.match_length = 0;
+    if (s.lookahead >= MIN_MATCH && s.strstart > 0) {
+      scan = s.strstart - 1;
+      prev = _win[scan];
+      if (prev === _win[++scan] && prev === _win[++scan] && prev === _win[++scan]) {
+        strend = s.strstart + MAX_MATCH;
+        do {
+          // Do nothing
+        } while (prev === _win[++scan] && prev === _win[++scan] &&
+                 prev === _win[++scan] && prev === _win[++scan] &&
+                 prev === _win[++scan] && prev === _win[++scan] &&
+                 prev === _win[++scan] && prev === _win[++scan] &&
+                 scan < strend);
+        s.match_length = MAX_MATCH - (strend - scan);
+        if (s.match_length > s.lookahead) {
+          s.match_length = s.lookahead;
+        }
+      }
+      //Assert(scan <= s->window+(uInt)(s->window_size-1), "wild scan");
+    }
+
+    /* Emit match if have run of MIN_MATCH or longer, else emit literal */
+    if (s.match_length >= MIN_MATCH) {
+      //check_match(s, s.strstart, s.strstart - 1, s.match_length);
+
+      /*** _tr_tally_dist(s, 1, s.match_length - MIN_MATCH, bflush); ***/
+      bflush = trees._tr_tally(s, 1, s.match_length - MIN_MATCH);
+
+      s.lookahead -= s.match_length;
+      s.strstart += s.match_length;
+      s.match_length = 0;
+    } else {
+      /* No match, output a literal byte */
+      //Tracevv((stderr,"%c", s->window[s->strstart]));
+      /*** _tr_tally_lit(s, s.window[s.strstart], bflush); ***/
+      bflush = trees._tr_tally(s, 0, s.window[s.strstart]);
+
+      s.lookahead--;
+      s.strstart++;
+    }
+    if (bflush) {
+      /*** FLUSH_BLOCK(s, 0); ***/
+      flush_block_only(s, false);
+      if (s.strm.avail_out === 0) {
+        return BS_NEED_MORE;
+      }
+      /***/
+    }
+  }
+  s.insert = 0;
+  if (flush === Z_FINISH) {
+    /*** FLUSH_BLOCK(s, 1); ***/
+    flush_block_only(s, true);
+    if (s.strm.avail_out === 0) {
+      return BS_FINISH_STARTED;
+    }
+    /***/
+    return BS_FINISH_DONE;
+  }
+  if (s.last_lit) {
+    /*** FLUSH_BLOCK(s, 0); ***/
+    flush_block_only(s, false);
+    if (s.strm.avail_out === 0) {
+      return BS_NEED_MORE;
+    }
+    /***/
+  }
+  return BS_BLOCK_DONE;
+}
+
+/* ===========================================================================
+ * For Z_HUFFMAN_ONLY, do not look for matches.  Do not maintain a hash table.
+ * (It will be regenerated if this run of deflate switches away from Huffman.)
+ */
+function deflate_huff(s, flush) {
+  var bflush;             /* set if current block must be flushed */
+
+  for (;;) {
+    /* Make sure that we have a literal to write. */
+    if (s.lookahead === 0) {
+      fill_window(s);
+      if (s.lookahead === 0) {
+        if (flush === Z_NO_FLUSH) {
+          return BS_NEED_MORE;
+        }
+        break;      /* flush the current block */
+      }
+    }
+
+    /* Output a literal byte */
+    s.match_length = 0;
+    //Tracevv((stderr,"%c", s->window[s->strstart]));
+    /*** _tr_tally_lit(s, s.window[s.strstart], bflush); ***/
+    bflush = trees._tr_tally(s, 0, s.window[s.strstart]);
+    s.lookahead--;
+    s.strstart++;
+    if (bflush) {
+      /*** FLUSH_BLOCK(s, 0); ***/
+      flush_block_only(s, false);
+      if (s.strm.avail_out === 0) {
+        return BS_NEED_MORE;
+      }
+      /***/
+    }
+  }
+  s.insert = 0;
+  if (flush === Z_FINISH) {
+    /*** FLUSH_BLOCK(s, 1); ***/
+    flush_block_only(s, true);
+    if (s.strm.avail_out === 0) {
+      return BS_FINISH_STARTED;
+    }
+    /***/
+    return BS_FINISH_DONE;
+  }
+  if (s.last_lit) {
+    /*** FLUSH_BLOCK(s, 0); ***/
+    flush_block_only(s, false);
+    if (s.strm.avail_out === 0) {
+      return BS_NEED_MORE;
+    }
+    /***/
+  }
+  return BS_BLOCK_DONE;
+}
+
+/* Values for max_lazy_match, good_match and max_chain_length, depending on
+ * the desired pack level (0..9). The values given below have been tuned to
+ * exclude worst case performance for pathological files. Better values may be
+ * found for specific files.
+ */
+function Config(good_length, max_lazy, nice_length, max_chain, func) {
+  this.good_length = good_length;
+  this.max_lazy = max_lazy;
+  this.nice_length = nice_length;
+  this.max_chain = max_chain;
+  this.func = func;
+}
+
+var configuration_table;
+
+configuration_table = [
+  /*      good lazy nice chain */
+  new Config(0, 0, 0, 0, deflate_stored),          /* 0 store only */
+  new Config(4, 4, 8, 4, deflate_fast),            /* 1 max speed, no lazy matches */
+  new Config(4, 5, 16, 8, deflate_fast),           /* 2 */
+  new Config(4, 6, 32, 32, deflate_fast),          /* 3 */
+
+  new Config(4, 4, 16, 16, deflate_slow),          /* 4 lazy matches */
+  new Config(8, 16, 32, 32, deflate_slow),         /* 5 */
+  new Config(8, 16, 128, 128, deflate_slow),       /* 6 */
+  new Config(8, 32, 128, 256, deflate_slow),       /* 7 */
+  new Config(32, 128, 258, 1024, deflate_slow),    /* 8 */
+  new Config(32, 258, 258, 4096, deflate_slow)     /* 9 max compression */
+];
+
+
+/* ===========================================================================
+ * Initialize the "longest match" routines for a new zlib stream
+ */
+function lm_init(s) {
+  s.window_size = 2 * s.w_size;
+
+  /*** CLEAR_HASH(s); ***/
+  zero(s.head); // Fill with NIL (= 0);
+
+  /* Set the default configuration parameters:
+   */
+  s.max_lazy_match = configuration_table[s.level].max_lazy;
+  s.good_match = configuration_table[s.level].good_length;
+  s.nice_match = configuration_table[s.level].nice_length;
+  s.max_chain_length = configuration_table[s.level].max_chain;
+
+  s.strstart = 0;
+  s.block_start = 0;
+  s.lookahead = 0;
+  s.insert = 0;
+  s.match_length = s.prev_length = MIN_MATCH - 1;
+  s.match_available = 0;
+  s.ins_h = 0;
+}
+
+
+function DeflateState() {
+  this.strm = null;            /* pointer back to this zlib stream */
+  this.status = 0;            /* as the name implies */
+  this.pending_buf = null;      /* output still pending */
+  this.pending_buf_size = 0;  /* size of pending_buf */
+  this.pending_out = 0;       /* next pending byte to output to the stream */
+  this.pending = 0;           /* nb of bytes in the pending buffer */
+  this.wrap = 0;              /* bit 0 true for zlib, bit 1 true for gzip */
+  this.gzhead = null;         /* gzip header information to write */
+  this.gzindex = 0;           /* where in extra, name, or comment */
+  this.method = Z_DEFLATED; /* can only be DEFLATED */
+  this.last_flush = -1;   /* value of flush param for previous deflate call */
+
+  this.w_size = 0;  /* LZ77 window size (32K by default) */
+  this.w_bits = 0;  /* log2(w_size)  (8..16) */
+  this.w_mask = 0;  /* w_size - 1 */
+
+  this.window = null;
+  /* Sliding window. Input bytes are read into the second half of the window,
+   * and move to the first half later to keep a dictionary of at least wSize
+   * bytes. With this organization, matches are limited to a distance of
+   * wSize-MAX_MATCH bytes, but this ensures that IO is always
+   * performed with a length multiple of the block size.
+   */
+
+  this.window_size = 0;
+  /* Actual size of window: 2*wSize, except when the user input buffer
+   * is directly used as sliding window.
+   */
+
+  this.prev = null;
+  /* Link to older string with same hash index. To limit the size of this
+   * array to 64K, this link is maintained only for the last 32K strings.
+   * An index in this array is thus a window index modulo 32K.
+   */
+
+  this.head = null;   /* Heads of the hash chains or NIL. */
+
+  this.ins_h = 0;       /* hash index of string to be inserted */
+  this.hash_size = 0;   /* number of elements in hash table */
+  this.hash_bits = 0;   /* log2(hash_size) */
+  this.hash_mask = 0;   /* hash_size-1 */
+
+  this.hash_shift = 0;
+  /* Number of bits by which ins_h must be shifted at each input
+   * step. It must be such that after MIN_MATCH steps, the oldest
+   * byte no longer takes part in the hash key, that is:
+   *   hash_shift * MIN_MATCH >= hash_bits
+   */
+
+  this.block_start = 0;
+  /* Window position at the beginning of the current output block. Gets
+   * negative when the window is moved backwards.
+   */
+
+  this.match_length = 0;      /* length of best match */
+  this.prev_match = 0;        /* previous match */
+  this.match_available = 0;   /* set if previous match exists */
+  this.strstart = 0;          /* start of string to insert */
+  this.match_start = 0;       /* start of matching string */
+  this.lookahead = 0;         /* number of valid bytes ahead in window */
+
+  this.prev_length = 0;
+  /* Length of the best match at previous step. Matches not greater than this
+   * are discarded. This is used in the lazy match evaluation.
+   */
+
+  this.max_chain_length = 0;
+  /* To speed up deflation, hash chains are never searched beyond this
+   * length.  A higher limit improves compression ratio but degrades the
+   * speed.
+   */
+
+  this.max_lazy_match = 0;
+  /* Attempt to find a better match only when the current match is strictly
+   * smaller than this value. This mechanism is used only for compression
+   * levels >= 4.
+   */
+  // That's alias to max_lazy_match, don't use directly
+  //this.max_insert_length = 0;
+  /* Insert new strings in the hash table only if the match length is not
+   * greater than this length. This saves time but degrades compression.
+   * max_insert_length is used only for compression levels <= 3.
+   */
+
+  this.level = 0;     /* compression level (1..9) */
+  this.strategy = 0;  /* favor or force Huffman coding*/
+
+  this.good_match = 0;
+  /* Use a faster search when the previous match is longer than this */
+
+  this.nice_match = 0; /* Stop searching when current match exceeds this */
+
+              /* used by trees.c: */
+
+  /* Didn't use ct_data typedef below to suppress compiler warning */
+
+  // struct ct_data_s dyn_ltree[HEAP_SIZE];   /* literal and length tree */
+  // struct ct_data_s dyn_dtree[2*D_CODES+1]; /* distance tree */
+  // struct ct_data_s bl_tree[2*BL_CODES+1];  /* Huffman tree for bit lengths */
+
+  // Use flat array of DOUBLE size, with interleaved fata,
+  // because JS does not support effective
+  this.dyn_ltree  = new utils.Buf16(HEAP_SIZE * 2);
+  this.dyn_dtree  = new utils.Buf16((2 * D_CODES + 1) * 2);
+  this.bl_tree    = new utils.Buf16((2 * BL_CODES + 1) * 2);
+  zero(this.dyn_ltree);
+  zero(this.dyn_dtree);
+  zero(this.bl_tree);
+
+  this.l_desc   = null;         /* desc. for literal tree */
+  this.d_desc   = null;         /* desc. for distance tree */
+  this.bl_desc  = null;         /* desc. for bit length tree */
+
+  //ush bl_count[MAX_BITS+1];
+  this.bl_count = new utils.Buf16(MAX_BITS + 1);
+  /* number of codes at each bit length for an optimal tree */
+
+  //int heap[2*L_CODES+1];      /* heap used to build the Huffman trees */
+  this.heap = new utils.Buf16(2 * L_CODES + 1);  /* heap used to build the Huffman trees */
+  zero(this.heap);
+
+  this.heap_len = 0;               /* number of elements in the heap */
+  this.heap_max = 0;               /* element of largest frequency */
+  /* The sons of heap[n] are heap[2*n] and heap[2*n+1]. heap[0] is not used.
+   * The same heap array is used to build all trees.
+   */
+
+  this.depth = new utils.Buf16(2 * L_CODES + 1); //uch depth[2*L_CODES+1];
+  zero(this.depth);
+  /* Depth of each subtree used as tie breaker for trees of equal frequency
+   */
+
+  this.l_buf = 0;          /* buffer index for literals or lengths */
+
+  this.lit_bufsize = 0;
+  /* Size of match buffer for literals/lengths.  There are 4 reasons for
+   * limiting lit_bufsize to 64K:
+   *   - frequencies can be kept in 16 bit counters
+   *   - if compression is not successful for the first block, all input
+   *     data is still in the window so we can still emit a stored block even
+   *     when input comes from standard input.  (This can also be done for
+   *     all blocks if lit_bufsize is not greater than 32K.)
+   *   - if compression is not successful for a file smaller than 64K, we can
+   *     even emit a stored file instead of a stored block (saving 5 bytes).
+   *     This is applicable only for zip (not gzip or zlib).
+   *   - creating new Huffman trees less frequently may not provide fast
+   *     adaptation to changes in the input data statistics. (Take for
+   *     example a binary file with poorly compressible code followed by
+   *     a highly compressible string table.) Smaller buffer sizes give
+   *     fast adaptation but have of course the overhead of transmitting
+   *     trees more frequently.
+   *   - I can't count above 4
+   */
+
+  this.last_lit = 0;      /* running index in l_buf */
+
+  this.d_buf = 0;
+  /* Buffer index for distances. To simplify the code, d_buf and l_buf have
+   * the same number of elements. To use different lengths, an extra flag
+   * array would be necessary.
+   */
+
+  this.opt_len = 0;       /* bit length of current block with optimal trees */
+  this.static_len = 0;    /* bit length of current block with static trees */
+  this.matches = 0;       /* number of string matches in current block */
+  this.insert = 0;        /* bytes at end of window left to insert */
+
+
+  this.bi_buf = 0;
+  /* Output buffer. bits are inserted starting at the bottom (least
+   * significant bits).
+   */
+  this.bi_valid = 0;
+  /* Number of valid bits in bi_buf.  All bits above the last valid bit
+   * are always zero.
+   */
+
+  // Used for window memory init. We safely ignore it for JS. That makes
+  // sense only for pointers and memory check tools.
+  //this.high_water = 0;
+  /* High water mark offset in window for initialized bytes -- bytes above
+   * this are set to zero in order to avoid memory check warnings when
+   * longest match routines access bytes past the input.  This is then
+   * updated to the new high water mark.
+   */
+}
+
+
+function deflateResetKeep(strm) {
+  var s;
+
+  if (!strm || !strm.state) {
+    return err(strm, Z_STREAM_ERROR);
+  }
+
+  strm.total_in = strm.total_out = 0;
+  strm.data_type = Z_UNKNOWN;
+
+  s = strm.state;
+  s.pending = 0;
+  s.pending_out = 0;
+
+  if (s.wrap < 0) {
+    s.wrap = -s.wrap;
+    /* was made negative by deflate(..., Z_FINISH); */
+  }
+  s.status = (s.wrap ? INIT_STATE : BUSY_STATE);
+  strm.adler = (s.wrap === 2) ?
+    0  // crc32(0, Z_NULL, 0)
+  :
+    1; // adler32(0, Z_NULL, 0)
+  s.last_flush = Z_NO_FLUSH;
+  trees._tr_init(s);
+  return Z_OK;
+}
+
+
+function deflateReset(strm) {
+  var ret = deflateResetKeep(strm);
+  if (ret === Z_OK) {
+    lm_init(strm.state);
+  }
+  return ret;
+}
+
+
+function deflateSetHeader(strm, head) {
+  if (!strm || !strm.state) { return Z_STREAM_ERROR; }
+  if (strm.state.wrap !== 2) { return Z_STREAM_ERROR; }
+  strm.state.gzhead = head;
+  return Z_OK;
+}
+
+
+function deflateInit2(strm, level, method, windowBits, memLevel, strategy) {
+  if (!strm) { // === Z_NULL
+    return Z_STREAM_ERROR;
+  }
+  var wrap = 1;
+
+  if (level === Z_DEFAULT_COMPRESSION) {
+    level = 6;
+  }
+
+  if (windowBits < 0) { /* suppress zlib wrapper */
+    wrap = 0;
+    windowBits = -windowBits;
+  }
+
+  else if (windowBits > 15) {
+    wrap = 2;           /* write gzip wrapper instead */
+    windowBits -= 16;
+  }
+
+
+  if (memLevel < 1 || memLevel > MAX_MEM_LEVEL || method !== Z_DEFLATED ||
+    windowBits < 8 || windowBits > 15 || level < 0 || level > 9 ||
+    strategy < 0 || strategy > Z_FIXED) {
+    return err(strm, Z_STREAM_ERROR);
+  }
+
+
+  if (windowBits === 8) {
+    windowBits = 9;
+  }
+  /* until 256-byte window bug fixed */
+
+  var s = new DeflateState();
+
+  strm.state = s;
+  s.strm = strm;
+
+  s.wrap = wrap;
+  s.gzhead = null;
+  s.w_bits = windowBits;
+  s.w_size = 1 << s.w_bits;
+  s.w_mask = s.w_size - 1;
+
+  s.hash_bits = memLevel + 7;
+  s.hash_size = 1 << s.hash_bits;
+  s.hash_mask = s.hash_size - 1;
+  s.hash_shift = ~~((s.hash_bits + MIN_MATCH - 1) / MIN_MATCH);
+
+  s.window = new utils.Buf8(s.w_size * 2);
+  s.head = new utils.Buf16(s.hash_size);
+  s.prev = new utils.Buf16(s.w_size);
+
+  // Don't need mem init magic for JS.
+  //s.high_water = 0;  /* nothing written to s->window yet */
+
+  s.lit_bufsize = 1 << (memLevel + 6); /* 16K elements by default */
+
+  s.pending_buf_size = s.lit_bufsize * 4;
+
+  //overlay = (ushf *) ZALLOC(strm, s->lit_bufsize, sizeof(ush)+2);
+  //s->pending_buf = (uchf *) overlay;
+  s.pending_buf = new utils.Buf8(s.pending_buf_size);
+
+  // It is offset from `s.pending_buf` (size is `s.lit_bufsize * 2`)
+  //s->d_buf = overlay + s->lit_bufsize/sizeof(ush);
+  s.d_buf = 1 * s.lit_bufsize;
+
+  //s->l_buf = s->pending_buf + (1+sizeof(ush))*s->lit_bufsize;
+  s.l_buf = (1 + 2) * s.lit_bufsize;
+
+  s.level = level;
+  s.strategy = strategy;
+  s.method = method;
+
+  return deflateReset(strm);
+}
+
+function deflateInit(strm, level) {
+  return deflateInit2(strm, level, Z_DEFLATED, MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY);
+}
+
+
+function deflate(strm, flush) {
+  var old_flush, s;
+  var beg, val; // for gzip header write only
+
+  if (!strm || !strm.state ||
+    flush > Z_BLOCK || flush < 0) {
+    return strm ? err(strm, Z_STREAM_ERROR) : Z_STREAM_ERROR;
+  }
+
+  s = strm.state;
+
+  if (!strm.output ||
+      (!strm.input && strm.avail_in !== 0) ||
+      (s.status === FINISH_STATE && flush !== Z_FINISH)) {
+    return err(strm, (strm.avail_out === 0) ? Z_BUF_ERROR : Z_STREAM_ERROR);
+  }
+
+  s.strm = strm; /* just in case */
+  old_flush = s.last_flush;
+  s.last_flush = flush;
+
+  /* Write the header */
+  if (s.status === INIT_STATE) {
+
+    if (s.wrap === 2) { // GZIP header
+      strm.adler = 0;  //crc32(0L, Z_NULL, 0);
+      put_byte(s, 31);
+      put_byte(s, 139);
+      put_byte(s, 8);
+      if (!s.gzhead) { // s->gzhead == Z_NULL
+        put_byte(s, 0);
+        put_byte(s, 0);
+        put_byte(s, 0);
+        put_byte(s, 0);
+        put_byte(s, 0);
+        put_byte(s, s.level === 9 ? 2 :
+                    (s.strategy >= Z_HUFFMAN_ONLY || s.level < 2 ?
+                     4 : 0));
+        put_byte(s, OS_CODE);
+        s.status = BUSY_STATE;
+      }
+      else {
+        put_byte(s, (s.gzhead.text ? 1 : 0) +
+                    (s.gzhead.hcrc ? 2 : 0) +
+                    (!s.gzhead.extra ? 0 : 4) +
+                    (!s.gzhead.name ? 0 : 8) +
+                    (!s.gzhead.comment ? 0 : 16)
+                );
+        put_byte(s, s.gzhead.time & 0xff);
+        put_byte(s, (s.gzhead.time >> 8) & 0xff);
+        put_byte(s, (s.gzhead.time >> 16) & 0xff);
+        put_byte(s, (s.gzhead.time >> 24) & 0xff);
+        put_byte(s, s.level === 9 ? 2 :
+                    (s.strategy >= Z_HUFFMAN_ONLY || s.level < 2 ?
+                     4 : 0));
+        put_byte(s, s.gzhead.os & 0xff);
+        if (s.gzhead.extra && s.gzhead.extra.length) {
+          put_byte(s, s.gzhead.extra.length & 0xff);
+          put_byte(s, (s.gzhead.extra.length >> 8) & 0xff);
+        }
+        if (s.gzhead.hcrc) {
+          strm.adler = crc32(strm.adler, s.pending_buf, s.pending, 0);
+        }
+        s.gzindex = 0;
+        s.status = EXTRA_STATE;
+      }
+    }
+    else // DEFLATE header
+    {
+      var header = (Z_DEFLATED + ((s.w_bits - 8) << 4)) << 8;
+      var level_flags = -1;
+
+      if (s.strategy >= Z_HUFFMAN_ONLY || s.level < 2) {
+        level_flags = 0;
+      } else if (s.level < 6) {
+        level_flags = 1;
+      } else if (s.level === 6) {
+        level_flags = 2;
+      } else {
+        level_flags = 3;
+      }
+      header |= (level_flags << 6);
+      if (s.strstart !== 0) { header |= PRESET_DICT; }
+      header += 31 - (header % 31);
+
+      s.status = BUSY_STATE;
+      putShortMSB(s, header);
+
+      /* Save the adler32 of the preset dictionary: */
+      if (s.strstart !== 0) {
+        putShortMSB(s, strm.adler >>> 16);
+        putShortMSB(s, strm.adler & 0xffff);
+      }
+      strm.adler = 1; // adler32(0L, Z_NULL, 0);
+    }
+  }
+
+//#ifdef GZIP
+  if (s.status === EXTRA_STATE) {
+    if (s.gzhead.extra/* != Z_NULL*/) {
+      beg = s.pending;  /* start of bytes to update crc */
+
+      while (s.gzindex < (s.gzhead.extra.length & 0xffff)) {
+        if (s.pending === s.pending_buf_size) {
+          if (s.gzhead.hcrc && s.pending > beg) {
+            strm.adler = crc32(strm.adler, s.pending_buf, s.pending - beg, beg);
+          }
+          flush_pending(strm);
+          beg = s.pending;
+          if (s.pending === s.pending_buf_size) {
+            break;
+          }
+        }
+        put_byte(s, s.gzhead.extra[s.gzindex] & 0xff);
+        s.gzindex++;
+      }
+      if (s.gzhead.hcrc && s.pending > beg) {
+        strm.adler = crc32(strm.adler, s.pending_buf, s.pending - beg, beg);
+      }
+      if (s.gzindex === s.gzhead.extra.length) {
+        s.gzindex = 0;
+        s.status = NAME_STATE;
+      }
+    }
+    else {
+      s.status = NAME_STATE;
+    }
+  }
+  if (s.status === NAME_STATE) {
+    if (s.gzhead.name/* != Z_NULL*/) {
+      beg = s.pending;  /* start of bytes to update crc */
+      //int val;
+
+      do {
+        if (s.pending === s.pending_buf_size) {
+          if (s.gzhead.hcrc && s.pending > beg) {
+            strm.adler = crc32(strm.adler, s.pending_buf, s.pending - beg, beg);
+          }
+          flush_pending(strm);
+          beg = s.pending;
+          if (s.pending === s.pending_buf_size) {
+            val = 1;
+            break;
+          }
+        }
+        // JS specific: little magic to add zero terminator to end of string
+        if (s.gzindex < s.gzhead.name.length) {
+          val = s.gzhead.name.charCodeAt(s.gzindex++) & 0xff;
+        } else {
+          val = 0;
+        }
+        put_byte(s, val);
+      } while (val !== 0);
+
+      if (s.gzhead.hcrc && s.pending > beg) {
+        strm.adler = crc32(strm.adler, s.pending_buf, s.pending - beg, beg);
+      }
+      if (val === 0) {
+        s.gzindex = 0;
+        s.status = COMMENT_STATE;
+      }
+    }
+    else {
+      s.status = COMMENT_STATE;
+    }
+  }
+  if (s.status === COMMENT_STATE) {
+    if (s.gzhead.comment/* != Z_NULL*/) {
+      beg = s.pending;  /* start of bytes to update crc */
+      //int val;
+
+      do {
+        if (s.pending === s.pending_buf_size) {
+          if (s.gzhead.hcrc && s.pending > beg) {
+            strm.adler = crc32(strm.adler, s.pending_buf, s.pending - beg, beg);
+          }
+          flush_pending(strm);
+          beg = s.pending;
+          if (s.pending === s.pending_buf_size) {
+            val = 1;
+            break;
+          }
+        }
+        // JS specific: little magic to add zero terminator to end of string
+        if (s.gzindex < s.gzhead.comment.length) {
+          val = s.gzhead.comment.charCodeAt(s.gzindex++) & 0xff;
+        } else {
+          val = 0;
+        }
+        put_byte(s, val);
+      } while (val !== 0);
+
+      if (s.gzhead.hcrc && s.pending > beg) {
+        strm.adler = crc32(strm.adler, s.pending_buf, s.pending - beg, beg);
+      }
+      if (val === 0) {
+        s.status = HCRC_STATE;
+      }
+    }
+    else {
+      s.status = HCRC_STATE;
+    }
+  }
+  if (s.status === HCRC_STATE) {
+    if (s.gzhead.hcrc) {
+      if (s.pending + 2 > s.pending_buf_size) {
+        flush_pending(strm);
+      }
+      if (s.pending + 2 <= s.pending_buf_size) {
+        put_byte(s, strm.adler & 0xff);
+        put_byte(s, (strm.adler >> 8) & 0xff);
+        strm.adler = 0; //crc32(0L, Z_NULL, 0);
+        s.status = BUSY_STATE;
+      }
+    }
+    else {
+      s.status = BUSY_STATE;
+    }
+  }
+//#endif
+
+  /* Flush as much pending output as possible */
+  if (s.pending !== 0) {
+    flush_pending(strm);
+    if (strm.avail_out === 0) {
+      /* Since avail_out is 0, deflate will be called again with
+       * more output space, but possibly with both pending and
+       * avail_in equal to zero. There won't be anything to do,
+       * but this is not an error situation so make sure we
+       * return OK instead of BUF_ERROR at next call of deflate:
+       */
+      s.last_flush = -1;
+      return Z_OK;
+    }
+
+    /* Make sure there is something to do and avoid duplicate consecutive
+     * flushes. For repeated and useless calls with Z_FINISH, we keep
+     * returning Z_STREAM_END instead of Z_BUF_ERROR.
+     */
+  } else if (strm.avail_in === 0 && rank(flush) <= rank(old_flush) &&
+    flush !== Z_FINISH) {
+    return err(strm, Z_BUF_ERROR);
+  }
+
+  /* User must not provide more input after the first FINISH: */
+  if (s.status === FINISH_STATE && strm.avail_in !== 0) {
+    return err(strm, Z_BUF_ERROR);
+  }
+
+  /* Start a new block or continue the current one.
+   */
+  if (strm.avail_in !== 0 || s.lookahead !== 0 ||
+    (flush !== Z_NO_FLUSH && s.status !== FINISH_STATE)) {
+    var bstate = (s.strategy === Z_HUFFMAN_ONLY) ? deflate_huff(s, flush) :
+      (s.strategy === Z_RLE ? deflate_rle(s, flush) :
+        configuration_table[s.level].func(s, flush));
+
+    if (bstate === BS_FINISH_STARTED || bstate === BS_FINISH_DONE) {
+      s.status = FINISH_STATE;
+    }
+    if (bstate === BS_NEED_MORE || bstate === BS_FINISH_STARTED) {
+      if (strm.avail_out === 0) {
+        s.last_flush = -1;
+        /* avoid BUF_ERROR next call, see above */
+      }
+      return Z_OK;
+      /* If flush != Z_NO_FLUSH && avail_out == 0, the next call
+       * of deflate should use the same flush parameter to make sure
+       * that the flush is complete. So we don't have to output an
+       * empty block here, this will be done at next call. This also
+       * ensures that for a very small output buffer, we emit at most
+       * one empty block.
+       */
+    }
+    if (bstate === BS_BLOCK_DONE) {
+      if (flush === Z_PARTIAL_FLUSH) {
+        trees._tr_align(s);
+      }
+      else if (flush !== Z_BLOCK) { /* FULL_FLUSH or SYNC_FLUSH */
+
+        trees._tr_stored_block(s, 0, 0, false);
+        /* For a full flush, this empty block will be recognized
+         * as a special marker by inflate_sync().
+         */
+        if (flush === Z_FULL_FLUSH) {
+          /*** CLEAR_HASH(s); ***/             /* forget history */
+          zero(s.head); // Fill with NIL (= 0);
+
+          if (s.lookahead === 0) {
+            s.strstart = 0;
+            s.block_start = 0;
+            s.insert = 0;
+          }
+        }
+      }
+      flush_pending(strm);
+      if (strm.avail_out === 0) {
+        s.last_flush = -1; /* avoid BUF_ERROR at next call, see above */
+        return Z_OK;
+      }
+    }
+  }
+  //Assert(strm->avail_out > 0, "bug2");
+  //if (strm.avail_out <= 0) { throw new Error("bug2");}
+
+  if (flush !== Z_FINISH) { return Z_OK; }
+  if (s.wrap <= 0) { return Z_STREAM_END; }
+
+  /* Write the trailer */
+  if (s.wrap === 2) {
+    put_byte(s, strm.adler & 0xff);
+    put_byte(s, (strm.adler >> 8) & 0xff);
+    put_byte(s, (strm.adler >> 16) & 0xff);
+    put_byte(s, (strm.adler >> 24) & 0xff);
+    put_byte(s, strm.total_in & 0xff);
+    put_byte(s, (strm.total_in >> 8) & 0xff);
+    put_byte(s, (strm.total_in >> 16) & 0xff);
+    put_byte(s, (strm.total_in >> 24) & 0xff);
+  }
+  else
+  {
+    putShortMSB(s, strm.adler >>> 16);
+    putShortMSB(s, strm.adler & 0xffff);
+  }
+
+  flush_pending(strm);
+  /* If avail_out is zero, the application will call deflate again
+   * to flush the rest.
+   */
+  if (s.wrap > 0) { s.wrap = -s.wrap; }
+  /* write the trailer only once! */
+  return s.pending !== 0 ? Z_OK : Z_STREAM_END;
+}
+
+function deflateEnd(strm) {
+  var status;
+
+  if (!strm/*== Z_NULL*/ || !strm.state/*== Z_NULL*/) {
+    return Z_STREAM_ERROR;
+  }
+
+  status = strm.state.status;
+  if (status !== INIT_STATE &&
+    status !== EXTRA_STATE &&
+    status !== NAME_STATE &&
+    status !== COMMENT_STATE &&
+    status !== HCRC_STATE &&
+    status !== BUSY_STATE &&
+    status !== FINISH_STATE
+  ) {
+    return err(strm, Z_STREAM_ERROR);
+  }
+
+  strm.state = null;
+
+  return status === BUSY_STATE ? err(strm, Z_DATA_ERROR) : Z_OK;
+}
+
+
+/* =========================================================================
+ * Initializes the compression dictionary from the given byte
+ * sequence without producing any compressed output.
+ */
+function deflateSetDictionary(strm, dictionary) {
+  var dictLength = dictionary.length;
+
+  var s;
+  var str, n;
+  var wrap;
+  var avail;
+  var next;
+  var input;
+  var tmpDict;
+
+  if (!strm/*== Z_NULL*/ || !strm.state/*== Z_NULL*/) {
+    return Z_STREAM_ERROR;
+  }
+
+  s = strm.state;
+  wrap = s.wrap;
+
+  if (wrap === 2 || (wrap === 1 && s.status !== INIT_STATE) || s.lookahead) {
+    return Z_STREAM_ERROR;
+  }
+
+  /* when using zlib wrappers, compute Adler-32 for provided dictionary */
+  if (wrap === 1) {
+    /* adler32(strm->adler, dictionary, dictLength); */
+    strm.adler = adler32(strm.adler, dictionary, dictLength, 0);
+  }
+
+  s.wrap = 0;   /* avoid computing Adler-32 in read_buf */
+
+  /* if dictionary would fill window, just replace the history */
+  if (dictLength >= s.w_size) {
+    if (wrap === 0) {            /* already empty otherwise */
+      /*** CLEAR_HASH(s); ***/
+      zero(s.head); // Fill with NIL (= 0);
+      s.strstart = 0;
+      s.block_start = 0;
+      s.insert = 0;
+    }
+    /* use the tail */
+    // dictionary = dictionary.slice(dictLength - s.w_size);
+    tmpDict = new utils.Buf8(s.w_size);
+    utils.arraySet(tmpDict, dictionary, dictLength - s.w_size, s.w_size, 0);
+    dictionary = tmpDict;
+    dictLength = s.w_size;
+  }
+  /* insert dictionary into window and hash */
+  avail = strm.avail_in;
+  next = strm.next_in;
+  input = strm.input;
+  strm.avail_in = dictLength;
+  strm.next_in = 0;
+  strm.input = dictionary;
+  fill_window(s);
+  while (s.lookahead >= MIN_MATCH) {
+    str = s.strstart;
+    n = s.lookahead - (MIN_MATCH - 1);
+    do {
+      /* UPDATE_HASH(s, s->ins_h, s->window[str + MIN_MATCH-1]); */
+      s.ins_h = ((s.ins_h << s.hash_shift) ^ s.window[str + MIN_MATCH - 1]) & s.hash_mask;
+
+      s.prev[str & s.w_mask] = s.head[s.ins_h];
+
+      s.head[s.ins_h] = str;
+      str++;
+    } while (--n);
+    s.strstart = str;
+    s.lookahead = MIN_MATCH - 1;
+    fill_window(s);
+  }
+  s.strstart += s.lookahead;
+  s.block_start = s.strstart;
+  s.insert = s.lookahead;
+  s.lookahead = 0;
+  s.match_length = s.prev_length = MIN_MATCH - 1;
+  s.match_available = 0;
+  strm.next_in = next;
+  strm.input = input;
+  strm.avail_in = avail;
+  s.wrap = wrap;
+  return Z_OK;
+}
+
+
+export { deflateInit, deflateInit2, deflateReset, deflateResetKeep, deflateSetHeader, deflate, deflateEnd, deflateSetDictionary };
+export var deflateInfo = 'pako deflate (from Nodeca project)';
+
+/* Not implemented
+exports.deflateBound = deflateBound;
+exports.deflateCopy = deflateCopy;
+exports.deflateParams = deflateParams;
+exports.deflatePending = deflatePending;
+exports.deflatePrime = deflatePrime;
+exports.deflateTune = deflateTune;
+*/
diff --git a/app/src/main/assets/novnc/vendor/pako/lib/zlib/gzheader.js b/app/src/main/assets/novnc/vendor/pako/lib/zlib/gzheader.js
new file mode 100644
index 00000000..2ec586d3
--- /dev/null
+++ b/app/src/main/assets/novnc/vendor/pako/lib/zlib/gzheader.js
@@ -0,0 +1,35 @@
+export default function GZheader() {
+  /* true if compressed data believed to be text */
+  this.text       = 0;
+  /* modification time */
+  this.time       = 0;
+  /* extra flags (not used when writing a gzip file) */
+  this.xflags     = 0;
+  /* operating system */
+  this.os         = 0;
+  /* pointer to extra field or Z_NULL if none */
+  this.extra      = null;
+  /* extra field length (valid if extra != Z_NULL) */
+  this.extra_len  = 0; // Actually, we don't need it in JS,
+                       // but leave for few code modifications
+
+  //
+  // Setup limits is not necessary because in js we should not preallocate memory
+  // for inflate use constant limit in 65536 bytes
+  //
+
+  /* space at extra (only when reading header) */
+  // this.extra_max  = 0;
+  /* pointer to zero-terminated file name or Z_NULL */
+  this.name       = '';
+  /* space at name (only when reading header) */
+  // this.name_max   = 0;
+  /* pointer to zero-terminated comment or Z_NULL */
+  this.comment    = '';
+  /* space at comment (only when reading header) */
+  // this.comm_max   = 0;
+  /* true if there was or will be a header crc */
+  this.hcrc       = 0;
+  /* true when done reading gzip header (not used when writing a gzip file) */
+  this.done       = false;
+}
diff --git a/app/src/main/assets/novnc/vendor/pako/lib/zlib/inffast.js b/app/src/main/assets/novnc/vendor/pako/lib/zlib/inffast.js
new file mode 100644
index 00000000..889dcc7a
--- /dev/null
+++ b/app/src/main/assets/novnc/vendor/pako/lib/zlib/inffast.js
@@ -0,0 +1,324 @@
+// See state defs from inflate.js
+var BAD = 30;       /* got a data error -- remain here until reset */
+var TYPE = 12;      /* i: waiting for type bits, including last-flag bit */
+
+/*
+   Decode literal, length, and distance codes and write out the resulting
+   literal and match bytes until either not enough input or output is
+   available, an end-of-block is encountered, or a data error is encountered.
+   When large enough input and output buffers are supplied to inflate(), for
+   example, a 16K input buffer and a 64K output buffer, more than 95% of the
+   inflate execution time is spent in this routine.
+
+   Entry assumptions:
+
+        state.mode === LEN
+        strm.avail_in >= 6
+        strm.avail_out >= 258
+        start >= strm.avail_out
+        state.bits < 8
+
+   On return, state.mode is one of:
+
+        LEN -- ran out of enough output space or enough available input
+        TYPE -- reached end of block code, inflate() to interpret next block
+        BAD -- error in block data
+
+   Notes:
+
+    - The maximum input bits used by a length/distance pair is 15 bits for the
+      length code, 5 bits for the length extra, 15 bits for the distance code,
+      and 13 bits for the distance extra.  This totals 48 bits, or six bytes.
+      Therefore if strm.avail_in >= 6, then there is enough input to avoid
+      checking for available input while decoding.
+
+    - The maximum bytes that a single length/distance pair can output is 258
+      bytes, which is the maximum length that can be coded.  inflate_fast()
+      requires strm.avail_out >= 258 for each loop to avoid checking for
+      output space.
+ */
+export default function inflate_fast(strm, start) {
+  var state;
+  var _in;                    /* local strm.input */
+  var last;                   /* have enough input while in < last */
+  var _out;                   /* local strm.output */
+  var beg;                    /* inflate()'s initial strm.output */
+  var end;                    /* while out < end, enough space available */
+//#ifdef INFLATE_STRICT
+  var dmax;                   /* maximum distance from zlib header */
+//#endif
+  var wsize;                  /* window size or zero if not using window */
+  var whave;                  /* valid bytes in the window */
+  var wnext;                  /* window write index */
+  // Use `s_window` instead `window`, avoid conflict with instrumentation tools
+  var s_window;               /* allocated sliding window, if wsize != 0 */
+  var hold;                   /* local strm.hold */
+  var bits;                   /* local strm.bits */
+  var lcode;                  /* local strm.lencode */
+  var dcode;                  /* local strm.distcode */
+  var lmask;                  /* mask for first level of length codes */
+  var dmask;                  /* mask for first level of distance codes */
+  var here;                   /* retrieved table entry */
+  var op;                     /* code bits, operation, extra bits, or */
+                              /*  window position, window bytes to copy */
+  var len;                    /* match length, unused bytes */
+  var dist;                   /* match distance */
+  var from;                   /* where to copy match from */
+  var from_source;
+
+
+  var input, output; // JS specific, because we have no pointers
+
+  /* copy state to local variables */
+  state = strm.state;
+  //here = state.here;
+  _in = strm.next_in;
+  input = strm.input;
+  last = _in + (strm.avail_in - 5);
+  _out = strm.next_out;
+  output = strm.output;
+  beg = _out - (start - strm.avail_out);
+  end = _out + (strm.avail_out - 257);
+//#ifdef INFLATE_STRICT
+  dmax = state.dmax;
+//#endif
+  wsize = state.wsize;
+  whave = state.whave;
+  wnext = state.wnext;
+  s_window = state.window;
+  hold = state.hold;
+  bits = state.bits;
+  lcode = state.lencode;
+  dcode = state.distcode;
+  lmask = (1 << state.lenbits) - 1;
+  dmask = (1 << state.distbits) - 1;
+
+
+  /* decode literals and length/distances until end-of-block or not enough
+     input data or output space */
+
+  top:
+  do {
+    if (bits < 15) {
+      hold += input[_in++] << bits;
+      bits += 8;
+      hold += input[_in++] << bits;
+      bits += 8;
+    }
+
+    here = lcode[hold & lmask];
+
+    dolen:
+    for (;;) { // Goto emulation
+      op = here >>> 24/*here.bits*/;
+      hold >>>= op;
+      bits -= op;
+      op = (here >>> 16) & 0xff/*here.op*/;
+      if (op === 0) {                          /* literal */
+        //Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ?
+        //        "inflate:         literal '%c'\n" :
+        //        "inflate:         literal 0x%02x\n", here.val));
+        output[_out++] = here & 0xffff/*here.val*/;
+      }
+      else if (op & 16) {                     /* length base */
+        len = here & 0xffff/*here.val*/;
+        op &= 15;                           /* number of extra bits */
+        if (op) {
+          if (bits < op) {
+            hold += input[_in++] << bits;
+            bits += 8;
+          }
+          len += hold & ((1 << op) - 1);
+          hold >>>= op;
+          bits -= op;
+        }
+        //Tracevv((stderr, "inflate:         length %u\n", len));
+        if (bits < 15) {
+          hold += input[_in++] << bits;
+          bits += 8;
+          hold += input[_in++] << bits;
+          bits += 8;
+        }
+        here = dcode[hold & dmask];
+
+        dodist:
+        for (;;) { // goto emulation
+          op = here >>> 24/*here.bits*/;
+          hold >>>= op;
+          bits -= op;
+          op = (here >>> 16) & 0xff/*here.op*/;
+
+          if (op & 16) {                      /* distance base */
+            dist = here & 0xffff/*here.val*/;
+            op &= 15;                       /* number of extra bits */
+            if (bits < op) {
+              hold += input[_in++] << bits;
+              bits += 8;
+              if (bits < op) {
+                hold += input[_in++] << bits;
+                bits += 8;
+              }
+            }
+            dist += hold & ((1 << op) - 1);
+//#ifdef INFLATE_STRICT
+            if (dist > dmax) {
+              strm.msg = 'invalid distance too far back';
+              state.mode = BAD;
+              break top;
+            }
+//#endif
+            hold >>>= op;
+            bits -= op;
+            //Tracevv((stderr, "inflate:         distance %u\n", dist));
+            op = _out - beg;                /* max distance in output */
+            if (dist > op) {                /* see if copy from window */
+              op = dist - op;               /* distance back in window */
+              if (op > whave) {
+                if (state.sane) {
+                  strm.msg = 'invalid distance too far back';
+                  state.mode = BAD;
+                  break top;
+                }
+
+// (!) This block is disabled in zlib defailts,
+// don't enable it for binary compatibility
+//#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR
+//                if (len <= op - whave) {
+//                  do {
+//                    output[_out++] = 0;
+//                  } while (--len);
+//                  continue top;
+//                }
+//                len -= op - whave;
+//                do {
+//                  output[_out++] = 0;
+//                } while (--op > whave);
+//                if (op === 0) {
+//                  from = _out - dist;
+//                  do {
+//                    output[_out++] = output[from++];
+//                  } while (--len);
+//                  continue top;
+//                }
+//#endif
+              }
+              from = 0; // window index
+              from_source = s_window;
+              if (wnext === 0) {           /* very common case */
+                from += wsize - op;
+                if (op < len) {         /* some from window */
+                  len -= op;
+                  do {
+                    output[_out++] = s_window[from++];
+                  } while (--op);
+                  from = _out - dist;  /* rest from output */
+                  from_source = output;
+                }
+              }
+              else if (wnext < op) {      /* wrap around window */
+                from += wsize + wnext - op;
+                op -= wnext;
+                if (op < len) {         /* some from end of window */
+                  len -= op;
+                  do {
+                    output[_out++] = s_window[from++];
+                  } while (--op);
+                  from = 0;
+                  if (wnext < len) {  /* some from start of window */
+                    op = wnext;
+                    len -= op;
+                    do {
+                      output[_out++] = s_window[from++];
+                    } while (--op);
+                    from = _out - dist;      /* rest from output */
+                    from_source = output;
+                  }
+                }
+              }
+              else {                      /* contiguous in window */
+                from += wnext - op;
+                if (op < len) {         /* some from window */
+                  len -= op;
+                  do {
+                    output[_out++] = s_window[from++];
+                  } while (--op);
+                  from = _out - dist;  /* rest from output */
+                  from_source = output;
+                }
+              }
+              while (len > 2) {
+                output[_out++] = from_source[from++];
+                output[_out++] = from_source[from++];
+                output[_out++] = from_source[from++];
+                len -= 3;
+              }
+              if (len) {
+                output[_out++] = from_source[from++];
+                if (len > 1) {
+                  output[_out++] = from_source[from++];
+                }
+              }
+            }
+            else {
+              from = _out - dist;          /* copy direct from output */
+              do {                        /* minimum length is three */
+                output[_out++] = output[from++];
+                output[_out++] = output[from++];
+                output[_out++] = output[from++];
+                len -= 3;
+              } while (len > 2);
+              if (len) {
+                output[_out++] = output[from++];
+                if (len > 1) {
+                  output[_out++] = output[from++];
+                }
+              }
+            }
+          }
+          else if ((op & 64) === 0) {          /* 2nd level distance code */
+            here = dcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))];
+            continue dodist;
+          }
+          else {
+            strm.msg = 'invalid distance code';
+            state.mode = BAD;
+            break top;
+          }
+
+          break; // need to emulate goto via "continue"
+        }
+      }
+      else if ((op & 64) === 0) {              /* 2nd level length code */
+        here = lcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))];
+        continue dolen;
+      }
+      else if (op & 32) {                     /* end-of-block */
+        //Tracevv((stderr, "inflate:         end of block\n"));
+        state.mode = TYPE;
+        break top;
+      }
+      else {
+        strm.msg = 'invalid literal/length code';
+        state.mode = BAD;
+        break top;
+      }
+
+      break; // need to emulate goto via "continue"
+    }
+  } while (_in < last && _out < end);
+
+  /* return unused bytes (on entry, bits < 8, so in won't go too far back) */
+  len = bits >> 3;
+  _in -= len;
+  bits -= len << 3;
+  hold &= (1 << bits) - 1;
+
+  /* update state and return */
+  strm.next_in = _in;
+  strm.next_out = _out;
+  strm.avail_in = (_in < last ? 5 + (last - _in) : 5 - (_in - last));
+  strm.avail_out = (_out < end ? 257 + (end - _out) : 257 - (_out - end));
+  state.hold = hold;
+  state.bits = bits;
+  return;
+};
diff --git a/app/src/main/assets/novnc/vendor/pako/lib/zlib/inflate.js b/app/src/main/assets/novnc/vendor/pako/lib/zlib/inflate.js
new file mode 100644
index 00000000..1d2063bc
--- /dev/null
+++ b/app/src/main/assets/novnc/vendor/pako/lib/zlib/inflate.js
@@ -0,0 +1,1527 @@
+import * as utils from "../utils/common.js";
+import adler32 from "./adler32.js";
+import crc32 from "./crc32.js";
+import inflate_fast from "./inffast.js";
+import inflate_table from "./inftrees.js";
+
+var CODES = 0;
+var LENS = 1;
+var DISTS = 2;
+
+/* Public constants ==========================================================*/
+/* ===========================================================================*/
+
+
+/* Allowed flush values; see deflate() and inflate() below for details */
+//export const Z_NO_FLUSH      = 0;
+//export const Z_PARTIAL_FLUSH = 1;
+//export const Z_SYNC_FLUSH    = 2;
+//export const Z_FULL_FLUSH    = 3;
+export const Z_FINISH        = 4;
+export const Z_BLOCK         = 5;
+export const Z_TREES         = 6;
+
+
+/* Return codes for the compression/decompression functions. Negative values
+ * are errors, positive values are used for special but normal events.
+ */
+export const Z_OK            = 0;
+export const Z_STREAM_END    = 1;
+export const Z_NEED_DICT     = 2;
+//export const Z_ERRNO         = -1;
+export const Z_STREAM_ERROR  = -2;
+export const Z_DATA_ERROR    = -3;
+export const Z_MEM_ERROR     = -4;
+export const Z_BUF_ERROR     = -5;
+//export const Z_VERSION_ERROR = -6;
+
+/* The deflate compression method */
+export const Z_DEFLATED  = 8;
+
+
+/* STATES ====================================================================*/
+/* ===========================================================================*/
+
+
+var    HEAD = 1;       /* i: waiting for magic header */
+var    FLAGS = 2;      /* i: waiting for method and flags (gzip) */
+var    TIME = 3;       /* i: waiting for modification time (gzip) */
+var    OS = 4;         /* i: waiting for extra flags and operating system (gzip) */
+var    EXLEN = 5;      /* i: waiting for extra length (gzip) */
+var    EXTRA = 6;      /* i: waiting for extra bytes (gzip) */
+var    NAME = 7;       /* i: waiting for end of file name (gzip) */
+var    COMMENT = 8;    /* i: waiting for end of comment (gzip) */
+var    HCRC = 9;       /* i: waiting for header crc (gzip) */
+var    DICTID = 10;    /* i: waiting for dictionary check value */
+var    DICT = 11;      /* waiting for inflateSetDictionary() call */
+var        TYPE = 12;      /* i: waiting for type bits, including last-flag bit */
+var        TYPEDO = 13;    /* i: same, but skip check to exit inflate on new block */
+var        STORED = 14;    /* i: waiting for stored size (length and complement) */
+var        COPY_ = 15;     /* i/o: same as COPY below, but only first time in */
+var        COPY = 16;      /* i/o: waiting for input or output to copy stored block */
+var        TABLE = 17;     /* i: waiting for dynamic block table lengths */
+var        LENLENS = 18;   /* i: waiting for code length code lengths */
+var        CODELENS = 19;  /* i: waiting for length/lit and distance code lengths */
+var            LEN_ = 20;      /* i: same as LEN below, but only first time in */
+var            LEN = 21;       /* i: waiting for length/lit/eob code */
+var            LENEXT = 22;    /* i: waiting for length extra bits */
+var            DIST = 23;      /* i: waiting for distance code */
+var            DISTEXT = 24;   /* i: waiting for distance extra bits */
+var            MATCH = 25;     /* o: waiting for output space to copy string */
+var            LIT = 26;       /* o: waiting for output space to write literal */
+var    CHECK = 27;     /* i: waiting for 32-bit check value */
+var    LENGTH = 28;    /* i: waiting for 32-bit length (gzip) */
+var    DONE = 29;      /* finished check, done -- remain here until reset */
+var    BAD = 30;       /* got a data error -- remain here until reset */
+var    MEM = 31;       /* got an inflate() memory error -- remain here until reset */
+var    SYNC = 32;      /* looking for synchronization bytes to restart inflate() */
+
+/* ===========================================================================*/
+
+
+
+var ENOUGH_LENS = 852;
+var ENOUGH_DISTS = 592;
+//var ENOUGH =  (ENOUGH_LENS+ENOUGH_DISTS);
+
+var MAX_WBITS = 15;
+/* 32K LZ77 window */
+var DEF_WBITS = MAX_WBITS;
+
+
+function zswap32(q) {
+  return  (((q >>> 24) & 0xff) +
+          ((q >>> 8) & 0xff00) +
+          ((q & 0xff00) << 8) +
+          ((q & 0xff) << 24));
+}
+
+
+function InflateState() {
+  this.mode = 0;             /* current inflate mode */
+  this.last = false;          /* true if processing last block */
+  this.wrap = 0;              /* bit 0 true for zlib, bit 1 true for gzip */
+  this.havedict = false;      /* true if dictionary provided */
+  this.flags = 0;             /* gzip header method and flags (0 if zlib) */
+  this.dmax = 0;              /* zlib header max distance (INFLATE_STRICT) */
+  this.check = 0;             /* protected copy of check value */
+  this.total = 0;             /* protected copy of output count */
+  // TODO: may be {}
+  this.head = null;           /* where to save gzip header information */
+
+  /* sliding window */
+  this.wbits = 0;             /* log base 2 of requested window size */
+  this.wsize = 0;             /* window size or zero if not using window */
+  this.whave = 0;             /* valid bytes in the window */
+  this.wnext = 0;             /* window write index */
+  this.window = null;         /* allocated sliding window, if needed */
+
+  /* bit accumulator */
+  this.hold = 0;              /* input bit accumulator */
+  this.bits = 0;              /* number of bits in "in" */
+
+  /* for string and stored block copying */
+  this.length = 0;            /* literal or length of data to copy */
+  this.offset = 0;            /* distance back to copy string from */
+
+  /* for table and code decoding */
+  this.extra = 0;             /* extra bits needed */
+
+  /* fixed and dynamic code tables */
+  this.lencode = null;          /* starting table for length/literal codes */
+  this.distcode = null;         /* starting table for distance codes */
+  this.lenbits = 0;           /* index bits for lencode */
+  this.distbits = 0;          /* index bits for distcode */
+
+  /* dynamic table building */
+  this.ncode = 0;             /* number of code length code lengths */
+  this.nlen = 0;              /* number of length code lengths */
+  this.ndist = 0;             /* number of distance code lengths */
+  this.have = 0;              /* number of code lengths in lens[] */
+  this.next = null;              /* next available space in codes[] */
+
+  this.lens = new utils.Buf16(320); /* temporary storage for code lengths */
+  this.work = new utils.Buf16(288); /* work area for code table building */
+
+  /*
+   because we don't have pointers in js, we use lencode and distcode directly
+   as buffers so we don't need codes
+  */
+  //this.codes = new utils.Buf32(ENOUGH);       /* space for code tables */
+  this.lendyn = null;              /* dynamic table for length/literal codes (JS specific) */
+  this.distdyn = null;             /* dynamic table for distance codes (JS specific) */
+  this.sane = 0;                   /* if false, allow invalid distance too far */
+  this.back = 0;                   /* bits back of last unprocessed length/lit */
+  this.was = 0;                    /* initial length of match */
+}
+
+function inflateResetKeep(strm) {
+  var state;
+
+  if (!strm || !strm.state) { return Z_STREAM_ERROR; }
+  state = strm.state;
+  strm.total_in = strm.total_out = state.total = 0;
+  strm.msg = ''; /*Z_NULL*/
+  if (state.wrap) {       /* to support ill-conceived Java test suite */
+    strm.adler = state.wrap & 1;
+  }
+  state.mode = HEAD;
+  state.last = 0;
+  state.havedict = 0;
+  state.dmax = 32768;
+  state.head = null/*Z_NULL*/;
+  state.hold = 0;
+  state.bits = 0;
+  //state.lencode = state.distcode = state.next = state.codes;
+  state.lencode = state.lendyn = new utils.Buf32(ENOUGH_LENS);
+  state.distcode = state.distdyn = new utils.Buf32(ENOUGH_DISTS);
+
+  state.sane = 1;
+  state.back = -1;
+  //Tracev((stderr, "inflate: reset\n"));
+  return Z_OK;
+}
+
+function inflateReset(strm) {
+  var state;
+
+  if (!strm || !strm.state) { return Z_STREAM_ERROR; }
+  state = strm.state;
+  state.wsize = 0;
+  state.whave = 0;
+  state.wnext = 0;
+  return inflateResetKeep(strm);
+
+}
+
+function inflateReset2(strm, windowBits) {
+  var wrap;
+  var state;
+
+  /* get the state */
+  if (!strm || !strm.state) { return Z_STREAM_ERROR; }
+  state = strm.state;
+
+  /* extract wrap request from windowBits parameter */
+  if (windowBits < 0) {
+    wrap = 0;
+    windowBits = -windowBits;
+  }
+  else {
+    wrap = (windowBits >> 4) + 1;
+    if (windowBits < 48) {
+      windowBits &= 15;
+    }
+  }
+
+  /* set number of window bits, free window if different */
+  if (windowBits && (windowBits < 8 || windowBits > 15)) {
+    return Z_STREAM_ERROR;
+  }
+  if (state.window !== null && state.wbits !== windowBits) {
+    state.window = null;
+  }
+
+  /* update state and reset the rest of it */
+  state.wrap = wrap;
+  state.wbits = windowBits;
+  return inflateReset(strm);
+}
+
+function inflateInit2(strm, windowBits) {
+  var ret;
+  var state;
+
+  if (!strm) { return Z_STREAM_ERROR; }
+  //strm.msg = Z_NULL;                 /* in case we return an error */
+
+  state = new InflateState();
+
+  //if (state === Z_NULL) return Z_MEM_ERROR;
+  //Tracev((stderr, "inflate: allocated\n"));
+  strm.state = state;
+  state.window = null/*Z_NULL*/;
+  ret = inflateReset2(strm, windowBits);
+  if (ret !== Z_OK) {
+    strm.state = null/*Z_NULL*/;
+  }
+  return ret;
+}
+
+function inflateInit(strm) {
+  return inflateInit2(strm, DEF_WBITS);
+}
+
+
+/*
+ Return state with length and distance decoding tables and index sizes set to
+ fixed code decoding.  Normally this returns fixed tables from inffixed.h.
+ If BUILDFIXED is defined, then instead this routine builds the tables the
+ first time it's called, and returns those tables the first time and
+ thereafter.  This reduces the size of the code by about 2K bytes, in
+ exchange for a little execution time.  However, BUILDFIXED should not be
+ used for threaded applications, since the rewriting of the tables and virgin
+ may not be thread-safe.
+ */
+var virgin = true;
+
+var lenfix, distfix; // We have no pointers in JS, so keep tables separate
+
+function fixedtables(state) {
+  /* build fixed huffman tables if first call (may not be thread safe) */
+  if (virgin) {
+    var sym;
+
+    lenfix = new utils.Buf32(512);
+    distfix = new utils.Buf32(32);
+
+    /* literal/length table */
+    sym = 0;
+    while (sym < 144) { state.lens[sym++] = 8; }
+    while (sym < 256) { state.lens[sym++] = 9; }
+    while (sym < 280) { state.lens[sym++] = 7; }
+    while (sym < 288) { state.lens[sym++] = 8; }
+
+    inflate_table(LENS,  state.lens, 0, 288, lenfix,   0, state.work, { bits: 9 });
+
+    /* distance table */
+    sym = 0;
+    while (sym < 32) { state.lens[sym++] = 5; }
+
+    inflate_table(DISTS, state.lens, 0, 32,   distfix, 0, state.work, { bits: 5 });
+
+    /* do this just once */
+    virgin = false;
+  }
+
+  state.lencode = lenfix;
+  state.lenbits = 9;
+  state.distcode = distfix;
+  state.distbits = 5;
+}
+
+
+/*
+ Update the window with the last wsize (normally 32K) bytes written before
+ returning.  If window does not exist yet, create it.  This is only called
+ when a window is already in use, or when output has been written during this
+ inflate call, but the end of the deflate stream has not been reached yet.
+ It is also called to create a window for dictionary data when a dictionary
+ is loaded.
+
+ Providing output buffers larger than 32K to inflate() should provide a speed
+ advantage, since only the last 32K of output is copied to the sliding window
+ upon return from inflate(), and since all distances after the first 32K of
+ output will fall in the output data, making match copies simpler and faster.
+ The advantage may be dependent on the size of the processor's data caches.
+ */
+function updatewindow(strm, src, end, copy) {
+  var dist;
+  var state = strm.state;
+
+  /* if it hasn't been done already, allocate space for the window */
+  if (state.window === null) {
+    state.wsize = 1 << state.wbits;
+    state.wnext = 0;
+    state.whave = 0;
+
+    state.window = new utils.Buf8(state.wsize);
+  }
+
+  /* copy state->wsize or less output bytes into the circular window */
+  if (copy >= state.wsize) {
+    utils.arraySet(state.window, src, end - state.wsize, state.wsize, 0);
+    state.wnext = 0;
+    state.whave = state.wsize;
+  }
+  else {
+    dist = state.wsize - state.wnext;
+    if (dist > copy) {
+      dist = copy;
+    }
+    //zmemcpy(state->window + state->wnext, end - copy, dist);
+    utils.arraySet(state.window, src, end - copy, dist, state.wnext);
+    copy -= dist;
+    if (copy) {
+      //zmemcpy(state->window, end - copy, copy);
+      utils.arraySet(state.window, src, end - copy, copy, 0);
+      state.wnext = copy;
+      state.whave = state.wsize;
+    }
+    else {
+      state.wnext += dist;
+      if (state.wnext === state.wsize) { state.wnext = 0; }
+      if (state.whave < state.wsize) { state.whave += dist; }
+    }
+  }
+  return 0;
+}
+
+function inflate(strm, flush) {
+  var state;
+  var input, output;          // input/output buffers
+  var next;                   /* next input INDEX */
+  var put;                    /* next output INDEX */
+  var have, left;             /* available input and output */
+  var hold;                   /* bit buffer */
+  var bits;                   /* bits in bit buffer */
+  var _in, _out;              /* save starting available input and output */
+  var copy;                   /* number of stored or match bytes to copy */
+  var from;                   /* where to copy match bytes from */
+  var from_source;
+  var here = 0;               /* current decoding table entry */
+  var here_bits, here_op, here_val; // paked "here" denormalized (JS specific)
+  //var last;                   /* parent table entry */
+  var last_bits, last_op, last_val; // paked "last" denormalized (JS specific)
+  var len;                    /* length to copy for repeats, bits to drop */
+  var ret;                    /* return code */
+  var hbuf = new utils.Buf8(4);    /* buffer for gzip header crc calculation */
+  var opts;
+
+  var n; // temporary var for NEED_BITS
+
+  var order = /* permutation of code lengths */
+    [ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 ];
+
+
+  if (!strm || !strm.state || !strm.output ||
+      (!strm.input && strm.avail_in !== 0)) {
+    return Z_STREAM_ERROR;
+  }
+
+  state = strm.state;
+  if (state.mode === TYPE) { state.mode = TYPEDO; }    /* skip check */
+
+
+  //--- LOAD() ---
+  put = strm.next_out;
+  output = strm.output;
+  left = strm.avail_out;
+  next = strm.next_in;
+  input = strm.input;
+  have = strm.avail_in;
+  hold = state.hold;
+  bits = state.bits;
+  //---
+
+  _in = have;
+  _out = left;
+  ret = Z_OK;
+
+  inf_leave: // goto emulation
+  for (;;) {
+    switch (state.mode) {
+    case HEAD:
+      if (state.wrap === 0) {
+        state.mode = TYPEDO;
+        break;
+      }
+      //=== NEEDBITS(16);
+      while (bits < 16) {
+        if (have === 0) { break inf_leave; }
+        have--;
+        hold += input[next++] << bits;
+        bits += 8;
+      }
+      //===//
+      if ((state.wrap & 2) && hold === 0x8b1f) {  /* gzip header */
+        state.check = 0/*crc32(0L, Z_NULL, 0)*/;
+        //=== CRC2(state.check, hold);
+        hbuf[0] = hold & 0xff;
+        hbuf[1] = (hold >>> 8) & 0xff;
+        state.check = crc32(state.check, hbuf, 2, 0);
+        //===//
+
+        //=== INITBITS();
+        hold = 0;
+        bits = 0;
+        //===//
+        state.mode = FLAGS;
+        break;
+      }
+      state.flags = 0;           /* expect zlib header */
+      if (state.head) {
+        state.head.done = false;
+      }
+      if (!(state.wrap & 1) ||   /* check if zlib header allowed */
+        (((hold & 0xff)/*BITS(8)*/ << 8) + (hold >> 8)) % 31) {
+        strm.msg = 'incorrect header check';
+        state.mode = BAD;
+        break;
+      }
+      if ((hold & 0x0f)/*BITS(4)*/ !== Z_DEFLATED) {
+        strm.msg = 'unknown compression method';
+        state.mode = BAD;
+        break;
+      }
+      //--- DROPBITS(4) ---//
+      hold >>>= 4;
+      bits -= 4;
+      //---//
+      len = (hold & 0x0f)/*BITS(4)*/ + 8;
+      if (state.wbits === 0) {
+        state.wbits = len;
+      }
+      else if (len > state.wbits) {
+        strm.msg = 'invalid window size';
+        state.mode = BAD;
+        break;
+      }
+      state.dmax = 1 << len;
+      //Tracev((stderr, "inflate:   zlib header ok\n"));
+      strm.adler = state.check = 1/*adler32(0L, Z_NULL, 0)*/;
+      state.mode = hold & 0x200 ? DICTID : TYPE;
+      //=== INITBITS();
+      hold = 0;
+      bits = 0;
+      //===//
+      break;
+    case FLAGS:
+      //=== NEEDBITS(16); */
+      while (bits < 16) {
+        if (have === 0) { break inf_leave; }
+        have--;
+        hold += input[next++] << bits;
+        bits += 8;
+      }
+      //===//
+      state.flags = hold;
+      if ((state.flags & 0xff) !== Z_DEFLATED) {
+        strm.msg = 'unknown compression method';
+        state.mode = BAD;
+        break;
+      }
+      if (state.flags & 0xe000) {
+        strm.msg = 'unknown header flags set';
+        state.mode = BAD;
+        break;
+      }
+      if (state.head) {
+        state.head.text = ((hold >> 8) & 1);
+      }
+      if (state.flags & 0x0200) {
+        //=== CRC2(state.check, hold);
+        hbuf[0] = hold & 0xff;
+        hbuf[1] = (hold >>> 8) & 0xff;
+        state.check = crc32(state.check, hbuf, 2, 0);
+        //===//
+      }
+      //=== INITBITS();
+      hold = 0;
+      bits = 0;
+      //===//
+      state.mode = TIME;
+      /* falls through */
+    case TIME:
+      //=== NEEDBITS(32); */
+      while (bits < 32) {
+        if (have === 0) { break inf_leave; }
+        have--;
+        hold += input[next++] << bits;
+        bits += 8;
+      }
+      //===//
+      if (state.head) {
+        state.head.time = hold;
+      }
+      if (state.flags & 0x0200) {
+        //=== CRC4(state.check, hold)
+        hbuf[0] = hold & 0xff;
+        hbuf[1] = (hold >>> 8) & 0xff;
+        hbuf[2] = (hold >>> 16) & 0xff;
+        hbuf[3] = (hold >>> 24) & 0xff;
+        state.check = crc32(state.check, hbuf, 4, 0);
+        //===
+      }
+      //=== INITBITS();
+      hold = 0;
+      bits = 0;
+      //===//
+      state.mode = OS;
+      /* falls through */
+    case OS:
+      //=== NEEDBITS(16); */
+      while (bits < 16) {
+        if (have === 0) { break inf_leave; }
+        have--;
+        hold += input[next++] << bits;
+        bits += 8;
+      }
+      //===//
+      if (state.head) {
+        state.head.xflags = (hold & 0xff);
+        state.head.os = (hold >> 8);
+      }
+      if (state.flags & 0x0200) {
+        //=== CRC2(state.check, hold);
+        hbuf[0] = hold & 0xff;
+        hbuf[1] = (hold >>> 8) & 0xff;
+        state.check = crc32(state.check, hbuf, 2, 0);
+        //===//
+      }
+      //=== INITBITS();
+      hold = 0;
+      bits = 0;
+      //===//
+      state.mode = EXLEN;
+      /* falls through */
+    case EXLEN:
+      if (state.flags & 0x0400) {
+        //=== NEEDBITS(16); */
+        while (bits < 16) {
+          if (have === 0) { break inf_leave; }
+          have--;
+          hold += input[next++] << bits;
+          bits += 8;
+        }
+        //===//
+        state.length = hold;
+        if (state.head) {
+          state.head.extra_len = hold;
+        }
+        if (state.flags & 0x0200) {
+          //=== CRC2(state.check, hold);
+          hbuf[0] = hold & 0xff;
+          hbuf[1] = (hold >>> 8) & 0xff;
+          state.check = crc32(state.check, hbuf, 2, 0);
+          //===//
+        }
+        //=== INITBITS();
+        hold = 0;
+        bits = 0;
+        //===//
+      }
+      else if (state.head) {
+        state.head.extra = null/*Z_NULL*/;
+      }
+      state.mode = EXTRA;
+      /* falls through */
+    case EXTRA:
+      if (state.flags & 0x0400) {
+        copy = state.length;
+        if (copy > have) { copy = have; }
+        if (copy) {
+          if (state.head) {
+            len = state.head.extra_len - state.length;
+            if (!state.head.extra) {
+              // Use untyped array for more conveniend processing later
+              state.head.extra = new Array(state.head.extra_len);
+            }
+            utils.arraySet(
+              state.head.extra,
+              input,
+              next,
+              // extra field is limited to 65536 bytes
+              // - no need for additional size check
+              copy,
+              /*len + copy > state.head.extra_max - len ? state.head.extra_max : copy,*/
+              len
+            );
+            //zmemcpy(state.head.extra + len, next,
+            //        len + copy > state.head.extra_max ?
+            //        state.head.extra_max - len : copy);
+          }
+          if (state.flags & 0x0200) {
+            state.check = crc32(state.check, input, copy, next);
+          }
+          have -= copy;
+          next += copy;
+          state.length -= copy;
+        }
+        if (state.length) { break inf_leave; }
+      }
+      state.length = 0;
+      state.mode = NAME;
+      /* falls through */
+    case NAME:
+      if (state.flags & 0x0800) {
+        if (have === 0) { break inf_leave; }
+        copy = 0;
+        do {
+          // TODO: 2 or 1 bytes?
+          len = input[next + copy++];
+          /* use constant limit because in js we should not preallocate memory */
+          if (state.head && len &&
+              (state.length < 65536 /*state.head.name_max*/)) {
+            state.head.name += String.fromCharCode(len);
+          }
+        } while (len && copy < have);
+
+        if (state.flags & 0x0200) {
+          state.check = crc32(state.check, input, copy, next);
+        }
+        have -= copy;
+        next += copy;
+        if (len) { break inf_leave; }
+      }
+      else if (state.head) {
+        state.head.name = null;
+      }
+      state.length = 0;
+      state.mode = COMMENT;
+      /* falls through */
+    case COMMENT:
+      if (state.flags & 0x1000) {
+        if (have === 0) { break inf_leave; }
+        copy = 0;
+        do {
+          len = input[next + copy++];
+          /* use constant limit because in js we should not preallocate memory */
+          if (state.head && len &&
+              (state.length < 65536 /*state.head.comm_max*/)) {
+            state.head.comment += String.fromCharCode(len);
+          }
+        } while (len && copy < have);
+        if (state.flags & 0x0200) {
+          state.check = crc32(state.check, input, copy, next);
+        }
+        have -= copy;
+        next += copy;
+        if (len) { break inf_leave; }
+      }
+      else if (state.head) {
+        state.head.comment = null;
+      }
+      state.mode = HCRC;
+      /* falls through */
+    case HCRC:
+      if (state.flags & 0x0200) {
+        //=== NEEDBITS(16); */
+        while (bits < 16) {
+          if (have === 0) { break inf_leave; }
+          have--;
+          hold += input[next++] << bits;
+          bits += 8;
+        }
+        //===//
+        if (hold !== (state.check & 0xffff)) {
+          strm.msg = 'header crc mismatch';
+          state.mode = BAD;
+          break;
+        }
+        //=== INITBITS();
+        hold = 0;
+        bits = 0;
+        //===//
+      }
+      if (state.head) {
+        state.head.hcrc = ((state.flags >> 9) & 1);
+        state.head.done = true;
+      }
+      strm.adler = state.check = 0;
+      state.mode = TYPE;
+      break;
+    case DICTID:
+      //=== NEEDBITS(32); */
+      while (bits < 32) {
+        if (have === 0) { break inf_leave; }
+        have--;
+        hold += input[next++] << bits;
+        bits += 8;
+      }
+      //===//
+      strm.adler = state.check = zswap32(hold);
+      //=== INITBITS();
+      hold = 0;
+      bits = 0;
+      //===//
+      state.mode = DICT;
+      /* falls through */
+    case DICT:
+      if (state.havedict === 0) {
+        //--- RESTORE() ---
+        strm.next_out = put;
+        strm.avail_out = left;
+        strm.next_in = next;
+        strm.avail_in = have;
+        state.hold = hold;
+        state.bits = bits;
+        //---
+        return Z_NEED_DICT;
+      }
+      strm.adler = state.check = 1/*adler32(0L, Z_NULL, 0)*/;
+      state.mode = TYPE;
+      /* falls through */
+    case TYPE:
+      if (flush === Z_BLOCK || flush === Z_TREES) { break inf_leave; }
+      /* falls through */
+    case TYPEDO:
+      if (state.last) {
+        //--- BYTEBITS() ---//
+        hold >>>= bits & 7;
+        bits -= bits & 7;
+        //---//
+        state.mode = CHECK;
+        break;
+      }
+      //=== NEEDBITS(3); */
+      while (bits < 3) {
+        if (have === 0) { break inf_leave; }
+        have--;
+        hold += input[next++] << bits;
+        bits += 8;
+      }
+      //===//
+      state.last = (hold & 0x01)/*BITS(1)*/;
+      //--- DROPBITS(1) ---//
+      hold >>>= 1;
+      bits -= 1;
+      //---//
+
+      switch ((hold & 0x03)/*BITS(2)*/) {
+      case 0:                             /* stored block */
+        //Tracev((stderr, "inflate:     stored block%s\n",
+        //        state.last ? " (last)" : ""));
+        state.mode = STORED;
+        break;
+      case 1:                             /* fixed block */
+        fixedtables(state);
+        //Tracev((stderr, "inflate:     fixed codes block%s\n",
+        //        state.last ? " (last)" : ""));
+        state.mode = LEN_;             /* decode codes */
+        if (flush === Z_TREES) {
+          //--- DROPBITS(2) ---//
+          hold >>>= 2;
+          bits -= 2;
+          //---//
+          break inf_leave;
+        }
+        break;
+      case 2:                             /* dynamic block */
+        //Tracev((stderr, "inflate:     dynamic codes block%s\n",
+        //        state.last ? " (last)" : ""));
+        state.mode = TABLE;
+        break;
+      case 3:
+        strm.msg = 'invalid block type';
+        state.mode = BAD;
+      }
+      //--- DROPBITS(2) ---//
+      hold >>>= 2;
+      bits -= 2;
+      //---//
+      break;
+    case STORED:
+      //--- BYTEBITS() ---// /* go to byte boundary */
+      hold >>>= bits & 7;
+      bits -= bits & 7;
+      //---//
+      //=== NEEDBITS(32); */
+      while (bits < 32) {
+        if (have === 0) { break inf_leave; }
+        have--;
+        hold += input[next++] << bits;
+        bits += 8;
+      }
+      //===//
+      if ((hold & 0xffff) !== ((hold >>> 16) ^ 0xffff)) {
+        strm.msg = 'invalid stored block lengths';
+        state.mode = BAD;
+        break;
+      }
+      state.length = hold & 0xffff;
+      //Tracev((stderr, "inflate:       stored length %u\n",
+      //        state.length));
+      //=== INITBITS();
+      hold = 0;
+      bits = 0;
+      //===//
+      state.mode = COPY_;
+      if (flush === Z_TREES) { break inf_leave; }
+      /* falls through */
+    case COPY_:
+      state.mode = COPY;
+      /* falls through */
+    case COPY:
+      copy = state.length;
+      if (copy) {
+        if (copy > have) { copy = have; }
+        if (copy > left) { copy = left; }
+        if (copy === 0) { break inf_leave; }
+        //--- zmemcpy(put, next, copy); ---
+        utils.arraySet(output, input, next, copy, put);
+        //---//
+        have -= copy;
+        next += copy;
+        left -= copy;
+        put += copy;
+        state.length -= copy;
+        break;
+      }
+      //Tracev((stderr, "inflate:       stored end\n"));
+      state.mode = TYPE;
+      break;
+    case TABLE:
+      //=== NEEDBITS(14); */
+      while (bits < 14) {
+        if (have === 0) { break inf_leave; }
+        have--;
+        hold += input[next++] << bits;
+        bits += 8;
+      }
+      //===//
+      state.nlen = (hold & 0x1f)/*BITS(5)*/ + 257;
+      //--- DROPBITS(5) ---//
+      hold >>>= 5;
+      bits -= 5;
+      //---//
+      state.ndist = (hold & 0x1f)/*BITS(5)*/ + 1;
+      //--- DROPBITS(5) ---//
+      hold >>>= 5;
+      bits -= 5;
+      //---//
+      state.ncode = (hold & 0x0f)/*BITS(4)*/ + 4;
+      //--- DROPBITS(4) ---//
+      hold >>>= 4;
+      bits -= 4;
+      //---//
+//#ifndef PKZIP_BUG_WORKAROUND
+      if (state.nlen > 286 || state.ndist > 30) {
+        strm.msg = 'too many length or distance symbols';
+        state.mode = BAD;
+        break;
+      }
+//#endif
+      //Tracev((stderr, "inflate:       table sizes ok\n"));
+      state.have = 0;
+      state.mode = LENLENS;
+      /* falls through */
+    case LENLENS:
+      while (state.have < state.ncode) {
+        //=== NEEDBITS(3);
+        while (bits < 3) {
+          if (have === 0) { break inf_leave; }
+          have--;
+          hold += input[next++] << bits;
+          bits += 8;
+        }
+        //===//
+        state.lens[order[state.have++]] = (hold & 0x07);//BITS(3);
+        //--- DROPBITS(3) ---//
+        hold >>>= 3;
+        bits -= 3;
+        //---//
+      }
+      while (state.have < 19) {
+        state.lens[order[state.have++]] = 0;
+      }
+      // We have separate tables & no pointers. 2 commented lines below not needed.
+      //state.next = state.codes;
+      //state.lencode = state.next;
+      // Switch to use dynamic table
+      state.lencode = state.lendyn;
+      state.lenbits = 7;
+
+      opts = { bits: state.lenbits };
+      ret = inflate_table(CODES, state.lens, 0, 19, state.lencode, 0, state.work, opts);
+      state.lenbits = opts.bits;
+
+      if (ret) {
+        strm.msg = 'invalid code lengths set';
+        state.mode = BAD;
+        break;
+      }
+      //Tracev((stderr, "inflate:       code lengths ok\n"));
+      state.have = 0;
+      state.mode = CODELENS;
+      /* falls through */
+    case CODELENS:
+      while (state.have < state.nlen + state.ndist) {
+        for (;;) {
+          here = state.lencode[hold & ((1 << state.lenbits) - 1)];/*BITS(state.lenbits)*/
+          here_bits = here >>> 24;
+          here_op = (here >>> 16) & 0xff;
+          here_val = here & 0xffff;
+
+          if ((here_bits) <= bits) { break; }
+          //--- PULLBYTE() ---//
+          if (have === 0) { break inf_leave; }
+          have--;
+          hold += input[next++] << bits;
+          bits += 8;
+          //---//
+        }
+        if (here_val < 16) {
+          //--- DROPBITS(here.bits) ---//
+          hold >>>= here_bits;
+          bits -= here_bits;
+          //---//
+          state.lens[state.have++] = here_val;
+        }
+        else {
+          if (here_val === 16) {
+            //=== NEEDBITS(here.bits + 2);
+            n = here_bits + 2;
+            while (bits < n) {
+              if (have === 0) { break inf_leave; }
+              have--;
+              hold += input[next++] << bits;
+              bits += 8;
+            }
+            //===//
+            //--- DROPBITS(here.bits) ---//
+            hold >>>= here_bits;
+            bits -= here_bits;
+            //---//
+            if (state.have === 0) {
+              strm.msg = 'invalid bit length repeat';
+              state.mode = BAD;
+              break;
+            }
+            len = state.lens[state.have - 1];
+            copy = 3 + (hold & 0x03);//BITS(2);
+            //--- DROPBITS(2) ---//
+            hold >>>= 2;
+            bits -= 2;
+            //---//
+          }
+          else if (here_val === 17) {
+            //=== NEEDBITS(here.bits + 3);
+            n = here_bits + 3;
+            while (bits < n) {
+              if (have === 0) { break inf_leave; }
+              have--;
+              hold += input[next++] << bits;
+              bits += 8;
+            }
+            //===//
+            //--- DROPBITS(here.bits) ---//
+            hold >>>= here_bits;
+            bits -= here_bits;
+            //---//
+            len = 0;
+            copy = 3 + (hold & 0x07);//BITS(3);
+            //--- DROPBITS(3) ---//
+            hold >>>= 3;
+            bits -= 3;
+            //---//
+          }
+          else {
+            //=== NEEDBITS(here.bits + 7);
+            n = here_bits + 7;
+            while (bits < n) {
+              if (have === 0) { break inf_leave; }
+              have--;
+              hold += input[next++] << bits;
+              bits += 8;
+            }
+            //===//
+            //--- DROPBITS(here.bits) ---//
+            hold >>>= here_bits;
+            bits -= here_bits;
+            //---//
+            len = 0;
+            copy = 11 + (hold & 0x7f);//BITS(7);
+            //--- DROPBITS(7) ---//
+            hold >>>= 7;
+            bits -= 7;
+            //---//
+          }
+          if (state.have + copy > state.nlen + state.ndist) {
+            strm.msg = 'invalid bit length repeat';
+            state.mode = BAD;
+            break;
+          }
+          while (copy--) {
+            state.lens[state.have++] = len;
+          }
+        }
+      }
+
+      /* handle error breaks in while */
+      if (state.mode === BAD) { break; }
+
+      /* check for end-of-block code (better have one) */
+      if (state.lens[256] === 0) {
+        strm.msg = 'invalid code -- missing end-of-block';
+        state.mode = BAD;
+        break;
+      }
+
+      /* build code tables -- note: do not change the lenbits or distbits
+         values here (9 and 6) without reading the comments in inftrees.h
+         concerning the ENOUGH constants, which depend on those values */
+      state.lenbits = 9;
+
+      opts = { bits: state.lenbits };
+      ret = inflate_table(LENS, state.lens, 0, state.nlen, state.lencode, 0, state.work, opts);
+      // We have separate tables & no pointers. 2 commented lines below not needed.
+      // state.next_index = opts.table_index;
+      state.lenbits = opts.bits;
+      // state.lencode = state.next;
+
+      if (ret) {
+        strm.msg = 'invalid literal/lengths set';
+        state.mode = BAD;
+        break;
+      }
+
+      state.distbits = 6;
+      //state.distcode.copy(state.codes);
+      // Switch to use dynamic table
+      state.distcode = state.distdyn;
+      opts = { bits: state.distbits };
+      ret = inflate_table(DISTS, state.lens, state.nlen, state.ndist, state.distcode, 0, state.work, opts);
+      // We have separate tables & no pointers. 2 commented lines below not needed.
+      // state.next_index = opts.table_index;
+      state.distbits = opts.bits;
+      // state.distcode = state.next;
+
+      if (ret) {
+        strm.msg = 'invalid distances set';
+        state.mode = BAD;
+        break;
+      }
+      //Tracev((stderr, 'inflate:       codes ok\n'));
+      state.mode = LEN_;
+      if (flush === Z_TREES) { break inf_leave; }
+      /* falls through */
+    case LEN_:
+      state.mode = LEN;
+      /* falls through */
+    case LEN:
+      if (have >= 6 && left >= 258) {
+        //--- RESTORE() ---
+        strm.next_out = put;
+        strm.avail_out = left;
+        strm.next_in = next;
+        strm.avail_in = have;
+        state.hold = hold;
+        state.bits = bits;
+        //---
+        inflate_fast(strm, _out);
+        //--- LOAD() ---
+        put = strm.next_out;
+        output = strm.output;
+        left = strm.avail_out;
+        next = strm.next_in;
+        input = strm.input;
+        have = strm.avail_in;
+        hold = state.hold;
+        bits = state.bits;
+        //---
+
+        if (state.mode === TYPE) {
+          state.back = -1;
+        }
+        break;
+      }
+      state.back = 0;
+      for (;;) {
+        here = state.lencode[hold & ((1 << state.lenbits) - 1)];  /*BITS(state.lenbits)*/
+        here_bits = here >>> 24;
+        here_op = (here >>> 16) & 0xff;
+        here_val = here & 0xffff;
+
+        if (here_bits <= bits) { break; }
+        //--- PULLBYTE() ---//
+        if (have === 0) { break inf_leave; }
+        have--;
+        hold += input[next++] << bits;
+        bits += 8;
+        //---//
+      }
+      if (here_op && (here_op & 0xf0) === 0) {
+        last_bits = here_bits;
+        last_op = here_op;
+        last_val = here_val;
+        for (;;) {
+          here = state.lencode[last_val +
+                  ((hold & ((1 << (last_bits + last_op)) - 1))/*BITS(last.bits + last.op)*/ >> last_bits)];
+          here_bits = here >>> 24;
+          here_op = (here >>> 16) & 0xff;
+          here_val = here & 0xffff;
+
+          if ((last_bits + here_bits) <= bits) { break; }
+          //--- PULLBYTE() ---//
+          if (have === 0) { break inf_leave; }
+          have--;
+          hold += input[next++] << bits;
+          bits += 8;
+          //---//
+        }
+        //--- DROPBITS(last.bits) ---//
+        hold >>>= last_bits;
+        bits -= last_bits;
+        //---//
+        state.back += last_bits;
+      }
+      //--- DROPBITS(here.bits) ---//
+      hold >>>= here_bits;
+      bits -= here_bits;
+      //---//
+      state.back += here_bits;
+      state.length = here_val;
+      if (here_op === 0) {
+        //Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ?
+        //        "inflate:         literal '%c'\n" :
+        //        "inflate:         literal 0x%02x\n", here.val));
+        state.mode = LIT;
+        break;
+      }
+      if (here_op & 32) {
+        //Tracevv((stderr, "inflate:         end of block\n"));
+        state.back = -1;
+        state.mode = TYPE;
+        break;
+      }
+      if (here_op & 64) {
+        strm.msg = 'invalid literal/length code';
+        state.mode = BAD;
+        break;
+      }
+      state.extra = here_op & 15;
+      state.mode = LENEXT;
+      /* falls through */
+    case LENEXT:
+      if (state.extra) {
+        //=== NEEDBITS(state.extra);
+        n = state.extra;
+        while (bits < n) {
+          if (have === 0) { break inf_leave; }
+          have--;
+          hold += input[next++] << bits;
+          bits += 8;
+        }
+        //===//
+        state.length += hold & ((1 << state.extra) - 1)/*BITS(state.extra)*/;
+        //--- DROPBITS(state.extra) ---//
+        hold >>>= state.extra;
+        bits -= state.extra;
+        //---//
+        state.back += state.extra;
+      }
+      //Tracevv((stderr, "inflate:         length %u\n", state.length));
+      state.was = state.length;
+      state.mode = DIST;
+      /* falls through */
+    case DIST:
+      for (;;) {
+        here = state.distcode[hold & ((1 << state.distbits) - 1)];/*BITS(state.distbits)*/
+        here_bits = here >>> 24;
+        here_op = (here >>> 16) & 0xff;
+        here_val = here & 0xffff;
+
+        if ((here_bits) <= bits) { break; }
+        //--- PULLBYTE() ---//
+        if (have === 0) { break inf_leave; }
+        have--;
+        hold += input[next++] << bits;
+        bits += 8;
+        //---//
+      }
+      if ((here_op & 0xf0) === 0) {
+        last_bits = here_bits;
+        last_op = here_op;
+        last_val = here_val;
+        for (;;) {
+          here = state.distcode[last_val +
+                  ((hold & ((1 << (last_bits + last_op)) - 1))/*BITS(last.bits + last.op)*/ >> last_bits)];
+          here_bits = here >>> 24;
+          here_op = (here >>> 16) & 0xff;
+          here_val = here & 0xffff;
+
+          if ((last_bits + here_bits) <= bits) { break; }
+          //--- PULLBYTE() ---//
+          if (have === 0) { break inf_leave; }
+          have--;
+          hold += input[next++] << bits;
+          bits += 8;
+          //---//
+        }
+        //--- DROPBITS(last.bits) ---//
+        hold >>>= last_bits;
+        bits -= last_bits;
+        //---//
+        state.back += last_bits;
+      }
+      //--- DROPBITS(here.bits) ---//
+      hold >>>= here_bits;
+      bits -= here_bits;
+      //---//
+      state.back += here_bits;
+      if (here_op & 64) {
+        strm.msg = 'invalid distance code';
+        state.mode = BAD;
+        break;
+      }
+      state.offset = here_val;
+      state.extra = (here_op) & 15;
+      state.mode = DISTEXT;
+      /* falls through */
+    case DISTEXT:
+      if (state.extra) {
+        //=== NEEDBITS(state.extra);
+        n = state.extra;
+        while (bits < n) {
+          if (have === 0) { break inf_leave; }
+          have--;
+          hold += input[next++] << bits;
+          bits += 8;
+        }
+        //===//
+        state.offset += hold & ((1 << state.extra) - 1)/*BITS(state.extra)*/;
+        //--- DROPBITS(state.extra) ---//
+        hold >>>= state.extra;
+        bits -= state.extra;
+        //---//
+        state.back += state.extra;
+      }
+//#ifdef INFLATE_STRICT
+      if (state.offset > state.dmax) {
+        strm.msg = 'invalid distance too far back';
+        state.mode = BAD;
+        break;
+      }
+//#endif
+      //Tracevv((stderr, "inflate:         distance %u\n", state.offset));
+      state.mode = MATCH;
+      /* falls through */
+    case MATCH:
+      if (left === 0) { break inf_leave; }
+      copy = _out - left;
+      if (state.offset > copy) {         /* copy from window */
+        copy = state.offset - copy;
+        if (copy > state.whave) {
+          if (state.sane) {
+            strm.msg = 'invalid distance too far back';
+            state.mode = BAD;
+            break;
+          }
+// (!) This block is disabled in zlib defailts,
+// don't enable it for binary compatibility
+//#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR
+//          Trace((stderr, "inflate.c too far\n"));
+//          copy -= state.whave;
+//          if (copy > state.length) { copy = state.length; }
+//          if (copy > left) { copy = left; }
+//          left -= copy;
+//          state.length -= copy;
+//          do {
+//            output[put++] = 0;
+//          } while (--copy);
+//          if (state.length === 0) { state.mode = LEN; }
+//          break;
+//#endif
+        }
+        if (copy > state.wnext) {
+          copy -= state.wnext;
+          from = state.wsize - copy;
+        }
+        else {
+          from = state.wnext - copy;
+        }
+        if (copy > state.length) { copy = state.length; }
+        from_source = state.window;
+      }
+      else {                              /* copy from output */
+        from_source = output;
+        from = put - state.offset;
+        copy = state.length;
+      }
+      if (copy > left) { copy = left; }
+      left -= copy;
+      state.length -= copy;
+      do {
+        output[put++] = from_source[from++];
+      } while (--copy);
+      if (state.length === 0) { state.mode = LEN; }
+      break;
+    case LIT:
+      if (left === 0) { break inf_leave; }
+      output[put++] = state.length;
+      left--;
+      state.mode = LEN;
+      break;
+    case CHECK:
+      if (state.wrap) {
+        //=== NEEDBITS(32);
+        while (bits < 32) {
+          if (have === 0) { break inf_leave; }
+          have--;
+          // Use '|' insdead of '+' to make sure that result is signed
+          hold |= input[next++] << bits;
+          bits += 8;
+        }
+        //===//
+        _out -= left;
+        strm.total_out += _out;
+        state.total += _out;
+        if (_out) {
+          strm.adler = state.check =
+              /*UPDATE(state.check, put - _out, _out);*/
+              (state.flags ? crc32(state.check, output, _out, put - _out) : adler32(state.check, output, _out, put - _out));
+
+        }
+        _out = left;
+        // NB: crc32 stored as signed 32-bit int, zswap32 returns signed too
+        if ((state.flags ? hold : zswap32(hold)) !== state.check) {
+          strm.msg = 'incorrect data check';
+          state.mode = BAD;
+          break;
+        }
+        //=== INITBITS();
+        hold = 0;
+        bits = 0;
+        //===//
+        //Tracev((stderr, "inflate:   check matches trailer\n"));
+      }
+      state.mode = LENGTH;
+      /* falls through */
+    case LENGTH:
+      if (state.wrap && state.flags) {
+        //=== NEEDBITS(32);
+        while (bits < 32) {
+          if (have === 0) { break inf_leave; }
+          have--;
+          hold += input[next++] << bits;
+          bits += 8;
+        }
+        //===//
+        if (hold !== (state.total & 0xffffffff)) {
+          strm.msg = 'incorrect length check';
+          state.mode = BAD;
+          break;
+        }
+        //=== INITBITS();
+        hold = 0;
+        bits = 0;
+        //===//
+        //Tracev((stderr, "inflate:   length matches trailer\n"));
+      }
+      state.mode = DONE;
+      /* falls through */
+    case DONE:
+      ret = Z_STREAM_END;
+      break inf_leave;
+    case BAD:
+      ret = Z_DATA_ERROR;
+      break inf_leave;
+    case MEM:
+      return Z_MEM_ERROR;
+    case SYNC:
+      /* falls through */
+    default:
+      return Z_STREAM_ERROR;
+    }
+  }
+
+  // inf_leave <- here is real place for "goto inf_leave", emulated via "break inf_leave"
+
+  /*
+     Return from inflate(), updating the total counts and the check value.
+     If there was no progress during the inflate() call, return a buffer
+     error.  Call updatewindow() to create and/or update the window state.
+     Note: a memory error from inflate() is non-recoverable.
+   */
+
+  //--- RESTORE() ---
+  strm.next_out = put;
+  strm.avail_out = left;
+  strm.next_in = next;
+  strm.avail_in = have;
+  state.hold = hold;
+  state.bits = bits;
+  //---
+
+  if (state.wsize || (_out !== strm.avail_out && state.mode < BAD &&
+                      (state.mode < CHECK || flush !== Z_FINISH))) {
+    if (updatewindow(strm, strm.output, strm.next_out, _out - strm.avail_out)) {
+      state.mode = MEM;
+      return Z_MEM_ERROR;
+    }
+  }
+  _in -= strm.avail_in;
+  _out -= strm.avail_out;
+  strm.total_in += _in;
+  strm.total_out += _out;
+  state.total += _out;
+  if (state.wrap && _out) {
+    strm.adler = state.check = /*UPDATE(state.check, strm.next_out - _out, _out);*/
+      (state.flags ? crc32(state.check, output, _out, strm.next_out - _out) : adler32(state.check, output, _out, strm.next_out - _out));
+  }
+  strm.data_type = state.bits + (state.last ? 64 : 0) +
+                    (state.mode === TYPE ? 128 : 0) +
+                    (state.mode === LEN_ || state.mode === COPY_ ? 256 : 0);
+  if (((_in === 0 && _out === 0) || flush === Z_FINISH) && ret === Z_OK) {
+    ret = Z_BUF_ERROR;
+  }
+  return ret;
+}
+
+function inflateEnd(strm) {
+
+  if (!strm || !strm.state /*|| strm->zfree == (free_func)0*/) {
+    return Z_STREAM_ERROR;
+  }
+
+  var state = strm.state;
+  if (state.window) {
+    state.window = null;
+  }
+  strm.state = null;
+  return Z_OK;
+}
+
+function inflateGetHeader(strm, head) {
+  var state;
+
+  /* check state */
+  if (!strm || !strm.state) { return Z_STREAM_ERROR; }
+  state = strm.state;
+  if ((state.wrap & 2) === 0) { return Z_STREAM_ERROR; }
+
+  /* save header structure */
+  state.head = head;
+  head.done = false;
+  return Z_OK;
+}
+
+function inflateSetDictionary(strm, dictionary) {
+  var dictLength = dictionary.length;
+
+  var state;
+  var dictid;
+  var ret;
+
+  /* check state */
+  if (!strm /* == Z_NULL */ || !strm.state /* == Z_NULL */) { return Z_STREAM_ERROR; }
+  state = strm.state;
+
+  if (state.wrap !== 0 && state.mode !== DICT) {
+    return Z_STREAM_ERROR;
+  }
+
+  /* check for correct dictionary identifier */
+  if (state.mode === DICT) {
+    dictid = 1; /* adler32(0, null, 0)*/
+    /* dictid = adler32(dictid, dictionary, dictLength); */
+    dictid = adler32(dictid, dictionary, dictLength, 0);
+    if (dictid !== state.check) {
+      return Z_DATA_ERROR;
+    }
+  }
+  /* copy dictionary to window using updatewindow(), which will amend the
+   existing dictionary if appropriate */
+  ret = updatewindow(strm, dictionary, dictLength, dictLength);
+  if (ret) {
+    state.mode = MEM;
+    return Z_MEM_ERROR;
+  }
+  state.havedict = 1;
+  // Tracev((stderr, "inflate:   dictionary set\n"));
+  return Z_OK;
+}
+
+export { inflateReset, inflateReset2, inflateResetKeep, inflateInit, inflateInit2, inflate, inflateEnd, inflateGetHeader, inflateSetDictionary };
+export var inflateInfo = 'pako inflate (from Nodeca project)';
+
+/* Not implemented
+exports.inflateCopy = inflateCopy;
+exports.inflateGetDictionary = inflateGetDictionary;
+exports.inflateMark = inflateMark;
+exports.inflatePrime = inflatePrime;
+exports.inflateSync = inflateSync;
+exports.inflateSyncPoint = inflateSyncPoint;
+exports.inflateUndermine = inflateUndermine;
+*/
diff --git a/app/src/main/assets/novnc/vendor/pako/lib/zlib/inftrees.js b/app/src/main/assets/novnc/vendor/pako/lib/zlib/inftrees.js
new file mode 100644
index 00000000..78b7c9eb
--- /dev/null
+++ b/app/src/main/assets/novnc/vendor/pako/lib/zlib/inftrees.js
@@ -0,0 +1,322 @@
+import * as utils from "../utils/common.js";
+
+var MAXBITS = 15;
+var ENOUGH_LENS = 852;
+var ENOUGH_DISTS = 592;
+//var ENOUGH = (ENOUGH_LENS+ENOUGH_DISTS);
+
+var CODES = 0;
+var LENS = 1;
+var DISTS = 2;
+
+var lbase = [ /* Length codes 257..285 base */
+  3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31,
+  35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0
+];
+
+var lext = [ /* Length codes 257..285 extra */
+  16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 18, 18, 18, 18,
+  19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 16, 72, 78
+];
+
+var dbase = [ /* Distance codes 0..29 base */
+  1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193,
+  257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145,
+  8193, 12289, 16385, 24577, 0, 0
+];
+
+var dext = [ /* Distance codes 0..29 extra */
+  16, 16, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22,
+  23, 23, 24, 24, 25, 25, 26, 26, 27, 27,
+  28, 28, 29, 29, 64, 64
+];
+
+export default function inflate_table(type, lens, lens_index, codes, table, table_index, work, opts)
+{
+  var bits = opts.bits;
+      //here = opts.here; /* table entry for duplication */
+
+  var len = 0;               /* a code's length in bits */
+  var sym = 0;               /* index of code symbols */
+  var min = 0, max = 0;          /* minimum and maximum code lengths */
+  var root = 0;              /* number of index bits for root table */
+  var curr = 0;              /* number of index bits for current table */
+  var drop = 0;              /* code bits to drop for sub-table */
+  var left = 0;                   /* number of prefix codes available */
+  var used = 0;              /* code entries in table used */
+  var huff = 0;              /* Huffman code */
+  var incr;              /* for incrementing code, index */
+  var fill;              /* index for replicating entries */
+  var low;               /* low bits for current root entry */
+  var mask;              /* mask for low root bits */
+  var next;             /* next available space in table */
+  var base = null;     /* base value table to use */
+  var base_index = 0;
+//  var shoextra;    /* extra bits table to use */
+  var end;                    /* use base and extra for symbol > end */
+  var count = new utils.Buf16(MAXBITS + 1); //[MAXBITS+1];    /* number of codes of each length */
+  var offs = new utils.Buf16(MAXBITS + 1); //[MAXBITS+1];     /* offsets in table for each length */
+  var extra = null;
+  var extra_index = 0;
+
+  var here_bits, here_op, here_val;
+
+  /*
+   Process a set of code lengths to create a canonical Huffman code.  The
+   code lengths are lens[0..codes-1].  Each length corresponds to the
+   symbols 0..codes-1.  The Huffman code is generated by first sorting the
+   symbols by length from short to long, and retaining the symbol order
+   for codes with equal lengths.  Then the code starts with all zero bits
+   for the first code of the shortest length, and the codes are integer
+   increments for the same length, and zeros are appended as the length
+   increases.  For the deflate format, these bits are stored backwards
+   from their more natural integer increment ordering, and so when the
+   decoding tables are built in the large loop below, the integer codes
+   are incremented backwards.
+
+   This routine assumes, but does not check, that all of the entries in
+   lens[] are in the range 0..MAXBITS.  The caller must assure this.
+   1..MAXBITS is interpreted as that code length.  zero means that that
+   symbol does not occur in this code.
+
+   The codes are sorted by computing a count of codes for each length,
+   creating from that a table of starting indices for each length in the
+   sorted table, and then entering the symbols in order in the sorted
+   table.  The sorted table is work[], with that space being provided by
+   the caller.
+
+   The length counts are used for other purposes as well, i.e. finding
+   the minimum and maximum length codes, determining if there are any
+   codes at all, checking for a valid set of lengths, and looking ahead
+   at length counts to determine sub-table sizes when building the
+   decoding tables.
+   */
+
+  /* accumulate lengths for codes (assumes lens[] all in 0..MAXBITS) */
+  for (len = 0; len <= MAXBITS; len++) {
+    count[len] = 0;
+  }
+  for (sym = 0; sym < codes; sym++) {
+    count[lens[lens_index + sym]]++;
+  }
+
+  /* bound code lengths, force root to be within code lengths */
+  root = bits;
+  for (max = MAXBITS; max >= 1; max--) {
+    if (count[max] !== 0) { break; }
+  }
+  if (root > max) {
+    root = max;
+  }
+  if (max === 0) {                     /* no symbols to code at all */
+    //table.op[opts.table_index] = 64;  //here.op = (var char)64;    /* invalid code marker */
+    //table.bits[opts.table_index] = 1;   //here.bits = (var char)1;
+    //table.val[opts.table_index++] = 0;   //here.val = (var short)0;
+    table[table_index++] = (1 << 24) | (64 << 16) | 0;
+
+
+    //table.op[opts.table_index] = 64;
+    //table.bits[opts.table_index] = 1;
+    //table.val[opts.table_index++] = 0;
+    table[table_index++] = (1 << 24) | (64 << 16) | 0;
+
+    opts.bits = 1;
+    return 0;     /* no symbols, but wait for decoding to report error */
+  }
+  for (min = 1; min < max; min++) {
+    if (count[min] !== 0) { break; }
+  }
+  if (root < min) {
+    root = min;
+  }
+
+  /* check for an over-subscribed or incomplete set of lengths */
+  left = 1;
+  for (len = 1; len <= MAXBITS; len++) {
+    left <<= 1;
+    left -= count[len];
+    if (left < 0) {
+      return -1;
+    }        /* over-subscribed */
+  }
+  if (left > 0 && (type === CODES || max !== 1)) {
+    return -1;                      /* incomplete set */
+  }
+
+  /* generate offsets into symbol table for each length for sorting */
+  offs[1] = 0;
+  for (len = 1; len < MAXBITS; len++) {
+    offs[len + 1] = offs[len] + count[len];
+  }
+
+  /* sort symbols by length, by symbol order within each length */
+  for (sym = 0; sym < codes; sym++) {
+    if (lens[lens_index + sym] !== 0) {
+      work[offs[lens[lens_index + sym]]++] = sym;
+    }
+  }
+
+  /*
+   Create and fill in decoding tables.  In this loop, the table being
+   filled is at next and has curr index bits.  The code being used is huff
+   with length len.  That code is converted to an index by dropping drop
+   bits off of the bottom.  For codes where len is less than drop + curr,
+   those top drop + curr - len bits are incremented through all values to
+   fill the table with replicated entries.
+
+   root is the number of index bits for the root table.  When len exceeds
+   root, sub-tables are created pointed to by the root entry with an index
+   of the low root bits of huff.  This is saved in low to check for when a
+   new sub-table should be started.  drop is zero when the root table is
+   being filled, and drop is root when sub-tables are being filled.
+
+   When a new sub-table is needed, it is necessary to look ahead in the
+   code lengths to determine what size sub-table is needed.  The length
+   counts are used for this, and so count[] is decremented as codes are
+   entered in the tables.
+
+   used keeps track of how many table entries have been allocated from the
+   provided *table space.  It is checked for LENS and DIST tables against
+   the constants ENOUGH_LENS and ENOUGH_DISTS to guard against changes in
+   the initial root table size constants.  See the comments in inftrees.h
+   for more information.
+
+   sym increments through all symbols, and the loop terminates when
+   all codes of length max, i.e. all codes, have been processed.  This
+   routine permits incomplete codes, so another loop after this one fills
+   in the rest of the decoding tables with invalid code markers.
+   */
+
+  /* set up for code type */
+  // poor man optimization - use if-else instead of switch,
+  // to avoid deopts in old v8
+  if (type === CODES) {
+    base = extra = work;    /* dummy value--not used */
+    end = 19;
+
+  } else if (type === LENS) {
+    base = lbase;
+    base_index -= 257;
+    extra = lext;
+    extra_index -= 257;
+    end = 256;
+
+  } else {                    /* DISTS */
+    base = dbase;
+    extra = dext;
+    end = -1;
+  }
+
+  /* initialize opts for loop */
+  huff = 0;                   /* starting code */
+  sym = 0;                    /* starting code symbol */
+  len = min;                  /* starting code length */
+  next = table_index;              /* current table to fill in */
+  curr = root;                /* current table index bits */
+  drop = 0;                   /* current bits to drop from code for index */
+  low = -1;                   /* trigger new sub-table when len > root */
+  used = 1 << root;          /* use root table entries */
+  mask = used - 1;            /* mask for comparing low */
+
+  /* check available table space */
+  if ((type === LENS && used > ENOUGH_LENS) ||
+    (type === DISTS && used > ENOUGH_DISTS)) {
+    return 1;
+  }
+
+  /* process all codes and make table entries */
+  for (;;) {
+    /* create table entry */
+    here_bits = len - drop;
+    if (work[sym] < end) {
+      here_op = 0;
+      here_val = work[sym];
+    }
+    else if (work[sym] > end) {
+      here_op = extra[extra_index + work[sym]];
+      here_val = base[base_index + work[sym]];
+    }
+    else {
+      here_op = 32 + 64;         /* end of block */
+      here_val = 0;
+    }
+
+    /* replicate for those indices with low len bits equal to huff */
+    incr = 1 << (len - drop);
+    fill = 1 << curr;
+    min = fill;                 /* save offset to next table */
+    do {
+      fill -= incr;
+      table[next + (huff >> drop) + fill] = (here_bits << 24) | (here_op << 16) | here_val |0;
+    } while (fill !== 0);
+
+    /* backwards increment the len-bit code huff */
+    incr = 1 << (len - 1);
+    while (huff & incr) {
+      incr >>= 1;
+    }
+    if (incr !== 0) {
+      huff &= incr - 1;
+      huff += incr;
+    } else {
+      huff = 0;
+    }
+
+    /* go to next symbol, update count, len */
+    sym++;
+    if (--count[len] === 0) {
+      if (len === max) { break; }
+      len = lens[lens_index + work[sym]];
+    }
+
+    /* create new sub-table if needed */
+    if (len > root && (huff & mask) !== low) {
+      /* if first time, transition to sub-tables */
+      if (drop === 0) {
+        drop = root;
+      }
+
+      /* increment past last table */
+      next += min;            /* here min is 1 << curr */
+
+      /* determine length of next table */
+      curr = len - drop;
+      left = 1 << curr;
+      while (curr + drop < max) {
+        left -= count[curr + drop];
+        if (left <= 0) { break; }
+        curr++;
+        left <<= 1;
+      }
+
+      /* check for enough space */
+      used += 1 << curr;
+      if ((type === LENS && used > ENOUGH_LENS) ||
+        (type === DISTS && used > ENOUGH_DISTS)) {
+        return 1;
+      }
+
+      /* point entry in root table to sub-table */
+      low = huff & mask;
+      /*table.op[low] = curr;
+      table.bits[low] = root;
+      table.val[low] = next - opts.table_index;*/
+      table[low] = (root << 24) | (curr << 16) | (next - table_index) |0;
+    }
+  }
+
+  /* fill in remaining table entry if code is incomplete (guaranteed to have
+   at most one remaining entry, since if the code is incomplete, the
+   maximum code length that was allowed to get this far is one bit) */
+  if (huff !== 0) {
+    //table.op[next + huff] = 64;            /* invalid code marker */
+    //table.bits[next + huff] = len - drop;
+    //table.val[next + huff] = 0;
+    table[next + huff] = ((len - drop) << 24) | (64 << 16) |0;
+  }
+
+  /* set return parameters */
+  //opts.table_index += used;
+  opts.bits = root;
+  return 0;
+};
diff --git a/app/src/main/assets/novnc/vendor/pako/lib/zlib/messages.js b/app/src/main/assets/novnc/vendor/pako/lib/zlib/messages.js
new file mode 100644
index 00000000..f95cb704
--- /dev/null
+++ b/app/src/main/assets/novnc/vendor/pako/lib/zlib/messages.js
@@ -0,0 +1,11 @@
+export default {
+  2:      'need dictionary',     /* Z_NEED_DICT       2  */
+  1:      'stream end',          /* Z_STREAM_END      1  */
+  0:      '',                    /* Z_OK              0  */
+  '-1':   'file error',          /* Z_ERRNO         (-1) */
+  '-2':   'stream error',        /* Z_STREAM_ERROR  (-2) */
+  '-3':   'data error',          /* Z_DATA_ERROR    (-3) */
+  '-4':   'insufficient memory', /* Z_MEM_ERROR     (-4) */
+  '-5':   'buffer error',        /* Z_BUF_ERROR     (-5) */
+  '-6':   'incompatible version' /* Z_VERSION_ERROR (-6) */
+};
diff --git a/app/src/main/assets/novnc/vendor/pako/lib/zlib/trees.js b/app/src/main/assets/novnc/vendor/pako/lib/zlib/trees.js
new file mode 100644
index 00000000..a69b8a59
--- /dev/null
+++ b/app/src/main/assets/novnc/vendor/pako/lib/zlib/trees.js
@@ -0,0 +1,1195 @@
+import * as utils from "../utils/common.js";
+
+/* Public constants ==========================================================*/
+/* ===========================================================================*/
+
+
+//var Z_FILTERED          = 1;
+//var Z_HUFFMAN_ONLY      = 2;
+//var Z_RLE               = 3;
+var Z_FIXED               = 4;
+//var Z_DEFAULT_STRATEGY  = 0;
+
+/* Possible values of the data_type field (though see inflate()) */
+var Z_BINARY              = 0;
+var Z_TEXT                = 1;
+//var Z_ASCII             = 1; // = Z_TEXT
+var Z_UNKNOWN             = 2;
+
+/*============================================================================*/
+
+
+function zero(buf) { var len = buf.length; while (--len >= 0) { buf[len] = 0; } }
+
+// From zutil.h
+
+var STORED_BLOCK = 0;
+var STATIC_TREES = 1;
+var DYN_TREES    = 2;
+/* The three kinds of block type */
+
+var MIN_MATCH    = 3;
+var MAX_MATCH    = 258;
+/* The minimum and maximum match lengths */
+
+// From deflate.h
+/* ===========================================================================
+ * Internal compression state.
+ */
+
+var LENGTH_CODES  = 29;
+/* number of length codes, not counting the special END_BLOCK code */
+
+var LITERALS      = 256;
+/* number of literal bytes 0..255 */
+
+var L_CODES       = LITERALS + 1 + LENGTH_CODES;
+/* number of Literal or Length codes, including the END_BLOCK code */
+
+var D_CODES       = 30;
+/* number of distance codes */
+
+var BL_CODES      = 19;
+/* number of codes used to transfer the bit lengths */
+
+var HEAP_SIZE     = 2 * L_CODES + 1;
+/* maximum heap size */
+
+var MAX_BITS      = 15;
+/* All codes must not exceed MAX_BITS bits */
+
+var Buf_size      = 16;
+/* size of bit buffer in bi_buf */
+
+
+/* ===========================================================================
+ * Constants
+ */
+
+var MAX_BL_BITS = 7;
+/* Bit length codes must not exceed MAX_BL_BITS bits */
+
+var END_BLOCK   = 256;
+/* end of block literal code */
+
+var REP_3_6     = 16;
+/* repeat previous bit length 3-6 times (2 bits of repeat count) */
+
+var REPZ_3_10   = 17;
+/* repeat a zero length 3-10 times  (3 bits of repeat count) */
+
+var REPZ_11_138 = 18;
+/* repeat a zero length 11-138 times  (7 bits of repeat count) */
+
+/* eslint-disable comma-spacing,array-bracket-spacing */
+var extra_lbits =   /* extra bits for each length code */
+  [0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0];
+
+var extra_dbits =   /* extra bits for each distance code */
+  [0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13];
+
+var extra_blbits =  /* extra bits for each bit length code */
+  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7];
+
+var bl_order =
+  [16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15];
+/* eslint-enable comma-spacing,array-bracket-spacing */
+
+/* The lengths of the bit length codes are sent in order of decreasing
+ * probability, to avoid transmitting the lengths for unused bit length codes.
+ */
+
+/* ===========================================================================
+ * Local data. These are initialized only once.
+ */
+
+// We pre-fill arrays with 0 to avoid uninitialized gaps
+
+var DIST_CODE_LEN = 512; /* see definition of array dist_code below */
+
+// !!!! Use flat array insdead of structure, Freq = i*2, Len = i*2+1
+var static_ltree  = new Array((L_CODES + 2) * 2);
+zero(static_ltree);
+/* The static literal tree. Since the bit lengths are imposed, there is no
+ * need for the L_CODES extra codes used during heap construction. However
+ * The codes 286 and 287 are needed to build a canonical tree (see _tr_init
+ * below).
+ */
+
+var static_dtree  = new Array(D_CODES * 2);
+zero(static_dtree);
+/* The static distance tree. (Actually a trivial tree since all codes use
+ * 5 bits.)
+ */
+
+var _dist_code    = new Array(DIST_CODE_LEN);
+zero(_dist_code);
+/* Distance codes. The first 256 values correspond to the distances
+ * 3 .. 258, the last 256 values correspond to the top 8 bits of
+ * the 15 bit distances.
+ */
+
+var _length_code  = new Array(MAX_MATCH - MIN_MATCH + 1);
+zero(_length_code);
+/* length code for each normalized match length (0 == MIN_MATCH) */
+
+var base_length   = new Array(LENGTH_CODES);
+zero(base_length);
+/* First normalized length for each code (0 = MIN_MATCH) */
+
+var base_dist     = new Array(D_CODES);
+zero(base_dist);
+/* First normalized distance for each code (0 = distance of 1) */
+
+
+function StaticTreeDesc(static_tree, extra_bits, extra_base, elems, max_length) {
+
+  this.static_tree  = static_tree;  /* static tree or NULL */
+  this.extra_bits   = extra_bits;   /* extra bits for each code or NULL */
+  this.extra_base   = extra_base;   /* base index for extra_bits */
+  this.elems        = elems;        /* max number of elements in the tree */
+  this.max_length   = max_length;   /* max bit length for the codes */
+
+  // show if `static_tree` has data or dummy - needed for monomorphic objects
+  this.has_stree    = static_tree && static_tree.length;
+}
+
+
+var static_l_desc;
+var static_d_desc;
+var static_bl_desc;
+
+
+function TreeDesc(dyn_tree, stat_desc) {
+  this.dyn_tree = dyn_tree;     /* the dynamic tree */
+  this.max_code = 0;            /* largest code with non zero frequency */
+  this.stat_desc = stat_desc;   /* the corresponding static tree */
+}
+
+
+
+function d_code(dist) {
+  return dist < 256 ? _dist_code[dist] : _dist_code[256 + (dist >>> 7)];
+}
+
+
+/* ===========================================================================
+ * Output a short LSB first on the stream.
+ * IN assertion: there is enough room in pendingBuf.
+ */
+function put_short(s, w) {
+//    put_byte(s, (uch)((w) & 0xff));
+//    put_byte(s, (uch)((ush)(w) >> 8));
+  s.pending_buf[s.pending++] = (w) & 0xff;
+  s.pending_buf[s.pending++] = (w >>> 8) & 0xff;
+}
+
+
+/* ===========================================================================
+ * Send a value on a given number of bits.
+ * IN assertion: length <= 16 and value fits in length bits.
+ */
+function send_bits(s, value, length) {
+  if (s.bi_valid > (Buf_size - length)) {
+    s.bi_buf |= (value << s.bi_valid) & 0xffff;
+    put_short(s, s.bi_buf);
+    s.bi_buf = value >> (Buf_size - s.bi_valid);
+    s.bi_valid += length - Buf_size;
+  } else {
+    s.bi_buf |= (value << s.bi_valid) & 0xffff;
+    s.bi_valid += length;
+  }
+}
+
+
+function send_code(s, c, tree) {
+  send_bits(s, tree[c * 2]/*.Code*/, tree[c * 2 + 1]/*.Len*/);
+}
+
+
+/* ===========================================================================
+ * Reverse the first len bits of a code, using straightforward code (a faster
+ * method would use a table)
+ * IN assertion: 1 <= len <= 15
+ */
+function bi_reverse(code, len) {
+  var res = 0;
+  do {
+    res |= code & 1;
+    code >>>= 1;
+    res <<= 1;
+  } while (--len > 0);
+  return res >>> 1;
+}
+
+
+/* ===========================================================================
+ * Flush the bit buffer, keeping at most 7 bits in it.
+ */
+function bi_flush(s) {
+  if (s.bi_valid === 16) {
+    put_short(s, s.bi_buf);
+    s.bi_buf = 0;
+    s.bi_valid = 0;
+
+  } else if (s.bi_valid >= 8) {
+    s.pending_buf[s.pending++] = s.bi_buf & 0xff;
+    s.bi_buf >>= 8;
+    s.bi_valid -= 8;
+  }
+}
+
+
+/* ===========================================================================
+ * Compute the optimal bit lengths for a tree and update the total bit length
+ * for the current block.
+ * IN assertion: the fields freq and dad are set, heap[heap_max] and
+ *    above are the tree nodes sorted by increasing frequency.
+ * OUT assertions: the field len is set to the optimal bit length, the
+ *     array bl_count contains the frequencies for each bit length.
+ *     The length opt_len is updated; static_len is also updated if stree is
+ *     not null.
+ */
+function gen_bitlen(s, desc)
+//    deflate_state *s;
+//    tree_desc *desc;    /* the tree descriptor */
+{
+  var tree            = desc.dyn_tree;
+  var max_code        = desc.max_code;
+  var stree           = desc.stat_desc.static_tree;
+  var has_stree       = desc.stat_desc.has_stree;
+  var extra           = desc.stat_desc.extra_bits;
+  var base            = desc.stat_desc.extra_base;
+  var max_length      = desc.stat_desc.max_length;
+  var h;              /* heap index */
+  var n, m;           /* iterate over the tree elements */
+  var bits;           /* bit length */
+  var xbits;          /* extra bits */
+  var f;              /* frequency */
+  var overflow = 0;   /* number of elements with bit length too large */
+
+  for (bits = 0; bits <= MAX_BITS; bits++) {
+    s.bl_count[bits] = 0;
+  }
+
+  /* In a first pass, compute the optimal bit lengths (which may
+   * overflow in the case of the bit length tree).
+   */
+  tree[s.heap[s.heap_max] * 2 + 1]/*.Len*/ = 0; /* root of the heap */
+
+  for (h = s.heap_max + 1; h < HEAP_SIZE; h++) {
+    n = s.heap[h];
+    bits = tree[tree[n * 2 + 1]/*.Dad*/ * 2 + 1]/*.Len*/ + 1;
+    if (bits > max_length) {
+      bits = max_length;
+      overflow++;
+    }
+    tree[n * 2 + 1]/*.Len*/ = bits;
+    /* We overwrite tree[n].Dad which is no longer needed */
+
+    if (n > max_code) { continue; } /* not a leaf node */
+
+    s.bl_count[bits]++;
+    xbits = 0;
+    if (n >= base) {
+      xbits = extra[n - base];
+    }
+    f = tree[n * 2]/*.Freq*/;
+    s.opt_len += f * (bits + xbits);
+    if (has_stree) {
+      s.static_len += f * (stree[n * 2 + 1]/*.Len*/ + xbits);
+    }
+  }
+  if (overflow === 0) { return; }
+
+  // Trace((stderr,"\nbit length overflow\n"));
+  /* This happens for example on obj2 and pic of the Calgary corpus */
+
+  /* Find the first bit length which could increase: */
+  do {
+    bits = max_length - 1;
+    while (s.bl_count[bits] === 0) { bits--; }
+    s.bl_count[bits]--;      /* move one leaf down the tree */
+    s.bl_count[bits + 1] += 2; /* move one overflow item as its brother */
+    s.bl_count[max_length]--;
+    /* The brother of the overflow item also moves one step up,
+     * but this does not affect bl_count[max_length]
+     */
+    overflow -= 2;
+  } while (overflow > 0);
+
+  /* Now recompute all bit lengths, scanning in increasing frequency.
+   * h is still equal to HEAP_SIZE. (It is simpler to reconstruct all
+   * lengths instead of fixing only the wrong ones. This idea is taken
+   * from 'ar' written by Haruhiko Okumura.)
+   */
+  for (bits = max_length; bits !== 0; bits--) {
+    n = s.bl_count[bits];
+    while (n !== 0) {
+      m = s.heap[--h];
+      if (m > max_code) { continue; }
+      if (tree[m * 2 + 1]/*.Len*/ !== bits) {
+        // Trace((stderr,"code %d bits %d->%d\n", m, tree[m].Len, bits));
+        s.opt_len += (bits - tree[m * 2 + 1]/*.Len*/) * tree[m * 2]/*.Freq*/;
+        tree[m * 2 + 1]/*.Len*/ = bits;
+      }
+      n--;
+    }
+  }
+}
+
+
+/* ===========================================================================
+ * Generate the codes for a given tree and bit counts (which need not be
+ * optimal).
+ * IN assertion: the array bl_count contains the bit length statistics for
+ * the given tree and the field len is set for all tree elements.
+ * OUT assertion: the field code is set for all tree elements of non
+ *     zero code length.
+ */
+function gen_codes(tree, max_code, bl_count)
+//    ct_data *tree;             /* the tree to decorate */
+//    int max_code;              /* largest code with non zero frequency */
+//    ushf *bl_count;            /* number of codes at each bit length */
+{
+  var next_code = new Array(MAX_BITS + 1); /* next code value for each bit length */
+  var code = 0;              /* running code value */
+  var bits;                  /* bit index */
+  var n;                     /* code index */
+
+  /* The distribution counts are first used to generate the code values
+   * without bit reversal.
+   */
+  for (bits = 1; bits <= MAX_BITS; bits++) {
+    next_code[bits] = code = (code + bl_count[bits - 1]) << 1;
+  }
+  /* Check that the bit counts in bl_count are consistent. The last code
+   * must be all ones.
+   */
+  //Assert (code + bl_count[MAX_BITS]-1 == (1<<MAX_BITS)-1,
+  //        "inconsistent bit counts");
+  //Tracev((stderr,"\ngen_codes: max_code %d ", max_code));
+
+  for (n = 0;  n <= max_code; n++) {
+    var len = tree[n * 2 + 1]/*.Len*/;
+    if (len === 0) { continue; }
+    /* Now reverse the bits */
+    tree[n * 2]/*.Code*/ = bi_reverse(next_code[len]++, len);
+
+    //Tracecv(tree != static_ltree, (stderr,"\nn %3d %c l %2d c %4x (%x) ",
+    //     n, (isgraph(n) ? n : ' '), len, tree[n].Code, next_code[len]-1));
+  }
+}
+
+
+/* ===========================================================================
+ * Initialize the various 'constant' tables.
+ */
+function tr_static_init() {
+  var n;        /* iterates over tree elements */
+  var bits;     /* bit counter */
+  var length;   /* length value */
+  var code;     /* code value */
+  var dist;     /* distance index */
+  var bl_count = new Array(MAX_BITS + 1);
+  /* number of codes at each bit length for an optimal tree */
+
+  // do check in _tr_init()
+  //if (static_init_done) return;
+
+  /* For some embedded targets, global variables are not initialized: */
+/*#ifdef NO_INIT_GLOBAL_POINTERS
+  static_l_desc.static_tree = static_ltree;
+  static_l_desc.extra_bits = extra_lbits;
+  static_d_desc.static_tree = static_dtree;
+  static_d_desc.extra_bits = extra_dbits;
+  static_bl_desc.extra_bits = extra_blbits;
+#endif*/
+
+  /* Initialize the mapping length (0..255) -> length code (0..28) */
+  length = 0;
+  for (code = 0; code < LENGTH_CODES - 1; code++) {
+    base_length[code] = length;
+    for (n = 0; n < (1 << extra_lbits[code]); n++) {
+      _length_code[length++] = code;
+    }
+  }
+  //Assert (length == 256, "tr_static_init: length != 256");
+  /* Note that the length 255 (match length 258) can be represented
+   * in two different ways: code 284 + 5 bits or code 285, so we
+   * overwrite length_code[255] to use the best encoding:
+   */
+  _length_code[length - 1] = code;
+
+  /* Initialize the mapping dist (0..32K) -> dist code (0..29) */
+  dist = 0;
+  for (code = 0; code < 16; code++) {
+    base_dist[code] = dist;
+    for (n = 0; n < (1 << extra_dbits[code]); n++) {
+      _dist_code[dist++] = code;
+    }
+  }
+  //Assert (dist == 256, "tr_static_init: dist != 256");
+  dist >>= 7; /* from now on, all distances are divided by 128 */
+  for (; code < D_CODES; code++) {
+    base_dist[code] = dist << 7;
+    for (n = 0; n < (1 << (extra_dbits[code] - 7)); n++) {
+      _dist_code[256 + dist++] = code;
+    }
+  }
+  //Assert (dist == 256, "tr_static_init: 256+dist != 512");
+
+  /* Construct the codes of the static literal tree */
+  for (bits = 0; bits <= MAX_BITS; bits++) {
+    bl_count[bits] = 0;
+  }
+
+  n = 0;
+  while (n <= 143) {
+    static_ltree[n * 2 + 1]/*.Len*/ = 8;
+    n++;
+    bl_count[8]++;
+  }
+  while (n <= 255) {
+    static_ltree[n * 2 + 1]/*.Len*/ = 9;
+    n++;
+    bl_count[9]++;
+  }
+  while (n <= 279) {
+    static_ltree[n * 2 + 1]/*.Len*/ = 7;
+    n++;
+    bl_count[7]++;
+  }
+  while (n <= 287) {
+    static_ltree[n * 2 + 1]/*.Len*/ = 8;
+    n++;
+    bl_count[8]++;
+  }
+  /* Codes 286 and 287 do not exist, but we must include them in the
+   * tree construction to get a canonical Huffman tree (longest code
+   * all ones)
+   */
+  gen_codes(static_ltree, L_CODES + 1, bl_count);
+
+  /* The static distance tree is trivial: */
+  for (n = 0; n < D_CODES; n++) {
+    static_dtree[n * 2 + 1]/*.Len*/ = 5;
+    static_dtree[n * 2]/*.Code*/ = bi_reverse(n, 5);
+  }
+
+  // Now data ready and we can init static trees
+  static_l_desc = new StaticTreeDesc(static_ltree, extra_lbits, LITERALS + 1, L_CODES, MAX_BITS);
+  static_d_desc = new StaticTreeDesc(static_dtree, extra_dbits, 0,          D_CODES, MAX_BITS);
+  static_bl_desc = new StaticTreeDesc(new Array(0), extra_blbits, 0,         BL_CODES, MAX_BL_BITS);
+
+  //static_init_done = true;
+}
+
+
+/* ===========================================================================
+ * Initialize a new block.
+ */
+function init_block(s) {
+  var n; /* iterates over tree elements */
+
+  /* Initialize the trees. */
+  for (n = 0; n < L_CODES;  n++) { s.dyn_ltree[n * 2]/*.Freq*/ = 0; }
+  for (n = 0; n < D_CODES;  n++) { s.dyn_dtree[n * 2]/*.Freq*/ = 0; }
+  for (n = 0; n < BL_CODES; n++) { s.bl_tree[n * 2]/*.Freq*/ = 0; }
+
+  s.dyn_ltree[END_BLOCK * 2]/*.Freq*/ = 1;
+  s.opt_len = s.static_len = 0;
+  s.last_lit = s.matches = 0;
+}
+
+
+/* ===========================================================================
+ * Flush the bit buffer and align the output on a byte boundary
+ */
+function bi_windup(s)
+{
+  if (s.bi_valid > 8) {
+    put_short(s, s.bi_buf);
+  } else if (s.bi_valid > 0) {
+    //put_byte(s, (Byte)s->bi_buf);
+    s.pending_buf[s.pending++] = s.bi_buf;
+  }
+  s.bi_buf = 0;
+  s.bi_valid = 0;
+}
+
+/* ===========================================================================
+ * Copy a stored block, storing first the length and its
+ * one's complement if requested.
+ */
+function copy_block(s, buf, len, header)
+//DeflateState *s;
+//charf    *buf;    /* the input data */
+//unsigned len;     /* its length */
+//int      header;  /* true if block header must be written */
+{
+  bi_windup(s);        /* align on byte boundary */
+
+  if (header) {
+    put_short(s, len);
+    put_short(s, ~len);
+  }
+//  while (len--) {
+//    put_byte(s, *buf++);
+//  }
+  utils.arraySet(s.pending_buf, s.window, buf, len, s.pending);
+  s.pending += len;
+}
+
+/* ===========================================================================
+ * Compares to subtrees, using the tree depth as tie breaker when
+ * the subtrees have equal frequency. This minimizes the worst case length.
+ */
+function smaller(tree, n, m, depth) {
+  var _n2 = n * 2;
+  var _m2 = m * 2;
+  return (tree[_n2]/*.Freq*/ < tree[_m2]/*.Freq*/ ||
+         (tree[_n2]/*.Freq*/ === tree[_m2]/*.Freq*/ && depth[n] <= depth[m]));
+}
+
+/* ===========================================================================
+ * Restore the heap property by moving down the tree starting at node k,
+ * exchanging a node with the smallest of its two sons if necessary, stopping
+ * when the heap property is re-established (each father smaller than its
+ * two sons).
+ */
+function pqdownheap(s, tree, k)
+//    deflate_state *s;
+//    ct_data *tree;  /* the tree to restore */
+//    int k;               /* node to move down */
+{
+  var v = s.heap[k];
+  var j = k << 1;  /* left son of k */
+  while (j <= s.heap_len) {
+    /* Set j to the smallest of the two sons: */
+    if (j < s.heap_len &&
+      smaller(tree, s.heap[j + 1], s.heap[j], s.depth)) {
+      j++;
+    }
+    /* Exit if v is smaller than both sons */
+    if (smaller(tree, v, s.heap[j], s.depth)) { break; }
+
+    /* Exchange v with the smallest son */
+    s.heap[k] = s.heap[j];
+    k = j;
+
+    /* And continue down the tree, setting j to the left son of k */
+    j <<= 1;
+  }
+  s.heap[k] = v;
+}
+
+
+// inlined manually
+// var SMALLEST = 1;
+
+/* ===========================================================================
+ * Send the block data compressed using the given Huffman trees
+ */
+function compress_block(s, ltree, dtree)
+//    deflate_state *s;
+//    const ct_data *ltree; /* literal tree */
+//    const ct_data *dtree; /* distance tree */
+{
+  var dist;           /* distance of matched string */
+  var lc;             /* match length or unmatched char (if dist == 0) */
+  var lx = 0;         /* running index in l_buf */
+  var code;           /* the code to send */
+  var extra;          /* number of extra bits to send */
+
+  if (s.last_lit !== 0) {
+    do {
+      dist = (s.pending_buf[s.d_buf + lx * 2] << 8) | (s.pending_buf[s.d_buf + lx * 2 + 1]);
+      lc = s.pending_buf[s.l_buf + lx];
+      lx++;
+
+      if (dist === 0) {
+        send_code(s, lc, ltree); /* send a literal byte */
+        //Tracecv(isgraph(lc), (stderr," '%c' ", lc));
+      } else {
+        /* Here, lc is the match length - MIN_MATCH */
+        code = _length_code[lc];
+        send_code(s, code + LITERALS + 1, ltree); /* send the length code */
+        extra = extra_lbits[code];
+        if (extra !== 0) {
+          lc -= base_length[code];
+          send_bits(s, lc, extra);       /* send the extra length bits */
+        }
+        dist--; /* dist is now the match distance - 1 */
+        code = d_code(dist);
+        //Assert (code < D_CODES, "bad d_code");
+
+        send_code(s, code, dtree);       /* send the distance code */
+        extra = extra_dbits[code];
+        if (extra !== 0) {
+          dist -= base_dist[code];
+          send_bits(s, dist, extra);   /* send the extra distance bits */
+        }
+      } /* literal or match pair ? */
+
+      /* Check that the overlay between pending_buf and d_buf+l_buf is ok: */
+      //Assert((uInt)(s->pending) < s->lit_bufsize + 2*lx,
+      //       "pendingBuf overflow");
+
+    } while (lx < s.last_lit);
+  }
+
+  send_code(s, END_BLOCK, ltree);
+}
+
+
+/* ===========================================================================
+ * Construct one Huffman tree and assigns the code bit strings and lengths.
+ * Update the total bit length for the current block.
+ * IN assertion: the field freq is set for all tree elements.
+ * OUT assertions: the fields len and code are set to the optimal bit length
+ *     and corresponding code. The length opt_len is updated; static_len is
+ *     also updated if stree is not null. The field max_code is set.
+ */
+function build_tree(s, desc)
+//    deflate_state *s;
+//    tree_desc *desc; /* the tree descriptor */
+{
+  var tree     = desc.dyn_tree;
+  var stree    = desc.stat_desc.static_tree;
+  var has_stree = desc.stat_desc.has_stree;
+  var elems    = desc.stat_desc.elems;
+  var n, m;          /* iterate over heap elements */
+  var max_code = -1; /* largest code with non zero frequency */
+  var node;          /* new node being created */
+
+  /* Construct the initial heap, with least frequent element in
+   * heap[SMALLEST]. The sons of heap[n] are heap[2*n] and heap[2*n+1].
+   * heap[0] is not used.
+   */
+  s.heap_len = 0;
+  s.heap_max = HEAP_SIZE;
+
+  for (n = 0; n < elems; n++) {
+    if (tree[n * 2]/*.Freq*/ !== 0) {
+      s.heap[++s.heap_len] = max_code = n;
+      s.depth[n] = 0;
+
+    } else {
+      tree[n * 2 + 1]/*.Len*/ = 0;
+    }
+  }
+
+  /* The pkzip format requires that at least one distance code exists,
+   * and that at least one bit should be sent even if there is only one
+   * possible code. So to avoid special checks later on we force at least
+   * two codes of non zero frequency.
+   */
+  while (s.heap_len < 2) {
+    node = s.heap[++s.heap_len] = (max_code < 2 ? ++max_code : 0);
+    tree[node * 2]/*.Freq*/ = 1;
+    s.depth[node] = 0;
+    s.opt_len--;
+
+    if (has_stree) {
+      s.static_len -= stree[node * 2 + 1]/*.Len*/;
+    }
+    /* node is 0 or 1 so it does not have extra bits */
+  }
+  desc.max_code = max_code;
+
+  /* The elements heap[heap_len/2+1 .. heap_len] are leaves of the tree,
+   * establish sub-heaps of increasing lengths:
+   */
+  for (n = (s.heap_len >> 1/*int /2*/); n >= 1; n--) { pqdownheap(s, tree, n); }
+
+  /* Construct the Huffman tree by repeatedly combining the least two
+   * frequent nodes.
+   */
+  node = elems;              /* next internal node of the tree */
+  do {
+    //pqremove(s, tree, n);  /* n = node of least frequency */
+    /*** pqremove ***/
+    n = s.heap[1/*SMALLEST*/];
+    s.heap[1/*SMALLEST*/] = s.heap[s.heap_len--];
+    pqdownheap(s, tree, 1/*SMALLEST*/);
+    /***/
+
+    m = s.heap[1/*SMALLEST*/]; /* m = node of next least frequency */
+
+    s.heap[--s.heap_max] = n; /* keep the nodes sorted by frequency */
+    s.heap[--s.heap_max] = m;
+
+    /* Create a new node father of n and m */
+    tree[node * 2]/*.Freq*/ = tree[n * 2]/*.Freq*/ + tree[m * 2]/*.Freq*/;
+    s.depth[node] = (s.depth[n] >= s.depth[m] ? s.depth[n] : s.depth[m]) + 1;
+    tree[n * 2 + 1]/*.Dad*/ = tree[m * 2 + 1]/*.Dad*/ = node;
+
+    /* and insert the new node in the heap */
+    s.heap[1/*SMALLEST*/] = node++;
+    pqdownheap(s, tree, 1/*SMALLEST*/);
+
+  } while (s.heap_len >= 2);
+
+  s.heap[--s.heap_max] = s.heap[1/*SMALLEST*/];
+
+  /* At this point, the fields freq and dad are set. We can now
+   * generate the bit lengths.
+   */
+  gen_bitlen(s, desc);
+
+  /* The field len is now set, we can generate the bit codes */
+  gen_codes(tree, max_code, s.bl_count);
+}
+
+
+/* ===========================================================================
+ * Scan a literal or distance tree to determine the frequencies of the codes
+ * in the bit length tree.
+ */
+function scan_tree(s, tree, max_code)
+//    deflate_state *s;
+//    ct_data *tree;   /* the tree to be scanned */
+//    int max_code;    /* and its largest code of non zero frequency */
+{
+  var n;                     /* iterates over all tree elements */
+  var prevlen = -1;          /* last emitted length */
+  var curlen;                /* length of current code */
+
+  var nextlen = tree[0 * 2 + 1]/*.Len*/; /* length of next code */
+
+  var count = 0;             /* repeat count of the current code */
+  var max_count = 7;         /* max repeat count */
+  var min_count = 4;         /* min repeat count */
+
+  if (nextlen === 0) {
+    max_count = 138;
+    min_count = 3;
+  }
+  tree[(max_code + 1) * 2 + 1]/*.Len*/ = 0xffff; /* guard */
+
+  for (n = 0; n <= max_code; n++) {
+    curlen = nextlen;
+    nextlen = tree[(n + 1) * 2 + 1]/*.Len*/;
+
+    if (++count < max_count && curlen === nextlen) {
+      continue;
+
+    } else if (count < min_count) {
+      s.bl_tree[curlen * 2]/*.Freq*/ += count;
+
+    } else if (curlen !== 0) {
+
+      if (curlen !== prevlen) { s.bl_tree[curlen * 2]/*.Freq*/++; }
+      s.bl_tree[REP_3_6 * 2]/*.Freq*/++;
+
+    } else if (count <= 10) {
+      s.bl_tree[REPZ_3_10 * 2]/*.Freq*/++;
+
+    } else {
+      s.bl_tree[REPZ_11_138 * 2]/*.Freq*/++;
+    }
+
+    count = 0;
+    prevlen = curlen;
+
+    if (nextlen === 0) {
+      max_count = 138;
+      min_count = 3;
+
+    } else if (curlen === nextlen) {
+      max_count = 6;
+      min_count = 3;
+
+    } else {
+      max_count = 7;
+      min_count = 4;
+    }
+  }
+}
+
+
+/* ===========================================================================
+ * Send a literal or distance tree in compressed form, using the codes in
+ * bl_tree.
+ */
+function send_tree(s, tree, max_code)
+//    deflate_state *s;
+//    ct_data *tree; /* the tree to be scanned */
+//    int max_code;       /* and its largest code of non zero frequency */
+{
+  var n;                     /* iterates over all tree elements */
+  var prevlen = -1;          /* last emitted length */
+  var curlen;                /* length of current code */
+
+  var nextlen = tree[0 * 2 + 1]/*.Len*/; /* length of next code */
+
+  var count = 0;             /* repeat count of the current code */
+  var max_count = 7;         /* max repeat count */
+  var min_count = 4;         /* min repeat count */
+
+  /* tree[max_code+1].Len = -1; */  /* guard already set */
+  if (nextlen === 0) {
+    max_count = 138;
+    min_count = 3;
+  }
+
+  for (n = 0; n <= max_code; n++) {
+    curlen = nextlen;
+    nextlen = tree[(n + 1) * 2 + 1]/*.Len*/;
+
+    if (++count < max_count && curlen === nextlen) {
+      continue;
+
+    } else if (count < min_count) {
+      do { send_code(s, curlen, s.bl_tree); } while (--count !== 0);
+
+    } else if (curlen !== 0) {
+      if (curlen !== prevlen) {
+        send_code(s, curlen, s.bl_tree);
+        count--;
+      }
+      //Assert(count >= 3 && count <= 6, " 3_6?");
+      send_code(s, REP_3_6, s.bl_tree);
+      send_bits(s, count - 3, 2);
+
+    } else if (count <= 10) {
+      send_code(s, REPZ_3_10, s.bl_tree);
+      send_bits(s, count - 3, 3);
+
+    } else {
+      send_code(s, REPZ_11_138, s.bl_tree);
+      send_bits(s, count - 11, 7);
+    }
+
+    count = 0;
+    prevlen = curlen;
+    if (nextlen === 0) {
+      max_count = 138;
+      min_count = 3;
+
+    } else if (curlen === nextlen) {
+      max_count = 6;
+      min_count = 3;
+
+    } else {
+      max_count = 7;
+      min_count = 4;
+    }
+  }
+}
+
+
+/* ===========================================================================
+ * Construct the Huffman tree for the bit lengths and return the index in
+ * bl_order of the last bit length code to send.
+ */
+function build_bl_tree(s) {
+  var max_blindex;  /* index of last bit length code of non zero freq */
+
+  /* Determine the bit length frequencies for literal and distance trees */
+  scan_tree(s, s.dyn_ltree, s.l_desc.max_code);
+  scan_tree(s, s.dyn_dtree, s.d_desc.max_code);
+
+  /* Build the bit length tree: */
+  build_tree(s, s.bl_desc);
+  /* opt_len now includes the length of the tree representations, except
+   * the lengths of the bit lengths codes and the 5+5+4 bits for the counts.
+   */
+
+  /* Determine the number of bit length codes to send. The pkzip format
+   * requires that at least 4 bit length codes be sent. (appnote.txt says
+   * 3 but the actual value used is 4.)
+   */
+  for (max_blindex = BL_CODES - 1; max_blindex >= 3; max_blindex--) {
+    if (s.bl_tree[bl_order[max_blindex] * 2 + 1]/*.Len*/ !== 0) {
+      break;
+    }
+  }
+  /* Update opt_len to include the bit length tree and counts */
+  s.opt_len += 3 * (max_blindex + 1) + 5 + 5 + 4;
+  //Tracev((stderr, "\ndyn trees: dyn %ld, stat %ld",
+  //        s->opt_len, s->static_len));
+
+  return max_blindex;
+}
+
+
+/* ===========================================================================
+ * Send the header for a block using dynamic Huffman trees: the counts, the
+ * lengths of the bit length codes, the literal tree and the distance tree.
+ * IN assertion: lcodes >= 257, dcodes >= 1, blcodes >= 4.
+ */
+function send_all_trees(s, lcodes, dcodes, blcodes)
+//    deflate_state *s;
+//    int lcodes, dcodes, blcodes; /* number of codes for each tree */
+{
+  var rank;                    /* index in bl_order */
+
+  //Assert (lcodes >= 257 && dcodes >= 1 && blcodes >= 4, "not enough codes");
+  //Assert (lcodes <= L_CODES && dcodes <= D_CODES && blcodes <= BL_CODES,
+  //        "too many codes");
+  //Tracev((stderr, "\nbl counts: "));
+  send_bits(s, lcodes - 257, 5); /* not +255 as stated in appnote.txt */
+  send_bits(s, dcodes - 1,   5);
+  send_bits(s, blcodes - 4,  4); /* not -3 as stated in appnote.txt */
+  for (rank = 0; rank < blcodes; rank++) {
+    //Tracev((stderr, "\nbl code %2d ", bl_order[rank]));
+    send_bits(s, s.bl_tree[bl_order[rank] * 2 + 1]/*.Len*/, 3);
+  }
+  //Tracev((stderr, "\nbl tree: sent %ld", s->bits_sent));
+
+  send_tree(s, s.dyn_ltree, lcodes - 1); /* literal tree */
+  //Tracev((stderr, "\nlit tree: sent %ld", s->bits_sent));
+
+  send_tree(s, s.dyn_dtree, dcodes - 1); /* distance tree */
+  //Tracev((stderr, "\ndist tree: sent %ld", s->bits_sent));
+}
+
+
+/* ===========================================================================
+ * Check if the data type is TEXT or BINARY, using the following algorithm:
+ * - TEXT if the two conditions below are satisfied:
+ *    a) There are no non-portable control characters belonging to the
+ *       "black list" (0..6, 14..25, 28..31).
+ *    b) There is at least one printable character belonging to the
+ *       "white list" (9 {TAB}, 10 {LF}, 13 {CR}, 32..255).
+ * - BINARY otherwise.
+ * - The following partially-portable control characters form a
+ *   "gray list" that is ignored in this detection algorithm:
+ *   (7 {BEL}, 8 {BS}, 11 {VT}, 12 {FF}, 26 {SUB}, 27 {ESC}).
+ * IN assertion: the fields Freq of dyn_ltree are set.
+ */
+function detect_data_type(s) {
+  /* black_mask is the bit mask of black-listed bytes
+   * set bits 0..6, 14..25, and 28..31
+   * 0xf3ffc07f = binary 11110011111111111100000001111111
+   */
+  var black_mask = 0xf3ffc07f;
+  var n;
+
+  /* Check for non-textual ("black-listed") bytes. */
+  for (n = 0; n <= 31; n++, black_mask >>>= 1) {
+    if ((black_mask & 1) && (s.dyn_ltree[n * 2]/*.Freq*/ !== 0)) {
+      return Z_BINARY;
+    }
+  }
+
+  /* Check for textual ("white-listed") bytes. */
+  if (s.dyn_ltree[9 * 2]/*.Freq*/ !== 0 || s.dyn_ltree[10 * 2]/*.Freq*/ !== 0 ||
+      s.dyn_ltree[13 * 2]/*.Freq*/ !== 0) {
+    return Z_TEXT;
+  }
+  for (n = 32; n < LITERALS; n++) {
+    if (s.dyn_ltree[n * 2]/*.Freq*/ !== 0) {
+      return Z_TEXT;
+    }
+  }
+
+  /* There are no "black-listed" or "white-listed" bytes:
+   * this stream either is empty or has tolerated ("gray-listed") bytes only.
+   */
+  return Z_BINARY;
+}
+
+
+var static_init_done = false;
+
+/* ===========================================================================
+ * Initialize the tree data structures for a new zlib stream.
+ */
+function _tr_init(s)
+{
+
+  if (!static_init_done) {
+    tr_static_init();
+    static_init_done = true;
+  }
+
+  s.l_desc  = new TreeDesc(s.dyn_ltree, static_l_desc);
+  s.d_desc  = new TreeDesc(s.dyn_dtree, static_d_desc);
+  s.bl_desc = new TreeDesc(s.bl_tree, static_bl_desc);
+
+  s.bi_buf = 0;
+  s.bi_valid = 0;
+
+  /* Initialize the first block of the first file: */
+  init_block(s);
+}
+
+
+/* ===========================================================================
+ * Send a stored block
+ */
+function _tr_stored_block(s, buf, stored_len, last)
+//DeflateState *s;
+//charf *buf;       /* input block */
+//ulg stored_len;   /* length of input block */
+//int last;         /* one if this is the last block for a file */
+{
+  send_bits(s, (STORED_BLOCK << 1) + (last ? 1 : 0), 3);    /* send block type */
+  copy_block(s, buf, stored_len, true); /* with header */
+}
+
+
+/* ===========================================================================
+ * Send one empty static block to give enough lookahead for inflate.
+ * This takes 10 bits, of which 7 may remain in the bit buffer.
+ */
+function _tr_align(s) {
+  send_bits(s, STATIC_TREES << 1, 3);
+  send_code(s, END_BLOCK, static_ltree);
+  bi_flush(s);
+}
+
+
+/* ===========================================================================
+ * Determine the best encoding for the current block: dynamic trees, static
+ * trees or store, and output the encoded block to the zip file.
+ */
+function _tr_flush_block(s, buf, stored_len, last)
+//DeflateState *s;
+//charf *buf;       /* input block, or NULL if too old */
+//ulg stored_len;   /* length of input block */
+//int last;         /* one if this is the last block for a file */
+{
+  var opt_lenb, static_lenb;  /* opt_len and static_len in bytes */
+  var max_blindex = 0;        /* index of last bit length code of non zero freq */
+
+  /* Build the Huffman trees unless a stored block is forced */
+  if (s.level > 0) {
+
+    /* Check if the file is binary or text */
+    if (s.strm.data_type === Z_UNKNOWN) {
+      s.strm.data_type = detect_data_type(s);
+    }
+
+    /* Construct the literal and distance trees */
+    build_tree(s, s.l_desc);
+    // Tracev((stderr, "\nlit data: dyn %ld, stat %ld", s->opt_len,
+    //        s->static_len));
+
+    build_tree(s, s.d_desc);
+    // Tracev((stderr, "\ndist data: dyn %ld, stat %ld", s->opt_len,
+    //        s->static_len));
+    /* At this point, opt_len and static_len are the total bit lengths of
+     * the compressed block data, excluding the tree representations.
+     */
+
+    /* Build the bit length tree for the above two trees, and get the index
+     * in bl_order of the last bit length code to send.
+     */
+    max_blindex = build_bl_tree(s);
+
+    /* Determine the best encoding. Compute the block lengths in bytes. */
+    opt_lenb = (s.opt_len + 3 + 7) >>> 3;
+    static_lenb = (s.static_len + 3 + 7) >>> 3;
+
+    // Tracev((stderr, "\nopt %lu(%lu) stat %lu(%lu) stored %lu lit %u ",
+    //        opt_lenb, s->opt_len, static_lenb, s->static_len, stored_len,
+    //        s->last_lit));
+
+    if (static_lenb <= opt_lenb) { opt_lenb = static_lenb; }
+
+  } else {
+    // Assert(buf != (char*)0, "lost buf");
+    opt_lenb = static_lenb = stored_len + 5; /* force a stored block */
+  }
+
+  if ((stored_len + 4 <= opt_lenb) && (buf !== -1)) {
+    /* 4: two words for the lengths */
+
+    /* The test buf != NULL is only necessary if LIT_BUFSIZE > WSIZE.
+     * Otherwise we can't have processed more than WSIZE input bytes since
+     * the last block flush, because compression would have been
+     * successful. If LIT_BUFSIZE <= WSIZE, it is never too late to
+     * transform a block into a stored block.
+     */
+    _tr_stored_block(s, buf, stored_len, last);
+
+  } else if (s.strategy === Z_FIXED || static_lenb === opt_lenb) {
+
+    send_bits(s, (STATIC_TREES << 1) + (last ? 1 : 0), 3);
+    compress_block(s, static_ltree, static_dtree);
+
+  } else {
+    send_bits(s, (DYN_TREES << 1) + (last ? 1 : 0), 3);
+    send_all_trees(s, s.l_desc.max_code + 1, s.d_desc.max_code + 1, max_blindex + 1);
+    compress_block(s, s.dyn_ltree, s.dyn_dtree);
+  }
+  // Assert (s->compressed_len == s->bits_sent, "bad compressed size");
+  /* The above check is made mod 2^32, for files larger than 512 MB
+   * and uLong implemented on 32 bits.
+   */
+  init_block(s);
+
+  if (last) {
+    bi_windup(s);
+  }
+  // Tracev((stderr,"\ncomprlen %lu(%lu) ", s->compressed_len>>3,
+  //       s->compressed_len-7*last));
+}
+
+/* ===========================================================================
+ * Save the match info and tally the frequency counts. Return true if
+ * the current block must be flushed.
+ */
+function _tr_tally(s, dist, lc)
+//    deflate_state *s;
+//    unsigned dist;  /* distance of matched string */
+//    unsigned lc;    /* match length-MIN_MATCH or unmatched char (if dist==0) */
+{
+  //var out_length, in_length, dcode;
+
+  s.pending_buf[s.d_buf + s.last_lit * 2]     = (dist >>> 8) & 0xff;
+  s.pending_buf[s.d_buf + s.last_lit * 2 + 1] = dist & 0xff;
+
+  s.pending_buf[s.l_buf + s.last_lit] = lc & 0xff;
+  s.last_lit++;
+
+  if (dist === 0) {
+    /* lc is the unmatched char */
+    s.dyn_ltree[lc * 2]/*.Freq*/++;
+  } else {
+    s.matches++;
+    /* Here, lc is the match length - MIN_MATCH */
+    dist--;             /* dist = match distance - 1 */
+    //Assert((ush)dist < (ush)MAX_DIST(s) &&
+    //       (ush)lc <= (ush)(MAX_MATCH-MIN_MATCH) &&
+    //       (ush)d_code(dist) < (ush)D_CODES,  "_tr_tally: bad match");
+
+    s.dyn_ltree[(_length_code[lc] + LITERALS + 1) * 2]/*.Freq*/++;
+    s.dyn_dtree[d_code(dist) * 2]/*.Freq*/++;
+  }
+
+// (!) This block is disabled in zlib defailts,
+// don't enable it for binary compatibility
+
+//#ifdef TRUNCATE_BLOCK
+//  /* Try to guess if it is profitable to stop the current block here */
+//  if ((s.last_lit & 0x1fff) === 0 && s.level > 2) {
+//    /* Compute an upper bound for the compressed length */
+//    out_length = s.last_lit*8;
+//    in_length = s.strstart - s.block_start;
+//
+//    for (dcode = 0; dcode < D_CODES; dcode++) {
+//      out_length += s.dyn_dtree[dcode*2]/*.Freq*/ * (5 + extra_dbits[dcode]);
+//    }
+//    out_length >>>= 3;
+//    //Tracev((stderr,"\nlast_lit %u, in %ld, out ~%ld(%ld%%) ",
+//    //       s->last_lit, in_length, out_length,
+//    //       100L - out_length*100L/in_length));
+//    if (s.matches < (s.last_lit>>1)/*int /2*/ && out_length < (in_length>>1)/*int /2*/) {
+//      return true;
+//    }
+//  }
+//#endif
+
+  return (s.last_lit === s.lit_bufsize - 1);
+  /* We avoid equality with lit_bufsize because of wraparound at 64K
+   * on 16 bit machines and because stored blocks are restricted to
+   * 64K-1 bytes.
+   */
+}
+
+export { _tr_init, _tr_stored_block, _tr_flush_block, _tr_tally, _tr_align };
diff --git a/app/src/main/assets/novnc/vendor/pako/lib/zlib/zstream.js b/app/src/main/assets/novnc/vendor/pako/lib/zlib/zstream.js
new file mode 100644
index 00000000..e7e674e7
--- /dev/null
+++ b/app/src/main/assets/novnc/vendor/pako/lib/zlib/zstream.js
@@ -0,0 +1,24 @@
+export default function ZStream() {
+  /* next input byte */
+  this.input = null; // JS specific, because we have no pointers
+  this.next_in = 0;
+  /* number of bytes available at input */
+  this.avail_in = 0;
+  /* total number of input bytes read so far */
+  this.total_in = 0;
+  /* next output byte should be put there */
+  this.output = null; // JS specific, because we have no pointers
+  this.next_out = 0;
+  /* remaining free space at output */
+  this.avail_out = 0;
+  /* total number of bytes output so far */
+  this.total_out = 0;
+  /* last error message, NULL if no error */
+  this.msg = ''/*Z_NULL*/;
+  /* not visible by applications */
+  this.state = null;
+  /* best guess about the data type: binary or text */
+  this.data_type = 2/*Z_UNKNOWN*/;
+  /* adler32 value of the uncompressed data */
+  this.adler = 0;
+}
diff --git a/app/src/main/assets/novnc/vnc.html b/app/src/main/assets/novnc/vnc.html
new file mode 100644
index 00000000..8d4b4979
--- /dev/null
+++ b/app/src/main/assets/novnc/vnc.html
@@ -0,0 +1,320 @@
+<!DOCTYPE html>
+<html lang="en" class="noVNC_loading">
+<head>
+
+    <!--
+    noVNC example: simple example using default UI
+    Copyright (C) 2019 The noVNC Authors
+    noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
+    This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
+
+    Connect parameters are provided in query string:
+        http://example.com/?host=HOST&port=PORT&encrypt=1
+    or the fragment:
+        http://example.com/#host=HOST&port=PORT&encrypt=1
+    -->
+    <title>noVNC</title>
+
+    <meta charset="utf-8">
+
+    <!-- Icons (see app/images/icons/Makefile for what the sizes are for) -->
+    <link rel="icon" sizes="16x16" type="image/png" href="app/images/icons/novnc-16x16.png">
+    <link rel="icon" sizes="24x24" type="image/png" href="app/images/icons/novnc-24x24.png">
+    <link rel="icon" sizes="32x32" type="image/png" href="app/images/icons/novnc-32x32.png">
+    <link rel="icon" sizes="48x48" type="image/png" href="app/images/icons/novnc-48x48.png">
+    <link rel="icon" sizes="60x60" type="image/png" href="app/images/icons/novnc-60x60.png">
+    <link rel="icon" sizes="64x64" type="image/png" href="app/images/icons/novnc-64x64.png">
+    <link rel="icon" sizes="72x72" type="image/png" href="app/images/icons/novnc-72x72.png">
+    <link rel="icon" sizes="76x76" type="image/png" href="app/images/icons/novnc-76x76.png">
+    <link rel="icon" sizes="96x96" type="image/png" href="app/images/icons/novnc-96x96.png">
+    <link rel="icon" sizes="120x120" type="image/png" href="app/images/icons/novnc-120x120.png">
+    <link rel="icon" sizes="144x144" type="image/png" href="app/images/icons/novnc-144x144.png">
+    <link rel="icon" sizes="152x152" type="image/png" href="app/images/icons/novnc-152x152.png">
+    <link rel="icon" sizes="192x192" type="image/png" href="app/images/icons/novnc-192x192.png">
+    <!-- Firefox currently mishandles SVG, see #1419039
+    <link rel="icon" sizes="any" type="image/svg+xml" href="app/images/icons/novnc-icon.svg">
+    -->
+    <!-- Repeated last so that legacy handling will pick this -->
+    <link rel="icon" sizes="16x16" type="image/png" href="app/images/icons/novnc-16x16.png">
+
+    <!-- Apple iOS Safari settings -->
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+    <meta name="apple-mobile-web-app-capable" content="yes">
+    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
+    <!-- Home Screen Icons (favourites and bookmarks use the normal icons) -->
+    <link rel="apple-touch-icon" sizes="60x60" type="image/png" href="app/images/icons/novnc-60x60.png">
+    <link rel="apple-touch-icon" sizes="76x76" type="image/png" href="app/images/icons/novnc-76x76.png">
+    <link rel="apple-touch-icon" sizes="120x120" type="image/png" href="app/images/icons/novnc-120x120.png">
+    <link rel="apple-touch-icon" sizes="152x152" type="image/png" href="app/images/icons/novnc-152x152.png">
+
+    <!-- Stylesheets -->
+    <link rel="stylesheet" href="app/styles/base.css">
+
+    <!-- Images that will later appear via CSS -->
+    <link rel="preload" as="image" href="app/images/info.svg">
+    <link rel="preload" as="image" href="app/images/error.svg">
+    <link rel="preload" as="image" href="app/images/warning.svg">
+
+    <script src="app/error-handler.js"></script>
+    <script type="module" crossorigin="anonymous" src="app/ui.js"></script>
+</head>
+
+<body>
+
+    <div id="noVNC_fallback_error" class="noVNC_center">
+        <div>
+            <div>noVNC encountered an error:</div>
+            <br>
+            <div id="noVNC_fallback_errormsg"></div>
+        </div>
+    </div>
+
+    <!-- noVNC Control Bar -->
+    <div id="noVNC_control_bar_anchor" class="noVNC_vcenter">
+
+        <div id="noVNC_control_bar">
+            <div id="noVNC_control_bar_handle" title="Hide/Show the control bar"><div></div></div>
+
+            <div class="noVNC_scroll">
+
+            <h1 class="noVNC_logo" translate="no"><span>no</span><br>VNC</h1>
+
+            <!-- Drag/Pan the viewport -->
+            <input type="image" alt="Drag" src="app/images/drag.svg"
+                id="noVNC_view_drag_button" class="noVNC_button noVNC_hidden"
+                title="Move/Drag Viewport">
+
+            <!--noVNC Touch Device only buttons-->
+            <div id="noVNC_mobile_buttons">
+                <input type="image" alt="Keyboard" src="app/images/keyboard.svg"
+                    id="noVNC_keyboard_button" class="noVNC_button" title="Show Keyboard">
+            </div>
+
+            <!-- Extra manual keys -->
+            <input type="image" alt="Extra keys" src="app/images/toggleextrakeys.svg"
+                id="noVNC_toggle_extra_keys_button" class="noVNC_button"
+                title="Show Extra Keys">
+            <div class="noVNC_vcenter">
+            <div id="noVNC_modifiers" class="noVNC_panel">
+                <input type="image" alt="Ctrl" src="app/images/ctrl.svg"
+                    id="noVNC_toggle_ctrl_button" class="noVNC_button"
+                    title="Toggle Ctrl">
+                <input type="image" alt="Alt" src="app/images/alt.svg"
+                    id="noVNC_toggle_alt_button" class="noVNC_button"
+                    title="Toggle Alt">
+                <input type="image" alt="Windows" src="app/images/windows.svg"
+                    id="noVNC_toggle_windows_button" class="noVNC_button"
+                    title="Toggle Windows">
+                <input type="image" alt="Tab" src="app/images/tab.svg"
+                    id="noVNC_send_tab_button" class="noVNC_button"
+                    title="Send Tab">
+                <input type="image" alt="Esc" src="app/images/esc.svg"
+                    id="noVNC_send_esc_button" class="noVNC_button"
+                    title="Send Escape">
+                <input type="image" alt="Ctrl+Alt+Del" src="app/images/ctrlaltdel.svg"
+                    id="noVNC_send_ctrl_alt_del_button" class="noVNC_button"
+                    title="Send Ctrl-Alt-Del">
+            </div>
+            </div>
+
+            <!-- Shutdown/Reboot -->
+            <input type="image" alt="Shutdown/Reboot" src="app/images/power.svg"
+                id="noVNC_power_button" class="noVNC_button"
+                title="Shutdown/Reboot...">
+            <div class="noVNC_vcenter">
+            <div id="noVNC_power" class="noVNC_panel">
+                <div class="noVNC_heading">
+                    <img alt="" src="app/images/power.svg"> Power
+                </div>
+                <input type="button" id="noVNC_shutdown_button" value="Shutdown">
+                <input type="button" id="noVNC_reboot_button" value="Reboot">
+                <input type="button" id="noVNC_reset_button" value="Reset">
+            </div>
+            </div>
+
+            <!-- Clipboard -->
+            <input type="image" alt="Clipboard" src="app/images/clipboard.svg"
+                id="noVNC_clipboard_button" class="noVNC_button"
+                title="Clipboard">
+            <div class="noVNC_vcenter">
+            <div id="noVNC_clipboard" class="noVNC_panel">
+                <div class="noVNC_heading">
+                    <img alt="" src="app/images/clipboard.svg"> Clipboard
+                </div>
+                <textarea id="noVNC_clipboard_text" rows=5></textarea>
+                <br>
+                <input id="noVNC_clipboard_clear_button" type="button"
+                    value="Clear" class="noVNC_submit">
+            </div>
+            </div>
+
+            <!-- Toggle fullscreen -->
+            <input type="image" alt="Fullscreen" src="app/images/fullscreen.svg"
+                id="noVNC_fullscreen_button" class="noVNC_button noVNC_hidden"
+                title="Fullscreen">
+
+            <!-- Settings -->
+            <input type="image" alt="Settings" src="app/images/settings.svg"
+                id="noVNC_settings_button" class="noVNC_button"
+                title="Settings">
+            <div class="noVNC_vcenter">
+            <div id="noVNC_settings" class="noVNC_panel">
+                <ul>
+                    <li class="noVNC_heading">
+                        <img alt="" src="app/images/settings.svg"> Settings
+                    </li>
+                    <li>
+                        <label><input id="noVNC_setting_shared" type="checkbox"> Shared Mode</label>
+                    </li>
+                    <li>
+                        <label><input id="noVNC_setting_view_only" type="checkbox"> View Only</label>
+                    </li>
+                    <li><hr></li>
+                    <li>
+                        <label><input id="noVNC_setting_view_clip" type="checkbox"> Clip to Window</label>
+                    </li>
+                    <li>
+                        <label for="noVNC_setting_resize">Scaling Mode:</label>
+                        <select id="noVNC_setting_resize" name="vncResize">
+                            <option value="off">None</option>
+                            <option value="scale">Local Scaling</option>
+                            <option value="remote">Remote Resizing</option>
+                        </select>
+                    </li>
+                    <li><hr></li>
+                    <li>
+                        <div class="noVNC_expander">Advanced</div>
+                        <div><ul>
+                            <li>
+                                <label for="noVNC_setting_quality">Quality:</label>
+                                <input id="noVNC_setting_quality" type="range" min="0" max="9" value="6">
+                            </li>
+                            <li>
+                                <label for="noVNC_setting_compression">Compression level:</label>
+                                <input id="noVNC_setting_compression" type="range" min="0" max="9" value="2">
+                            </li>
+                            <li><hr></li>
+                            <li>
+                                <label for="noVNC_setting_repeaterID">Repeater ID:</label>
+                                <input id="noVNC_setting_repeaterID" type="text" value="">
+                            </li>
+                            <li>
+                                <div class="noVNC_expander">WebSocket</div>
+                                <div><ul>
+                                    <li>
+                                        <label><input id="noVNC_setting_encrypt" type="checkbox"> Encrypt</label>
+                                    </li>
+                                    <li>
+                                        <label for="noVNC_setting_host">Host:</label>
+                                        <input id="noVNC_setting_host">
+                                    </li>
+                                    <li>
+                                        <label for="noVNC_setting_port">Port:</label>
+                                        <input id="noVNC_setting_port" type="number">
+                                    </li>
+                                    <li>
+                                        <label for="noVNC_setting_path">Path:</label>
+                                        <input id="noVNC_setting_path" type="text" value="websockify">
+                                    </li>
+                                </ul></div>
+                            </li>
+                            <li><hr></li>
+                            <li>
+                                <label><input id="noVNC_setting_reconnect" type="checkbox"> Automatic Reconnect</label>
+                            </li>
+                            <li>
+                                <label for="noVNC_setting_reconnect_delay">Reconnect Delay (ms):</label>
+                                <input id="noVNC_setting_reconnect_delay" type="number">
+                            </li>
+                            <li><hr></li>
+                            <li>
+                                <label><input id="noVNC_setting_show_dot" type="checkbox"> Show Dot when No Cursor</label>
+                            </li>
+                            <li><hr></li>
+                            <!-- Logging selection dropdown -->
+                            <li>
+                                <label>Logging:
+                                    <select id="noVNC_setting_logging" name="vncLogging">
+                                    </select>
+                                </label>
+                            </li>
+                        </ul></div>
+                    </li>
+                    <li class="noVNC_version_separator"><hr></li>
+                    <li class="noVNC_version_wrapper">
+                        <span>Version:</span>
+                        <span class="noVNC_version"></span>
+                    </li>
+                </ul>
+            </div>
+            </div>
+
+            <!-- Connection Controls -->
+            <input type="image" alt="Disconnect" src="app/images/disconnect.svg"
+                id="noVNC_disconnect_button" class="noVNC_button"
+                title="Disconnect">
+
+            </div>
+        </div>
+
+        <div id="noVNC_control_bar_hint"></div>
+
+    </div> <!-- End of noVNC_control_bar -->
+
+    <!-- Status Dialog -->
+    <div id="noVNC_status"></div>
+
+    <!-- Connect button -->
+    <div class="noVNC_center">
+        <div id="noVNC_connect_dlg">
+            <div class="noVNC_logo" translate="no"><span>no</span>VNC</div>
+            <div id="noVNC_connect_button"><div>
+                <img alt="" src="app/images/connect.svg"> Connect
+            </div></div>
+        </div>
+    </div>
+
+    <!-- Password Dialog -->
+    <div class="noVNC_center noVNC_connect_layer">
+    <div id="noVNC_credentials_dlg" class="noVNC_panel"><form>
+        <ul>
+            <li id="noVNC_username_block">
+                <label>Username:</label>
+                <input id="noVNC_username_input">
+            </li>
+            <li id="noVNC_password_block">
+                <label>Password:</label>
+                <input id="noVNC_password_input" type="password">
+            </li>
+            <li>
+                <input id="noVNC_credentials_button" type="submit" value="Send Credentials" class="noVNC_submit">
+            </li>
+        </ul>
+    </form></div>
+    </div>
+
+    <!-- Transition Screens -->
+    <div id="noVNC_transition">
+        <div id="noVNC_transition_text"></div>
+        <div>
+        <input type="button" id="noVNC_cancel_reconnect_button" value="Cancel" class="noVNC_submit">
+        </div>
+        <div class="noVNC_spinner"></div>
+    </div>
+
+    <!-- This is where the RFB elements will attach -->
+    <div id="noVNC_container">
+        <!-- Note that Google Chrome on Android doesn't respect any of these,
+             html attributes which attempt to disable text suggestions on the
+             on-screen keyboard. Let's hope Chrome implements the ime-mode
+             style for example -->
+        <textarea id="noVNC_keyboardinput" autocapitalize="off"
+            autocomplete="off" spellcheck="false" tabindex="-1"></textarea>
+    </div>
+
+    <audio id="noVNC_bell">
+        <source src="app/sounds/bell.oga" type="audio/ogg">
+        <source src="app/sounds/bell.mp3" type="audio/mpeg">
+    </audio>
+ </body>
+</html>
diff --git a/app/src/main/assets/novnc/vnc_lite.html b/app/src/main/assets/novnc/vnc_lite.html
new file mode 100644
index 00000000..8e2f5cbf
--- /dev/null
+++ b/app/src/main/assets/novnc/vnc_lite.html
@@ -0,0 +1,189 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+
+    <!--
+    noVNC example: lightweight example using minimal UI and features
+
+    This is a self-contained file which doesn't import WebUtil or external CSS.
+
+    Copyright (C) 2019 The noVNC Authors
+    noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
+    This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
+
+    Connect parameters are provided in query string:
+        http://example.com/?host=HOST&port=PORT&scale=true
+    -->
+    <title>noVNC</title>
+
+    <meta charset="utf-8">
+
+    <style>
+
+        body {
+            margin: 0;
+            background-color: dimgrey;
+            height: 100%;
+            display: flex;
+            flex-direction: column;
+        }
+        html {
+            height: 100%;
+        }
+
+        #top_bar {
+            background-color: #6e84a3;
+            color: white;
+            font: bold 12px Helvetica;
+            padding: 6px 5px 4px 5px;
+            border-bottom: 1px outset;
+        }
+        #status {
+            text-align: center;
+        }
+        #sendCtrlAltDelButton {
+            position: fixed;
+            top: 0px;
+            right: 0px;
+            border: 1px outset;
+            padding: 5px 5px 4px 5px;
+            cursor: pointer;
+        }
+
+        #screen {
+            flex: 1; /* fill remaining space */
+            overflow: hidden;
+        }
+
+    </style>
+
+    <script type="module" crossorigin="anonymous">
+        // RFB holds the API to connect and communicate with a VNC server
+        import RFB from './core/rfb.js';
+
+        let rfb;
+        let desktopName;
+
+        // When this function is called we have
+        // successfully connected to a server
+        function connectedToServer(e) {
+            status("Connected to " + desktopName);
+        }
+
+        // This function is called when we are disconnected
+        function disconnectedFromServer(e) {
+            if (e.detail.clean) {
+                status("Disconnected");
+            } else {
+                status("Something went wrong, connection is closed");
+            }
+        }
+
+        // When this function is called, the server requires
+        // credentials to authenticate
+        function credentialsAreRequired(e) {
+            const password = prompt("Password Required:");
+            rfb.sendCredentials({ password: password });
+        }
+
+        // When this function is called we have received
+        // a desktop name from the server
+        function updateDesktopName(e) {
+            desktopName = e.detail.name;
+        }
+
+        // Since most operating systems will catch Ctrl+Alt+Del
+        // before they get a chance to be intercepted by the browser,
+        // we provide a way to emulate this key sequence.
+        function sendCtrlAltDel() {
+            rfb.sendCtrlAltDel();
+            return false;
+        }
+
+        // Show a status text in the top bar
+        function status(text) {
+            document.getElementById('status').textContent = text;
+        }
+
+        // This function extracts the value of one variable from the
+        // query string. If the variable isn't defined in the URL
+        // it returns the default value instead.
+        function readQueryVariable(name, defaultValue) {
+            // A URL with a query parameter can look like this (But will most probably get logged on the http server):
+            // https://www.example.com?myqueryparam=myvalue
+            //
+            // For privacy (Using a hastag #, the parameters will not be sent to the server)
+            // the url can be requested in the following way:
+            // https://www.example.com#myqueryparam=myvalue&password=secreatvalue
+            //
+            // Even Mixing public and non public parameters will work:
+            // https://www.example.com?nonsecretparam=example.com#password=secreatvalue
+            //
+            // Note that we use location.href instead of location.search
+            // because Firefox < 53 has a bug w.r.t location.search
+            const re = new RegExp('.*[?&]' + name + '=([^&#]*)'),
+                  match = ''.concat(document.location.href, window.location.hash).match(re);
+
+            if (match) {
+                // We have to decode the URL since want the cleartext value
+                return decodeURIComponent(match[1]);
+            }
+
+            return defaultValue;
+        }
+
+        document.getElementById('sendCtrlAltDelButton')
+            .onclick = sendCtrlAltDel;
+
+        // Read parameters specified in the URL query string
+        // By default, use the host and port of server that served this file
+        const host = readQueryVariable('host', window.location.hostname);
+        let port = readQueryVariable('port', window.location.port);
+        const password = readQueryVariable('password');
+        const path = readQueryVariable('path', 'websockify');
+
+        // | | |         | | |
+        // | | | Connect | | |
+        // v v v         v v v
+
+        status("Connecting");
+
+        // Build the websocket URL used to connect
+        let url;
+        if (window.location.protocol === "https:") {
+            url = 'wss';
+        } else {
+            url = 'ws';
+        }
+        url += '://' + host;
+        if(port) {
+            url += ':' + port;
+        }
+        url += '/' + path;
+
+        // Creating a new RFB object will start a new connection
+        rfb = new RFB(document.getElementById('screen'), url,
+                      { credentials: { password: password } });
+
+        // Add listeners to important events from the RFB module
+        rfb.addEventListener("connect",  connectedToServer);
+        rfb.addEventListener("disconnect", disconnectedFromServer);
+        rfb.addEventListener("credentialsrequired", credentialsAreRequired);
+        rfb.addEventListener("desktopname", updateDesktopName);
+
+        // Set parameters that can be changed on an active connection
+        rfb.viewOnly = readQueryVariable('view_only', false);
+        rfb.scaleViewport = readQueryVariable('scale', false);
+    </script>
+</head>
+
+<body>
+    <div id="top_bar">
+        <div id="status">Loading</div>
+        <div id="sendCtrlAltDelButton">Send CtrlAltDel</div>
+    </div>
+    <div id="screen">
+        <!-- This is where the remote screen will appear -->
+    </div>
+</body>
+</html>