Compare commits

...

41 Commits

Author SHA1 Message Date
Samuel Mannehed (ThinLinc team) fc5b83c08f
Merge pull request #2043 from tobfah/enable-horizontal-control-bar
Enable attaching the control bar to top and bottom
2026-05-23 19:34:51 +02:00
Samuel Mannehed (ThinLinc team) 2ea0e7d4eb
Merge pull request #2059 from novnc/macos26
Use macOS 26 runner instead of macOS 10.15
2026-05-23 15:08:17 +02:00
Samuel Mannehed ad25fdb20b Use macOS 26 runner instead of macOS 10.15
This fixes failing tests on Safari.
2026-05-23 14:47:37 +02:00
Samuel Mannehed 094fd0509f Update version in package.json to 1.7.0
This was missed when doing the 1.7.0 release.
2026-05-23 10:55:19 +02:00
Samuel Mannehed (ThinLinc team) fb8f30a1dd
Merge pull request #2058 from novnc/github-actions
Update github actions to latest versions
2026-05-23 10:49:58 +02:00
Samuel Mannehed b4c4cb75d5 Update github actions to latest versions 2026-05-23 10:45:56 +02:00
掌控-物质 7834e66733
Workaround endless loading on Android Chrome
Replace top-level await with then() in browser.js, due to a bug in
chromium browsers.

https://github.com/novnc/noVNC/pull/2053
2026-05-23 10:32:46 +02:00
dmotte 5c2025d5a6
Only focus username field if empty
https://github.com/novnc/noVNC/pull/2057
2026-05-22 20:07:04 +02:00
Samuel Mannehed 4bce309c3b Fix broken clipboard unittests
We can't compare with a new CustomEvent, lets check the call arguments
instead. The field is also called "text".
2026-05-22 17:59:54 +02:00
Samuel Mannehed 816a4c7f24 Remove validation exception for "scrollbar-gutter"
This issue has now been fixed in https://validator.w3.org/nu, see:

https://github.com/w3c/css-validator/issues/473
2026-05-22 16:51:37 +02:00
Tobias 052242df5d Remove redundant mobile buttons grouping
There's only one button in the group, and the wrapping div messed with
the flex box layout
2026-04-03 12:50:47 +02:00
Tobias 44343556e0 Enable attaching control bar to top and bottom 2026-04-03 12:50:47 +02:00
Tobias 52bc5a5f9f Use svg for control bar icon
Simplifies alignment. In preparation for horizontal control bar modes.
2026-04-03 12:49:48 +02:00
Tobias f0d217719c Make scroll bar and key modifiers flex box
In preparation for horizontal control bar modes
2026-04-01 17:14:57 +02:00
Pierre Ossman 8e1ebdffba Remove some unused assignments
Mainly to keep eslint happy with its new recommended rules. But it also
makes the code more clear to not have unused stuff in it.

The des code got a bit more refactoring since eslint was upset about the
final "i++" as it was technically an unused assignment.
2026-02-13 12:33:11 +01:00
Pierre Ossman 4d16cc5270 Adapt to latest version of eslint 2026-02-13 12:17:10 +01:00
Pierre Ossman 1640a5e21f Remove deprecated host and port from example
We shouldn't be advertising these if we'd like people to stop using
them.
2026-02-11 16:08:45 +01:00
Samuel Mannehed (ThinLinc team) 6d0a974665
Merge pull request #2035 from MantriHimaVamshi-AidenAI/patch-1
Fix comment typo in display.js
2026-01-11 00:45:47 +01:00
Samuel Mannehed (ThinLinc team) d1a124401b
Merge pull request #2029 from nykula/nykula-patch-1
Translate to Ukrainian
2026-01-11 00:44:19 +01:00
Samuel Mannehed (ThinLinc team) 40cc57460d
Merge pull request #1984 from tsukasa-au/add-wakelock-support
Add wakelock support
2026-01-11 00:33:32 +01:00
MantriHimaVamshi-AidenAI ef3257037f
Fix comment typo in display.js
I noticed a simple typo - "inoder rendering"
2026-01-05 12:34:06 +05:30
Greg Darke 988e9da7fa Add tests for the wakelock feature.
Add tests to for both the `rfb` side (calling into the new wakelock
code), and the new wakelock class (which tracks the desired state and
how to get there).
2025-12-06 19:00:42 +11:00
Greg Darke f4f2f8d725 Expose WakeLockManager state transitions for tests
Dispatch an event on each state transition inside the WakeLockManager.
This gives the unit tests something to synchronise on, allowing us to
write fast, flake-free tests.
2025-12-06 19:00:42 +11:00
Greg Darke 077c54f312 Add error state for wakelock testing.
Add an error state to the WakeLockManager state machine. This adds a
transition that can be detected from tests (it otherwise serves no
purpose, and the system immediatly transitions back into the released
state).
2025-12-06 19:00:42 +11:00
Greg Darke 8341fdf846 Add wakelock support
Add a new configuration option `keep_device_awake` to allow noVNC to
stop the local display from going to sleep. This is especially useful
with view-only sessions.

This new option has been added to the configuration UI, making it easier
for users to configure. When this option is changed at runtime, we will
request/release the wake lock.

We only hold the view lock while connected to a server. We will also
attempt to reacquire the wakelock if we lost it due to a visibility
change (the tab becoming inactive, or during the transition into/from
fullscreen).

All existing unittests have been run, and the change has been manually
tested in Firefox 145. Additional tests will be added later.
2025-12-06 19:00:42 +11:00
Denys Nykula b6136b5386 Translate to Ukrainian 2025-11-22 18:40:41 +00:00
Alexander Zeijlon d44f7e04fc Remove show_dot from docs/EMBEDDING.md
This should have been a part of commit 243d7fd
2025-11-04 09:44:21 +01:00
Alexander Zeijlon fb97e0f1e2 Remove showDotCursor from docs/API.md
This should have been a part of commit 243d7fd
2025-11-04 09:37:09 +01:00
Alexander Zeijlon d3d69a8118 Use Node version 24 when publishing to npmjs
Node 24 comes with npm > 11.5.1 which we need in order to publish with
OIDC auth.

This also reverts the change in commit e03a54e, which broke the
publishing step.
2025-11-03 15:50:14 +01:00
Alexander Zeijlon e03a54e12a Publish with latest npm version
Apparently publishing with OIDC as an auth method only works with npm
11.5.1 and onward.
2025-11-03 13:58:03 +01:00
Alexander Zeijlon 99865e5aba Stop using access tokens when publishing to npmjs
We are using OIDC now instead of the old style access tokens.
2025-11-03 13:47:28 +01:00
Alexander Zeijlon b29cc6493d Allow publishing to npmjs.com with OIDC 2025-11-03 12:56:06 +01:00
Alexander Zeijlon c2d89730dd Update Swedish translation 2025-10-31 10:53:45 +01:00
Alexander Zeijlon 9d77c806fe Update translation template file 2025-10-31 09:18:04 +01:00
Tobias 63c2c14a50 Display warning prompt before closing tab
The warning prompt is only displayed if there is an active connected
session, when viewOnly is disabled.
2025-10-30 16:23:11 +01:00
Alexander Zeijlon 91d12106df Merge branch 'master' of https://github.com/Deni42/noVNC 2025-10-30 15:49:43 +01:00
Alexander Zeijlon fb7e891841 Merge branch 'clipboard-async' of https://github.com/tobfah/noVNC 2025-10-22 11:30:44 +02:00
Tobias 3d5698c71c Add async clipboard module to internal API docs 2025-10-21 16:21:55 +02:00
Tobias d9b45d390b Disable clipboard button with async clipboard
With async clipboard available, the fallback clipboard textarea adds
mostly confusion. If async clipboard is out right denied, users most
likely don't want to see any clipboard activity.
2025-10-21 16:21:55 +02:00
Tobias f5a4eedcea Add permissions-exclusive async clipboard
Clipboard permissions must be supported, with states "granted" or
"prompt" for both write and read.
2025-10-21 16:21:55 +02:00
Felso Daniel (McP/MFE4) cf6fe1063c Add Hungarian translation 2025-10-08 13:04:27 +02:00
34 changed files with 2686 additions and 576 deletions

View File

@ -9,39 +9,39 @@ on:
jobs:
npm:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- run: |
GITREV=$(git rev-parse --short HEAD)
echo $GITREV
sed -i "s/^\(.*\"version\".*\)\"\([^\"]\+\)\"\(.*\)\$/\1\"\2-g$GITREV\"\3/" package.json
if: github.event_name != 'release'
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
# Node 24 is needed to get npm > 11.5.1, which is a requirement for
# OIDC auth.
node-version: 24
# Needs to be explicitly specified for auth to work
registry-url: 'https://registry.npmjs.org'
- run: npm install
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v7
with:
name: npm
path: lib
- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
if: |
github.repository == 'novnc/noVNC' &&
github.event_name == 'release' &&
!github.event.release.prerelease
- run: npm publish --access public --tag beta
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
if: |
github.repository == 'novnc/noVNC' &&
github.event_name == 'release' &&
github.event.release.prerelease
- run: npm publish --access public --tag dev
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
if: |
github.repository == 'novnc/noVNC' &&
github.event_name == 'push' &&
@ -49,7 +49,7 @@ jobs:
snap:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- run: |
GITREV=$(git rev-parse --short HEAD)
echo $GITREV
@ -61,7 +61,7 @@ jobs:
sed -i "s/^version:.*/version: '$VERSION'/" snap/snapcraft.yaml
- uses: snapcore/action-build@v1
id: snapcraft
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v7
with:
name: snap
path: ${{ steps.snapcraft.outputs.snap }}

View File

@ -6,14 +6,14 @@ jobs:
eslint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- run: npm update
- run: npm run lint
html:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- run: npm update
- run: git ls-tree --name-only -r HEAD | grep -E "[.](html|css)$" | xargs ./utils/validate

View File

@ -13,15 +13,18 @@ jobs:
- ChromeHeadless
- FirefoxHeadless
include:
- os: macos-latest
# FIXME: We'd like to use "macos-latest", but Safari tests
# are flaky on it and timeout often. As of 2026-05-23
# macos-latest is still on 10.15.
- os: macos-26
browser: Safari
- os: windows-latest
browser: EdgeHeadless
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- run: npm update
- run: npm run test
env:

View File

@ -6,8 +6,8 @@ jobs:
translate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- run: npm update
- run: sudo apt-get install gettext
- run: make -C po update-pot

View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="35"
height="21"
viewBox="0 0 35 21.000001"
id="svg2"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs4" />
<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" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
transform="translate(0,-1004.3621)">
<g
id="g4300"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.10948"
transform="matrix(0.90909091,0,0,0.893617,-4.0909091,96.570459)">
<g
id="g4302"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.10948">
<path
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 v 6.8586 h -2 v -6.8914 c 0,-0.072 -0.03404,-0.1086 -0.102113,-0.1086 H 7.1021125 C 7.0340375,1018.3621 7,1018.3983 7,1018.4707 v 6.8914 H 5 v -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:1.10948px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4304" />
<path
d="m 17.013073,1016.3621 h 4.973854 c 0.554325,0 1.025987,0.2121 1.414986,0.6362 0.398725,0.4138 0.598087,0.9155 0.598087,1.5052 v 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 h -4.973854 c -0.554325,0 -1.030849,-0.2069 -1.429574,-0.6207 C 15.1945,1024.3173 15,1023.8104 15,1023.2207 v -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 h -4.795776 c -0.06808,0 -0.102112,0.036 -0.102112,0.1086 v 4.7828 c 0,0.072 0.03404,0.1086 0.102112,0.1086 h 4.795776 c 0.06807,0 0.102112,-0.036 0.102112,-0.1086 v -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:1.10948px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4306" />
</g>
<g
id="g4308"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.10948">
<path
d="m 12,1036.9177 4.768114,-8.5556 H 19 l -6,11 h -2 l -6,-11 h 2.2318854 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:1.10948px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4310" />
<path
d="m 29,1036.3621 v -8 h 2 v 11 h -2 l -7,-8 v 8 h -2 v -11 h 2 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:1.10948px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4312" />
<path
d="m 43,1030.3621 h -8.897887 c -0.06808,0 -0.102113,0.036 -0.102113,0.1069 v 6.7862 c 0,0.071 0.03404,0.1069 0.102113,0.1069 H 43 v 2 h -8.972339 c -0.56405,0 -1.045437,-0.2037 -1.444162,-0.6111 C 32.1945,1038.3334 32,1037.8292 32,1037.2385 v -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 H 43 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:1.10948px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4314" />
</g>
</g>
<g
id="g4291"
style="stroke:none;stroke-width:1.10948"
transform="matrix(0.90909091,0,0,0.893617,-4.5454545,96.123649)">
<g
id="g4282"
style="stroke:none;stroke-width:1.10948">
<path
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:1.10948px;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 v 6.8586 h -2 v -6.8914 c 0,-0.072 -0.03404,-0.1086 -0.102113,-0.1086 H 7.1021125 C 7.0340375,1018.3621 7,1018.3983 7,1018.4707 v 6.8914 H 5 v -9 z" />
<path
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:1.10948px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 17.013073,1016.3621 h 4.973854 c 0.554325,0 1.025987,0.2121 1.414986,0.6362 0.398725,0.4138 0.598087,0.9155 0.598087,1.5052 v 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 h -4.973854 c -0.554325,0 -1.030849,-0.2069 -1.429574,-0.6207 C 15.1945,1024.3173 15,1023.8104 15,1023.2207 v -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 h -4.795776 c -0.06808,0 -0.102112,0.036 -0.102112,0.1086 v 4.7828 c 0,0.072 0.03404,0.1086 0.102112,0.1086 h 4.795776 c 0.06807,0 0.102112,-0.036 0.102112,-0.1086 v -4.7828 c 0,-0.072 -0.03404,-0.1086 -0.102112,-0.1086 z" />
</g>
<g
id="g4286"
style="stroke:none;stroke-width:1.10948">
<path
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:1.10948px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 12,1036.9177 4.768114,-8.5556 H 19 l -6,11 h -2 l -6,-11 h 2.2318854 z" />
<path
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:1.10948px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 29,1036.3621 v -8 h 2 v 11 h -2 l -7,-8 v 8 h -2 v -11 h 2 z" />
<path
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:1.10948px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 43,1030.3621 h -8.897887 c -0.06808,0 -0.102113,0.036 -0.102113,0.1069 v 6.7862 c 0,0.071 0.03404,0.1069 0.102113,0.1069 H 43 v 2 h -8.972339 c -0.56405,0 -1.045437,-0.2037 -1.444162,-0.6111 C 32.1945,1038.3334 32,1037.8292 32,1037.2385 v -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 H 43 Z" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.8 KiB

80
app/locale/hu.json Normal file
View File

@ -0,0 +1,80 @@
{
"Running without HTTPS is not recommended, crashes or other issues are likely.": "HTTPS nélkül futtatni nem ajánlott, összeomlások vagy más problémák várhatók.",
"Connecting...": "Kapcsolódás...",
"Disconnecting...": "Kapcsolat bontása...",
"Reconnecting...": "Újrakapcsolódás...",
"Internal error": "Belső hiba",
"Failed to connect to server: ": "Nem sikerült csatlakozni a szerverhez: ",
"Connected (encrypted) to ": "Kapcsolódva (titkosítva) ehhez: ",
"Connected (unencrypted) to ": "Kapcsolódva (titkosítatlanul) ehhez: ",
"Something went wrong, connection is closed": "Valami hiba történt, a kapcsolat lezárult",
"Failed to connect to server": "Nem sikerült csatlakozni a szerverhez",
"Disconnected": "Kapcsolat bontva",
"New connection has been rejected with reason: ": "Az új kapcsolat elutasítva, indok: ",
"New connection has been rejected": "Az új kapcsolat elutasítva",
"Credentials are required": "Hitelesítő adatok szükségesek",
"noVNC encountered an error:": "A noVNC hibát észlelt:",
"Hide/Show the control bar": "Vezérlősáv elrejtése/megjelenítése",
"Drag": "Húzás",
"Move/Drag viewport": "Nézet mozgatása/húzása",
"Keyboard": "Billentyűzet",
"Show keyboard": "Billentyűzet megjelenítése",
"Extra keys": "Extra billentyűk",
"Show extra keys": "Extra billentyűk megjelenítése",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Ctrl lenyomása/felengedése",
"Alt": "Alt",
"Toggle Alt": "Alt lenyomása/felengedése",
"Toggle Windows": "Windows lenyomása/felengedése",
"Windows": "Windows",
"Send Tab": "Tab küldése",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Escape küldése",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Ctrl-Alt-Del küldése",
"Shutdown/Reboot": "Leállítás/Újraindítás",
"Shutdown/Reboot...": "Leállítás/Újraindítás...",
"Power": "Bekapcsolás",
"Shutdown": "Leállítás",
"Reboot": "Újraindítás",
"Reset": "Reset",
"Clipboard": "Vágólap",
"Edit clipboard content in the textarea below.": "Itt tudod módosítani a vágólap tartalmát.",
"Full screen": "Teljes képernyő",
"Settings": "Beállítások",
"Shared mode": "Megosztott mód",
"View only": "Csak megtekintés",
"Clip to window": "Ablakhoz igazítás",
"Scaling mode:": "Méretezési mód:",
"None": "Nincs",
"Local scaling": "Helyi méretezés",
"Remote resizing": "Távoli átméretezés",
"Advanced": "Speciális",
"Quality:": "Minőség:",
"Compression level:": "Tömörítési szint:",
"Repeater ID:": "Ismétlő azonosító:",
"WebSocket": "WebSocket",
"Encrypt": "Titkosítás",
"Host:": "Hoszt:",
"Port:": "Port:",
"Path:": "Útvonal:",
"Automatic reconnect": "Automatikus újracsatlakozás",
"Reconnect delay (ms):": "Újracsatlakozás késleltetése (ms):",
"Show dot when no cursor": "Kurzor hiányában pont mutatása",
"Logging:": "Naplózás:",
"Version:": "Verzió:",
"Disconnect": "Kapcsolat bontása",
"Connect": "Csatlakozás",
"Server identity": "Szerver azonosító",
"The server has provided the following identifying information:": "A szerver a következő azonosító információt adta meg:",
"Fingerprint:": "Ujjlenyomat:",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Ellenőrizze, hogy az információ helyes-e és nyomja meg a \"Jóváhagyás\" gombot. Ellenkező esetben nyomja meg az \"Elutasítás\" gombot.",
"Approve": "Jóváhagyás",
"Reject": "Elutasítás",
"Credentials": "Hitelesítő adatok",
"Username:": "Felhasználónév:",
"Password:": "Jelszó:",
"Send credentials": "Hitelesítő adatok küldése",
"Cancel": "Mégse"
}

81
app/locale/uk.json Normal file
View File

@ -0,0 +1,81 @@
{
"Running without HTTPS is not recommended, crashes or other issues are likely.": "Робота без HTTPS не рекомендується, ймовірні збої чи інші проблеми.",
"Connecting...": "З'єднання...",
"Disconnecting...": "Від'єднання...",
"Reconnecting...": "Перез'єднання...",
"Internal error": "Внутрішня помилка",
"Failed to connect to server: ": "Не вдалося з'єднатися з сервером: ",
"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": "Нове з'єднання відхилено",
"Are you sure you want to disconnect the session?": "Точно від'єднати сеанс?",
"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": "Буфер обміну",
"Edit clipboard content in the textarea below.": "Редагуйте вміст буфера обміну в текстовій зоні внизу.",
"Full screen": "Повний екран",
"Settings": "Параметри",
"Shared mode": "Спільний режим",
"View only": "Лише перегляд",
"Clip to window": "До розмірів вікна",
"Scaling mode:": "Режим масштабування:",
"None": "Вимкнено",
"Local scaling": "Локальне масштабування",
"Remote resizing": "Віддалене масштабування",
"Advanced": "Додатково",
"Quality:": "Якість:",
"Compression level:": "Рівень стиснення:",
"Repeater ID:": "Ідентифікатор репітера:",
"WebSocket": "WebSocket",
"Encrypt": "Шифрування",
"Host:": "Сервер:",
"Port:": "Порт:",
"Path:": "Шлях:",
"Automatic reconnect": "Автоматичне перез'єднання",
"Reconnect delay (ms):": "Затримка перез'єднання (мс):",
"Show dot when no cursor": "Показувати крапку, коли нема курсора",
"Logging:": "Журнал:",
"Version:": "Версія:",
"Disconnect": "Від'єднати",
"Connect": "З'єднати",
"Server identity": "Ідентифікація сервера",
"The server has provided the following identifying information:": "Сервер надає такі ідентифікаційні дані:",
"Fingerprint:": "Відбиток:",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Перевірте, чи дані коректні, й натисніть «Схвалити». Інакше натисніть «Відхилити».",
"Approve": "Схвалити",
"Reject": "Відхилити",
"Credentials": "Особові дані",
"Username:": "Користувацьке ім'я:",
"Password:": "Пароль:",
"Send credentials": "Надіслати особові дані",
"Cancel": "Скасувати"
}

View File

@ -117,7 +117,8 @@ html {
.noVNC_center > * {
pointer-events: auto;
}
.noVNC_vcenter {
.noVNC_crosscenter {
display: flex !important;
flex-direction: column;
justify-content: center;
@ -129,9 +130,29 @@ html {
padding: 0 !important;
pointer-events: none;
}
.noVNC_vcenter > * {
.noVNC_crosscenter > * {
pointer-events: auto;
}
.noVNC_right .noVNC_crosscenter {
left: auto;
right: 0;
}
.noVNC_top.noVNC_crosscenter,
.noVNC_top .noVNC_crosscenter {
flex-direction: row;
width: 100%;
height: auto;
}
.noVNC_bottom.noVNC_crosscenter,
.noVNC_bottom .noVNC_crosscenter {
flex-direction: row;
width: 100%;
height: auto;
}
.noVNC_bottom .noVNC_crosscenter {
top: auto;
bottom: 0;
}
/* ----------------------------------------
* Layering
@ -231,10 +252,18 @@ html {
:root.noVNC_connected #noVNC_control_bar_anchor.noVNC_idle {
opacity: 0.8;
}
#noVNC_control_bar_anchor:is(.noVNC_top, .noVNC_bottom) {
/* Edge misrenders animations wihthout this */
transform: translateY(0);
}
#noVNC_control_bar_anchor.noVNC_right {
left: auto;
right: 0;
}
#noVNC_control_bar_anchor.noVNC_bottom {
top: auto;
bottom: 0;
}
#noVNC_control_bar {
position: relative;
@ -249,10 +278,34 @@ html {
-webkit-user-select: none;
-webkit-touch-callout: none; /* Disable iOS image long-press popup */
}
.noVNC_right #noVNC_control_bar {
left: 100%;
border-radius: 12px 0 0 12px;
}
.noVNC_top #noVNC_control_bar {
left: auto;
/* FIXME: We want to mirror the left and right modes here and use a
relative top offset (-100%), but it doesn't resolve
correctly against the anchor height reference */
top: -55px;
border-radius: 0 0 12px 12px;
}
.noVNC_bottom #noVNC_control_bar {
left: auto;
/* FIXME: We want to mirror the left and right modes here and use a
relative top offset (100%), but it doesn't resolve
correctly against the anchor height reference */
top: 55px;
border-radius: 12px 12px 0 0;
}
#noVNC_control_bar.noVNC_open {
box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
left: 0;
}
:is(.noVNC_top, .noVNC_bottom) #noVNC_control_bar.noVNC_open {
left: auto;
top: 0;
}
#noVNC_control_bar::before {
/* This extra element is to get a proper shadow */
content: "";
@ -263,19 +316,22 @@ html {
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: 12px 0 0 12px;
}
.noVNC_right #noVNC_control_bar.noVNC_open {
left: 0;
}
.noVNC_right #noVNC_control_bar::before {
visibility: hidden;
}
.noVNC_top #noVNC_control_bar::before {
height: 30px;
width: 100%;
top: -30px;
bottom: auto;
}
.noVNC_bottom #noVNC_control_bar::before {
visibility: hidden;
}
#noVNC_control_bar.noVNC_open::before {
box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
}
#noVNC_control_bar_handle {
position: absolute;
@ -288,41 +344,96 @@ html {
cursor: pointer;
border-radius: 6px;
background-color: var(--novnc-darkblue);
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;
:is(.noVNC_top, .noVNC_bottom) #noVNC_control_bar_handle {
transform: translateX(35px);
top: -15px;
left: 0;
width: 50px;
height: calc(100% + 30px);
}
#noVNC_control_bar_handle::before {
content: "";
background: url("../images/handle_bg.svg");
background-repeat: no-repeat;
position: absolute;
top: 0;
right: 0;
width: 15px;
height: 50px;
}
.noVNC_right #noVNC_control_bar_handle::before {
left: 0;
right: auto;
}
.noVNC_top #noVNC_control_bar_handle::before {
left: 0;
right: auto;
transform-origin: bottom left;
transform: rotate(90deg) translateX(20px);
}
.noVNC_bottom #noVNC_control_bar_handle::before {
left: 0;
right: auto;
transform-origin: bottom left;
transform: rotate(90deg) translateX(-50px);
}
#noVNC_control_bar_handle:after {
content: "";
transition: transform 0.5s ease-in-out;
background: url("../images/handle.svg") no-repeat center;
background-size: 5px 6px;
position: absolute;
top: 20px; /* (50px-10px)/2 */
right: 3px;
transform: none;
width: 10px;
height: 10px;
transform-origin: center;
}
.noVNC_right #noVNC_control_bar_handle:after {
left: 5px;
right: 0;
left: 3px;
right: auto;
transform: rotate(180deg);
}
.noVNC_top #noVNC_control_bar_handle:after {
left: 20px;
right: auto;
top: auto;
bottom: 3px;
transform: rotate(90deg);
}
.noVNC_bottom #noVNC_control_bar_handle:after {
left: 20px;
right: auto;
top: 3px;
bottom: auto;
transform: rotate(-90deg);
}
#noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
transform: translateX(1px) rotate(180deg);
}
.noVNC_right #noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
transform: none;
transform: translateX(-1px);
}
.noVNC_top #noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
transform: translateY(1px) rotate(-90deg);
}
.noVNC_bottom #noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
transform: translateY(-1px) rotate(90deg);
}
/* Larger touch area for the handle, used when a touch screen is available */
#noVNC_control_bar_handle div {
position: absolute;
left: auto;
right: -35px;
top: 0;
width: 50px;
@ -338,35 +449,66 @@ html {
left: -35px;
right: auto;
}
.noVNC_top #noVNC_control_bar_handle div {
left: 0;
right: auto;
top: auto;
bottom: -35px;
width: 100%;
height: 50px;
}
.noVNC_bottom #noVNC_control_bar_handle div {
left: 0;
right: auto;
top: -35px;
bottom: auto;
width: 100%;
height: 50px;
}
#noVNC_control_bar > .noVNC_scroll {
max-height: 100vh; /* Chrome is buggy with 100% */
overflow-x: hidden;
overflow-y: auto;
padding: 0 10px;
padding: 10px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
gap: 10px 0;
}
#noVNC_control_bar > .noVNC_scroll > * {
display: block;
margin: 10px auto;
:is(.noVNC_top, .noVNC_bottom) > #noVNC_control_bar > .noVNC_scroll {
max-width: 100vw; /* Chrome is buggy with 100% */
overflow-x: auto;
overflow-y: hidden;
flex-direction: row;
gap: 0 10px;
}
/* Control bar hint */
#noVNC_hint_anchor {
.noVNC_hint_anchor {
position: fixed;
right: -50px;
left: auto;
}
#noVNC_control_bar_anchor.noVNC_right + #noVNC_hint_anchor {
left: -50px;
right: auto;
}
#noVNC_control_bar_hint {
position: relative;
transform: scale(0);
.noVNC_hint_anchor.noVNC_right {
left: auto;
right: -50px;
}
.noVNC_hint_anchor.noVNC_top {
left: auto;
top: -50px;
}
.noVNC_hint_anchor.noVNC_bottom {
left: auto;
top: auto;
bottom: -50px;
}
.noVNC_control_bar_hint {
width: 100px;
height: 50%;
max-height: 600px;
position: relative;
transform: scale(0);
visibility: hidden;
opacity: 0;
@ -376,13 +518,19 @@ html {
border-radius: 12px;
transition-delay: 0s;
}
#noVNC_control_bar_hint.noVNC_active {
:is(.noVNC_top, .noVNC_bottom) .noVNC_control_bar_hint {
width: 50%;
height: 100px;
max-width: 600px;
max-height: none;
}
.noVNC_control_bar_hint.noVNC_active {
visibility: visible;
opacity: 1;
transition-delay: 0.2s;
transform: scale(1);
}
#noVNC_control_bar_hint.noVNC_notransition {
.noVNC_control_bar_hint.noVNC_notransition {
transition: none !important;
}
@ -390,7 +538,6 @@ html {
#noVNC_control_bar .noVNC_button {
min-width: unset;
padding: 4px 4px;
vertical-align: middle;
border:1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
background-color: transparent;
@ -411,7 +558,7 @@ html {
box-sizing: border-box; /* so max-width don't have to care about padding */
max-width: calc(100vw - 75px - 25px); /* minus left and right margins */
max-height: 100vh; /* Chrome is buggy with 100% */
max-height: calc(100vh - 75px - 25px); /* minus top and bottom margins */
overflow-x: hidden;
overflow-y: auto;
@ -431,16 +578,24 @@ html {
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_top .noVNC_panel {
transform: translateY(25px);
}
.noVNC_top .noVNC_panel.noVNC_open {
transform: translateY(75px);
}
.noVNC_bottom .noVNC_panel {
transform: translateY(-25px);
}
.noVNC_bottom .noVNC_panel.noVNC_open {
transform: translateY(-75px);
}
.noVNC_panel > * {
display: block;
@ -536,13 +691,26 @@ html {
/* Control bar content */
#noVNC_control_bar .noVNC_logo {
font-size: 13px;
display: block;
max-width: 35px;
max-height: 35px;
object-fit: contain;
}
.noVNC_logo + hr {
/* Remove all but top border */
border: none;
border-top: 1px solid rgba(255, 255, 255, 0.2);
width: 35px;
height: 1px;
margin: 0;
}
:is(.noVNC_top, .noVNC_bottom) .noVNC_logo + hr {
/* Remove all but left border */
border-left: 1px solid rgba(255, 255, 255, 0.2);
border-top: none;
width: 1px;
height: 35px;
}
:root:not(.noVNC_connected) #noVNC_view_drag_button {
@ -550,16 +718,15 @@ html {
}
/* noVNC Touch Device only buttons */
:root:not(.noVNC_connected) #noVNC_mobile_buttons {
:root:not(.noVNC_connected) #noVNC_keyboard_button {
display: none;
}
@media not all and (any-pointer: coarse) {
/* FIXME: The button for the virtual keyboard is the only button in this
group of "mobile buttons". It is bad to assume that no touch
devices have physical keyboards available. Hopefully we can get
a media query for this:
/* FIXME: It is bad to assume that no touch devices have physical
keyboards available. Hopefully we can get a media query
for this:
https://github.com/w3c/csswg-drafts/issues/3871 */
:root.noVNC_connected #noVNC_mobile_buttons {
:root.noVNC_connected #noVNC_keyboard_button {
display: none;
}
}
@ -573,6 +740,18 @@ html {
background-color: var(--novnc-darkgrey);
border: none;
padding: 10px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
gap: 10px 0;
}
#noVNC_modifiers > * {
margin: 0;
}
:is(.noVNC_top, .noVNC_bottom) #noVNC_modifiers {
flex-direction: row;
gap: 0 10px;
}
/* Shutdown/Reboot */

258
app/ui.js
View File

@ -9,18 +9,19 @@
import * as Log from '../core/util/logging.js';
import _, { l10n } from './localization.js';
import { isTouchDevice, isMac, isIOS, isAndroid, isChromeOS, isSafari,
hasScrollbarGutter, dragThreshold }
hasScrollbarGutter, dragThreshold, browserAsyncClipboardSupport }
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 WakeLockManager from './wakelock.js';
import * as WebUtil from "./webutil.js";
const PAGE_TITLE = "noVNC";
const LINGUAS = ["cs", "de", "el", "es", "fr", "hr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"];
const LINGUAS = ["cs", "de", "el", "es", "fr", "hr", "hu", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "uk", "zh_CN", "zh_TW"];
const UI = {
@ -36,6 +37,8 @@ const UI = {
controlbarGrabbed: false,
controlbarDrag: false,
controlbarMouseDownClientX: 0,
controlbarMouseDownOffsetX: 0,
controlbarMouseDownClientY: 0,
controlbarMouseDownOffsetY: 0,
@ -46,6 +49,8 @@ const UI = {
reconnectCallback: null,
reconnectPassword: null,
wakeLockManager: new WakeLockManager(),
async start(options={}) {
UI.customSettings = options.settings || {};
if (UI.customSettings.defaults === undefined) {
@ -107,8 +112,11 @@ const UI = {
}
// Restore control bar position
if (WebUtil.readSetting('controlbar_pos') === 'right') {
UI.toggleControlbarSide();
const pos = WebUtil.readSetting('controlbar_pos');
if (['left', 'right', 'top', 'bottom'].includes(pos)) {
UI.toggleControlbarSide(pos);
} else {
UI.toggleControlbarSide('left');
}
UI.initFullscreen();
@ -135,10 +143,8 @@ const UI = {
let autoconnect = UI.getSetting('autoconnect');
if (autoconnect === 'true' || autoconnect == '1') {
autoconnect = true;
UI.connect();
} else {
autoconnect = false;
// Show the connect panel on first load unless autoconnecting
UI.openConnectPanel();
}
@ -189,6 +195,7 @@ const UI = {
UI.initSetting('repeaterID', '');
UI.initSetting('reconnect', false);
UI.initSetting('reconnect_delay', 5000);
UI.initSetting('keep_device_awake', false);
},
// Adds a link to the label elements on the corresponding input elements
setupSettingLabels() {
@ -371,6 +378,8 @@ const UI = {
UI.addSettingChangeHandler('view_only', UI.updateViewOnly);
UI.addSettingChangeHandler('show_dot');
UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor);
UI.addSettingChangeHandler('keep_device_awake');
UI.addSettingChangeHandler('keep_device_awake', UI.updateRequestWakelock);
UI.addSettingChangeHandler('host');
UI.addSettingChangeHandler('port');
UI.addSettingChangeHandler('path');
@ -571,7 +580,15 @@ const UI = {
}
},
toggleControlbarSide() {
getControlbarPos() {
const anchor = document.getElementById('noVNC_control_bar_anchor');
if (anchor.classList.contains('noVNC_right')) return 'right';
if (anchor.classList.contains('noVNC_top')) return 'top';
if (anchor.classList.contains('noVNC_bottom')) return 'bottom';
return 'left';
},
toggleControlbarSide(pos) {
// 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');
@ -582,13 +599,12 @@ const UI = {
}
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");
anchor.classList.remove('noVNC_right', 'noVNC_top', 'noVNC_bottom');
if (['right', 'top', 'bottom'].includes(pos)) {
anchor.classList.add(`noVNC_${pos}`);
}
WebUtil.writeSetting('controlbar_pos', pos);
// Consider this a movement of the handle
UI.controlbarDrag = true;
@ -598,19 +614,21 @@ const UI = {
},
showControlbarHint(show, animate=true) {
const hint = document.getElementById('noVNC_control_bar_hint');
const getPos = element =>
['right', 'top', 'bottom'].find(pos =>
element.classList.contains(`noVNC_${pos}`)
) ?? 'left';
if (animate) {
hint.classList.remove("noVNC_notransition");
} else {
hint.classList.add("noVNC_notransition");
}
const anchor = document.getElementById('noVNC_control_bar_anchor');
const anchorPos = getPos(anchor);
if (show) {
hint.classList.add("noVNC_active");
} else {
hint.classList.remove("noVNC_active");
}
document.querySelectorAll('.noVNC_control_bar_hint').forEach((hint) => {
const hintPos = getPos(hint.parentElement);
const shouldShow = show && (hintPos !== anchorPos);
hint.classList.toggle('noVNC_active', shouldShow);
hint.classList.toggle('noVNC_notransition', !animate || !shouldShow);
});
},
dragControlbarHandle(e) {
@ -618,28 +636,62 @@ const UI = {
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();
let controlBarPos = UI.getControlbarPos();
if (ptr.clientX < (window.innerWidth * 0.1) &&
ptr.clientY > (window.innerHeight * 0.25) &&
ptr.clientY < (window.innerHeight * 0.75)) {
if (controlBarPos !== 'left') {
UI.toggleControlbarSide('left');
controlBarPos = 'left';
}
} else if (ptr.clientX > (window.innerWidth * 0.9)) {
if (!anchor.classList.contains("noVNC_right")) {
UI.toggleControlbarSide();
} else if (ptr.clientX > (window.innerWidth * 0.9) &&
ptr.clientY > (window.innerHeight * 0.25) &&
ptr.clientY < (window.innerHeight * 0.75)) {
if (controlBarPos !== 'right') {
UI.toggleControlbarSide('right');
controlBarPos = 'right';
}
// Slightly increased height thresholds since 10% of the
// height proved small in practice
} else if (ptr.clientX > (window.innerWidth * 0.25) &&
ptr.clientX < (window.innerWidth * 0.75) &&
ptr.clientY < (window.innerHeight * 0.2)) {
if (controlBarPos !== 'top') {
UI.toggleControlbarSide('top');
controlBarPos = 'top';
}
} else if (ptr.clientX > (window.innerWidth * 0.25) &&
ptr.clientX < (window.innerWidth * 0.75) &&
ptr.clientY > (window.innerHeight * 0.8)) {
if (controlBarPos !== 'bottom') {
UI.toggleControlbarSide("bottom");
controlBarPos = 'bottom';
}
}
const isVertical = controlBarPos === 'left' || controlBarPos === 'right';
if (!UI.controlbarDrag) {
const dragDistance = Math.abs(ptr.clientY - UI.controlbarMouseDownClientY);
const dragDistance = isVertical
? Math.abs(ptr.clientY - UI.controlbarMouseDownClientY)
: Math.abs(ptr.clientX - UI.controlbarMouseDownClientX);
if (dragDistance < dragThreshold) return;
UI.controlbarDrag = true;
}
const eventY = ptr.clientY - UI.controlbarMouseDownOffsetY;
UI.moveControlbarHandle(eventY);
if (isVertical) {
const eventY = ptr.clientY - UI.controlbarMouseDownOffsetY;
UI.moveControlbarHandle(eventY, true);
} else {
const eventX = ptr.clientX - UI.controlbarMouseDownOffsetX;
UI.moveControlbarHandle(eventX, false);
}
e.preventDefault();
e.stopPropagation();
@ -648,41 +700,56 @@ const UI = {
},
// Move the handle but don't allow any position outside the bounds
moveControlbarHandle(viewportRelativeY) {
moveControlbarHandle(viewportRelativeCoord, isVertical) {
const handle = document.getElementById("noVNC_control_bar_handle");
const handleHeight = handle.getBoundingClientRect().height;
const handleSpan = isVertical
? handle.getBoundingClientRect().height
: handle.getBoundingClientRect().width;
const controlbarBounds = document.getElementById("noVNC_control_bar")
.getBoundingClientRect();
const controlbarBoundsStart = isVertical
? controlbarBounds.top
: controlbarBounds.left;
const controlbarBoundsSpan = isVertical
? controlbarBounds.height
: controlbarBounds.width;
const margin = 10;
// These heights need to be non-zero for the below logic to work
if (handleHeight === 0 || controlbarBounds.height === 0) {
if (handleSpan === 0 || controlbarBoundsSpan === 0) {
return;
}
let newY = viewportRelativeY;
let newCoord = viewportRelativeCoord;
// 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;
if (newCoord < controlbarBoundsStart + margin) {
// Force coordinates to be below the start of the control bar
newCoord = controlbarBoundsStart + 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;
} else if (newCoord > controlbarBoundsStart +
controlbarBoundsSpan - handleSpan - margin) {
// Force coordinates to be before the end of the control bar
newCoord = controlbarBoundsStart +
controlbarBoundsSpan - handleSpan - margin;
}
// Corner case: control bar too small for stable position
if (controlbarBounds.height < (handleHeight + margin * 2)) {
newY = controlbarBounds.top +
(controlbarBounds.height - handleHeight) / 2;
if (controlbarBoundsSpan < (handleSpan + margin * 2)) {
newCoord = controlbarBoundsStart +
(controlbarBoundsSpan - handleSpan) / 2;
}
// The transform needs coordinates that are relative to the parent
const parentRelativeY = newY - controlbarBounds.top;
handle.style.transform = "translateY(" + parentRelativeY + "px)";
const parentRelativeCoord = newCoord - controlbarBoundsStart;
if (isVertical) {
handle.style.transform = "translateY(" + parentRelativeCoord + "px)";
} else {
handle.style.transform = "translateX(" + parentRelativeCoord + "px)";
}
},
updateControlbarHandle() {
@ -690,7 +757,15 @@ const UI = {
// 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);
const controlBarPos = UI.getControlbarPos();
const isVertical = controlBarPos === 'left' || controlBarPos === 'right';
if (isVertical) {
UI.moveControlbarHandle(handleBounds.top, true);
} else {
UI.moveControlbarHandle(handleBounds.left, false);
}
},
controlbarHandleMouseUp(e) {
@ -728,6 +803,8 @@ const UI = {
UI.controlbarMouseDownClientY = ptr.clientY;
UI.controlbarMouseDownOffsetY = ptr.clientY - bounds.top;
UI.controlbarMouseDownClientX = ptr.clientX;
UI.controlbarMouseDownOffsetX = ptr.clientX - bounds.left;
e.preventDefault();
e.stopPropagation();
UI.keepControlbar();
@ -1072,6 +1149,10 @@ const UI = {
url.protocol = (window.location.protocol === "https:") ? 'wss:' : 'ws:';
}
if (UI.getSetting('keep_device_awake')) {
UI.wakeLockManager.acquire();
}
try {
UI.rfb = new RFB(document.getElementById('noVNC_container'),
url.href,
@ -1103,6 +1184,7 @@ const UI = {
UI.rfb.showDotCursor = UI.getSetting('show_dot');
UI.updateViewOnly(); // requires UI.rfb
UI.updateClipboard();
},
disconnect() {
@ -1154,6 +1236,8 @@ const UI = {
UI.showStatus(msg);
UI.updateVisualState('connected');
UI.updateBeforeUnload();
// Do this last because it can only be used on rendered elements
UI.rfb.focus();
},
@ -1168,6 +1252,7 @@ const UI = {
UI.connected = false;
UI.rfb = undefined;
UI.wakeLockManager.release();
if (!e.detail.clean) {
UI.updateVisualState('disconnected');
@ -1190,6 +1275,8 @@ const UI = {
UI.showStatus(_("Disconnected"), 'normal');
}
UI.updateBeforeUnload();
document.title = PAGE_TITLE;
UI.openControlbar();
@ -1197,7 +1284,7 @@ const UI = {
},
securityFailed(e) {
let msg = "";
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.
@ -1210,6 +1297,24 @@ const UI = {
UI.showStatus(msg, 'error');
},
handleBeforeUnload(e) {
// Trigger a "Leave site?" warning prompt before closing the
// page. Modern browsers (Oct 2025) accept either (or both)
// preventDefault() or a nonempty returnValue, though the latter is
// considered legacy. The custom string is ignored by modern browsers,
// which display a native message, but older browsers will show it.
e.preventDefault();
e.returnValue = _("Are you sure you want to disconnect the session?");
},
updateBeforeUnload() {
// Remove first to avoid adding duplicates
window.removeEventListener("beforeunload", UI.handleBeforeUnload);
if (!UI.rfb?.viewOnly && UI.connected) {
window.addEventListener("beforeunload", UI.handleBeforeUnload);
}
},
/* ------^-------
* /CONNECTION
* ==============
@ -1256,13 +1361,13 @@ const UI = {
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;
} else if (document.getElementById("noVNC_username_input").value === "") {
inputFocus = "noVNC_username_input";
}
if (e.detail.types.indexOf("password") === -1) {
document.getElementById("noVNC_password_block").classList.add("noVNC_hidden");
} else {
inputFocus = inputFocus === "none" ? "noVNC_password_input" : inputFocus;
} else if (inputFocus === "none") {
inputFocus = "noVNC_password_input";
}
document.getElementById('noVNC_credentials_dlg')
.classList.add('noVNC_open');
@ -1736,6 +1841,8 @@ const UI = {
if (!UI.rfb) return;
UI.rfb.viewOnly = UI.getSetting('view_only');
UI.updateBeforeUnload();
// Hide input related buttons in view only mode
if (UI.rfb.viewOnly) {
document.getElementById('noVNC_keyboard_button')
@ -1754,6 +1861,31 @@ const UI = {
}
},
updateClipboard() {
browserAsyncClipboardSupport()
.then((support) => {
if (support === 'unsupported') {
// Use fallback clipboard panel
return;
}
if (support === 'denied' || support === 'available') {
UI.closeClipboardPanel();
document.getElementById('noVNC_clipboard_button')
.classList.add('noVNC_hidden');
document.getElementById('noVNC_clipboard_button')
.removeEventListener('click', UI.toggleClipboardPanel);
document.getElementById('noVNC_clipboard_text')
.removeEventListener('change', UI.clipboardSend);
if (UI.rfb) {
UI.rfb.removeEventListener('clipboard', UI.clipboardReceive);
}
}
})
.catch(() => {
// Treat as unsupported
});
},
updateShowDotCursor() {
if (!UI.rfb) return;
UI.rfb.showDotCursor = UI.getSetting('show_dot');
@ -1769,6 +1901,16 @@ const UI = {
document.title = e.detail.name + " - " + PAGE_TITLE;
},
updateRequestWakelock() {
if (!UI.rfb) return;
if (UI.getSetting('keep_device_awake')) {
UI.wakeLockManager.acquire();
} else {
UI.wakeLockManager.release();
}
},
bell(e) {
if (UI.getSetting('bell') === 'on') {
const promise = document.getElementById('noVNC_bell').play();

199
app/wakelock.js Normal file
View File

@ -0,0 +1,199 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2025 The noVNC authors
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
*
* Wrapper around the `navigator.wakeLock` api that handles reacquiring the
* lock on visiblility changes.
*
* The `acquire` and `release` methods may be called any number of times. The
* most recent call dictates the desired end-state (if `acquire` was most
* recently called, then we will try to acquire and hold the wake lock).
*/
import * as Log from '../core/util/logging.js';
const _STATES = {
/* No wake lock.
*
* Can transition to:
* - AWAITING_VISIBLE: `acquire` called when document is hidden.
* - ACQUIRING: `acquire` called.
* - ERROR: `acquired` called when the api is not available.
*/
RELEASED: 'released',
/* Wake lock requested, waiting for browser.
*
* Can transition to:
* - ACQUIRED: success
* - ACQUIRING_WANT_RELEASE: `release` called while waiting
* - ERROR
*/
ACQUIRING: 'acquiring',
/* Wake lock requested, release called, still waiting for browser.
*
* Can transition to:
* - ACQUIRING: `acquire` called (but promise has not resolved yet)
* - RELEASED: success
*/
ACQUIRING_WANT_RELEASE: 'releasing',
/* Wake lock held.
*
* Can transition to:
* - AWAITING_VISIBLE: wakelock lost due to visibility change
* - RELEASED: success
*/
ACQUIRED: 'acquired',
/* Caller wants wakelock, but we can not get it due to visibility.
*
* Can transition to:
* - ACQUIRING: document is now visible, attempting to get wakelock.
* - RELEASED: when release is called.
*/
AWAITING_VISIBLE: 'awaiting_visible',
/* An error has occurred.
*
* Can transition to:
* - RELEASED: will happen immediately.
*/
ERROR: 'error',
};
class TestOnlyWakeLockManagerStateChangeEvent extends Event {
constructor(oldState, newState) {
super("testOnlyStateChange");
this.oldState = oldState;
this.newState = newState;
}
}
export default class WakeLockManager extends EventTarget {
constructor() {
super();
this._state = _STATES.RELEASED;
this._wakelock = null;
this._eventHandlers = {
wakelockAcquired: this._wakelockAcquired.bind(this),
wakelockReleased: this._wakelockReleased.bind(this),
documentVisibilityChange: this._documentVisibilityChange.bind(this),
};
}
acquire() {
switch (this._state) {
case _STATES.ACQUIRING_WANT_RELEASE:
// We are currently waiting to acquire the wakelock. While
// waiting, `release()` was called. By transitioning back to
// ACQUIRING, we will keep the lock after we receive it.
this._transitionTo(_STATES.ACQUIRING);
break;
case _STATES.AWAITING_VISIBLE:
case _STATES.ACQUIRING:
case _STATES.ACQUIRED:
break;
case _STATES.ERROR:
case _STATES.RELEASED:
if (document.hidden) {
// We can not acquire the wakelock while the document is
// hidden (eg, not the active tab). Wait until it is
// visible, then acquire the wakelock.
this._awaitVisible();
break;
}
this._acquireWakelockNow();
break;
}
}
release() {
switch (this._state) {
case _STATES.ERROR:
case _STATES.RELEASED:
case _STATES.ACQUIRING_WANT_RELEASE:
break;
case _STATES.ACQUIRING:
// We are have requested (but not yet received) the wakelock.
// Give it up as soon as we acquire it.
this._transitionTo(_STATES.ACQUIRING_WANT_RELEASE);
break;
case _STATES.ACQUIRED:
// We remove the event listener first, as we don't want to be
// notified about this release (it is expected).
this._wakelock.removeEventListener("release", this._eventHandlers.wakelockReleased);
this._wakelock.release();
this._wakelock = null;
this._transitionTo(_STATES.RELEASED);
break;
case _STATES.AWAITING_VISIBLE:
// We don't currently have the lock, but are waiting for the
// document to become visible. By removing the event listener,
// we will not attempt to get the wakelock in the future.
document.removeEventListener("visibilitychange", this._eventHandlers.documentVisibilityChange);
this._transitionTo(_STATES.RELEASED);
break;
}
}
_transitionTo(newState) {
let oldState = this._state;
Log.Debug(`WakelockManager transitioning ${oldState} -> ${newState}`);
this._state = newState;
this.dispatchEvent(new TestOnlyWakeLockManagerStateChangeEvent(oldState, newState));
}
_awaitVisible() {
document.addEventListener("visibilitychange", this._eventHandlers.documentVisibilityChange);
this._transitionTo(_STATES.AWAITING_VISIBLE);
}
_acquireWakelockNow() {
if (!("wakeLock" in navigator)) {
Log.Warn("Unable to request wakeLock, Browser does not have wakeLock api");
this._transitionTo(_STATES.ERROR);
this._transitionTo(_STATES.RELEASED);
return;
}
navigator.wakeLock.request("screen")
.then(this._eventHandlers.wakelockAcquired)
.catch((err) => {
Log.Warn("Error occurred while acquiring wakelock: " + err);
this._transitionTo(_STATES.ERROR);
this._transitionTo(_STATES.RELEASED);
});
this._transitionTo(_STATES.ACQUIRING);
}
_wakelockAcquired(wakelock) {
if (this._state === _STATES.ACQUIRING_WANT_RELEASE) {
// We were requested to release the wakelock while we were trying to
// acquire it. Now that we have acquired it, immediately release it.
wakelock.release();
this._transitionTo(_STATES.RELEASED);
return;
}
this._wakelock = wakelock;
this._wakelock.addEventListener("release", this._eventHandlers.wakelockReleased);
this._transitionTo(_STATES.ACQUIRED);
}
_wakelockReleased(event) {
this._wakelock = null;
if (document.visibilityState === "visible") {
Log.Warn("Lost wakelock, but document is still visible. Not reacquiring");
this._transitionTo(_STATES.RELEASED);
return;
}
this._awaitVisible();
}
_documentVisibilityChange(event) {
if (document.visibilityState !== "visible") {
return;
}
document.removeEventListener("visibilitychange", this._eventHandlers.documentVisibilityChange);
this._acquireWakelockNow();
}
}

72
core/clipboard.js Normal file
View File

@ -0,0 +1,72 @@
/*
* noVNC: HTML5 VNC client
* Copyright (c) 2025 The noVNC authors
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
*/
import * as Log from './util/logging.js';
import { browserAsyncClipboardSupport } from './util/browser.js';
export default class AsyncClipboard {
constructor(target) {
this._target = target || null;
this._isAvailable = null;
this._eventHandlers = {
'focus': this._handleFocus.bind(this),
};
// ===== EVENT HANDLERS =====
this.onpaste = () => {};
}
// ===== PRIVATE METHODS =====
async _ensureAvailable() {
if (this._isAvailable !== null) return this._isAvailable;
try {
const status = await browserAsyncClipboardSupport();
this._isAvailable = (status === 'available');
} catch {
this._isAvailable = false;
}
return this._isAvailable;
}
async _handleFocus(event) {
if (!(await this._ensureAvailable())) return;
try {
const text = await navigator.clipboard.readText();
this.onpaste(text);
} catch (error) {
Log.Error("Clipboard read failed: ", error);
}
}
// ===== PUBLIC METHODS =====
writeClipboard(text) {
// Can lazily check cached availability
if (!this._isAvailable) return false;
navigator.clipboard.writeText(text)
.catch(error => Log.Error("Clipboard write failed: ", error));
return true;
}
grab() {
if (!this._target) return;
this._ensureAvailable()
.then((isAvailable) => {
if (isAvailable) {
this._target.addEventListener('focus', this._eventHandlers.focus);
}
});
}
ungrab() {
if (!this._target) return;
this._target.removeEventListener('focus', this._eventHandlers.focus);
}
}

View File

@ -181,11 +181,11 @@ class DES {
// Encrypt 8 bytes of text
enc8(text) {
const b = text.slice();
let i = 0, l, r, x; // left, right, accumulator
let 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++];
l = b[0]<<24 | b[1]<<16 | b[2]<<8 | b[3];
r = b[4]<<24 | b[5]<<16 | b[6]<<8 | b[7];
x = ((l >>> 4) ^ r) & 0x0f0f0f0f;
r ^= x;
@ -252,7 +252,7 @@ class DES {
// Spread ints to bytes
x = [r, l];
for (i = 0; i < 8; i++) {
for (let i = 0; i < 8; i++) {
b[i] = (x[i>>>2] >>> (8 * (3 - (i % 4)))) % 256;
if (b[i] < 0) { b[i] += 256; } // unsigned
}

View File

@ -175,7 +175,7 @@ export default class ZRLEDecoder {
_readRLELength() {
let length = 0;
let current = 0;
let current;
do {
current = this._inflator.inflate(1)[0];
length += current;

View File

@ -14,7 +14,7 @@ export default class Display {
constructor(target) {
this._drawCtx = null;
this._renderQ = []; // queue drawing actions for in-oder rendering
this._renderQ = []; // queue drawing actions for in-order rendering
this._flushPromise = null;
// the full frame buffer (logical canvas) size

View File

@ -15,6 +15,7 @@ import { clientToElement } from './util/element.js';
import { setCapture } from './util/events.js';
import EventTargetMixin from './util/eventtarget.js';
import Display from "./display.js";
import AsyncClipboard from "./clipboard.js";
import Inflator from "./inflator.js";
import Deflator from "./deflator.js";
import Keyboard from "./input/keyboard.js";
@ -164,6 +165,7 @@ export default class RFB extends EventTargetMixin {
this._sock = null; // Websock object
this._display = null; // Display object
this._flushing = false; // Display flushing state
this._asyncClipboard = null; // Async clipboard object
this._keyboard = null; // Keyboard input handler object
this._gestures = null; // Gesture input handler object
this._resizeObserver = null; // Resize observer object
@ -266,6 +268,9 @@ export default class RFB extends EventTargetMixin {
throw exc;
}
this._asyncClipboard = new AsyncClipboard(this._canvas);
this._asyncClipboard.onpaste = this.clipboardPasteFrom.bind(this);
this._keyboard = new Keyboard(this._canvas);
this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
this._remoteCapsLock = null; // Null indicates unknown or irrelevant
@ -315,8 +320,10 @@ export default class RFB extends EventTargetMixin {
this._rfbConnectionState === "connected") {
if (viewOnly) {
this._keyboard.ungrab();
this._asyncClipboard.ungrab();
} else {
this._keyboard.grab();
this._asyncClipboard.grab();
}
}
}
@ -2208,7 +2215,10 @@ export default class RFB extends EventTargetMixin {
this._setDesktopName(name);
this._resize(width, height);
if (!this._viewOnly) { this._keyboard.grab(); }
if (!this._viewOnly) {
this._keyboard.grab();
this._asyncClipboard.grab();
}
this._fbDepth = 24;
@ -2323,6 +2333,15 @@ export default class RFB extends EventTargetMixin {
return this._fail("Unexpected SetColorMapEntries message");
}
_writeClipboard(text) {
if (this._viewOnly) return;
if (this._asyncClipboard.writeClipboard(text)) return;
// Fallback clipboard
this.dispatchEvent(
new CustomEvent("clipboard", {detail: {text: text}})
);
}
_handleServerCutText() {
Log.Debug("ServerCutText");
@ -2342,9 +2361,7 @@ export default class RFB extends EventTargetMixin {
return true;
}
this.dispatchEvent(new CustomEvent(
"clipboard",
{ detail: { text: text } }));
this._writeClipboard(text);
} else {
//Extended msg.
@ -2480,9 +2497,7 @@ export default class RFB extends EventTargetMixin {
textData = textData.replaceAll("\r\n", "\n");
this.dispatchEvent(new CustomEvent(
"clipboard",
{ detail: { text: textData } }));
this._writeClipboard(textData);
}
} else {
return this._fail("Unexpected action in extended clipboard message: " + actions);
@ -2934,7 +2949,7 @@ export default class RFB extends EventTargetMixin {
// We need to handle errors when we requested the resize.
if (this._FBU.x === 1 && this._FBU.y !== 0) {
let msg = "";
let msg;
// The y-position indicates the status code from the server
switch (this._FBU.y) {
case 1:

View File

@ -11,6 +11,39 @@
import * as Log from './logging.js';
import Base64 from '../base64.js';
// Async clipboard detection
/* Evaluates if there is browser support for the async clipboard API and
* relevant clipboard permissions. Returns 'unsupported' if permission states
* cannot be resolved. On the other hand, detecting 'granted' or 'prompt'
* permission states for both read and write indicates full API support with no
* imposed native browser paste prompt. Conversely, detecting 'denied' indicates
* the user elected to disable clipboard.
*/
export async function browserAsyncClipboardSupport() {
if (!(navigator?.permissions?.query &&
navigator?.clipboard?.writeText &&
navigator?.clipboard?.readText)) {
return 'unsupported';
}
try {
const writePerm = await navigator.permissions.query(
{name: "clipboard-write", allowWithoutGesture: true});
const readPerm = await navigator.permissions.query(
{name: "clipboard-read", allowWithoutGesture: false});
if (writePerm.state === "denied" || readPerm.state === "denied") {
return 'denied';
}
if ((writePerm.state === "granted" || writePerm.state === "prompt") &&
(readPerm.state === "granted" || readPerm.state === "prompt")) {
return 'available';
}
} catch {
return 'unsupported';
}
return 'unsupported';
}
// Touch detection
export let isTouchDevice = ('ontouchstart' in document.documentElement) ||
// required for Chrome debugger
@ -149,7 +182,12 @@ async function _checkWebCodecsH264DecodeSupport() {
return true;
}
supportsWebCodecsH264Decode = await _checkWebCodecsH264DecodeSupport();
// FIXME: Avoid top-level await due to a Chromium bug where Decoder.flush()
// can hang indefinitely on some Android devices, blocking module evaluation.
_checkWebCodecsH264DecodeSupport().then((result) => {
supportsWebCodecsH264Decode = result;
});
/*
* The functions for detection of platforms and browsers below are exported

View File

@ -18,6 +18,8 @@ keysym values.
* __Display__ (core/display.js): Efficient 2D rendering abstraction
layered on the HTML5 canvas element.
* __Clipboard__ (core/clipboard.js): Clipboard event handler.
* __Websock__ (core/websock.js): Websock client from websockify
with transparent binary data support.
[Websock API](https://github.com/novnc/websockify-js/wiki/websock.js) wiki page.
@ -25,10 +27,10 @@ with transparent binary data support.
## 1.2 Callbacks
For the Mouse, Keyboard and Display objects the callback functions are
assigned to configuration attributes, just as for the RFB object. The
WebSock module has a method named 'on' that takes two parameters: the
callback event name, and the callback function.
For the Mouse, Keyboard, Display, and Clipboard objects, the callback
functions are assigned to configuration attributes, just as for the RFB
object. The WebSock module has a method named 'on' that takes two
parameters: the callback event name, and the callback function.
## 2. Modules
@ -81,3 +83,23 @@ None
| blitImage | (x, y, width, height, arr, offset, from_queue) | Blit pixels (of R,G,B,A) to the display
| drawImage | (img, x, y) | Draw image and track damage
| autoscale | (containerWidth, containerHeight) | Scale the display
## 2.3 Clipboard module
### 2.3.1 Configuration attributes
None
### 2.3.2 Methods
| name | parameters | description
| ------------------ | ----------------- | ------------
| writeClipboard | (text) | An async write text to clipboard
| grab | () | Begin capturing clipboard events
| ungrab | () | Stop capturing clipboard events
### 2.3.3 Callbacks
| name | parameters | description
| ------- | ---------- | ------------
| onpaste | (text) | Called following a target focus event and an async clipboard read

View File

@ -77,11 +77,6 @@ protocol stream.
if the remote session is smaller than its container, or handled
according to `clipViewport` if it is larger. Disabled by default.
`showDotCursor`
- Is a `boolean` indicating whether a dot cursor should be shown
instead of a zero-sized or fully-transparent cursor if the server
sets such invisible cursor. Disabled by default.
`viewOnly`
- Is a `boolean` indicating if any events (e.g. key presses or mouse
movement) should be prevented from being sent to the server.

View File

@ -89,12 +89,13 @@ Currently, the following options are available:
* `compression` - The session compression level. Can be `0` to `9`.
* `show_dot` - If a dot cursor should be shown when the remote server provides
no local cursor, or provides a fully-transparent (invisible) cursor.
* `logging` - The console log level. Can be one of `error`, `warn`, `info` or
`debug`.
* `keep_device_awake` - Should we prevent the (local) display from going into
sleep mode while a connection is active? Useful for view-only sessions where
there unlikely to be any keyboard/mouse activity to keep the device active.
## HTTP serving considerations
### Browser cache issue

View File

@ -1,7 +1,8 @@
import globals from "globals";
import { defineConfig } from "eslint/config";
import js from "@eslint/js";
export default [
export default defineConfig([
js.configs.recommended,
{
languageOptions: {
@ -99,4 +100,4 @@ export default [
"no-console": 0,
},
},
];
]);

View File

@ -37,6 +37,7 @@ module.exports = (config) => {
{ pattern: 'node_modules/sinon-chai/**', included: false },
// modules to test
{ pattern: 'app/localization.js', included: false, type: 'module' },
{ pattern: 'app/wakelock.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' },

View File

@ -1,6 +1,6 @@
{
"name": "@novnc/novnc",
"version": "1.6.0",
"version": "1.7.0",
"description": "An HTML5 VNC client",
"type": "module",
"files": [
@ -39,6 +39,7 @@
"chai": "latest",
"commander": "latest",
"eslint": "latest",
"@eslint/js": "latest",
"fs-extra": "latest",
"globals": "latest",
"jsdom": "latest",

View File

@ -2,7 +2,7 @@ all:
.PHONY: update-po update-js update-pot
.PHONY: FORCE
LINGUAS := cs de el es fr hr it ja ko nl pl pt_BR ru sv tr zh_CN zh_TW
LINGUAS := cs de el es fr hr hu it ja ko nl pl pt_BR ru sv tr uk zh_CN zh_TW
VERSION := $(shell grep '"version"' ../package.json | cut -d '"' -f 4)

332
po/hu.po Normal file
View File

@ -0,0 +1,332 @@
# Hungarian translations for noVNC package.
# Copyright (C) 2025 The noVNC authors
# This file is distributed under the same license as the noVNC package.
# Daniel Felso <danielfelso@protonmail.com>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: noVNC 1.6.0\n"
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
"POT-Creation-Date: 2025-02-14 10:14+0100\n"
"PO-Revision-Date: 2025-10-06 14:38+0200\n"
"Last-Translator: Daniel Felso <danielfelso@protonmail.com>\n"
"Language-Team: \n"
"Language: hu\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: ../app/ui.js:84
msgid ""
"Running without HTTPS is not recommended, crashes or other issues are likely."
msgstr "HTTPS nélkül futtatni nem ajánlott, összeomlások vagy más problémák várhatók."
#: ../app/ui.js:413
msgid "Connecting..."
msgstr "Kapcsolódás..."
#: ../app/ui.js:420
msgid "Disconnecting..."
msgstr "Kapcsolat bontása..."
#: ../app/ui.js:426
msgid "Reconnecting..."
msgstr "Újrakapcsolódás..."
#: ../app/ui.js:431
msgid "Internal error"
msgstr "Belső hiba"
#: ../app/ui.js:1079
msgid "Failed to connect to server: "
msgstr "Nem sikerült csatlakozni a szerverhez: "
#: ../app/ui.js:1145
msgid "Connected (encrypted) to "
msgstr "Kapcsolódva (titkosítva) ehhez: "
#: ../app/ui.js:1147
msgid "Connected (unencrypted) to "
msgstr "Kapcsolódva (titkosítatlanul) ehhez: "
#: ../app/ui.js:1170
msgid "Something went wrong, connection is closed"
msgstr "Valami hiba történt, a kapcsolat lezárult"
#: ../app/ui.js:1173
msgid "Failed to connect to server"
msgstr "Nem sikerült csatlakozni a szerverhez"
#: ../app/ui.js:1185
msgid "Disconnected"
msgstr "Kapcsolat bontva"
#: ../app/ui.js:1200
msgid "New connection has been rejected with reason: "
msgstr "Az új kapcsolat elutasítva, indok: "
#: ../app/ui.js:1203
msgid "New connection has been rejected"
msgstr "Az új kapcsolat elutasítva"
#: ../app/ui.js:1269
msgid "Credentials are required"
msgstr "Hitelesítő adatok szükségesek"
#: ../vnc.html:106
msgid "noVNC encountered an error:"
msgstr "A noVNC hibát észlelt:"
#: ../vnc.html:116
msgid "Hide/Show the control bar"
msgstr "Vezérlősáv elrejtése/megjelenítése"
#: ../vnc.html:125
msgid "Drag"
msgstr "Húzás"
#: ../vnc.html:125
msgid "Move/Drag viewport"
msgstr "Nézet mozgatása/húzása"
#: ../vnc.html:131
msgid "Keyboard"
msgstr "Billentyűzet"
#: ../vnc.html:131
msgid "Show keyboard"
msgstr "Billentyűzet megjelenítése"
#: ../vnc.html:136
msgid "Extra keys"
msgstr "Extra billentyűk"
#: ../vnc.html:136
msgid "Show extra keys"
msgstr "Extra billentyűk megjelenítése"
#: ../vnc.html:141
msgid "Ctrl"
msgstr "Ctrl"
#: ../vnc.html:141
msgid "Toggle Ctrl"
msgstr "Ctrl lenyomása/felengedése"
#: ../vnc.html:144
msgid "Alt"
msgstr "Alt"
#: ../vnc.html:144
msgid "Toggle Alt"
msgstr "Alt lenyomása/felengedése"
#: ../vnc.html:147
msgid "Toggle Windows"
msgstr "Windows lenyomása/felengedése"
#: ../vnc.html:147
msgid "Windows"
msgstr "Windows"
#: ../vnc.html:150
msgid "Send Tab"
msgstr "Tab küldése"
#: ../vnc.html:150
msgid "Tab"
msgstr "Tab"
#: ../vnc.html:153
msgid "Esc"
msgstr "Esc"
#: ../vnc.html:153
msgid "Send Escape"
msgstr "Escape küldése"
#: ../vnc.html:156
msgid "Ctrl+Alt+Del"
msgstr "Ctrl+Alt+Del"
#: ../vnc.html:156
msgid "Send Ctrl-Alt-Del"
msgstr "Ctrl-Alt-Del küldése"
#: ../vnc.html:163
msgid "Shutdown/Reboot"
msgstr "Leállítás/Újraindítás"
#: ../vnc.html:163
msgid "Shutdown/Reboot..."
msgstr "Leállítás/Újraindítás..."
#: ../vnc.html:169
msgid "Power"
msgstr "Bekapcsolás"
#: ../vnc.html:171
msgid "Shutdown"
msgstr "Leállítás"
#: ../vnc.html:172
msgid "Reboot"
msgstr "Újraindítás"
#: ../vnc.html:173
msgid "Reset"
msgstr "Reset"
#: ../vnc.html:178 ../vnc.html:184
msgid "Clipboard"
msgstr "Vágólap"
#: ../vnc.html:186
msgid "Edit clipboard content in the textarea below."
msgstr "Itt tudod módosítani a vágólap tartalmát."
#: ../vnc.html:194
msgid "Full screen"
msgstr "Teljes képernyő"
#: ../vnc.html:199 ../vnc.html:205
msgid "Settings"
msgstr "Beállítások"
#: ../vnc.html:211
msgid "Shared mode"
msgstr "Megosztott mód"
#: ../vnc.html:218
msgid "View only"
msgstr "Csak megtekintés"
#: ../vnc.html:226
msgid "Clip to window"
msgstr "Ablakhoz igazítás"
#: ../vnc.html:231
msgid "Scaling mode:"
msgstr "Méretezési mód:"
#: ../vnc.html:233
msgid "None"
msgstr "Nincs"
#: ../vnc.html:234
msgid "Local scaling"
msgstr "Helyi méretezés"
#: ../vnc.html:235
msgid "Remote resizing"
msgstr "Távoli átméretezés"
#: ../vnc.html:240
msgid "Advanced"
msgstr "Speciális"
#: ../vnc.html:243
msgid "Quality:"
msgstr "Minőség:"
#: ../vnc.html:247
msgid "Compression level:"
msgstr "Tömörítési szint:"
#: ../vnc.html:252
msgid "Repeater ID:"
msgstr "Ismétlő azonosító:"
#: ../vnc.html:256
msgid "WebSocket"
msgstr "WebSocket"
#: ../vnc.html:261
msgid "Encrypt"
msgstr "Titkosítás"
#: ../vnc.html:266
msgid "Host:"
msgstr "Hoszt:"
#: ../vnc.html:270
msgid "Port:"
msgstr "Port:"
#: ../vnc.html:274
msgid "Path:"
msgstr "Útvonal:"
#: ../vnc.html:283
msgid "Automatic reconnect"
msgstr "Automatikus újracsatlakozás"
#: ../vnc.html:288
msgid "Reconnect delay (ms):"
msgstr "Újracsatlakozás késleltetése (ms):"
#: ../vnc.html:295
msgid "Show dot when no cursor"
msgstr "Kurzor hiányában pont mutatása"
#: ../vnc.html:302
msgid "Logging:"
msgstr "Naplózás:"
#: ../vnc.html:311
msgid "Version:"
msgstr "Verzió:"
#: ../vnc.html:319
msgid "Disconnect"
msgstr "Kapcsolat bontása"
#: ../vnc.html:342
msgid "Connect"
msgstr "Csatlakozás"
#: ../vnc.html:351
msgid "Server identity"
msgstr "Szerver azonosító"
#: ../vnc.html:354
msgid "The server has provided the following identifying information:"
msgstr "A szerver a következő azonosító információt adta meg:"
#: ../vnc.html:357
msgid "Fingerprint:"
msgstr "Ujjlenyomat:"
#: ../vnc.html:361
msgid ""
"Please verify that the information is correct and press \"Approve\". "
"Otherwise press \"Reject\"."
msgstr "Ellenőrizze, hogy az információ helyes-e és nyomja meg a \"Jóváhagyás\" gombot. Ellenkező esetben nyomja meg az \"Elutasítás\" gombot."
#: ../vnc.html:366
msgid "Approve"
msgstr "Jóváhagyás"
#: ../vnc.html:367
msgid "Reject"
msgstr "Elutasítás"
#: ../vnc.html:375
msgid "Credentials"
msgstr "Hitelesítő adatok"
#: ../vnc.html:379
msgid "Username:"
msgstr "Felhasználónév:"
#: ../vnc.html:383
msgid "Password:"
msgstr "Jelszó:"
#: ../vnc.html:387
msgid "Send credentials"
msgstr "Hitelesítő adatok küldése"
#: ../vnc.html:396
msgid "Cancel"
msgstr "Mégse"

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: noVNC 1.6.0\n"
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
"POT-Creation-Date: 2025-02-14 10:14+0100\n"
"POT-Creation-Date: 2025-10-31 09:17+0100\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"
@ -38,39 +38,43 @@ msgstr ""
msgid "Internal error"
msgstr ""
#: ../app/ui.js:1079
#: ../app/ui.js:1084
msgid "Failed to connect to server: "
msgstr ""
#: ../app/ui.js:1145
#: ../app/ui.js:1151
msgid "Connected (encrypted) to "
msgstr ""
#: ../app/ui.js:1147
#: ../app/ui.js:1153
msgid "Connected (unencrypted) to "
msgstr ""
#: ../app/ui.js:1170
#: ../app/ui.js:1178
msgid "Something went wrong, connection is closed"
msgstr ""
#: ../app/ui.js:1173
#: ../app/ui.js:1181
msgid "Failed to connect to server"
msgstr ""
#: ../app/ui.js:1185
#: ../app/ui.js:1193
msgid "Disconnected"
msgstr ""
#: ../app/ui.js:1200
#: ../app/ui.js:1210
msgid "New connection has been rejected with reason: "
msgstr ""
#: ../app/ui.js:1203
#: ../app/ui.js:1213
msgid "New connection has been rejected"
msgstr ""
#: ../app/ui.js:1269
#: ../app/ui.js:1225
msgid "Are you sure you want to disconnect the session?"
msgstr ""
#: ../app/ui.js:1297
msgid "Credentials are required"
msgstr ""

700
po/sv.po
View File

@ -1,348 +1,352 @@
# Swedish translations for noVNC package
# Svenska översättningar för paketet noVNC.
# Copyright (C) 2025 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.6.0\n"
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
"POT-Creation-Date: 2025-02-14 10:14+0100\n"
"PO-Revision-Date: 2025-02-14 10:29+0100\n"
"Last-Translator: Alexander Zeijlon <aleze@cendio.com>\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 3.5\n"
#: ../app/ui.js:84
msgid ""
"Running without HTTPS is not recommended, crashes or other issues are likely."
msgstr ""
"Det är ej rekommenderat att köra utan HTTPS, krascher och andra problem är "
"troliga."
#: ../app/ui.js:413
msgid "Connecting..."
msgstr "Ansluter..."
#: ../app/ui.js:420
msgid "Disconnecting..."
msgstr "Kopplar ner..."
#: ../app/ui.js:426
msgid "Reconnecting..."
msgstr "Återansluter..."
#: ../app/ui.js:431
msgid "Internal error"
msgstr "Internt fel"
#: ../app/ui.js:1079
msgid "Failed to connect to server: "
msgstr "Misslyckades att ansluta till servern: "
#: ../app/ui.js:1145
msgid "Connected (encrypted) to "
msgstr "Ansluten (krypterat) till "
#: ../app/ui.js:1147
msgid "Connected (unencrypted) to "
msgstr "Ansluten (okrypterat) till "
#: ../app/ui.js:1170
msgid "Something went wrong, connection is closed"
msgstr "Något gick fel, anslutningen avslutades"
#: ../app/ui.js:1173
msgid "Failed to connect to server"
msgstr "Misslyckades att ansluta till servern"
#: ../app/ui.js:1185
msgid "Disconnected"
msgstr "Frånkopplad"
#: ../app/ui.js:1200
msgid "New connection has been rejected with reason: "
msgstr "Ny anslutning har blivit nekad med följande skäl: "
#: ../app/ui.js:1203
msgid "New connection has been rejected"
msgstr "Ny anslutning har blivit nekad"
#: ../app/ui.js:1269
msgid "Credentials are required"
msgstr "Användaruppgifter krävs"
#: ../vnc.html:106
msgid "noVNC encountered an error:"
msgstr "noVNC stötte på ett problem:"
#: ../vnc.html:116
msgid "Hide/Show the control bar"
msgstr "Göm/Visa kontrollbaren"
#: ../vnc.html:125
msgid "Drag"
msgstr "Dra"
#: ../vnc.html:125
msgid "Move/Drag viewport"
msgstr "Flytta/Dra vyn"
#: ../vnc.html:131
msgid "Keyboard"
msgstr "Tangentbord"
#: ../vnc.html:131
msgid "Show keyboard"
msgstr "Visa tangentbord"
#: ../vnc.html:136
msgid "Extra keys"
msgstr "Extraknappar"
#: ../vnc.html:136
msgid "Show extra keys"
msgstr "Visa extraknappar"
#: ../vnc.html:141
msgid "Ctrl"
msgstr "Ctrl"
#: ../vnc.html:141
msgid "Toggle Ctrl"
msgstr "Växla Ctrl"
#: ../vnc.html:144
msgid "Alt"
msgstr "Alt"
#: ../vnc.html:144
msgid "Toggle Alt"
msgstr "Växla Alt"
#: ../vnc.html:147
msgid "Toggle Windows"
msgstr "Växla Windows"
#: ../vnc.html:147
msgid "Windows"
msgstr "Windows"
#: ../vnc.html:150
msgid "Send Tab"
msgstr "Skicka Tab"
#: ../vnc.html:150
msgid "Tab"
msgstr "Tab"
#: ../vnc.html:153
msgid "Esc"
msgstr "Esc"
#: ../vnc.html:153
msgid "Send Escape"
msgstr "Skicka Escape"
#: ../vnc.html:156
msgid "Ctrl+Alt+Del"
msgstr "Ctrl+Alt+Del"
#: ../vnc.html:156
msgid "Send Ctrl-Alt-Del"
msgstr "Skicka Ctrl-Alt-Del"
#: ../vnc.html:163
msgid "Shutdown/Reboot"
msgstr "Stäng av/Boota om"
#: ../vnc.html:163
msgid "Shutdown/Reboot..."
msgstr "Stäng av/Boota om..."
#: ../vnc.html:169
msgid "Power"
msgstr "Ström"
#: ../vnc.html:171
msgid "Shutdown"
msgstr "Stäng av"
#: ../vnc.html:172
msgid "Reboot"
msgstr "Boota om"
#: ../vnc.html:173
msgid "Reset"
msgstr "Återställ"
#: ../vnc.html:178 ../vnc.html:184
msgid "Clipboard"
msgstr "Urklipp"
#: ../vnc.html:186
msgid "Edit clipboard content in the textarea below."
msgstr "Redigera urklippets innehåll i fältet nedan."
#: ../vnc.html:194
msgid "Full screen"
msgstr "Fullskärm"
#: ../vnc.html:199 ../vnc.html:205
msgid "Settings"
msgstr "Inställningar"
#: ../vnc.html:211
msgid "Shared mode"
msgstr "Delat läge"
#: ../vnc.html:218
msgid "View only"
msgstr "Endast visning"
#: ../vnc.html:226
msgid "Clip to window"
msgstr "Begränsa till fönster"
#: ../vnc.html:231
msgid "Scaling mode:"
msgstr "Skalningsläge:"
#: ../vnc.html:233
msgid "None"
msgstr "Ingen"
#: ../vnc.html:234
msgid "Local scaling"
msgstr "Lokal skalning"
#: ../vnc.html:235
msgid "Remote resizing"
msgstr "Ändra storlek"
#: ../vnc.html:240
msgid "Advanced"
msgstr "Avancerat"
#: ../vnc.html:243
msgid "Quality:"
msgstr "Kvalitet:"
#: ../vnc.html:247
msgid "Compression level:"
msgstr "Kompressionsnivå:"
#: ../vnc.html:252
msgid "Repeater ID:"
msgstr "Repeater-ID:"
#: ../vnc.html:256
msgid "WebSocket"
msgstr "WebSocket"
#: ../vnc.html:261
msgid "Encrypt"
msgstr "Kryptera"
#: ../vnc.html:266
msgid "Host:"
msgstr "Värd:"
#: ../vnc.html:270
msgid "Port:"
msgstr "Port:"
#: ../vnc.html:274
msgid "Path:"
msgstr "Sökväg:"
#: ../vnc.html:283
msgid "Automatic reconnect"
msgstr "Automatisk återanslutning"
#: ../vnc.html:288
msgid "Reconnect delay (ms):"
msgstr "Fördröjning (ms):"
#: ../vnc.html:295
msgid "Show dot when no cursor"
msgstr "Visa prick när ingen muspekare finns"
#: ../vnc.html:302
msgid "Logging:"
msgstr "Loggning:"
#: ../vnc.html:311
msgid "Version:"
msgstr "Version:"
#: ../vnc.html:319
msgid "Disconnect"
msgstr "Koppla från"
#: ../vnc.html:342
msgid "Connect"
msgstr "Anslut"
#: ../vnc.html:351
msgid "Server identity"
msgstr "Server-identitet"
#: ../vnc.html:354
msgid "The server has provided the following identifying information:"
msgstr "Servern har gett följande identifierande information:"
#: ../vnc.html:357
msgid "Fingerprint:"
msgstr "Fingeravtryck:"
#: ../vnc.html:361
msgid ""
"Please verify that the information is correct and press \"Approve\". "
"Otherwise press \"Reject\"."
msgstr ""
"Kontrollera att informationen är korrekt och tryck sedan \"Godkänn\". Tryck "
"annars \"Neka\"."
#: ../vnc.html:366
msgid "Approve"
msgstr "Godkänn"
#: ../vnc.html:367
msgid "Reject"
msgstr "Neka"
#: ../vnc.html:375
msgid "Credentials"
msgstr "Användaruppgifter"
#: ../vnc.html:379
msgid "Username:"
msgstr "Användarnamn:"
#: ../vnc.html:383
msgid "Password:"
msgstr "Lösenord:"
#: ../vnc.html:387
msgid "Send credentials"
msgstr "Skicka användaruppgifter"
#: ../vnc.html:396
msgid "Cancel"
msgstr "Avbryt"
#~ msgid "Must set host"
#~ msgstr "Du måste specifiera en värd"
#~ msgid "HTTPS is required for full functionality"
#~ msgstr "HTTPS krävs för full funktionalitet"
#~ msgid "Clear"
#~ msgstr "Rensa"
# Swedish translations for noVNC package
# Svenska översättningar för paketet noVNC.
# Copyright (C) 2025 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.6.0\n"
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
"POT-Creation-Date: 2025-10-31 09:17+0100\n"
"PO-Revision-Date: 2025-10-31 10:48+0100\n"
"Last-Translator: Alexander Zeijlon <aleze@cendio.com>\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 3.7\n"
#: ../app/ui.js:84
msgid ""
"Running without HTTPS is not recommended, crashes or other issues are likely."
msgstr ""
"Det är ej rekommenderat att köra utan HTTPS, krascher och andra problem är "
"troliga."
#: ../app/ui.js:413
msgid "Connecting..."
msgstr "Ansluter..."
#: ../app/ui.js:420
msgid "Disconnecting..."
msgstr "Kopplar ifrån..."
#: ../app/ui.js:426
msgid "Reconnecting..."
msgstr "Återansluter..."
#: ../app/ui.js:431
msgid "Internal error"
msgstr "Internt fel"
#: ../app/ui.js:1084
msgid "Failed to connect to server: "
msgstr "Misslyckades att ansluta till servern: "
#: ../app/ui.js:1151
msgid "Connected (encrypted) to "
msgstr "Ansluten (krypterat) till "
#: ../app/ui.js:1153
msgid "Connected (unencrypted) to "
msgstr "Ansluten (okrypterat) till "
#: ../app/ui.js:1178
msgid "Something went wrong, connection is closed"
msgstr "Något gick fel, anslutningen avslutades"
#: ../app/ui.js:1181
msgid "Failed to connect to server"
msgstr "Misslyckades att ansluta till servern"
#: ../app/ui.js:1193
msgid "Disconnected"
msgstr "Frånkopplad"
#: ../app/ui.js:1210
msgid "New connection has been rejected with reason: "
msgstr "Ny anslutning har blivit nekad med följande skäl: "
#: ../app/ui.js:1213
msgid "New connection has been rejected"
msgstr "Ny anslutning har blivit nekad"
#: ../app/ui.js:1225
msgid "Are you sure you want to disconnect the session?"
msgstr "Är du säker på att du vill koppla ifrån sessionen?"
#: ../app/ui.js:1297
msgid "Credentials are required"
msgstr "Användaruppgifter krävs"
#: ../vnc.html:106
msgid "noVNC encountered an error:"
msgstr "noVNC stötte på ett problem:"
#: ../vnc.html:116
msgid "Hide/Show the control bar"
msgstr "Göm/Visa kontrollbaren"
#: ../vnc.html:125
msgid "Drag"
msgstr "Dra"
#: ../vnc.html:125
msgid "Move/Drag viewport"
msgstr "Flytta/Dra vyn"
#: ../vnc.html:131
msgid "Keyboard"
msgstr "Tangentbord"
#: ../vnc.html:131
msgid "Show keyboard"
msgstr "Visa tangentbord"
#: ../vnc.html:136
msgid "Extra keys"
msgstr "Extraknappar"
#: ../vnc.html:136
msgid "Show extra keys"
msgstr "Visa extraknappar"
#: ../vnc.html:141
msgid "Ctrl"
msgstr "Ctrl"
#: ../vnc.html:141
msgid "Toggle Ctrl"
msgstr "Växla Ctrl"
#: ../vnc.html:144
msgid "Alt"
msgstr "Alt"
#: ../vnc.html:144
msgid "Toggle Alt"
msgstr "Växla Alt"
#: ../vnc.html:147
msgid "Toggle Windows"
msgstr "Växla Windows"
#: ../vnc.html:147
msgid "Windows"
msgstr "Windows"
#: ../vnc.html:150
msgid "Send Tab"
msgstr "Skicka Tab"
#: ../vnc.html:150
msgid "Tab"
msgstr "Tab"
#: ../vnc.html:153
msgid "Esc"
msgstr "Esc"
#: ../vnc.html:153
msgid "Send Escape"
msgstr "Skicka Escape"
#: ../vnc.html:156
msgid "Ctrl+Alt+Del"
msgstr "Ctrl+Alt+Del"
#: ../vnc.html:156
msgid "Send Ctrl-Alt-Del"
msgstr "Skicka Ctrl-Alt-Del"
#: ../vnc.html:163
msgid "Shutdown/Reboot"
msgstr "Stäng av/Starta om"
#: ../vnc.html:163
msgid "Shutdown/Reboot..."
msgstr "Stäng av/Starta om..."
#: ../vnc.html:169
msgid "Power"
msgstr "Ström"
#: ../vnc.html:171
msgid "Shutdown"
msgstr "Stäng av"
#: ../vnc.html:172
msgid "Reboot"
msgstr "Starta om"
#: ../vnc.html:173
msgid "Reset"
msgstr "Återställ"
#: ../vnc.html:178 ../vnc.html:184
msgid "Clipboard"
msgstr "Urklipp"
#: ../vnc.html:186
msgid "Edit clipboard content in the textarea below."
msgstr "Redigera urklippets innehåll i fältet nedan."
#: ../vnc.html:194
msgid "Full screen"
msgstr "Fullskärm"
#: ../vnc.html:199 ../vnc.html:205
msgid "Settings"
msgstr "Inställningar"
#: ../vnc.html:211
msgid "Shared mode"
msgstr "Delat läge"
#: ../vnc.html:218
msgid "View only"
msgstr "Endast visning"
#: ../vnc.html:226
msgid "Clip to window"
msgstr "Begränsa till fönster"
#: ../vnc.html:231
msgid "Scaling mode:"
msgstr "Skalningsläge:"
#: ../vnc.html:233
msgid "None"
msgstr "Ingen"
#: ../vnc.html:234
msgid "Local scaling"
msgstr "Lokal skalning"
#: ../vnc.html:235
msgid "Remote resizing"
msgstr "Ändra storlek"
#: ../vnc.html:240
msgid "Advanced"
msgstr "Avancerat"
#: ../vnc.html:243
msgid "Quality:"
msgstr "Kvalitet:"
#: ../vnc.html:247
msgid "Compression level:"
msgstr "Kompressionsnivå:"
#: ../vnc.html:252
msgid "Repeater ID:"
msgstr "Repeater-ID:"
#: ../vnc.html:256
msgid "WebSocket"
msgstr "WebSocket"
#: ../vnc.html:261
msgid "Encrypt"
msgstr "Kryptera"
#: ../vnc.html:266
msgid "Host:"
msgstr "Värd:"
#: ../vnc.html:270
msgid "Port:"
msgstr "Port:"
#: ../vnc.html:274
msgid "Path:"
msgstr "Sökväg:"
#: ../vnc.html:283
msgid "Automatic reconnect"
msgstr "Automatisk återanslutning"
#: ../vnc.html:288
msgid "Reconnect delay (ms):"
msgstr "Fördröjning (ms):"
#: ../vnc.html:295
msgid "Show dot when no cursor"
msgstr "Visa prick när ingen muspekare finns"
#: ../vnc.html:302
msgid "Logging:"
msgstr "Loggning:"
#: ../vnc.html:311
msgid "Version:"
msgstr "Version:"
#: ../vnc.html:319
msgid "Disconnect"
msgstr "Koppla ifrån"
#: ../vnc.html:342
msgid "Connect"
msgstr "Anslut"
#: ../vnc.html:351
msgid "Server identity"
msgstr "Serveridentitet"
#: ../vnc.html:354
msgid "The server has provided the following identifying information:"
msgstr "Servern har gett följande identifierande information:"
#: ../vnc.html:357
msgid "Fingerprint:"
msgstr "Fingeravtryck:"
#: ../vnc.html:361
msgid ""
"Please verify that the information is correct and press \"Approve\". "
"Otherwise press \"Reject\"."
msgstr ""
"Kontrollera att informationen är korrekt och tryck sedan \"Godkänn\". Tryck "
"annars \"Avvisa\"."
#: ../vnc.html:366
msgid "Approve"
msgstr "Godkänn"
#: ../vnc.html:367
msgid "Reject"
msgstr "Avvisa"
#: ../vnc.html:375
msgid "Credentials"
msgstr "Användaruppgifter"
#: ../vnc.html:379
msgid "Username:"
msgstr "Användarnamn:"
#: ../vnc.html:383
msgid "Password:"
msgstr "Lösenord:"
#: ../vnc.html:387
msgid "Send credentials"
msgstr "Skicka användaruppgifter"
#: ../vnc.html:396
msgid "Cancel"
msgstr "Avbryt"
#~ msgid "Must set host"
#~ msgstr "Du måste specifiera en värd"
#~ msgid "HTTPS is required for full functionality"
#~ msgstr "HTTPS krävs för full funktionalitet"
#~ msgid "Clear"
#~ msgstr "Rensa"

341
po/uk.po Normal file
View File

@ -0,0 +1,341 @@
# Ukrainian translation of noVNC.
# Copyright (C) 2025 The noVNC authors
# This file is distributed under the same license as the noVNC package.
# Denys Nykula <nykula@ukr.net>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: noVNC 1.6.0\n"
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
"POT-Creation-Date: 2025-10-31 09:17+0100\n"
"PO-Revision-Date: 2025-11-22 20:21+0200\n"
"Last-Translator: Denys Nykula <nykula@ukr.net>\n"
"Language-Team: Ukrainian <uk@li.org>\n"
"Language: uk\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"
#: ../app/ui.js:84
msgid ""
"Running without HTTPS is not recommended, crashes or other issues are likely."
msgstr ""
"Робота без HTTPS не рекомендується, ймовірні збої чи інші проблеми."
#: ../app/ui.js:413
msgid "Connecting..."
msgstr "З'єднання..."
#: ../app/ui.js:420
msgid "Disconnecting..."
msgstr "Від'єднання..."
#: ../app/ui.js:426
msgid "Reconnecting..."
msgstr "Перез'єднання..."
#: ../app/ui.js:431
msgid "Internal error"
msgstr "Внутрішня помилка"
#: ../app/ui.js:1084
msgid "Failed to connect to server: "
msgstr "Не вдалося з'єднатися з сервером: "
#: ../app/ui.js:1151
msgid "Connected (encrypted) to "
msgstr "З'єднано (з шифруванням) з "
#: ../app/ui.js:1153
msgid "Connected (unencrypted) to "
msgstr "З'єднано (без шифрування) з "
#: ../app/ui.js:1178
msgid "Something went wrong, connection is closed"
msgstr "Щось пішло не так, з'єднання закрито"
#: ../app/ui.js:1181
msgid "Failed to connect to server"
msgstr "Не вдалося з'єднатися з сервером"
#: ../app/ui.js:1193
msgid "Disconnected"
msgstr "Від'єднано"
#: ../app/ui.js:1210
msgid "New connection has been rejected with reason: "
msgstr "Нове з'єднання відхилено. Причина: "
#: ../app/ui.js:1213
msgid "New connection has been rejected"
msgstr "Нове з'єднання відхилено"
#: ../app/ui.js:1225
msgid "Are you sure you want to disconnect the session?"
msgstr "Точно від'єднати сеанс?"
#: ../app/ui.js:1297
msgid "Credentials are required"
msgstr "Треба особові дані"
#: ../vnc.html:106
msgid "noVNC encountered an error:"
msgstr "Помилка noVNC:"
#: ../vnc.html:116
msgid "Hide/Show the control bar"
msgstr "Сховати/показати панель керування"
#: ../vnc.html:125
msgid "Drag"
msgstr "Посунути"
#: ../vnc.html:125
msgid "Move/Drag viewport"
msgstr "Змістити область огляду"
#: ../vnc.html:131
msgid "Keyboard"
msgstr "Клавіатура"
#: ../vnc.html:131
msgid "Show keyboard"
msgstr "Показати клавіатуру"
#: ../vnc.html:136
msgid "Extra keys"
msgstr "Додаткові клавіші"
#: ../vnc.html:136
msgid "Show extra keys"
msgstr "Показати додаткові клавіші"
#: ../vnc.html:141
msgid "Ctrl"
msgstr "Ctrl"
#: ../vnc.html:141
msgid "Toggle Ctrl"
msgstr "Затиснути Ctrl"
#: ../vnc.html:144
msgid "Alt"
msgstr "Alt"
#: ../vnc.html:144
msgid "Toggle Alt"
msgstr "Затиснути Alt"
#: ../vnc.html:147
msgid "Toggle Windows"
msgstr "Затиснути Windows"
#: ../vnc.html:147
msgid "Windows"
msgstr "Windows"
#: ../vnc.html:150
msgid "Send Tab"
msgstr "Натиснути Tab"
#: ../vnc.html:150
msgid "Tab"
msgstr "Tab"
#: ../vnc.html:153
msgid "Esc"
msgstr "Esc"
#: ../vnc.html:153
msgid "Send Escape"
msgstr "Натиснути Escape"
#: ../vnc.html:156
msgid "Ctrl+Alt+Del"
msgstr "Ctrl+Alt+Del"
#: ../vnc.html:156
msgid "Send Ctrl-Alt-Del"
msgstr "Натиснути Ctrl+Alt+Del"
#: ../vnc.html:163
msgid "Shutdown/Reboot"
msgstr "Вимкнути/перезавантажити"
#: ../vnc.html:163
msgid "Shutdown/Reboot..."
msgstr "Вимкнути/перезавантажити..."
#: ../vnc.html:169
msgid "Power"
msgstr "Живлення"
#: ../vnc.html:171
msgid "Shutdown"
msgstr "Вимкнути"
#: ../vnc.html:172
msgid "Reboot"
msgstr "Перезавантажити"
#: ../vnc.html:173
msgid "Reset"
msgstr "Скинути"
#: ../vnc.html:178 ../vnc.html:184
msgid "Clipboard"
msgstr "Буфер обміну"
#: ../vnc.html:186
msgid "Edit clipboard content in the textarea below."
msgstr "Редагуйте вміст буфера обміну в текстовій зоні внизу."
#: ../vnc.html:194
msgid "Full screen"
msgstr "Повний екран"
#: ../vnc.html:199 ../vnc.html:205
msgid "Settings"
msgstr "Параметри"
#: ../vnc.html:211
msgid "Shared mode"
msgstr "Спільний режим"
#: ../vnc.html:218
msgid "View only"
msgstr "Лише перегляд"
#: ../vnc.html:226
msgid "Clip to window"
msgstr "До розмірів вікна"
#: ../vnc.html:231
msgid "Scaling mode:"
msgstr "Режим масштабування:"
#: ../vnc.html:233
msgid "None"
msgstr "Вимкнено"
#: ../vnc.html:234
msgid "Local scaling"
msgstr "Локальне масштабування"
#: ../vnc.html:235
msgid "Remote resizing"
msgstr "Віддалене масштабування"
#: ../vnc.html:240
msgid "Advanced"
msgstr "Додатково"
#: ../vnc.html:243
msgid "Quality:"
msgstr "Якість:"
#: ../vnc.html:247
msgid "Compression level:"
msgstr "Рівень стиснення:"
#: ../vnc.html:252
msgid "Repeater ID:"
msgstr "Ідентифікатор репітера:"
#: ../vnc.html:256
msgid "WebSocket"
msgstr "WebSocket"
#: ../vnc.html:261
msgid "Encrypt"
msgstr "Шифрування"
#: ../vnc.html:266
msgid "Host:"
msgstr "Сервер:"
#: ../vnc.html:270
msgid "Port:"
msgstr "Порт:"
#: ../vnc.html:274
msgid "Path:"
msgstr "Шлях:"
#: ../vnc.html:283
msgid "Automatic reconnect"
msgstr "Автоматичне перез'єднання"
#: ../vnc.html:288
msgid "Reconnect delay (ms):"
msgstr "Затримка перез'єднання (мс):"
#: ../vnc.html:295
msgid "Show dot when no cursor"
msgstr "Показувати крапку, коли нема курсора"
#: ../vnc.html:302
msgid "Logging:"
msgstr "Журнал:"
#: ../vnc.html:311
msgid "Version:"
msgstr "Версія:"
#: ../vnc.html:319
msgid "Disconnect"
msgstr "Від'єднати"
#: ../vnc.html:342
msgid "Connect"
msgstr "З'єднати"
#: ../vnc.html:351
msgid "Server identity"
msgstr "Ідентифікація сервера"
#: ../vnc.html:354
msgid "The server has provided the following identifying information:"
msgstr "Сервер надає такі ідентифікаційні дані:"
#: ../vnc.html:357
msgid "Fingerprint:"
msgstr "Відбиток:"
#: ../vnc.html:361
msgid ""
"Please verify that the information is correct and press \"Approve\". "
"Otherwise press \"Reject\"."
msgstr ""
"Перевірте, чи дані коректні, й натисніть «Схвалити». "
"Інакше натисніть «Відхилити»."
#: ../vnc.html:366
msgid "Approve"
msgstr "Схвалити"
#: ../vnc.html:367
msgid "Reject"
msgstr "Відхилити"
#: ../vnc.html:375
msgid "Credentials"
msgstr "Особові дані"
#: ../vnc.html:379
msgid "Username:"
msgstr "Користувацьке ім'я:"
#: ../vnc.html:383
msgid "Password:"
msgstr "Пароль:"
#: ../vnc.html:387
msgid "Send credentials"
msgstr "Надіслати особові дані"
#: ../vnc.html:396
msgid "Cancel"
msgstr "Скасувати"

View File

@ -1,6 +1,74 @@
import { isMac, isWindows, isIOS, isAndroid, isChromeOS,
isSafari, isFirefox, isChrome, isChromium, isOpera, isEdge,
isGecko, isWebKit, isBlink } from '../core/util/browser.js';
isGecko, isWebKit, isBlink,
browserAsyncClipboardSupport } from '../core/util/browser.js';
describe('Async clipboard', function () {
"use strict";
beforeEach(function () {
sinon.stub(navigator, "clipboard").value({
writeText: sinon.stub(),
readText: sinon.stub(),
});
sinon.stub(navigator, "permissions").value({
query: sinon.stub().resolves({ state: "granted" })
});
});
afterEach(function () {
sinon.restore();
});
it("queries permissions with correct parameters", async function () {
const queryStub = navigator.permissions.query;
await browserAsyncClipboardSupport();
expect(queryStub.firstCall).to.have.been.calledWithExactly({
name: "clipboard-write",
allowWithoutGesture: true
});
expect(queryStub.secondCall).to.have.been.calledWithExactly({
name: "clipboard-read",
allowWithoutGesture: false
});
});
it("is available when API present and permissions granted", async function () {
navigator.permissions.query.resolves({ state: "granted" });
const result = await browserAsyncClipboardSupport();
expect(result).to.equal('available');
});
it("is available when API present and permissions yield 'prompt'", async function () {
navigator.permissions.query.resolves({ state: "prompt" });
const result = await browserAsyncClipboardSupport();
expect(result).to.equal('available');
});
it("is unavailable when permissions denied", async function () {
navigator.permissions.query.resolves({ state: "denied" });
const result = await browserAsyncClipboardSupport();
expect(result).to.equal('denied');
});
it("is unavailable when permissions API fails", async function () {
navigator.permissions.query.rejects(new Error("fail"));
const result = await browserAsyncClipboardSupport();
expect(result).to.equal('unsupported');
});
it("is unavailable when write text API missing", async function () {
navigator.clipboard.writeText = undefined;
const result = await browserAsyncClipboardSupport();
expect(result).to.equal('unsupported');
});
it("is unavailable when read text API missing", async function () {
navigator.clipboard.readText = undefined;
const result = await browserAsyncClipboardSupport();
expect(result).to.equal('unsupported');
});
});
describe('OS detection', function () {
let origNavigator;

121
tests/test.clipboard.js Normal file
View File

@ -0,0 +1,121 @@
import AsyncClipboard from '../core/clipboard.js';
describe('Async Clipboard', function () {
"use strict";
let targetMock;
let clipboard;
beforeEach(function () {
sinon.stub(navigator, "clipboard").value({
writeText: sinon.stub().resolves(),
readText: sinon.stub().resolves(),
});
sinon.stub(navigator, "permissions").value({
query: sinon.stub(),
});
targetMock = document.createElement("canvas");
clipboard = new AsyncClipboard(targetMock);
});
afterEach(function () {
sinon.restore();
targetMock = null;
clipboard = null;
});
function stubClipboardPermissions(state) {
navigator.permissions.query
.withArgs({ name: 'clipboard-write', allowWithoutGesture: true })
.resolves({ state: state });
navigator.permissions.query
.withArgs({ name: 'clipboard-read', allowWithoutGesture: false })
.resolves({ state: state });
}
function nextTick() {
return new Promise(resolve => setTimeout(resolve, 0));
}
it('grab() adds listener if permissions granted', async function () {
stubClipboardPermissions('granted');
const addListenerSpy = sinon.spy(targetMock, 'addEventListener');
clipboard.grab();
await nextTick();
expect(addListenerSpy.calledWith('focus')).to.be.true;
});
it('grab() does not add listener if permissions denied', async function () {
stubClipboardPermissions('denied');
const addListenerSpy = sinon.spy(targetMock, 'addEventListener');
clipboard.grab();
await nextTick();
expect(addListenerSpy.calledWith('focus')).to.be.false;
});
it('focus event triggers onpaste() if permissions granted', async function () {
stubClipboardPermissions('granted');
const text = 'hello clipboard world';
navigator.clipboard.readText.resolves(text);
const spyPromise = new Promise(resolve => clipboard.onpaste = resolve);
clipboard.grab();
await nextTick();
targetMock.dispatchEvent(new Event('focus'));
const res = await spyPromise;
expect(res).to.equal(text);
});
it('focus event does not trigger onpaste() if permissions denied', async function () {
stubClipboardPermissions('denied');
const text = 'should not read';
navigator.clipboard.readText.resolves(text);
clipboard.onpaste = sinon.spy();
clipboard.grab();
await nextTick();
targetMock.dispatchEvent(new Event('focus'));
expect(clipboard.onpaste.called).to.be.false;
});
it('writeClipboard() calls navigator.clipboard.writeText() if permissions granted', async function () {
stubClipboardPermissions('granted');
clipboard._isAvailable = true;
const text = 'writing to clipboard';
const result = clipboard.writeClipboard(text);
expect(navigator.clipboard.writeText.calledWith(text)).to.be.true;
expect(result).to.be.true;
});
it('writeClipboard() does not call navigator.clipboard.writeText() if permissions denied', async function () {
stubClipboardPermissions('denied');
clipboard._isAvailable = false;
const text = 'should not write';
const result = clipboard.writeClipboard(text);
expect(navigator.clipboard.writeText.called).to.be.false;
expect(result).to.be.false;
});
});

View File

@ -3467,17 +3467,47 @@ describe('Remote Frame Buffer protocol client', function () {
});
describe('Normal clipboard handling receive', function () {
it('should fire the clipboard callback with the retrieved text on ServerCutText', function () {
it('should not dispatch a clipboard event following successful async write clipboard', async function () {
client._viewOnly = false;
client._asyncClipboard = {
writeClipboard: sinon.stub().returns(true),
};
const expectedStr = 'cheese!';
const data = [3, 0, 0, 0];
push32(data, expectedStr.length);
for (let i = 0; i < expectedStr.length; i++) { data.push(expectedStr.charCodeAt(i)); }
const spy = sinon.spy();
client.addEventListener("clipboard", spy);
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
client._sock._websocket._receiveData(new Uint8Array(data));
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.text).to.equal(expectedStr);
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
expectedStr
)).to.be.true;
expect(dispatchEventSpy.calledWith(
new CustomEvent("clipboard", {detail: {text: expectedStr}})
)).to.be.false;
});
it('should dispatch a clipboard event following unsuccessful async write clipboard', async function () {
client._viewOnly = false;
client._asyncClipboard = {
writeClipboard: sinon.stub().returns(false),
};
const expectedStr = 'cheese!';
const data = [3, 0, 0, 0];
push32(data, expectedStr.length);
for (let i = 0; i < expectedStr.length; i++) { data.push(expectedStr.charCodeAt(i)); }
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
client._sock._websocket._receiveData(new Uint8Array(data));
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
expectedStr
)).to.be.true;
expect(dispatchEventSpy.args[0][0].type).to.equal("clipboard");
expect(dispatchEventSpy.args[0][0].detail.text).to.equal(expectedStr);
});
});
@ -3530,8 +3560,70 @@ describe('Remote Frame Buffer protocol client', function () {
client._sock._websocket._receiveData(new Uint8Array(data));
});
it('should not dispatch a clipboard event following successful async write clipboard', async function () {
client._viewOnly = false;
client._asyncClipboard = {
writeClipboard: sinon.stub().returns(true),
};
let expectedData = "Schnitzel";
let data = [3, 0, 0, 0];
const flags = [0x10, 0x00, 0x00, 0x01];
let text = encodeUTF8("Schnitzel");
let deflatedText = deflateWithSize(text);
// How much data we are sending.
push32(data, toUnsigned32bit(-(4 + deflatedText.length)));
data = data.concat(flags);
data = data.concat(Array.from(deflatedText));
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
client._sock._websocket._receiveData(new Uint8Array(data));
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
expectedData
)).to.be.true;
expect(dispatchEventSpy.calledOnceWith(
new CustomEvent("clipboard", {detail: {text: expectedData}})
)).to.be.false;
});
it('should dispatch a clipboard event following unsuccessful async write clipboard', async function () {
client._viewOnly = false;
client._asyncClipboard = {
writeClipboard: sinon.stub().returns(false),
};
let expectedData = "Potatoes";
let data = [3, 0, 0, 0];
const flags = [0x10, 0x00, 0x00, 0x01];
let text = encodeUTF8("Potatoes");
let deflatedText = deflateWithSize(text);
// How much data we are sending.
push32(data, toUnsigned32bit(-(4 + deflatedText.length)));
data = data.concat(flags);
data = data.concat(Array.from(deflatedText));
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
client._sock._websocket._receiveData(new Uint8Array(data));
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
expectedData
)).to.be.true;
expect(dispatchEventSpy.args[0][0].type).to.equal("clipboard");
expect(dispatchEventSpy.args[0][0].detail.text).to.equal(expectedData);
});
describe('Handle Provide', function () {
it('should update clipboard with correct Unicode data from a Provide message', function () {
it('should update clipboard with correct Unicode data from a Provide message', async function () {
client._viewOnly = false;
client._asyncClipboard = {
writeClipboard: sinon.stub().returns(false),
};
let expectedData = "Aå漢字!";
let data = [3, 0, 0, 0];
const flags = [0x10, 0x00, 0x00, 0x01];
@ -3545,16 +3637,22 @@ describe('Remote Frame Buffer protocol client', function () {
data = data.concat(flags);
data = data.concat(Array.from(deflatedText));
const spy = sinon.spy();
client.addEventListener("clipboard", spy);
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
client._sock._websocket._receiveData(new Uint8Array(data));
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.text).to.equal(expectedData);
client.removeEventListener("clipboard", spy);
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
expectedData
)).to.be.true;
expect(dispatchEventSpy.args[0][0].type).to.equal("clipboard");
expect(dispatchEventSpy.args[0][0].detail.text).to.equal(expectedData);
});
it('should update clipboard with correct escape characters from a Provide message ', function () {
it('should update clipboard with correct escape characters from a Provide message ', async function () {
client._viewOnly = false;
client._asyncClipboard = {
writeClipboard: sinon.stub().returns(false),
};
let expectedData = "Oh\nmy\n!";
let data = [3, 0, 0, 0];
const flags = [0x10, 0x00, 0x00, 0x01];
@ -3569,16 +3667,22 @@ describe('Remote Frame Buffer protocol client', function () {
data = data.concat(flags);
data = data.concat(Array.from(deflatedText));
const spy = sinon.spy();
client.addEventListener("clipboard", spy);
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
client._sock._websocket._receiveData(new Uint8Array(data));
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.text).to.equal(expectedData);
client.removeEventListener("clipboard", spy);
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
expectedData
)).to.be.true;
expect(dispatchEventSpy.args[0][0].type).to.equal("clipboard");
expect(dispatchEventSpy.args[0][0].detail.text).to.equal(expectedData);
});
it('should be able to handle large Provide messages', function () {
it('should be able to handle large Provide messages', async function () {
client._viewOnly = false;
client._asyncClipboard = {
writeClipboard: sinon.stub().returns(false),
};
let expectedData = "hello".repeat(100000);
let data = [3, 0, 0, 0];
const flags = [0x10, 0x00, 0x00, 0x01];
@ -3593,13 +3697,15 @@ describe('Remote Frame Buffer protocol client', function () {
data = data.concat(flags);
data = data.concat(Array.from(deflatedText));
const spy = sinon.spy();
client.addEventListener("clipboard", spy);
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
client._sock._websocket._receiveData(new Uint8Array(data));
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.text).to.equal(expectedData);
client.removeEventListener("clipboard", spy);
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
expectedData
)).to.be.true;
expect(dispatchEventSpy.args[0][0].type).to.equal("clipboard");
expect(dispatchEventSpy.args[0][0].detail.text).to.equal(expectedData);
});
});

197
tests/test.wakelock.js Normal file
View File

@ -0,0 +1,197 @@
/* jshint expr: true */
import WakeLockManager from '../app/wakelock.js';
class FakeWakeLockSentinal extends EventTarget {
constructor() {
super();
this.released = false;
}
async release() {
if (this.released) {
return;
}
this.released = true;
this.dispatchEvent(new Event("release"));
}
}
function waitForStateTransition(wakelockManager, newState) {
const {promise, resolve} = Promise.withResolvers();
const eventListener = (event) => {
if (event.newState !== newState) {
return;
}
wakelockManager.removeEventListener("testOnlyStateChange", eventListener);
resolve();
};
wakelockManager.addEventListener("testOnlyStateChange", eventListener);
return promise;
}
describe('WakeLockManager', function () {
"use strict";
let wakelockRequest;
beforeEach(function () {
wakelockRequest = sinon.stub(navigator.wakeLock, 'request');
});
afterEach(function () {
wakelockRequest.restore();
});
it('can acquire and release lock', async function () {
let wakeLockSentinal = new FakeWakeLockSentinal();
wakelockRequest.onFirstCall().resolves(wakeLockSentinal);
let wlm = new WakeLockManager();
expect(wakelockRequest).to.not.have.been.called;
let done = waitForStateTransition(wlm, 'acquired');
wlm.acquire();
await done;
expect(wakelockRequest).to.have.been.calledOnce;
expect(wakeLockSentinal.released).to.be.false;
done = waitForStateTransition(wlm, 'released');
wlm.release();
await done;
expect(wakelockRequest).to.have.been.calledOnce;
expect(wakeLockSentinal.released).to.be.true;
});
it('can release without holding wakelock', async function () {
let wlm = new WakeLockManager();
wlm.release();
expect(wakelockRequest).to.not.have.been.called;
});
it('can release while waiting for wakelock', async function () {
let wakeLockSentinal = new FakeWakeLockSentinal();
let {promise, resolve} = Promise.withResolvers();
wakelockRequest.onFirstCall().returns(promise);
let wlm = new WakeLockManager();
expect(wakelockRequest).to.not.have.been.called;
let seenAcquiring = waitForStateTransition(wlm, 'acquiring');
let seenReleasing = waitForStateTransition(wlm, 'releasing');
let seenReleased = waitForStateTransition(wlm, 'released');
wlm.acquire();
await seenAcquiring;
expect(wakelockRequest).to.have.been.calledOnce;
// We can call acquire multiple times, while waiting for the promise
// to resolve.
wlm.acquire();
// It should not request a second wakelock.
expect(wakelockRequest).to.have.been.calledOnce;
wlm.release();
await seenReleasing;
expect(wakeLockSentinal.released).to.be.false;
// Now return the wake lock, we should immediately release it.
resolve(wakeLockSentinal);
await seenReleased;
expect(wakeLockSentinal.released).to.be.true;
});
it('handles visibility loss', async function () {
let documentHidden = sinon.stub(document, 'hidden');
let documentVisibility = sinon.stub(document, 'visibilityState');
afterEach(function () {
documentHidden.restore();
documentVisibility.restore();
});
documentHidden.value(false);
documentVisibility.value('visible');
let wakeLockSentinal1 = new FakeWakeLockSentinal();
let wakeLockSentinal2 = new FakeWakeLockSentinal();
wakelockRequest.onFirstCall().resolves(wakeLockSentinal1);
wakelockRequest.onSecondCall().resolves(wakeLockSentinal2);
let wlm = new WakeLockManager();
let seenAcquired = waitForStateTransition(wlm, 'acquired');
let seenAwaitingVisible = waitForStateTransition(wlm, 'awaiting_visible');
wlm.acquire();
await seenAcquired;
expect(wakelockRequest).to.have.been.calledOnce;
// Fake a visibility change.
documentHidden.value(true);
documentVisibility.value('hidden');
wakeLockSentinal1.release();
await seenAwaitingVisible;
seenAcquired = waitForStateTransition(wlm, 'acquired');
// Fake a visibility change back
documentHidden.value(false);
documentVisibility.value('visible');
document.dispatchEvent(new Event('visibilitychange'));
await seenAcquired;
expect(wakelockRequest).to.have.been.calledTwice;
expect(wakeLockSentinal2.released).to.be.false;
});
it('can start hidden', async function () {
let documentHidden = sinon.stub(document, 'hidden');
let documentVisibility = sinon.stub(document, 'visibilityState');
afterEach(function () {
documentHidden.restore();
documentVisibility.restore();
});
documentHidden.value(true);
documentVisibility.value('hidden');
let wakeLockSentinal = new FakeWakeLockSentinal();
wakelockRequest.onFirstCall().resolves(wakeLockSentinal);
let wlm = new WakeLockManager();
let seenAwaitingVisible = waitForStateTransition(wlm, 'awaiting_visible');
let seenAcquired = waitForStateTransition(wlm, 'acquired');
wlm.acquire();
await seenAwaitingVisible;
expect(wakelockRequest).to.not.have.been.called;
// Fake a visibility change.
documentHidden.value(false);
documentVisibility.value('visible');
document.dispatchEvent(new Event('visibilitychange'));
await seenAcquired;
expect(wakelockRequest).to.have.been.calledOnce;
expect(wakeLockSentinal.released).to.be.false;
});
it('handles acquire errors', async function () {
wakelockRequest.onFirstCall().rejects('WakeLockError');
let wakeLockSentinal = new FakeWakeLockSentinal();
wakelockRequest.onSecondCall().resolves(wakeLockSentinal);
let wlm = new WakeLockManager();
let seenError = waitForStateTransition(wlm, 'error');
wlm.acquire();
await seenError;
expect(wakelockRequest).to.have.been.calledOnce;
// Even though we saw an error previously, it will retry when
// requested.
let seenAcquired = waitForStateTransition(wlm, 'acquired');
wlm.acquire();
await seenAcquired;
expect(wakelockRequest).to.have.been.calledTwice;
});
});

View File

@ -222,9 +222,9 @@ fi
echo -e "\n\nNavigate to this URL:\n"
if [ "x$SSLONLY" == "x" ]; then
echo -e " http://${HOST}:${PORT}/vnc.html?host=${HOST}&port=${PORT}\n"
echo -e " http://${HOST}:${PORT}/vnc.html\n"
else
echo -e " https://${HOST}:${PORT}/vnc.html?host=${HOST}&port=${PORT}\n"
echo -e " https://${HOST}:${PORT}/vnc.html\n"
fi
echo -e "Press Ctrl-C to exit\n\n"

View File

@ -50,14 +50,6 @@ for fn in "$@"; do
error=$(echo $line | cut -d ":" -f 4-)
case $error in
*"\"scrollbar-gutter\": Property \"scrollbar-gutter\" doesn't exist.")
# FIXME: https://github.com/validator/validator/issues/1788
echo "Ignoring below error on line ${line_start}," \
"the scrollbar-gutter property actually exist and is widely" \
"supported:"
echo $error
continue
;;
*"\"clip-path\": \"path("*)
# FIXME: https://github.com/validator/validator/issues/1786
echo "Ignoring below error on line ${line_start}," \

View File

@ -110,14 +110,14 @@
</div>
<!-- noVNC control bar -->
<div id="noVNC_control_bar_anchor" class="noVNC_vcenter">
<div id="noVNC_control_bar_anchor" class="noVNC_crosscenter">
<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>
<img class="noVNC_logo" src="app/images/icons/novnc-icon-35x21.svg"
alt="noVNC">
<hr>
@ -127,16 +127,14 @@
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>
<input type="image" alt="Keyboard" src="app/images/keyboard.svg"
id="noVNC_keyboard_button" class="noVNC_button" title="Show keyboard">
<!-- 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 class="noVNC_crosscenter">
<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"
@ -163,7 +161,7 @@
<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 class="noVNC_crosscenter">
<div id="noVNC_power" class="noVNC_panel">
<div class="noVNC_heading">
<img alt="" src="app/images/power.svg"> Power
@ -178,7 +176,7 @@
<input type="image" alt="Clipboard" src="app/images/clipboard.svg"
id="noVNC_clipboard_button" class="noVNC_button"
title="Clipboard">
<div class="noVNC_vcenter">
<div class="noVNC_crosscenter">
<div id="noVNC_clipboard" class="noVNC_panel">
<div class="noVNC_heading">
<img alt="" src="app/images/clipboard.svg"> Clipboard
@ -199,7 +197,7 @@
<input type="image" alt="Settings" src="app/images/settings.svg"
id="noVNC_settings_button" class="noVNC_button"
title="Settings">
<div class="noVNC_vcenter">
<div class="noVNC_crosscenter">
<div id="noVNC_settings" class="noVNC_panel">
<div class="noVNC_heading">
<img alt="" src="app/images/settings.svg"> Settings
@ -296,6 +294,13 @@
Show dot when no cursor
</label>
</li>
<li>
<label>
<input id="noVNC_setting_keep_device_awake" type="checkbox"
class="toggle">
Keep client display awake while connected
</label>
</li>
<li><hr></li>
<!-- Logging selection dropdown -->
<li>
@ -325,8 +330,20 @@
</div> <!-- End of noVNC_control_bar -->
<div id="noVNC_hint_anchor" class="noVNC_vcenter">
<div id="noVNC_control_bar_hint">
<div class="noVNC_hint_anchor noVNC_crosscenter">
<div class="noVNC_control_bar_hint">
</div>
</div>
<div class="noVNC_hint_anchor noVNC_right noVNC_crosscenter">
<div class="noVNC_control_bar_hint">
</div>
</div>
<div class="noVNC_hint_anchor noVNC_top noVNC_crosscenter">
<div class="noVNC_control_bar_hint">
</div>
</div>
<div class="noVNC_hint_anchor noVNC_bottom noVNC_crosscenter">
<div class="noVNC_control_bar_hint">
</div>
</div>