Compare commits

..

13 Commits

Author SHA1 Message Date
Alexander Zeijlon 63107bd06d noVNC 1.7.0 2026-04-28 12:41:46 +02:00
Alexander Zeijlon 18cabdf39b Update generated json files 2026-04-28 11:15:01 +02:00
Alexander Zeijlon 85ae81a35b noVNC 1.7.0 beta 2025-11-04 10:06:45 +01:00
Alexander Zeijlon 7a96227f13 Remove show_dot from docs/EMBEDDING.md
This should have been a part of commit 243d7fd

(cherry picked from commit d44f7e04fc)
2025-11-04 09:45:18 +01:00
Alexander Zeijlon 43266f4917 Remove showDotCursor from docs/API.md
This should have been a part of commit 243d7fd

(cherry picked from commit fb97e0f1e2)
2025-11-04 09:41:31 +01:00
Alexander Zeijlon 4ccc3b44db 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.

(cherry picked from commit d3d69a8118)
2025-11-03 16:06:43 +01:00
Alexander Zeijlon 8f3555b0ee Publish with latest npm version
Apparently publishing with OIDC as an auth method only works with npm
11.5.1 and onward.

(cherry picked from commit e03a54e12a)
2025-11-03 16:06:36 +01:00
Alexander Zeijlon 7808f579df Stop using access tokens when publishing to npmjs
We are using OIDC now instead of the old style access tokens.

(cherry picked from commit 99865e5aba)
2025-11-03 16:06:27 +01:00
Alexander Zeijlon 603d63fc0f Allow publishing to npmjs.com with OIDC
(cherry picked from commit b29cc6493d)
2025-11-03 16:06:20 +01:00
Alexander Zeijlon 5ac7bd2819 Update Swedish translation
(cherry picked from commit c2d89730dd)
2025-10-31 10:57:28 +01:00
Alexander Zeijlon f15966359d Update translation template file
(cherry picked from commit 9d77c806fe)
2025-10-31 10:57:14 +01:00
Tobias 8823149810 Display warning prompt before closing tab
The warning prompt is only displayed if there is an active connected
session, when viewOnly is disabled.

(cherry picked from commit 63c2c14a50)
2025-10-31 09:08:52 +01:00
Felso Daniel (McP/MFE4) f6a4d0a50a Add Hungarian translation
(cherry picked from commit cf6fe1063c)
2025-10-30 16:08:51 +01:00
37 changed files with 297 additions and 1970 deletions

View File

@ -13,13 +13,13 @@ jobs:
id-token: write
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- 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@v6
- uses: actions/setup-node@v4
with:
# Node 24 is needed to get npm > 11.5.1, which is a requirement for
# OIDC auth.
@ -27,7 +27,7 @@ jobs:
# Needs to be explicitly specified for auth to work
registry-url: 'https://registry.npmjs.org'
- run: npm install
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@v4
with:
name: npm
path: lib
@ -49,7 +49,7 @@ jobs:
snap:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- 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@v7
- uses: actions/upload-artifact@v4
with:
name: snap
path: ${{ steps.snapcraft.outputs.snap }}

View File

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

View File

@ -13,18 +13,15 @@ jobs:
- ChromeHeadless
- FirefoxHeadless
include:
# 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
- os: macos-latest
browser: Safari
- os: windows-latest
browser: EdgeHeadless
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm update
- run: npm run test
env:

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -65,10 +65,5 @@
"Connect": "Verbinden",
"Password:": "Passwort:",
"Cancel": "Abbrechen",
"Canvas not supported.": "Canvas nicht unterstützt.",
"Disconnect timeout": "Zeitüberschreitung beim Trennen",
"Local Downscaling": "Lokales herunterskalieren",
"Local Cursor": "Lokaler Mauszeiger",
"Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "'Clipping-Modus' aktiviert, Scrollbalken in 'IE-Vollbildmodus' werden nicht unterstützt",
"True Color": "True Color"
"Canvas not supported.": "Canvas nicht unterstützt."
}

View File

@ -41,7 +41,6 @@
"Reset": "Επαναφορά",
"Clipboard": "Πρόχειρο",
"Edit clipboard content in the textarea below.": "Επεξεργαστείτε το περιεχόμενο του πρόχειρου στην περιοχή κειμένου παρακάτω.",
"Full Screen": "Πλήρης Οθόνη",
"Settings": "Ρυθμίσεις",
"Shared Mode": "Κοινόχρηστη Λειτουργία",
"View Only": "Μόνο Θέαση",
@ -76,25 +75,5 @@
"Username:": "Κωδικός Χρήστη:",
"Password:": "Κωδικός Πρόσβασης:",
"Send Credentials": "Αποστολή Διαπιστευτηρίων",
"Cancel": "Ακύρωση",
"Password is required": "Απαιτείται ο κωδικός πρόσβασης",
"viewport drag": "σύρσιμο θεατού πεδίου",
"Active Mouse Button": "Ενεργό Πλήκτρο Ποντικιού",
"No mousebutton": "Χωρίς Πλήκτρο Ποντικιού",
"Left mousebutton": "Αριστερό Πλήκτρο Ποντικιού",
"Middle mousebutton": "Μεσαίο Πλήκτρο Ποντικιού",
"Right mousebutton": "Δεξί Πλήκτρο Ποντικιού",
"Clear": "Καθάρισμα",
"Canvas not supported.": "Δεν υποστηρίζεται το στοιχείο Canvas",
"Disconnect timeout": "Παρέλευση χρονικού ορίου αποσύνδεσης",
"Local Downscaling": "Τοπική Συρρίκνωση",
"Local Cursor": "Τοπικός Δρομέας",
"Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "Εφαρμογή λειτουργίας αποκοπής αφού δεν υποστηρίζονται οι λωρίδες κύλισης σε πλήρη οθόνη στον IE",
"True Color": "Πραγματικά Χρώματα",
"Style:": "Στυλ:",
"default": "προεπιλεγμένο",
"Apply": "Εφαρμογή",
"Connection": "Σύνδεση",
"Token:": "Διακριτικό:",
"Send Password": "Αποστολή Κωδικού Πρόσβασης"
"Cancel": "Ακύρωση"
}

View File

@ -76,7 +76,5 @@
"Username:": "Nom d'utilisateur :",
"Password:": "Mot de passe :",
"Send credentials": "Envoyer les identifiants",
"Cancel": "Annuler",
"Must set host": "Doit définir l'hôte",
"Clear": "Effacer"
"Cancel": "Annuler"
}

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

@ -0,0 +1,80 @@
{
"Running without HTTPS is not recommended, crashes or other issues are likely.": "Pokretanje bez HTTPS-a se ne preporučuje, vjerojatno će se dogoditi prekidi rada ili drugi problemi.",
"Connecting...": "Povezivanje …",
"Disconnecting...": "Odspajanje …",
"Reconnecting...": "Ponovno povezivanje …",
"Internal error": "Interna greška",
"Failed to connect to server: ": "Povezivanje sa serverom nije uspjelo: ",
"Connected (encrypted) to ": "Povezano (šifrirano) na ",
"Connected (unencrypted) to ": "Povezano (nešifrirano) na ",
"Something went wrong, connection is closed": "Nešto nije u redu, veza je zatvorena",
"Failed to connect to server": "Povezivanje sa serverom nije uspjelo",
"Disconnected": "Odspojeno",
"New connection has been rejected with reason: ": "Nova veza je odbijena s razlogom: ",
"New connection has been rejected": "Nova veza je odbijena",
"Credentials are required": "Podaci za prijavu su obavezni",
"noVNC encountered an error:": "noVNC je naišao na grešku:",
"Hide/Show the control bar": "Sakrij/Prikaži traku kontrola",
"Drag": "Povuci",
"Move/Drag viewport": "Pomakni/Povuci vidljivo područje",
"Keyboard": "Tipkovnica",
"Show keyboard": "Prikaži tipkovnicu",
"Extra keys": "Dodatne tipke",
"Show extra keys": "Prikaži dodatne tipke",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Uključi/Isključi Ctrl",
"Alt": "Alt",
"Toggle Alt": "Uključi/Isključi Alt",
"Toggle Windows": "Uključi/Isključi Windows",
"Windows": "Windows",
"Send Tab": "Pošalji tabulator",
"Tab": "Tabulator",
"Esc": "Esc",
"Send Escape": "Pošalji Escape",
"Ctrl+Alt+Del": "Ctrl + Alt + Del",
"Send Ctrl-Alt-Del": "Pošalji Ctrl+Alt+Del",
"Shutdown/Reboot": "Isključi/Ponovo pokreni",
"Shutdown/Reboot...": "Isključi/Ponovo pokreni …",
"Power": "Napajanje",
"Shutdown": "Isključi",
"Reboot": "Ponovo pokreni",
"Reset": "Resetiraj",
"Clipboard": "Međuspremnik",
"Edit clipboard content in the textarea below.": "Uredi sadržaj međuspremnika u donjem području teksta.",
"Full screen": "Cjeloekranski prikaz",
"Settings": "Postavke",
"Shared mode": "Dijeljeni modus",
"View only": "Samo prikaz",
"Clip to window": "Isijeci na veličinu prozora",
"Scaling mode:": "Modus skaliranja:",
"None": "Bez",
"Local scaling": "Lokalno skaliranje",
"Remote resizing": "Daljinsko mijenjanje veličine",
"Advanced": "Napredno",
"Quality:": "Kvaliteta:",
"Compression level:": "Razina kompresije:",
"Repeater ID:": "ID repetitora:",
"WebSocket": "WebSocket",
"Encrypt": "Šifriraj",
"Host:": "Host:",
"Port:": "Priključak:",
"Path:": "Putanja:",
"Automatic reconnect": "Automatsko ponovno povezivanje",
"Reconnect delay (ms):": "Kašnjenje ponovnog povezivanja (ms):",
"Show dot when no cursor": "Prikaži točku kada nema pokazivača",
"Logging:": "Zapisivanje:",
"Version:": "Verzija:",
"Disconnect": "Odspoji",
"Connect": "Poveži",
"Server identity": "Identitet servera",
"The server has provided the following identifying information:": "Server je pružio sljedeće identifikacijske podatke:",
"Fingerprint:": "Otisak:",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Provjeri jesu li podaci točni i pritisni „Odobri“. U suprotnom pritisni „Odbaci“.",
"Approve": "Odobri",
"Reject": "Odbij",
"Credentials": "Podaci za prijavu",
"Username:": "Korisničko ime:",
"Password:": "Lozinka:",
"Send credentials": "Pošalji podatke za prijavu",
"Cancel": "Odustani"
}

View File

@ -4,7 +4,6 @@
"Disconnecting...": "Bezig om verbinding te verbreken...",
"Reconnecting...": "Opnieuw verbinding maken...",
"Internal error": "Interne fout",
"Failed to connect to server: ": "Verbinding maken met server is mislukt",
"Connected (encrypted) to ": "Verbonden (versleuteld) met ",
"Connected (unencrypted) to ": "Verbonden (onversleuteld) met ",
"Something went wrong, connection is closed": "Er iets fout gelopen, verbinding werd verbroken",
@ -16,11 +15,8 @@
"noVNC encountered an error:": "noVNC heeft een fout bemerkt:",
"Hide/Show the control bar": "Verberg/Toon de bedieningsbalk",
"Drag": "Sleep",
"Move/Drag viewport": "Verplaats/Versleep Kijkvenster",
"Keyboard": "Toetsenbord",
"Show keyboard": "Toon Toetsenbord",
"Extra keys": "Extra toetsen",
"Show extra keys": "Toon Extra Toetsen",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Ctrl omschakelen",
"Alt": "Alt",
@ -41,15 +37,10 @@
"Reset": "Resetten",
"Clipboard": "Klembord",
"Edit clipboard content in the textarea below.": "Edit de inhoud van het klembord in het tekstveld hieronder",
"Full screen": "Volledig Scherm",
"Settings": "Instellingen",
"Shared mode": "Gedeelde Modus",
"View only": "Alleen Kijken",
"Clip to window": "Randen buiten venster afsnijden",
"Scaling mode:": "Schaalmodus:",
"None": "Geen",
"Local scaling": "Lokaal Schalen",
"Remote resizing": "Op Afstand Formaat Wijzigen",
"Advanced": "Geavanceerd",
"Quality:": "Kwaliteit:",
"Compression level:": "Compressieniveau:",
@ -59,8 +50,6 @@
"Host:": "Host:",
"Port:": "Poort:",
"Path:": "Pad:",
"Automatic reconnect": "Automatisch Opnieuw Verbinden",
"Reconnect delay (ms):": "Vertraging voor Opnieuw Verbinden (ms):",
"Show dot when no cursor": "Geef stip weer indien geen cursor",
"Logging:": "Logmeldingen:",
"Version:": "Versie:",
@ -76,20 +65,5 @@
"Username:": "Gebruikersnaam:",
"Password:": "Wachtwoord:",
"Send credentials": "Stuur inloggegevens",
"Cancel": "Annuleren",
"Must set host": "Host moeten worden ingesteld",
"Password is required": "Wachtwoord is vereist",
"viewport drag": "kijkvenster slepen",
"Active Mouse Button": "Actieve Muisknop",
"No mousebutton": "Geen muisknop",
"Left mousebutton": "Linker muisknop",
"Middle mousebutton": "Middelste muisknop",
"Right mousebutton": "Rechter muisknop",
"Clear": "Wissen",
"Send Password": "Verzend Wachtwoord:",
"Disconnect timeout": "Timeout tijdens verbreken van verbinding",
"Local Downscaling": "Lokaal Neerschalen",
"Local Cursor": "Lokale Cursor",
"Canvas not supported.": "Canvas wordt niet ondersteund.",
"Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "''Clipping mode' ingeschakeld, omdat schuifbalken in volledige-scherm-modus in IE niet worden ondersteund"
"Cancel": "Annuleren"
}

View File

@ -65,16 +65,5 @@
"Connect": "Połącz",
"Password:": "Hasło:",
"Cancel": "Anuluj",
"Canvas not supported.": "Element Canvas nie jest wspierany.",
"Disconnect timeout": "Timeout rozłączenia",
"Local Downscaling": "Downscaling lokalny",
"Local Cursor": "Lokalny kursor",
"Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "Wymuszam clipping mode ponieważ paski przewijania nie są wspierane przez IE w trybie pełnoekranowym",
"True Color": "True Color",
"Style:": "Styl:",
"default": "domyślny",
"Apply": "Zapisz",
"Connection": "Połączenie",
"Token:": "Token:",
"Send Password": "Wyślij Hasło"
"Canvas not supported.": "Element Canvas nie jest wspierany."
}

View File

@ -1,7 +1,7 @@
{
"Running without HTTPS is not recommended, crashes or other issues are likely.": "Det är ej rekommenderat att köra utan HTTPS, krascher och andra problem är troliga.",
"Connecting...": "Ansluter...",
"Disconnecting...": "Kopplar ner...",
"Disconnecting...": "Kopplar ifrån...",
"Reconnecting...": "Återansluter...",
"Internal error": "Internt fel",
"Failed to connect to server: ": "Misslyckades att ansluta till servern: ",
@ -12,6 +12,7 @@
"Disconnected": "Frånkopplad",
"New connection has been rejected with reason: ": "Ny anslutning har blivit nekad med följande skäl: ",
"New connection has been rejected": "Ny anslutning har blivit nekad",
"Are you sure you want to disconnect the session?": "Är du säker på att du vill koppla ifrån sessionen?",
"Credentials are required": "Användaruppgifter krävs",
"noVNC encountered an error:": "noVNC stötte på ett problem:",
"Hide/Show the control bar": "Göm/Visa kontrollbaren",
@ -33,11 +34,11 @@
"Send Escape": "Skicka Escape",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Skicka Ctrl-Alt-Del",
"Shutdown/Reboot": "Stäng av/Boota om",
"Shutdown/Reboot...": "Stäng av/Boota om...",
"Shutdown/Reboot": "Stäng av/Starta om",
"Shutdown/Reboot...": "Stäng av/Starta om...",
"Power": "Ström",
"Shutdown": "Stäng av",
"Reboot": "Boota om",
"Reboot": "Starta om",
"Reset": "Återställ",
"Clipboard": "Urklipp",
"Edit clipboard content in the textarea below.": "Redigera urklippets innehåll i fältet nedan.",
@ -64,20 +65,17 @@
"Show dot when no cursor": "Visa prick när ingen muspekare finns",
"Logging:": "Loggning:",
"Version:": "Version:",
"Disconnect": "Koppla från",
"Disconnect": "Koppla ifrån",
"Connect": "Anslut",
"Server identity": "Server-identitet",
"Server identity": "Serveridentitet",
"The server has provided the following identifying information:": "Servern har gett följande identifierande information:",
"Fingerprint:": "Fingeravtryck:",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Kontrollera att informationen är korrekt och tryck sedan \"Godkänn\". Tryck annars \"Neka\".",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Kontrollera att informationen är korrekt och tryck sedan \"Godkänn\". Tryck annars \"Avvisa\".",
"Approve": "Godkänn",
"Reject": "Neka",
"Reject": "Avvisa",
"Credentials": "Användaruppgifter",
"Username:": "Användarnamn:",
"Password:": "Lösenord:",
"Send credentials": "Skicka användaruppgifter",
"Cancel": "Avbryt",
"Must set host": "Du måste specifiera en värd",
"HTTPS is required for full functionality": "HTTPS krävs för full funktionalitet",
"Clear": "Rensa"
"Cancel": "Avbryt"
}

View File

@ -1,81 +0,0 @@
{
"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

@ -77,17 +77,5 @@
"Username:": "用户名:",
"Password:": "密码:",
"Send credentials": "发送凭证",
"Cancel": "取消",
"Password is required": "请提供密码",
"Disconnect timeout": "超时断开",
"viewport drag": "窗口拖动",
"Active Mouse Button": "启动鼠标按键",
"No mousebutton": "禁用鼠标按键",
"Left mousebutton": "鼠标左键",
"Middle mousebutton": "鼠标中键",
"Right mousebutton": "鼠标右键",
"Clear": "清除",
"Local Downscaling": "降低本地尺寸",
"Local Cursor": "本地光标",
"Canvas not supported.": "不支持 Canvas。"
"Cancel": "取消"
}

View File

@ -117,8 +117,7 @@ html {
.noVNC_center > * {
pointer-events: auto;
}
.noVNC_crosscenter {
.noVNC_vcenter {
display: flex !important;
flex-direction: column;
justify-content: center;
@ -130,29 +129,9 @@ html {
padding: 0 !important;
pointer-events: none;
}
.noVNC_crosscenter > * {
.noVNC_vcenter > * {
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
@ -252,18 +231,10 @@ 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;
@ -278,34 +249,10 @@ 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: "";
@ -316,22 +263,19 @@ html {
left: -30px;
transition: box-shadow 0.5s ease-in-out;
}
.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_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_control_bar_handle {
position: absolute;
@ -344,96 +288,41 @@ 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;
}
: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 {
background-position: left;
}
.noVNC_right #noVNC_control_bar_handle:after {
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 {
left: 5px;
right: 0;
transform: translateX(1px) rotate(180deg);
}
.noVNC_right #noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
transform: translateX(-1px);
transform: none;
}
.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;
@ -449,66 +338,35 @@ 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: 10px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
gap: 10px 0;
padding: 0 10px;
}
: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;
#noVNC_control_bar > .noVNC_scroll > * {
display: block;
margin: 10px auto;
}
/* Control bar hint */
.noVNC_hint_anchor {
#noVNC_hint_anchor {
position: fixed;
left: -50px;
}
.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_anchor.noVNC_right + #noVNC_hint_anchor {
left: -50px;
right: auto;
}
.noVNC_control_bar_hint {
#noVNC_control_bar_hint {
position: relative;
transform: scale(0);
width: 100px;
height: 50%;
max-height: 600px;
position: relative;
transform: scale(0);
visibility: hidden;
opacity: 0;
@ -518,19 +376,13 @@ html {
border-radius: 12px;
transition-delay: 0s;
}
: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 {
#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;
}
@ -538,6 +390,7 @@ 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;
@ -558,7 +411,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: calc(100vh - 75px - 25px); /* minus top and bottom margins */
max-height: 100vh; /* Chrome is buggy with 100% */
overflow-x: hidden;
overflow-y: auto;
@ -578,24 +431,16 @@ 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;
@ -691,26 +536,13 @@ html {
/* Control bar content */
#noVNC_control_bar .noVNC_logo {
display: block;
max-width: 35px;
max-height: 35px;
object-fit: contain;
font-size: 13px;
}
.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 {
@ -718,15 +550,16 @@ html {
}
/* noVNC Touch Device only buttons */
:root:not(.noVNC_connected) #noVNC_keyboard_button {
:root:not(.noVNC_connected) #noVNC_mobile_buttons {
display: none;
}
@media not all and (any-pointer: coarse) {
/* FIXME: It is bad to assume that no touch devices have physical
keyboards available. Hopefully we can get a media query
for this:
/* 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:
https://github.com/w3c/csswg-drafts/issues/3871 */
:root.noVNC_connected #noVNC_keyboard_button {
:root.noVNC_connected #noVNC_mobile_buttons {
display: none;
}
}
@ -740,18 +573,6 @@ 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 */

234
app/ui.js
View File

@ -9,19 +9,18 @@
import * as Log from '../core/util/logging.js';
import _, { l10n } from './localization.js';
import { isTouchDevice, isMac, isIOS, isAndroid, isChromeOS, isSafari,
hasScrollbarGutter, dragThreshold, browserAsyncClipboardSupport }
hasScrollbarGutter, dragThreshold }
from '../core/util/browser.js';
import { setCapture, getPointerEvent } from '../core/util/events.js';
import KeyTable from "../core/input/keysym.js";
import keysyms from "../core/input/keysymdef.js";
import Keyboard from "../core/input/keyboard.js";
import RFB from "../core/rfb.js";
import WakeLockManager from './wakelock.js';
import * as WebUtil from "./webutil.js";
const PAGE_TITLE = "noVNC";
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 LINGUAS = ["cs", "de", "el", "es", "fr", "hr", "hu", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"];
const UI = {
@ -37,8 +36,6 @@ const UI = {
controlbarGrabbed: false,
controlbarDrag: false,
controlbarMouseDownClientX: 0,
controlbarMouseDownOffsetX: 0,
controlbarMouseDownClientY: 0,
controlbarMouseDownOffsetY: 0,
@ -49,8 +46,6 @@ const UI = {
reconnectCallback: null,
reconnectPassword: null,
wakeLockManager: new WakeLockManager(),
async start(options={}) {
UI.customSettings = options.settings || {};
if (UI.customSettings.defaults === undefined) {
@ -112,11 +107,8 @@ const UI = {
}
// Restore control bar position
const pos = WebUtil.readSetting('controlbar_pos');
if (['left', 'right', 'top', 'bottom'].includes(pos)) {
UI.toggleControlbarSide(pos);
} else {
UI.toggleControlbarSide('left');
if (WebUtil.readSetting('controlbar_pos') === 'right') {
UI.toggleControlbarSide();
}
UI.initFullscreen();
@ -143,8 +135,10 @@ 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();
}
@ -195,7 +189,6 @@ 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() {
@ -378,8 +371,6 @@ 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');
@ -580,15 +571,7 @@ const UI = {
}
},
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) {
toggleControlbarSide() {
// Temporarily disable animation, if bar is displayed, to avoid weird
// movement. The transitionend-event will not fire when display=none.
const bar = document.getElementById('noVNC_control_bar');
@ -599,12 +582,13 @@ const UI = {
}
const anchor = document.getElementById('noVNC_control_bar_anchor');
anchor.classList.remove('noVNC_right', 'noVNC_top', 'noVNC_bottom');
if (['right', 'top', 'bottom'].includes(pos)) {
anchor.classList.add(`noVNC_${pos}`);
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");
}
WebUtil.writeSetting('controlbar_pos', pos);
// Consider this a movement of the handle
UI.controlbarDrag = true;
@ -614,21 +598,19 @@ const UI = {
},
showControlbarHint(show, animate=true) {
const getPos = element =>
['right', 'top', 'bottom'].find(pos =>
element.classList.contains(`noVNC_${pos}`)
) ?? 'left';
const hint = document.getElementById('noVNC_control_bar_hint');
const anchor = document.getElementById('noVNC_control_bar_anchor');
const anchorPos = getPos(anchor);
if (animate) {
hint.classList.remove("noVNC_notransition");
} else {
hint.classList.add("noVNC_notransition");
}
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);
});
if (show) {
hint.classList.add("noVNC_active");
} else {
hint.classList.remove("noVNC_active");
}
},
dragControlbarHandle(e) {
@ -636,62 +618,28 @@ const UI = {
const ptr = getPointerEvent(e);
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';
const anchor = document.getElementById('noVNC_control_bar_anchor');
if (ptr.clientX < (window.innerWidth * 0.1)) {
if (anchor.classList.contains("noVNC_right")) {
UI.toggleControlbarSide();
}
} else if (ptr.clientX > (window.innerWidth * 0.9) &&
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';
} else if (ptr.clientX > (window.innerWidth * 0.9)) {
if (!anchor.classList.contains("noVNC_right")) {
UI.toggleControlbarSide();
}
}
const isVertical = controlBarPos === 'left' || controlBarPos === 'right';
if (!UI.controlbarDrag) {
const dragDistance = isVertical
? Math.abs(ptr.clientY - UI.controlbarMouseDownClientY)
: Math.abs(ptr.clientX - UI.controlbarMouseDownClientX);
const dragDistance = Math.abs(ptr.clientY - UI.controlbarMouseDownClientY);
if (dragDistance < dragThreshold) return;
UI.controlbarDrag = true;
}
if (isVertical) {
const eventY = ptr.clientY - UI.controlbarMouseDownOffsetY;
UI.moveControlbarHandle(eventY, true);
} else {
const eventX = ptr.clientX - UI.controlbarMouseDownOffsetX;
UI.moveControlbarHandle(eventX, false);
}
const eventY = ptr.clientY - UI.controlbarMouseDownOffsetY;
UI.moveControlbarHandle(eventY);
e.preventDefault();
e.stopPropagation();
@ -700,56 +648,41 @@ const UI = {
},
// Move the handle but don't allow any position outside the bounds
moveControlbarHandle(viewportRelativeCoord, isVertical) {
moveControlbarHandle(viewportRelativeY) {
const handle = document.getElementById("noVNC_control_bar_handle");
const handleSpan = isVertical
? handle.getBoundingClientRect().height
: handle.getBoundingClientRect().width;
const handleHeight = handle.getBoundingClientRect().height;
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 (handleSpan === 0 || controlbarBoundsSpan === 0) {
if (handleHeight === 0 || controlbarBounds.height === 0) {
return;
}
let newCoord = viewportRelativeCoord;
let newY = viewportRelativeY;
// Check if the coordinates are outside the control bar
if (newCoord < controlbarBoundsStart + margin) {
// Force coordinates to be below the start of the control bar
newCoord = controlbarBoundsStart + margin;
if (newY < controlbarBounds.top + margin) {
// Force coordinates to be below the top of the control bar
newY = controlbarBounds.top + margin;
} else if (newCoord > controlbarBoundsStart +
controlbarBoundsSpan - handleSpan - margin) {
// Force coordinates to be before the end of the control bar
newCoord = controlbarBoundsStart +
controlbarBoundsSpan - handleSpan - margin;
} else if (newY > controlbarBounds.top +
controlbarBounds.height - handleHeight - margin) {
// Force coordinates to be above the bottom of the control bar
newY = controlbarBounds.top +
controlbarBounds.height - handleHeight - margin;
}
// Corner case: control bar too small for stable position
if (controlbarBoundsSpan < (handleSpan + margin * 2)) {
newCoord = controlbarBoundsStart +
(controlbarBoundsSpan - handleSpan) / 2;
if (controlbarBounds.height < (handleHeight + margin * 2)) {
newY = controlbarBounds.top +
(controlbarBounds.height - handleHeight) / 2;
}
// The transform needs coordinates that are relative to the parent
const parentRelativeCoord = newCoord - controlbarBoundsStart;
if (isVertical) {
handle.style.transform = "translateY(" + parentRelativeCoord + "px)";
} else {
handle.style.transform = "translateX(" + parentRelativeCoord + "px)";
}
const parentRelativeY = newY - controlbarBounds.top;
handle.style.transform = "translateY(" + parentRelativeY + "px)";
},
updateControlbarHandle() {
@ -757,15 +690,7 @@ const UI = {
// the move function expects coordinates relative the the viewport.
const handle = document.getElementById("noVNC_control_bar_handle");
const handleBounds = handle.getBoundingClientRect();
const controlBarPos = UI.getControlbarPos();
const isVertical = controlBarPos === 'left' || controlBarPos === 'right';
if (isVertical) {
UI.moveControlbarHandle(handleBounds.top, true);
} else {
UI.moveControlbarHandle(handleBounds.left, false);
}
UI.moveControlbarHandle(handleBounds.top);
},
controlbarHandleMouseUp(e) {
@ -803,8 +728,6 @@ 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();
@ -1149,10 +1072,6 @@ 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,
@ -1184,7 +1103,6 @@ const UI = {
UI.rfb.showDotCursor = UI.getSetting('show_dot');
UI.updateViewOnly(); // requires UI.rfb
UI.updateClipboard();
},
disconnect() {
@ -1252,7 +1170,6 @@ const UI = {
UI.connected = false;
UI.rfb = undefined;
UI.wakeLockManager.release();
if (!e.detail.clean) {
UI.updateVisualState('disconnected');
@ -1284,7 +1201,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.
@ -1361,13 +1278,13 @@ const UI = {
let inputFocus = "none";
if (e.detail.types.indexOf("username") === -1) {
document.getElementById("noVNC_username_block").classList.add("noVNC_hidden");
} else if (document.getElementById("noVNC_username_input").value === "") {
inputFocus = "noVNC_username_input";
} else {
inputFocus = inputFocus === "none" ? "noVNC_username_input" : inputFocus;
}
if (e.detail.types.indexOf("password") === -1) {
document.getElementById("noVNC_password_block").classList.add("noVNC_hidden");
} else if (inputFocus === "none") {
inputFocus = "noVNC_password_input";
} else {
inputFocus = inputFocus === "none" ? "noVNC_password_input" : inputFocus;
}
document.getElementById('noVNC_credentials_dlg')
.classList.add('noVNC_open');
@ -1861,31 +1778,6 @@ 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');
@ -1901,16 +1793,6 @@ 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();

View File

@ -1,199 +0,0 @@
/*
* 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();
}
}

View File

@ -1,72 +0,0 @@
/*
* 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 l, r, x; // left, right, accumulator
let i = 0, l, r, x; // left, right, accumulator
// Squash 8 bytes to 2 ints
l = b[0]<<24 | b[1]<<16 | b[2]<<8 | b[3];
r = b[4]<<24 | b[5]<<16 | b[6]<<8 | b[7];
l = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++];
r = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++];
x = ((l >>> 4) ^ r) & 0x0f0f0f0f;
r ^= x;
@ -252,7 +252,7 @@ class DES {
// Spread ints to bytes
x = [r, l];
for (let i = 0; i < 8; i++) {
for (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;
let current = 0;
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-order rendering
this._renderQ = []; // queue drawing actions for in-oder rendering
this._flushPromise = null;
// the full frame buffer (logical canvas) size

View File

@ -15,7 +15,6 @@ 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";
@ -165,7 +164,6 @@ 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
@ -268,9 +266,6 @@ 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
@ -320,10 +315,8 @@ export default class RFB extends EventTargetMixin {
this._rfbConnectionState === "connected") {
if (viewOnly) {
this._keyboard.ungrab();
this._asyncClipboard.ungrab();
} else {
this._keyboard.grab();
this._asyncClipboard.grab();
}
}
}
@ -2215,10 +2208,7 @@ export default class RFB extends EventTargetMixin {
this._setDesktopName(name);
this._resize(width, height);
if (!this._viewOnly) {
this._keyboard.grab();
this._asyncClipboard.grab();
}
if (!this._viewOnly) { this._keyboard.grab(); }
this._fbDepth = 24;
@ -2333,15 +2323,6 @@ 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");
@ -2361,7 +2342,9 @@ export default class RFB extends EventTargetMixin {
return true;
}
this._writeClipboard(text);
this.dispatchEvent(new CustomEvent(
"clipboard",
{ detail: { text: text } }));
} else {
//Extended msg.
@ -2497,7 +2480,9 @@ export default class RFB extends EventTargetMixin {
textData = textData.replaceAll("\r\n", "\n");
this._writeClipboard(textData);
this.dispatchEvent(new CustomEvent(
"clipboard",
{ detail: { text: textData } }));
}
} else {
return this._fail("Unexpected action in extended clipboard message: " + actions);
@ -2949,7 +2934,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,39 +11,6 @@
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
@ -182,12 +149,7 @@ async function _checkWebCodecsH264DecodeSupport() {
return true;
}
// 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;
});
supportsWebCodecsH264Decode = await _checkWebCodecsH264DecodeSupport();
/*
* The functions for detection of platforms and browsers below are exported

View File

@ -18,8 +18,6 @@ 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.
@ -27,10 +25,10 @@ with transparent binary data support.
## 1.2 Callbacks
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.
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.
## 2. Modules
@ -83,23 +81,3 @@ 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

@ -92,10 +92,6 @@ Currently, the following options are available:
* `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,8 +1,7 @@
import globals from "globals";
import { defineConfig } from "eslint/config";
import js from "@eslint/js";
export default defineConfig([
export default [
js.configs.recommended,
{
languageOptions: {
@ -100,4 +99,4 @@ export default defineConfig([
"no-console": 0,
},
},
]);
];

View File

@ -37,7 +37,6 @@ 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

@ -39,7 +39,6 @@
"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 hu it ja ko nl pl pt_BR ru sv tr uk zh_CN zh_TW
LINGUAS := cs de el es fr hr hu it ja ko nl pl pt_BR ru sv tr zh_CN zh_TW
VERSION := $(shell grep '"version"' ../package.json | cut -d '"' -f 4)

341
po/uk.po
View File

@ -1,341 +0,0 @@
# 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,74 +1,6 @@
import { isMac, isWindows, isIOS, isAndroid, isChromeOS,
isSafari, isFirefox, isChrome, isChromium, isOpera, isEdge,
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');
});
});
isGecko, isWebKit, isBlink } from '../core/util/browser.js';
describe('OS detection', function () {
let origNavigator;

View File

@ -1,121 +0,0 @@
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,47 +3467,17 @@ describe('Remote Frame Buffer protocol client', function () {
});
describe('Normal clipboard handling receive', 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),
};
it('should fire the clipboard callback with the retrieved text on ServerCutText', function () {
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');
const spy = sinon.spy();
client.addEventListener("clipboard", spy);
client._sock._websocket._receiveData(new Uint8Array(data));
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);
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.text).to.equal(expectedStr);
});
});
@ -3560,70 +3530,8 @@ 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', async function () {
client._viewOnly = false;
client._asyncClipboard = {
writeClipboard: sinon.stub().returns(false),
};
it('should update clipboard with correct Unicode data from a Provide message', function () {
let expectedData = "Aå漢字!";
let data = [3, 0, 0, 0];
const flags = [0x10, 0x00, 0x00, 0x01];
@ -3637,22 +3545,16 @@ describe('Remote Frame Buffer protocol client', function () {
data = data.concat(flags);
data = data.concat(Array.from(deflatedText));
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
const spy = sinon.spy();
client.addEventListener("clipboard", spy);
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);
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.text).to.equal(expectedData);
client.removeEventListener("clipboard", spy);
});
it('should update clipboard with correct escape characters from a Provide message ', async function () {
client._viewOnly = false;
client._asyncClipboard = {
writeClipboard: sinon.stub().returns(false),
};
it('should update clipboard with correct escape characters from a Provide message ', function () {
let expectedData = "Oh\nmy\n!";
let data = [3, 0, 0, 0];
const flags = [0x10, 0x00, 0x00, 0x01];
@ -3667,22 +3569,16 @@ describe('Remote Frame Buffer protocol client', function () {
data = data.concat(flags);
data = data.concat(Array.from(deflatedText));
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
const spy = sinon.spy();
client.addEventListener("clipboard", spy);
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);
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.text).to.equal(expectedData);
client.removeEventListener("clipboard", spy);
});
it('should be able to handle large Provide messages', async function () {
client._viewOnly = false;
client._asyncClipboard = {
writeClipboard: sinon.stub().returns(false),
};
it('should be able to handle large Provide messages', function () {
let expectedData = "hello".repeat(100000);
let data = [3, 0, 0, 0];
const flags = [0x10, 0x00, 0x00, 0x01];
@ -3697,15 +3593,13 @@ describe('Remote Frame Buffer protocol client', function () {
data = data.concat(flags);
data = data.concat(Array.from(deflatedText));
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
const spy = sinon.spy();
client.addEventListener("clipboard", spy);
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);
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.text).to.equal(expectedData);
client.removeEventListener("clipboard", spy);
});
});

View File

@ -1,197 +0,0 @@
/* 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\n"
echo -e " http://${HOST}:${PORT}/vnc.html?host=${HOST}&port=${PORT}\n"
else
echo -e " https://${HOST}:${PORT}/vnc.html\n"
echo -e " https://${HOST}:${PORT}/vnc.html?host=${HOST}&port=${PORT}\n"
fi
echo -e "Press Ctrl-C to exit\n\n"

View File

@ -50,6 +50,14 @@ 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_crosscenter">
<div id="noVNC_control_bar_anchor" class="noVNC_vcenter">
<div id="noVNC_control_bar">
<div id="noVNC_control_bar_handle" title="Hide/Show the control bar"><div></div></div>
<div class="noVNC_scroll">
<img class="noVNC_logo" src="app/images/icons/novnc-icon-35x21.svg"
alt="noVNC">
<h1 class="noVNC_logo" translate="no"><span>no</span><br>VNC</h1>
<hr>
@ -127,14 +127,16 @@
title="Move/Drag viewport">
<!--noVNC touch device only buttons-->
<input type="image" alt="Keyboard" src="app/images/keyboard.svg"
id="noVNC_keyboard_button" class="noVNC_button" title="Show keyboard">
<div id="noVNC_mobile_buttons">
<input type="image" alt="Keyboard" src="app/images/keyboard.svg"
id="noVNC_keyboard_button" class="noVNC_button" title="Show keyboard">
</div>
<!-- Extra manual keys -->
<input type="image" alt="Extra keys" src="app/images/toggleextrakeys.svg"
id="noVNC_toggle_extra_keys_button" class="noVNC_button"
title="Show extra keys">
<div class="noVNC_crosscenter">
<div class="noVNC_vcenter">
<div id="noVNC_modifiers" class="noVNC_panel">
<input type="image" alt="Ctrl" src="app/images/ctrl.svg"
id="noVNC_toggle_ctrl_button" class="noVNC_button"
@ -161,7 +163,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_crosscenter">
<div class="noVNC_vcenter">
<div id="noVNC_power" class="noVNC_panel">
<div class="noVNC_heading">
<img alt="" src="app/images/power.svg"> Power
@ -176,7 +178,7 @@
<input type="image" alt="Clipboard" src="app/images/clipboard.svg"
id="noVNC_clipboard_button" class="noVNC_button"
title="Clipboard">
<div class="noVNC_crosscenter">
<div class="noVNC_vcenter">
<div id="noVNC_clipboard" class="noVNC_panel">
<div class="noVNC_heading">
<img alt="" src="app/images/clipboard.svg"> Clipboard
@ -197,7 +199,7 @@
<input type="image" alt="Settings" src="app/images/settings.svg"
id="noVNC_settings_button" class="noVNC_button"
title="Settings">
<div class="noVNC_crosscenter">
<div class="noVNC_vcenter">
<div id="noVNC_settings" class="noVNC_panel">
<div class="noVNC_heading">
<img alt="" src="app/images/settings.svg"> Settings
@ -294,13 +296,6 @@
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>
@ -330,20 +325,8 @@
</div> <!-- End of noVNC_control_bar -->
<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 id="noVNC_hint_anchor" class="noVNC_vcenter">
<div id="noVNC_control_bar_hint">
</div>
</div>