Compare commits
41 Commits
1.7-branch
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
fc5b83c08f | |
|
|
2ea0e7d4eb | |
|
|
ad25fdb20b | |
|
|
094fd0509f | |
|
|
fb8f30a1dd | |
|
|
b4c4cb75d5 | |
|
|
7834e66733 | |
|
|
5c2025d5a6 | |
|
|
4bce309c3b | |
|
|
816a4c7f24 | |
|
|
052242df5d | |
|
|
44343556e0 | |
|
|
52bc5a5f9f | |
|
|
f0d217719c | |
|
|
8e1ebdffba | |
|
|
4d16cc5270 | |
|
|
1640a5e21f | |
|
|
6d0a974665 | |
|
|
d1a124401b | |
|
|
40cc57460d | |
|
|
ef3257037f | |
|
|
988e9da7fa | |
|
|
f4f2f8d725 | |
|
|
077c54f312 | |
|
|
8341fdf846 | |
|
|
b6136b5386 | |
|
|
d44f7e04fc | |
|
|
fb97e0f1e2 | |
|
|
d3d69a8118 | |
|
|
e03a54e12a | |
|
|
99865e5aba | |
|
|
b29cc6493d | |
|
|
c2d89730dd | |
|
|
9d77c806fe | |
|
|
63c2c14a50 | |
|
|
91d12106df | |
|
|
fb7e891841 | |
|
|
3d5698c71c | |
|
|
d9b45d390b | |
|
|
f5a4eedcea | |
|
|
cf6fe1063c |
|
|
@ -9,39 +9,39 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
npm:
|
npm:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- run: |
|
- run: |
|
||||||
GITREV=$(git rev-parse --short HEAD)
|
GITREV=$(git rev-parse --short HEAD)
|
||||||
echo $GITREV
|
echo $GITREV
|
||||||
sed -i "s/^\(.*\"version\".*\)\"\([^\"]\+\)\"\(.*\)\$/\1\"\2-g$GITREV\"\3/" package.json
|
sed -i "s/^\(.*\"version\".*\)\"\([^\"]\+\)\"\(.*\)\$/\1\"\2-g$GITREV\"\3/" package.json
|
||||||
if: github.event_name != 'release'
|
if: github.event_name != 'release'
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
|
# Node 24 is needed to get npm > 11.5.1, which is a requirement for
|
||||||
|
# OIDC auth.
|
||||||
|
node-version: 24
|
||||||
# Needs to be explicitly specified for auth to work
|
# Needs to be explicitly specified for auth to work
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: npm
|
name: npm
|
||||||
path: lib
|
path: lib
|
||||||
- run: npm publish --access public
|
- run: npm publish --access public
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
|
||||||
if: |
|
if: |
|
||||||
github.repository == 'novnc/noVNC' &&
|
github.repository == 'novnc/noVNC' &&
|
||||||
github.event_name == 'release' &&
|
github.event_name == 'release' &&
|
||||||
!github.event.release.prerelease
|
!github.event.release.prerelease
|
||||||
- run: npm publish --access public --tag beta
|
- run: npm publish --access public --tag beta
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
|
||||||
if: |
|
if: |
|
||||||
github.repository == 'novnc/noVNC' &&
|
github.repository == 'novnc/noVNC' &&
|
||||||
github.event_name == 'release' &&
|
github.event_name == 'release' &&
|
||||||
github.event.release.prerelease
|
github.event.release.prerelease
|
||||||
- run: npm publish --access public --tag dev
|
- run: npm publish --access public --tag dev
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
|
||||||
if: |
|
if: |
|
||||||
github.repository == 'novnc/noVNC' &&
|
github.repository == 'novnc/noVNC' &&
|
||||||
github.event_name == 'push' &&
|
github.event_name == 'push' &&
|
||||||
|
|
@ -49,7 +49,7 @@ jobs:
|
||||||
snap:
|
snap:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- run: |
|
- run: |
|
||||||
GITREV=$(git rev-parse --short HEAD)
|
GITREV=$(git rev-parse --short HEAD)
|
||||||
echo $GITREV
|
echo $GITREV
|
||||||
|
|
@ -61,7 +61,7 @@ jobs:
|
||||||
sed -i "s/^version:.*/version: '$VERSION'/" snap/snapcraft.yaml
|
sed -i "s/^version:.*/version: '$VERSION'/" snap/snapcraft.yaml
|
||||||
- uses: snapcore/action-build@v1
|
- uses: snapcore/action-build@v1
|
||||||
id: snapcraft
|
id: snapcraft
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: snap
|
name: snap
|
||||||
path: ${{ steps.snapcraft.outputs.snap }}
|
path: ${{ steps.snapcraft.outputs.snap }}
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,14 @@ jobs:
|
||||||
eslint:
|
eslint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
- run: npm update
|
- run: npm update
|
||||||
- run: npm run lint
|
- run: npm run lint
|
||||||
html:
|
html:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
- run: npm update
|
- run: npm update
|
||||||
- run: git ls-tree --name-only -r HEAD | grep -E "[.](html|css)$" | xargs ./utils/validate
|
- run: git ls-tree --name-only -r HEAD | grep -E "[.](html|css)$" | xargs ./utils/validate
|
||||||
|
|
|
||||||
|
|
@ -13,15 +13,18 @@ jobs:
|
||||||
- ChromeHeadless
|
- ChromeHeadless
|
||||||
- FirefoxHeadless
|
- FirefoxHeadless
|
||||||
include:
|
include:
|
||||||
- os: macos-latest
|
# FIXME: We'd like to use "macos-latest", but Safari tests
|
||||||
|
# are flaky on it and timeout often. As of 2026-05-23
|
||||||
|
# macos-latest is still on 10.15.
|
||||||
|
- os: macos-26
|
||||||
browser: Safari
|
browser: Safari
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
browser: EdgeHeadless
|
browser: EdgeHeadless
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
- run: npm update
|
- run: npm update
|
||||||
- run: npm run test
|
- run: npm run test
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ jobs:
|
||||||
translate:
|
translate:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
- run: npm update
|
- run: npm update
|
||||||
- run: sudo apt-get install gettext
|
- run: sudo apt-get install gettext
|
||||||
- run: make -C po update-pot
|
- run: make -C po update-pot
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="35"
|
||||||
|
height="21"
|
||||||
|
viewBox="0 0 35 21.000001"
|
||||||
|
id="svg2"
|
||||||
|
version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<defs
|
||||||
|
id="defs4" />
|
||||||
|
<metadata
|
||||||
|
id="metadata7">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-1004.3621)">
|
||||||
|
<g
|
||||||
|
id="g4300"
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.10948"
|
||||||
|
transform="matrix(0.90909091,0,0,0.893617,-4.0909091,96.570459)">
|
||||||
|
<g
|
||||||
|
id="g4302"
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.10948">
|
||||||
|
<path
|
||||||
|
d="m 11.986926,1016.3621 c 0.554325,0 1.025987,0.2121 1.414987,0.6362 0.398725,0.4138 0.600909,0.9155 0.598087,1.5052 v 6.8586 h -2 v -6.8914 c 0,-0.072 -0.03404,-0.1086 -0.102113,-0.1086 H 7.1021125 C 7.0340375,1018.3621 7,1018.3983 7,1018.4707 v 6.8914 H 5 v -9 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.10948px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
id="path4304" />
|
||||||
|
<path
|
||||||
|
d="m 17.013073,1016.3621 h 4.973854 c 0.554325,0 1.025987,0.2121 1.414986,0.6362 0.398725,0.4138 0.598087,0.9155 0.598087,1.5052 v 4.7172 c 0,0.5897 -0.199362,1.0966 -0.598087,1.5207 -0.388999,0.4138 -0.860661,0.6207 -1.414986,0.6207 h -4.973854 c -0.554325,0 -1.030849,-0.2069 -1.429574,-0.6207 C 15.1945,1024.3173 15,1023.8104 15,1023.2207 v -4.7172 c 0,-0.5897 0.1945,-1.0914 0.583499,-1.5052 0.398725,-0.4241 0.875249,-0.6362 1.429574,-0.6362 z m 4.884815,2 h -4.795776 c -0.06808,0 -0.102112,0.036 -0.102112,0.1086 v 4.7828 c 0,0.072 0.03404,0.1086 0.102112,0.1086 h 4.795776 c 0.06807,0 0.102112,-0.036 0.102112,-0.1086 v -4.7828 c 0,-0.072 -0.03404,-0.1086 -0.102112,-0.1086 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.10948px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
id="path4306" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g4308"
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.10948">
|
||||||
|
<path
|
||||||
|
d="m 12,1036.9177 4.768114,-8.5556 H 19 l -6,11 h -2 l -6,-11 h 2.2318854 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.10948px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
id="path4310" />
|
||||||
|
<path
|
||||||
|
d="m 29,1036.3621 v -8 h 2 v 11 h -2 l -7,-8 v 8 h -2 v -11 h 2 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.10948px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
id="path4312" />
|
||||||
|
<path
|
||||||
|
d="m 43,1030.3621 h -8.897887 c -0.06808,0 -0.102113,0.036 -0.102113,0.1069 v 6.7862 c 0,0.071 0.03404,0.1069 0.102113,0.1069 H 43 v 2 h -8.972339 c -0.56405,0 -1.045437,-0.2037 -1.444162,-0.6111 C 32.1945,1038.3334 32,1037.8292 32,1037.2385 v -6.7528 c 0,-0.5907 0.1945,-1.0898 0.583499,-1.4972 0.398725,-0.4176 0.880112,-0.6264 1.444162,-0.6264 H 43 Z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.10948px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
id="path4314" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g4291"
|
||||||
|
style="stroke:none;stroke-width:1.10948"
|
||||||
|
transform="matrix(0.90909091,0,0,0.893617,-4.5454545,96.123649)">
|
||||||
|
<g
|
||||||
|
id="g4282"
|
||||||
|
style="stroke:none;stroke-width:1.10948">
|
||||||
|
<path
|
||||||
|
id="path4143"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1.10948px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 11.986926,1016.3621 c 0.554325,0 1.025987,0.2121 1.414987,0.6362 0.398725,0.4138 0.600909,0.9155 0.598087,1.5052 v 6.8586 h -2 v -6.8914 c 0,-0.072 -0.03404,-0.1086 -0.102113,-0.1086 H 7.1021125 C 7.0340375,1018.3621 7,1018.3983 7,1018.4707 v 6.8914 H 5 v -9 z" />
|
||||||
|
<path
|
||||||
|
id="path4145"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1.10948px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 17.013073,1016.3621 h 4.973854 c 0.554325,0 1.025987,0.2121 1.414986,0.6362 0.398725,0.4138 0.598087,0.9155 0.598087,1.5052 v 4.7172 c 0,0.5897 -0.199362,1.0966 -0.598087,1.5207 -0.388999,0.4138 -0.860661,0.6207 -1.414986,0.6207 h -4.973854 c -0.554325,0 -1.030849,-0.2069 -1.429574,-0.6207 C 15.1945,1024.3173 15,1023.8104 15,1023.2207 v -4.7172 c 0,-0.5897 0.1945,-1.0914 0.583499,-1.5052 0.398725,-0.4241 0.875249,-0.6362 1.429574,-0.6362 z m 4.884815,2 h -4.795776 c -0.06808,0 -0.102112,0.036 -0.102112,0.1086 v 4.7828 c 0,0.072 0.03404,0.1086 0.102112,0.1086 h 4.795776 c 0.06807,0 0.102112,-0.036 0.102112,-0.1086 v -4.7828 c 0,-0.072 -0.03404,-0.1086 -0.102112,-0.1086 z" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g4286"
|
||||||
|
style="stroke:none;stroke-width:1.10948">
|
||||||
|
<path
|
||||||
|
id="path4147"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1.10948px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 12,1036.9177 4.768114,-8.5556 H 19 l -6,11 h -2 l -6,-11 h 2.2318854 z" />
|
||||||
|
<path
|
||||||
|
id="path4149"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1.10948px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 29,1036.3621 v -8 h 2 v 11 h -2 l -7,-8 v 8 h -2 v -11 h 2 z" />
|
||||||
|
<path
|
||||||
|
id="path4151"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1.10948px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 43,1030.3621 h -8.897887 c -0.06808,0 -0.102113,0.036 -0.102113,0.1069 v 6.7862 c 0,0.071 0.03404,0.1069 0.102113,0.1069 H 43 v 2 h -8.972339 c -0.56405,0 -1.045437,-0.2037 -1.444162,-0.6111 C 32.1945,1038.3334 32,1037.8292 32,1037.2385 v -6.7528 c 0,-0.5907 0.1945,-1.0898 0.583499,-1.4972 0.398725,-0.4176 0.880112,-0.6264 1.444162,-0.6264 H 43 Z" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 8.8 KiB |
|
|
@ -0,0 +1,80 @@
|
||||||
|
{
|
||||||
|
"Running without HTTPS is not recommended, crashes or other issues are likely.": "HTTPS nélkül futtatni nem ajánlott, összeomlások vagy más problémák várhatók.",
|
||||||
|
"Connecting...": "Kapcsolódás...",
|
||||||
|
"Disconnecting...": "Kapcsolat bontása...",
|
||||||
|
"Reconnecting...": "Újrakapcsolódás...",
|
||||||
|
"Internal error": "Belső hiba",
|
||||||
|
"Failed to connect to server: ": "Nem sikerült csatlakozni a szerverhez: ",
|
||||||
|
"Connected (encrypted) to ": "Kapcsolódva (titkosítva) ehhez: ",
|
||||||
|
"Connected (unencrypted) to ": "Kapcsolódva (titkosítatlanul) ehhez: ",
|
||||||
|
"Something went wrong, connection is closed": "Valami hiba történt, a kapcsolat lezárult",
|
||||||
|
"Failed to connect to server": "Nem sikerült csatlakozni a szerverhez",
|
||||||
|
"Disconnected": "Kapcsolat bontva",
|
||||||
|
"New connection has been rejected with reason: ": "Az új kapcsolat elutasítva, indok: ",
|
||||||
|
"New connection has been rejected": "Az új kapcsolat elutasítva",
|
||||||
|
"Credentials are required": "Hitelesítő adatok szükségesek",
|
||||||
|
"noVNC encountered an error:": "A noVNC hibát észlelt:",
|
||||||
|
"Hide/Show the control bar": "Vezérlősáv elrejtése/megjelenítése",
|
||||||
|
"Drag": "Húzás",
|
||||||
|
"Move/Drag viewport": "Nézet mozgatása/húzása",
|
||||||
|
"Keyboard": "Billentyűzet",
|
||||||
|
"Show keyboard": "Billentyűzet megjelenítése",
|
||||||
|
"Extra keys": "Extra billentyűk",
|
||||||
|
"Show extra keys": "Extra billentyűk megjelenítése",
|
||||||
|
"Ctrl": "Ctrl",
|
||||||
|
"Toggle Ctrl": "Ctrl lenyomása/felengedése",
|
||||||
|
"Alt": "Alt",
|
||||||
|
"Toggle Alt": "Alt lenyomása/felengedése",
|
||||||
|
"Toggle Windows": "Windows lenyomása/felengedése",
|
||||||
|
"Windows": "Windows",
|
||||||
|
"Send Tab": "Tab küldése",
|
||||||
|
"Tab": "Tab",
|
||||||
|
"Esc": "Esc",
|
||||||
|
"Send Escape": "Escape küldése",
|
||||||
|
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
|
||||||
|
"Send Ctrl-Alt-Del": "Ctrl-Alt-Del küldése",
|
||||||
|
"Shutdown/Reboot": "Leállítás/Újraindítás",
|
||||||
|
"Shutdown/Reboot...": "Leállítás/Újraindítás...",
|
||||||
|
"Power": "Bekapcsolás",
|
||||||
|
"Shutdown": "Leállítás",
|
||||||
|
"Reboot": "Újraindítás",
|
||||||
|
"Reset": "Reset",
|
||||||
|
"Clipboard": "Vágólap",
|
||||||
|
"Edit clipboard content in the textarea below.": "Itt tudod módosítani a vágólap tartalmát.",
|
||||||
|
"Full screen": "Teljes képernyő",
|
||||||
|
"Settings": "Beállítások",
|
||||||
|
"Shared mode": "Megosztott mód",
|
||||||
|
"View only": "Csak megtekintés",
|
||||||
|
"Clip to window": "Ablakhoz igazítás",
|
||||||
|
"Scaling mode:": "Méretezési mód:",
|
||||||
|
"None": "Nincs",
|
||||||
|
"Local scaling": "Helyi méretezés",
|
||||||
|
"Remote resizing": "Távoli átméretezés",
|
||||||
|
"Advanced": "Speciális",
|
||||||
|
"Quality:": "Minőség:",
|
||||||
|
"Compression level:": "Tömörítési szint:",
|
||||||
|
"Repeater ID:": "Ismétlő azonosító:",
|
||||||
|
"WebSocket": "WebSocket",
|
||||||
|
"Encrypt": "Titkosítás",
|
||||||
|
"Host:": "Hoszt:",
|
||||||
|
"Port:": "Port:",
|
||||||
|
"Path:": "Útvonal:",
|
||||||
|
"Automatic reconnect": "Automatikus újracsatlakozás",
|
||||||
|
"Reconnect delay (ms):": "Újracsatlakozás késleltetése (ms):",
|
||||||
|
"Show dot when no cursor": "Kurzor hiányában pont mutatása",
|
||||||
|
"Logging:": "Naplózás:",
|
||||||
|
"Version:": "Verzió:",
|
||||||
|
"Disconnect": "Kapcsolat bontása",
|
||||||
|
"Connect": "Csatlakozás",
|
||||||
|
"Server identity": "Szerver azonosító",
|
||||||
|
"The server has provided the following identifying information:": "A szerver a következő azonosító információt adta meg:",
|
||||||
|
"Fingerprint:": "Ujjlenyomat:",
|
||||||
|
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Ellenőrizze, hogy az információ helyes-e és nyomja meg a \"Jóváhagyás\" gombot. Ellenkező esetben nyomja meg az \"Elutasítás\" gombot.",
|
||||||
|
"Approve": "Jóváhagyás",
|
||||||
|
"Reject": "Elutasítás",
|
||||||
|
"Credentials": "Hitelesítő adatok",
|
||||||
|
"Username:": "Felhasználónév:",
|
||||||
|
"Password:": "Jelszó:",
|
||||||
|
"Send credentials": "Hitelesítő adatok küldése",
|
||||||
|
"Cancel": "Mégse"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
{
|
||||||
|
"Running without HTTPS is not recommended, crashes or other issues are likely.": "Робота без HTTPS не рекомендується, ймовірні збої чи інші проблеми.",
|
||||||
|
"Connecting...": "З'єднання...",
|
||||||
|
"Disconnecting...": "Від'єднання...",
|
||||||
|
"Reconnecting...": "Перез'єднання...",
|
||||||
|
"Internal error": "Внутрішня помилка",
|
||||||
|
"Failed to connect to server: ": "Не вдалося з'єднатися з сервером: ",
|
||||||
|
"Connected (encrypted) to ": "З'єднано (з шифруванням) з ",
|
||||||
|
"Connected (unencrypted) to ": "З'єднано (без шифрування) з ",
|
||||||
|
"Something went wrong, connection is closed": "Щось пішло не так, з'єднання закрито",
|
||||||
|
"Failed to connect to server": "Не вдалося з'єднатися з сервером",
|
||||||
|
"Disconnected": "Від'єднано",
|
||||||
|
"New connection has been rejected with reason: ": "Нове з'єднання відхилено. Причина: ",
|
||||||
|
"New connection has been rejected": "Нове з'єднання відхилено",
|
||||||
|
"Are you sure you want to disconnect the session?": "Точно від'єднати сеанс?",
|
||||||
|
"Credentials are required": "Треба особові дані",
|
||||||
|
"noVNC encountered an error:": "Помилка noVNC:",
|
||||||
|
"Hide/Show the control bar": "Сховати/показати панель керування",
|
||||||
|
"Drag": "Посунути",
|
||||||
|
"Move/Drag viewport": "Змістити область огляду",
|
||||||
|
"Keyboard": "Клавіатура",
|
||||||
|
"Show keyboard": "Показати клавіатуру",
|
||||||
|
"Extra keys": "Додаткові клавіші",
|
||||||
|
"Show extra keys": "Показати додаткові клавіші",
|
||||||
|
"Ctrl": "Ctrl",
|
||||||
|
"Toggle Ctrl": "Затиснути Ctrl",
|
||||||
|
"Alt": "Alt",
|
||||||
|
"Toggle Alt": "Затиснути Alt",
|
||||||
|
"Toggle Windows": "Затиснути Windows",
|
||||||
|
"Windows": "Windows",
|
||||||
|
"Send Tab": "Натиснути Tab",
|
||||||
|
"Tab": "Tab",
|
||||||
|
"Esc": "Esc",
|
||||||
|
"Send Escape": "Натиснути Escape",
|
||||||
|
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
|
||||||
|
"Send Ctrl-Alt-Del": "Натиснути Ctrl+Alt+Del",
|
||||||
|
"Shutdown/Reboot": "Вимкнути/перезавантажити",
|
||||||
|
"Shutdown/Reboot...": "Вимкнути/перезавантажити...",
|
||||||
|
"Power": "Живлення",
|
||||||
|
"Shutdown": "Вимкнути",
|
||||||
|
"Reboot": "Перезавантажити",
|
||||||
|
"Reset": "Скинути",
|
||||||
|
"Clipboard": "Буфер обміну",
|
||||||
|
"Edit clipboard content in the textarea below.": "Редагуйте вміст буфера обміну в текстовій зоні внизу.",
|
||||||
|
"Full screen": "Повний екран",
|
||||||
|
"Settings": "Параметри",
|
||||||
|
"Shared mode": "Спільний режим",
|
||||||
|
"View only": "Лише перегляд",
|
||||||
|
"Clip to window": "До розмірів вікна",
|
||||||
|
"Scaling mode:": "Режим масштабування:",
|
||||||
|
"None": "Вимкнено",
|
||||||
|
"Local scaling": "Локальне масштабування",
|
||||||
|
"Remote resizing": "Віддалене масштабування",
|
||||||
|
"Advanced": "Додатково",
|
||||||
|
"Quality:": "Якість:",
|
||||||
|
"Compression level:": "Рівень стиснення:",
|
||||||
|
"Repeater ID:": "Ідентифікатор репітера:",
|
||||||
|
"WebSocket": "WebSocket",
|
||||||
|
"Encrypt": "Шифрування",
|
||||||
|
"Host:": "Сервер:",
|
||||||
|
"Port:": "Порт:",
|
||||||
|
"Path:": "Шлях:",
|
||||||
|
"Automatic reconnect": "Автоматичне перез'єднання",
|
||||||
|
"Reconnect delay (ms):": "Затримка перез'єднання (мс):",
|
||||||
|
"Show dot when no cursor": "Показувати крапку, коли нема курсора",
|
||||||
|
"Logging:": "Журнал:",
|
||||||
|
"Version:": "Версія:",
|
||||||
|
"Disconnect": "Від'єднати",
|
||||||
|
"Connect": "З'єднати",
|
||||||
|
"Server identity": "Ідентифікація сервера",
|
||||||
|
"The server has provided the following identifying information:": "Сервер надає такі ідентифікаційні дані:",
|
||||||
|
"Fingerprint:": "Відбиток:",
|
||||||
|
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Перевірте, чи дані коректні, й натисніть «Схвалити». Інакше натисніть «Відхилити».",
|
||||||
|
"Approve": "Схвалити",
|
||||||
|
"Reject": "Відхилити",
|
||||||
|
"Credentials": "Особові дані",
|
||||||
|
"Username:": "Користувацьке ім'я:",
|
||||||
|
"Password:": "Пароль:",
|
||||||
|
"Send credentials": "Надіслати особові дані",
|
||||||
|
"Cancel": "Скасувати"
|
||||||
|
}
|
||||||
|
|
@ -117,7 +117,8 @@ html {
|
||||||
.noVNC_center > * {
|
.noVNC_center > * {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
.noVNC_vcenter {
|
|
||||||
|
.noVNC_crosscenter {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
@ -129,9 +130,29 @@ html {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.noVNC_vcenter > * {
|
.noVNC_crosscenter > * {
|
||||||
pointer-events: auto;
|
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
|
* Layering
|
||||||
|
|
@ -231,10 +252,18 @@ html {
|
||||||
:root.noVNC_connected #noVNC_control_bar_anchor.noVNC_idle {
|
:root.noVNC_connected #noVNC_control_bar_anchor.noVNC_idle {
|
||||||
opacity: 0.8;
|
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 {
|
#noVNC_control_bar_anchor.noVNC_right {
|
||||||
left: auto;
|
left: auto;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
#noVNC_control_bar_anchor.noVNC_bottom {
|
||||||
|
top: auto;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
#noVNC_control_bar {
|
#noVNC_control_bar {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -249,10 +278,34 @@ html {
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-webkit-touch-callout: none; /* Disable iOS image long-press popup */
|
-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 {
|
#noVNC_control_bar.noVNC_open {
|
||||||
box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
|
box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
:is(.noVNC_top, .noVNC_bottom) #noVNC_control_bar.noVNC_open {
|
||||||
|
left: auto;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
#noVNC_control_bar::before {
|
#noVNC_control_bar::before {
|
||||||
/* This extra element is to get a proper shadow */
|
/* This extra element is to get a proper shadow */
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -263,19 +316,22 @@ html {
|
||||||
left: -30px;
|
left: -30px;
|
||||||
transition: box-shadow 0.5s ease-in-out;
|
transition: box-shadow 0.5s ease-in-out;
|
||||||
}
|
}
|
||||||
#noVNC_control_bar.noVNC_open::before {
|
|
||||||
box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
.noVNC_right #noVNC_control_bar {
|
|
||||||
left: 100%;
|
|
||||||
border-radius: 12px 0 0 12px;
|
|
||||||
}
|
|
||||||
.noVNC_right #noVNC_control_bar.noVNC_open {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
.noVNC_right #noVNC_control_bar::before {
|
.noVNC_right #noVNC_control_bar::before {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
.noVNC_top #noVNC_control_bar::before {
|
||||||
|
height: 30px;
|
||||||
|
width: 100%;
|
||||||
|
top: -30px;
|
||||||
|
bottom: auto;
|
||||||
|
}
|
||||||
|
.noVNC_bottom #noVNC_control_bar::before {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#noVNC_control_bar.noVNC_open::before {
|
||||||
|
box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
#noVNC_control_bar_handle {
|
#noVNC_control_bar_handle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -288,41 +344,96 @@ html {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background-color: var(--novnc-darkblue);
|
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);
|
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 {
|
:root:not(.noVNC_connected) #noVNC_control_bar_handle {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.noVNC_right #noVNC_control_bar_handle {
|
:is(.noVNC_top, .noVNC_bottom) #noVNC_control_bar_handle {
|
||||||
background-position: left;
|
transform: translateX(35px);
|
||||||
|
top: -15px;
|
||||||
|
left: 0;
|
||||||
|
width: 50px;
|
||||||
|
height: calc(100% + 30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#noVNC_control_bar_handle::before {
|
||||||
|
content: "";
|
||||||
|
background: url("../images/handle_bg.svg");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 15px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
.noVNC_right #noVNC_control_bar_handle::before {
|
||||||
|
left: 0;
|
||||||
|
right: auto;
|
||||||
|
}
|
||||||
|
.noVNC_top #noVNC_control_bar_handle::before {
|
||||||
|
left: 0;
|
||||||
|
right: auto;
|
||||||
|
transform-origin: bottom left;
|
||||||
|
transform: rotate(90deg) translateX(20px);
|
||||||
|
}
|
||||||
|
.noVNC_bottom #noVNC_control_bar_handle::before {
|
||||||
|
left: 0;
|
||||||
|
right: auto;
|
||||||
|
transform-origin: bottom left;
|
||||||
|
transform: rotate(90deg) translateX(-50px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#noVNC_control_bar_handle:after {
|
||||||
|
content: "";
|
||||||
|
transition: transform 0.5s ease-in-out;
|
||||||
|
background: url("../images/handle.svg") no-repeat center;
|
||||||
|
background-size: 5px 6px;
|
||||||
|
position: absolute;
|
||||||
|
top: 20px; /* (50px-10px)/2 */
|
||||||
|
right: 3px;
|
||||||
|
transform: none;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
transform-origin: center;
|
||||||
}
|
}
|
||||||
.noVNC_right #noVNC_control_bar_handle:after {
|
.noVNC_right #noVNC_control_bar_handle:after {
|
||||||
left: 5px;
|
left: 3px;
|
||||||
right: 0;
|
right: auto;
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
.noVNC_top #noVNC_control_bar_handle:after {
|
||||||
|
left: 20px;
|
||||||
|
right: auto;
|
||||||
|
top: auto;
|
||||||
|
bottom: 3px;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
.noVNC_bottom #noVNC_control_bar_handle:after {
|
||||||
|
left: 20px;
|
||||||
|
right: auto;
|
||||||
|
top: 3px;
|
||||||
|
bottom: auto;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
|
||||||
transform: translateX(1px) rotate(180deg);
|
transform: translateX(1px) rotate(180deg);
|
||||||
}
|
}
|
||||||
.noVNC_right #noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
|
.noVNC_right #noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
|
||||||
transform: none;
|
transform: translateX(-1px);
|
||||||
}
|
}
|
||||||
|
.noVNC_top #noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
|
||||||
|
transform: translateY(1px) rotate(-90deg);
|
||||||
|
}
|
||||||
|
.noVNC_bottom #noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
|
||||||
|
transform: translateY(-1px) rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
/* Larger touch area for the handle, used when a touch screen is available */
|
/* Larger touch area for the handle, used when a touch screen is available */
|
||||||
#noVNC_control_bar_handle div {
|
#noVNC_control_bar_handle div {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
left: auto;
|
||||||
right: -35px;
|
right: -35px;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
|
|
@ -338,35 +449,66 @@ html {
|
||||||
left: -35px;
|
left: -35px;
|
||||||
right: auto;
|
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 {
|
#noVNC_control_bar > .noVNC_scroll {
|
||||||
max-height: 100vh; /* Chrome is buggy with 100% */
|
max-height: 100vh; /* Chrome is buggy with 100% */
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0 10px;
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px 0;
|
||||||
}
|
}
|
||||||
|
:is(.noVNC_top, .noVNC_bottom) > #noVNC_control_bar > .noVNC_scroll {
|
||||||
#noVNC_control_bar > .noVNC_scroll > * {
|
max-width: 100vw; /* Chrome is buggy with 100% */
|
||||||
display: block;
|
overflow-x: auto;
|
||||||
margin: 10px auto;
|
overflow-y: hidden;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Control bar hint */
|
/* Control bar hint */
|
||||||
#noVNC_hint_anchor {
|
.noVNC_hint_anchor {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: -50px;
|
|
||||||
left: auto;
|
|
||||||
}
|
|
||||||
#noVNC_control_bar_anchor.noVNC_right + #noVNC_hint_anchor {
|
|
||||||
left: -50px;
|
left: -50px;
|
||||||
right: auto;
|
|
||||||
}
|
}
|
||||||
#noVNC_control_bar_hint {
|
.noVNC_hint_anchor.noVNC_right {
|
||||||
position: relative;
|
left: auto;
|
||||||
transform: scale(0);
|
right: -50px;
|
||||||
|
}
|
||||||
|
.noVNC_hint_anchor.noVNC_top {
|
||||||
|
left: auto;
|
||||||
|
top: -50px;
|
||||||
|
}
|
||||||
|
.noVNC_hint_anchor.noVNC_bottom {
|
||||||
|
left: auto;
|
||||||
|
top: auto;
|
||||||
|
bottom: -50px;
|
||||||
|
}
|
||||||
|
.noVNC_control_bar_hint {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
height: 50%;
|
height: 50%;
|
||||||
max-height: 600px;
|
max-height: 600px;
|
||||||
|
position: relative;
|
||||||
|
transform: scale(0);
|
||||||
|
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
@ -376,13 +518,19 @@ html {
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
transition-delay: 0s;
|
transition-delay: 0s;
|
||||||
}
|
}
|
||||||
#noVNC_control_bar_hint.noVNC_active {
|
:is(.noVNC_top, .noVNC_bottom) .noVNC_control_bar_hint {
|
||||||
|
width: 50%;
|
||||||
|
height: 100px;
|
||||||
|
max-width: 600px;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
.noVNC_control_bar_hint.noVNC_active {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition-delay: 0.2s;
|
transition-delay: 0.2s;
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
#noVNC_control_bar_hint.noVNC_notransition {
|
.noVNC_control_bar_hint.noVNC_notransition {
|
||||||
transition: none !important;
|
transition: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -390,7 +538,6 @@ html {
|
||||||
#noVNC_control_bar .noVNC_button {
|
#noVNC_control_bar .noVNC_button {
|
||||||
min-width: unset;
|
min-width: unset;
|
||||||
padding: 4px 4px;
|
padding: 4px 4px;
|
||||||
vertical-align: middle;
|
|
||||||
border:1px solid rgba(255, 255, 255, 0.2);
|
border:1px solid rgba(255, 255, 255, 0.2);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|
@ -411,7 +558,7 @@ html {
|
||||||
|
|
||||||
box-sizing: border-box; /* so max-width don't have to care about padding */
|
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-width: calc(100vw - 75px - 25px); /* minus left and right margins */
|
||||||
max-height: 100vh; /* Chrome is buggy with 100% */
|
max-height: calc(100vh - 75px - 25px); /* minus top and bottom margins */
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
|
|
@ -431,16 +578,24 @@ html {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateX(75px);
|
transform: translateX(75px);
|
||||||
}
|
}
|
||||||
.noVNC_right .noVNC_vcenter {
|
|
||||||
left: auto;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
.noVNC_right .noVNC_panel {
|
.noVNC_right .noVNC_panel {
|
||||||
transform: translateX(-25px);
|
transform: translateX(-25px);
|
||||||
}
|
}
|
||||||
.noVNC_right .noVNC_panel.noVNC_open {
|
.noVNC_right .noVNC_panel.noVNC_open {
|
||||||
transform: translateX(-75px);
|
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 > * {
|
.noVNC_panel > * {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
@ -536,13 +691,26 @@ html {
|
||||||
/* Control bar content */
|
/* Control bar content */
|
||||||
|
|
||||||
#noVNC_control_bar .noVNC_logo {
|
#noVNC_control_bar .noVNC_logo {
|
||||||
font-size: 13px;
|
display: block;
|
||||||
|
max-width: 35px;
|
||||||
|
max-height: 35px;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.noVNC_logo + hr {
|
.noVNC_logo + hr {
|
||||||
/* Remove all but top border */
|
/* Remove all but top border */
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
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 {
|
:root:not(.noVNC_connected) #noVNC_view_drag_button {
|
||||||
|
|
@ -550,16 +718,15 @@ html {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* noVNC Touch Device only buttons */
|
/* noVNC Touch Device only buttons */
|
||||||
:root:not(.noVNC_connected) #noVNC_mobile_buttons {
|
:root:not(.noVNC_connected) #noVNC_keyboard_button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@media not all and (any-pointer: coarse) {
|
@media not all and (any-pointer: coarse) {
|
||||||
/* FIXME: The button for the virtual keyboard is the only button in this
|
/* FIXME: It is bad to assume that no touch devices have physical
|
||||||
group of "mobile buttons". It is bad to assume that no touch
|
keyboards available. Hopefully we can get a media query
|
||||||
devices have physical keyboards available. Hopefully we can get
|
for this:
|
||||||
a media query for this:
|
|
||||||
https://github.com/w3c/csswg-drafts/issues/3871 */
|
https://github.com/w3c/csswg-drafts/issues/3871 */
|
||||||
:root.noVNC_connected #noVNC_mobile_buttons {
|
:root.noVNC_connected #noVNC_keyboard_button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -573,6 +740,18 @@ html {
|
||||||
background-color: var(--novnc-darkgrey);
|
background-color: var(--novnc-darkgrey);
|
||||||
border: none;
|
border: none;
|
||||||
padding: 10px;
|
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 */
|
/* Shutdown/Reboot */
|
||||||
|
|
|
||||||
256
app/ui.js
256
app/ui.js
|
|
@ -9,18 +9,19 @@
|
||||||
import * as Log from '../core/util/logging.js';
|
import * as Log from '../core/util/logging.js';
|
||||||
import _, { l10n } from './localization.js';
|
import _, { l10n } from './localization.js';
|
||||||
import { isTouchDevice, isMac, isIOS, isAndroid, isChromeOS, isSafari,
|
import { isTouchDevice, isMac, isIOS, isAndroid, isChromeOS, isSafari,
|
||||||
hasScrollbarGutter, dragThreshold }
|
hasScrollbarGutter, dragThreshold, browserAsyncClipboardSupport }
|
||||||
from '../core/util/browser.js';
|
from '../core/util/browser.js';
|
||||||
import { setCapture, getPointerEvent } from '../core/util/events.js';
|
import { setCapture, getPointerEvent } from '../core/util/events.js';
|
||||||
import KeyTable from "../core/input/keysym.js";
|
import KeyTable from "../core/input/keysym.js";
|
||||||
import keysyms from "../core/input/keysymdef.js";
|
import keysyms from "../core/input/keysymdef.js";
|
||||||
import Keyboard from "../core/input/keyboard.js";
|
import Keyboard from "../core/input/keyboard.js";
|
||||||
import RFB from "../core/rfb.js";
|
import RFB from "../core/rfb.js";
|
||||||
|
import WakeLockManager from './wakelock.js';
|
||||||
import * as WebUtil from "./webutil.js";
|
import * as WebUtil from "./webutil.js";
|
||||||
|
|
||||||
const PAGE_TITLE = "noVNC";
|
const PAGE_TITLE = "noVNC";
|
||||||
|
|
||||||
const LINGUAS = ["cs", "de", "el", "es", "fr", "hr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"];
|
const LINGUAS = ["cs", "de", "el", "es", "fr", "hr", "hu", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "uk", "zh_CN", "zh_TW"];
|
||||||
|
|
||||||
const UI = {
|
const UI = {
|
||||||
|
|
||||||
|
|
@ -36,6 +37,8 @@ const UI = {
|
||||||
|
|
||||||
controlbarGrabbed: false,
|
controlbarGrabbed: false,
|
||||||
controlbarDrag: false,
|
controlbarDrag: false,
|
||||||
|
controlbarMouseDownClientX: 0,
|
||||||
|
controlbarMouseDownOffsetX: 0,
|
||||||
controlbarMouseDownClientY: 0,
|
controlbarMouseDownClientY: 0,
|
||||||
controlbarMouseDownOffsetY: 0,
|
controlbarMouseDownOffsetY: 0,
|
||||||
|
|
||||||
|
|
@ -46,6 +49,8 @@ const UI = {
|
||||||
reconnectCallback: null,
|
reconnectCallback: null,
|
||||||
reconnectPassword: null,
|
reconnectPassword: null,
|
||||||
|
|
||||||
|
wakeLockManager: new WakeLockManager(),
|
||||||
|
|
||||||
async start(options={}) {
|
async start(options={}) {
|
||||||
UI.customSettings = options.settings || {};
|
UI.customSettings = options.settings || {};
|
||||||
if (UI.customSettings.defaults === undefined) {
|
if (UI.customSettings.defaults === undefined) {
|
||||||
|
|
@ -107,8 +112,11 @@ const UI = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore control bar position
|
// Restore control bar position
|
||||||
if (WebUtil.readSetting('controlbar_pos') === 'right') {
|
const pos = WebUtil.readSetting('controlbar_pos');
|
||||||
UI.toggleControlbarSide();
|
if (['left', 'right', 'top', 'bottom'].includes(pos)) {
|
||||||
|
UI.toggleControlbarSide(pos);
|
||||||
|
} else {
|
||||||
|
UI.toggleControlbarSide('left');
|
||||||
}
|
}
|
||||||
|
|
||||||
UI.initFullscreen();
|
UI.initFullscreen();
|
||||||
|
|
@ -135,10 +143,8 @@ const UI = {
|
||||||
|
|
||||||
let autoconnect = UI.getSetting('autoconnect');
|
let autoconnect = UI.getSetting('autoconnect');
|
||||||
if (autoconnect === 'true' || autoconnect == '1') {
|
if (autoconnect === 'true' || autoconnect == '1') {
|
||||||
autoconnect = true;
|
|
||||||
UI.connect();
|
UI.connect();
|
||||||
} else {
|
} else {
|
||||||
autoconnect = false;
|
|
||||||
// Show the connect panel on first load unless autoconnecting
|
// Show the connect panel on first load unless autoconnecting
|
||||||
UI.openConnectPanel();
|
UI.openConnectPanel();
|
||||||
}
|
}
|
||||||
|
|
@ -189,6 +195,7 @@ const UI = {
|
||||||
UI.initSetting('repeaterID', '');
|
UI.initSetting('repeaterID', '');
|
||||||
UI.initSetting('reconnect', false);
|
UI.initSetting('reconnect', false);
|
||||||
UI.initSetting('reconnect_delay', 5000);
|
UI.initSetting('reconnect_delay', 5000);
|
||||||
|
UI.initSetting('keep_device_awake', false);
|
||||||
},
|
},
|
||||||
// Adds a link to the label elements on the corresponding input elements
|
// Adds a link to the label elements on the corresponding input elements
|
||||||
setupSettingLabels() {
|
setupSettingLabels() {
|
||||||
|
|
@ -371,6 +378,8 @@ const UI = {
|
||||||
UI.addSettingChangeHandler('view_only', UI.updateViewOnly);
|
UI.addSettingChangeHandler('view_only', UI.updateViewOnly);
|
||||||
UI.addSettingChangeHandler('show_dot');
|
UI.addSettingChangeHandler('show_dot');
|
||||||
UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor);
|
UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor);
|
||||||
|
UI.addSettingChangeHandler('keep_device_awake');
|
||||||
|
UI.addSettingChangeHandler('keep_device_awake', UI.updateRequestWakelock);
|
||||||
UI.addSettingChangeHandler('host');
|
UI.addSettingChangeHandler('host');
|
||||||
UI.addSettingChangeHandler('port');
|
UI.addSettingChangeHandler('port');
|
||||||
UI.addSettingChangeHandler('path');
|
UI.addSettingChangeHandler('path');
|
||||||
|
|
@ -571,7 +580,15 @@ const UI = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleControlbarSide() {
|
getControlbarPos() {
|
||||||
|
const anchor = document.getElementById('noVNC_control_bar_anchor');
|
||||||
|
if (anchor.classList.contains('noVNC_right')) return 'right';
|
||||||
|
if (anchor.classList.contains('noVNC_top')) return 'top';
|
||||||
|
if (anchor.classList.contains('noVNC_bottom')) return 'bottom';
|
||||||
|
return 'left';
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleControlbarSide(pos) {
|
||||||
// Temporarily disable animation, if bar is displayed, to avoid weird
|
// Temporarily disable animation, if bar is displayed, to avoid weird
|
||||||
// movement. The transitionend-event will not fire when display=none.
|
// movement. The transitionend-event will not fire when display=none.
|
||||||
const bar = document.getElementById('noVNC_control_bar');
|
const bar = document.getElementById('noVNC_control_bar');
|
||||||
|
|
@ -582,13 +599,12 @@ const UI = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const anchor = document.getElementById('noVNC_control_bar_anchor');
|
const anchor = document.getElementById('noVNC_control_bar_anchor');
|
||||||
if (anchor.classList.contains("noVNC_right")) {
|
|
||||||
WebUtil.writeSetting('controlbar_pos', 'left');
|
anchor.classList.remove('noVNC_right', 'noVNC_top', 'noVNC_bottom');
|
||||||
anchor.classList.remove("noVNC_right");
|
if (['right', 'top', 'bottom'].includes(pos)) {
|
||||||
} else {
|
anchor.classList.add(`noVNC_${pos}`);
|
||||||
WebUtil.writeSetting('controlbar_pos', 'right');
|
|
||||||
anchor.classList.add("noVNC_right");
|
|
||||||
}
|
}
|
||||||
|
WebUtil.writeSetting('controlbar_pos', pos);
|
||||||
|
|
||||||
// Consider this a movement of the handle
|
// Consider this a movement of the handle
|
||||||
UI.controlbarDrag = true;
|
UI.controlbarDrag = true;
|
||||||
|
|
@ -598,19 +614,21 @@ const UI = {
|
||||||
},
|
},
|
||||||
|
|
||||||
showControlbarHint(show, animate=true) {
|
showControlbarHint(show, animate=true) {
|
||||||
const hint = document.getElementById('noVNC_control_bar_hint');
|
const getPos = element =>
|
||||||
|
['right', 'top', 'bottom'].find(pos =>
|
||||||
|
element.classList.contains(`noVNC_${pos}`)
|
||||||
|
) ?? 'left';
|
||||||
|
|
||||||
if (animate) {
|
const anchor = document.getElementById('noVNC_control_bar_anchor');
|
||||||
hint.classList.remove("noVNC_notransition");
|
const anchorPos = getPos(anchor);
|
||||||
} else {
|
|
||||||
hint.classList.add("noVNC_notransition");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (show) {
|
document.querySelectorAll('.noVNC_control_bar_hint').forEach((hint) => {
|
||||||
hint.classList.add("noVNC_active");
|
const hintPos = getPos(hint.parentElement);
|
||||||
} else {
|
const shouldShow = show && (hintPos !== anchorPos);
|
||||||
hint.classList.remove("noVNC_active");
|
|
||||||
}
|
hint.classList.toggle('noVNC_active', shouldShow);
|
||||||
|
hint.classList.toggle('noVNC_notransition', !animate || !shouldShow);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
dragControlbarHandle(e) {
|
dragControlbarHandle(e) {
|
||||||
|
|
@ -618,28 +636,62 @@ const UI = {
|
||||||
|
|
||||||
const ptr = getPointerEvent(e);
|
const ptr = getPointerEvent(e);
|
||||||
|
|
||||||
const anchor = document.getElementById('noVNC_control_bar_anchor');
|
let controlBarPos = UI.getControlbarPos();
|
||||||
if (ptr.clientX < (window.innerWidth * 0.1)) {
|
|
||||||
if (anchor.classList.contains("noVNC_right")) {
|
if (ptr.clientX < (window.innerWidth * 0.1) &&
|
||||||
UI.toggleControlbarSide();
|
ptr.clientY > (window.innerHeight * 0.25) &&
|
||||||
|
ptr.clientY < (window.innerHeight * 0.75)) {
|
||||||
|
if (controlBarPos !== 'left') {
|
||||||
|
UI.toggleControlbarSide('left');
|
||||||
|
controlBarPos = 'left';
|
||||||
}
|
}
|
||||||
} else if (ptr.clientX > (window.innerWidth * 0.9)) {
|
|
||||||
if (!anchor.classList.contains("noVNC_right")) {
|
} else if (ptr.clientX > (window.innerWidth * 0.9) &&
|
||||||
UI.toggleControlbarSide();
|
ptr.clientY > (window.innerHeight * 0.25) &&
|
||||||
|
ptr.clientY < (window.innerHeight * 0.75)) {
|
||||||
|
if (controlBarPos !== 'right') {
|
||||||
|
UI.toggleControlbarSide('right');
|
||||||
|
controlBarPos = 'right';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slightly increased height thresholds since 10% of the
|
||||||
|
// height proved small in practice
|
||||||
|
} else if (ptr.clientX > (window.innerWidth * 0.25) &&
|
||||||
|
ptr.clientX < (window.innerWidth * 0.75) &&
|
||||||
|
ptr.clientY < (window.innerHeight * 0.2)) {
|
||||||
|
if (controlBarPos !== 'top') {
|
||||||
|
UI.toggleControlbarSide('top');
|
||||||
|
controlBarPos = 'top';
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (ptr.clientX > (window.innerWidth * 0.25) &&
|
||||||
|
ptr.clientX < (window.innerWidth * 0.75) &&
|
||||||
|
ptr.clientY > (window.innerHeight * 0.8)) {
|
||||||
|
if (controlBarPos !== 'bottom') {
|
||||||
|
UI.toggleControlbarSide("bottom");
|
||||||
|
controlBarPos = 'bottom';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isVertical = controlBarPos === 'left' || controlBarPos === 'right';
|
||||||
|
|
||||||
if (!UI.controlbarDrag) {
|
if (!UI.controlbarDrag) {
|
||||||
const dragDistance = Math.abs(ptr.clientY - UI.controlbarMouseDownClientY);
|
const dragDistance = isVertical
|
||||||
|
? Math.abs(ptr.clientY - UI.controlbarMouseDownClientY)
|
||||||
|
: Math.abs(ptr.clientX - UI.controlbarMouseDownClientX);
|
||||||
|
|
||||||
if (dragDistance < dragThreshold) return;
|
if (dragDistance < dragThreshold) return;
|
||||||
|
|
||||||
UI.controlbarDrag = true;
|
UI.controlbarDrag = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isVertical) {
|
||||||
const eventY = ptr.clientY - UI.controlbarMouseDownOffsetY;
|
const eventY = ptr.clientY - UI.controlbarMouseDownOffsetY;
|
||||||
|
UI.moveControlbarHandle(eventY, true);
|
||||||
UI.moveControlbarHandle(eventY);
|
} else {
|
||||||
|
const eventX = ptr.clientX - UI.controlbarMouseDownOffsetX;
|
||||||
|
UI.moveControlbarHandle(eventX, false);
|
||||||
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -648,41 +700,56 @@ const UI = {
|
||||||
},
|
},
|
||||||
|
|
||||||
// Move the handle but don't allow any position outside the bounds
|
// Move the handle but don't allow any position outside the bounds
|
||||||
moveControlbarHandle(viewportRelativeY) {
|
moveControlbarHandle(viewportRelativeCoord, isVertical) {
|
||||||
const handle = document.getElementById("noVNC_control_bar_handle");
|
const handle = document.getElementById("noVNC_control_bar_handle");
|
||||||
const handleHeight = handle.getBoundingClientRect().height;
|
|
||||||
|
const handleSpan = isVertical
|
||||||
|
? handle.getBoundingClientRect().height
|
||||||
|
: handle.getBoundingClientRect().width;
|
||||||
|
|
||||||
const controlbarBounds = document.getElementById("noVNC_control_bar")
|
const controlbarBounds = document.getElementById("noVNC_control_bar")
|
||||||
.getBoundingClientRect();
|
.getBoundingClientRect();
|
||||||
|
const controlbarBoundsStart = isVertical
|
||||||
|
? controlbarBounds.top
|
||||||
|
: controlbarBounds.left;
|
||||||
|
const controlbarBoundsSpan = isVertical
|
||||||
|
? controlbarBounds.height
|
||||||
|
: controlbarBounds.width;
|
||||||
|
|
||||||
const margin = 10;
|
const margin = 10;
|
||||||
|
|
||||||
// These heights need to be non-zero for the below logic to work
|
// These heights need to be non-zero for the below logic to work
|
||||||
if (handleHeight === 0 || controlbarBounds.height === 0) {
|
if (handleSpan === 0 || controlbarBoundsSpan === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let newY = viewportRelativeY;
|
let newCoord = viewportRelativeCoord;
|
||||||
|
|
||||||
// Check if the coordinates are outside the control bar
|
// Check if the coordinates are outside the control bar
|
||||||
if (newY < controlbarBounds.top + margin) {
|
if (newCoord < controlbarBoundsStart + margin) {
|
||||||
// Force coordinates to be below the top of the control bar
|
// Force coordinates to be below the start of the control bar
|
||||||
newY = controlbarBounds.top + margin;
|
newCoord = controlbarBoundsStart + margin;
|
||||||
|
|
||||||
} else if (newY > controlbarBounds.top +
|
} else if (newCoord > controlbarBoundsStart +
|
||||||
controlbarBounds.height - handleHeight - margin) {
|
controlbarBoundsSpan - handleSpan - margin) {
|
||||||
// Force coordinates to be above the bottom of the control bar
|
// Force coordinates to be before the end of the control bar
|
||||||
newY = controlbarBounds.top +
|
newCoord = controlbarBoundsStart +
|
||||||
controlbarBounds.height - handleHeight - margin;
|
controlbarBoundsSpan - handleSpan - margin;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Corner case: control bar too small for stable position
|
// Corner case: control bar too small for stable position
|
||||||
if (controlbarBounds.height < (handleHeight + margin * 2)) {
|
if (controlbarBoundsSpan < (handleSpan + margin * 2)) {
|
||||||
newY = controlbarBounds.top +
|
newCoord = controlbarBoundsStart +
|
||||||
(controlbarBounds.height - handleHeight) / 2;
|
(controlbarBoundsSpan - handleSpan) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The transform needs coordinates that are relative to the parent
|
// The transform needs coordinates that are relative to the parent
|
||||||
const parentRelativeY = newY - controlbarBounds.top;
|
const parentRelativeCoord = newCoord - controlbarBoundsStart;
|
||||||
handle.style.transform = "translateY(" + parentRelativeY + "px)";
|
if (isVertical) {
|
||||||
|
handle.style.transform = "translateY(" + parentRelativeCoord + "px)";
|
||||||
|
} else {
|
||||||
|
handle.style.transform = "translateX(" + parentRelativeCoord + "px)";
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateControlbarHandle() {
|
updateControlbarHandle() {
|
||||||
|
|
@ -690,7 +757,15 @@ const UI = {
|
||||||
// the move function expects coordinates relative the the viewport.
|
// the move function expects coordinates relative the the viewport.
|
||||||
const handle = document.getElementById("noVNC_control_bar_handle");
|
const handle = document.getElementById("noVNC_control_bar_handle");
|
||||||
const handleBounds = handle.getBoundingClientRect();
|
const handleBounds = handle.getBoundingClientRect();
|
||||||
UI.moveControlbarHandle(handleBounds.top);
|
|
||||||
|
const controlBarPos = UI.getControlbarPos();
|
||||||
|
const isVertical = controlBarPos === 'left' || controlBarPos === 'right';
|
||||||
|
|
||||||
|
if (isVertical) {
|
||||||
|
UI.moveControlbarHandle(handleBounds.top, true);
|
||||||
|
} else {
|
||||||
|
UI.moveControlbarHandle(handleBounds.left, false);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
controlbarHandleMouseUp(e) {
|
controlbarHandleMouseUp(e) {
|
||||||
|
|
@ -728,6 +803,8 @@ const UI = {
|
||||||
|
|
||||||
UI.controlbarMouseDownClientY = ptr.clientY;
|
UI.controlbarMouseDownClientY = ptr.clientY;
|
||||||
UI.controlbarMouseDownOffsetY = ptr.clientY - bounds.top;
|
UI.controlbarMouseDownOffsetY = ptr.clientY - bounds.top;
|
||||||
|
UI.controlbarMouseDownClientX = ptr.clientX;
|
||||||
|
UI.controlbarMouseDownOffsetX = ptr.clientX - bounds.left;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
UI.keepControlbar();
|
UI.keepControlbar();
|
||||||
|
|
@ -1072,6 +1149,10 @@ const UI = {
|
||||||
url.protocol = (window.location.protocol === "https:") ? 'wss:' : 'ws:';
|
url.protocol = (window.location.protocol === "https:") ? 'wss:' : 'ws:';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (UI.getSetting('keep_device_awake')) {
|
||||||
|
UI.wakeLockManager.acquire();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
UI.rfb = new RFB(document.getElementById('noVNC_container'),
|
UI.rfb = new RFB(document.getElementById('noVNC_container'),
|
||||||
url.href,
|
url.href,
|
||||||
|
|
@ -1103,6 +1184,7 @@ const UI = {
|
||||||
UI.rfb.showDotCursor = UI.getSetting('show_dot');
|
UI.rfb.showDotCursor = UI.getSetting('show_dot');
|
||||||
|
|
||||||
UI.updateViewOnly(); // requires UI.rfb
|
UI.updateViewOnly(); // requires UI.rfb
|
||||||
|
UI.updateClipboard();
|
||||||
},
|
},
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
|
|
@ -1154,6 +1236,8 @@ const UI = {
|
||||||
UI.showStatus(msg);
|
UI.showStatus(msg);
|
||||||
UI.updateVisualState('connected');
|
UI.updateVisualState('connected');
|
||||||
|
|
||||||
|
UI.updateBeforeUnload();
|
||||||
|
|
||||||
// Do this last because it can only be used on rendered elements
|
// Do this last because it can only be used on rendered elements
|
||||||
UI.rfb.focus();
|
UI.rfb.focus();
|
||||||
},
|
},
|
||||||
|
|
@ -1168,6 +1252,7 @@ const UI = {
|
||||||
UI.connected = false;
|
UI.connected = false;
|
||||||
|
|
||||||
UI.rfb = undefined;
|
UI.rfb = undefined;
|
||||||
|
UI.wakeLockManager.release();
|
||||||
|
|
||||||
if (!e.detail.clean) {
|
if (!e.detail.clean) {
|
||||||
UI.updateVisualState('disconnected');
|
UI.updateVisualState('disconnected');
|
||||||
|
|
@ -1190,6 +1275,8 @@ const UI = {
|
||||||
UI.showStatus(_("Disconnected"), 'normal');
|
UI.showStatus(_("Disconnected"), 'normal');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UI.updateBeforeUnload();
|
||||||
|
|
||||||
document.title = PAGE_TITLE;
|
document.title = PAGE_TITLE;
|
||||||
|
|
||||||
UI.openControlbar();
|
UI.openControlbar();
|
||||||
|
|
@ -1197,7 +1284,7 @@ const UI = {
|
||||||
},
|
},
|
||||||
|
|
||||||
securityFailed(e) {
|
securityFailed(e) {
|
||||||
let msg = "";
|
let msg;
|
||||||
// On security failures we might get a string with a reason
|
// On security failures we might get a string with a reason
|
||||||
// directly from the server. Note that we can't control if
|
// directly from the server. Note that we can't control if
|
||||||
// this string is translated or not.
|
// this string is translated or not.
|
||||||
|
|
@ -1210,6 +1297,24 @@ const UI = {
|
||||||
UI.showStatus(msg, 'error');
|
UI.showStatus(msg, 'error');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleBeforeUnload(e) {
|
||||||
|
// Trigger a "Leave site?" warning prompt before closing the
|
||||||
|
// page. Modern browsers (Oct 2025) accept either (or both)
|
||||||
|
// preventDefault() or a nonempty returnValue, though the latter is
|
||||||
|
// considered legacy. The custom string is ignored by modern browsers,
|
||||||
|
// which display a native message, but older browsers will show it.
|
||||||
|
e.preventDefault();
|
||||||
|
e.returnValue = _("Are you sure you want to disconnect the session?");
|
||||||
|
},
|
||||||
|
|
||||||
|
updateBeforeUnload() {
|
||||||
|
// Remove first to avoid adding duplicates
|
||||||
|
window.removeEventListener("beforeunload", UI.handleBeforeUnload);
|
||||||
|
if (!UI.rfb?.viewOnly && UI.connected) {
|
||||||
|
window.addEventListener("beforeunload", UI.handleBeforeUnload);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/* ------^-------
|
/* ------^-------
|
||||||
* /CONNECTION
|
* /CONNECTION
|
||||||
* ==============
|
* ==============
|
||||||
|
|
@ -1256,13 +1361,13 @@ const UI = {
|
||||||
let inputFocus = "none";
|
let inputFocus = "none";
|
||||||
if (e.detail.types.indexOf("username") === -1) {
|
if (e.detail.types.indexOf("username") === -1) {
|
||||||
document.getElementById("noVNC_username_block").classList.add("noVNC_hidden");
|
document.getElementById("noVNC_username_block").classList.add("noVNC_hidden");
|
||||||
} else {
|
} else if (document.getElementById("noVNC_username_input").value === "") {
|
||||||
inputFocus = inputFocus === "none" ? "noVNC_username_input" : inputFocus;
|
inputFocus = "noVNC_username_input";
|
||||||
}
|
}
|
||||||
if (e.detail.types.indexOf("password") === -1) {
|
if (e.detail.types.indexOf("password") === -1) {
|
||||||
document.getElementById("noVNC_password_block").classList.add("noVNC_hidden");
|
document.getElementById("noVNC_password_block").classList.add("noVNC_hidden");
|
||||||
} else {
|
} else if (inputFocus === "none") {
|
||||||
inputFocus = inputFocus === "none" ? "noVNC_password_input" : inputFocus;
|
inputFocus = "noVNC_password_input";
|
||||||
}
|
}
|
||||||
document.getElementById('noVNC_credentials_dlg')
|
document.getElementById('noVNC_credentials_dlg')
|
||||||
.classList.add('noVNC_open');
|
.classList.add('noVNC_open');
|
||||||
|
|
@ -1736,6 +1841,8 @@ const UI = {
|
||||||
if (!UI.rfb) return;
|
if (!UI.rfb) return;
|
||||||
UI.rfb.viewOnly = UI.getSetting('view_only');
|
UI.rfb.viewOnly = UI.getSetting('view_only');
|
||||||
|
|
||||||
|
UI.updateBeforeUnload();
|
||||||
|
|
||||||
// Hide input related buttons in view only mode
|
// Hide input related buttons in view only mode
|
||||||
if (UI.rfb.viewOnly) {
|
if (UI.rfb.viewOnly) {
|
||||||
document.getElementById('noVNC_keyboard_button')
|
document.getElementById('noVNC_keyboard_button')
|
||||||
|
|
@ -1754,6 +1861,31 @@ const UI = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateClipboard() {
|
||||||
|
browserAsyncClipboardSupport()
|
||||||
|
.then((support) => {
|
||||||
|
if (support === 'unsupported') {
|
||||||
|
// Use fallback clipboard panel
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (support === 'denied' || support === 'available') {
|
||||||
|
UI.closeClipboardPanel();
|
||||||
|
document.getElementById('noVNC_clipboard_button')
|
||||||
|
.classList.add('noVNC_hidden');
|
||||||
|
document.getElementById('noVNC_clipboard_button')
|
||||||
|
.removeEventListener('click', UI.toggleClipboardPanel);
|
||||||
|
document.getElementById('noVNC_clipboard_text')
|
||||||
|
.removeEventListener('change', UI.clipboardSend);
|
||||||
|
if (UI.rfb) {
|
||||||
|
UI.rfb.removeEventListener('clipboard', UI.clipboardReceive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Treat as unsupported
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
updateShowDotCursor() {
|
updateShowDotCursor() {
|
||||||
if (!UI.rfb) return;
|
if (!UI.rfb) return;
|
||||||
UI.rfb.showDotCursor = UI.getSetting('show_dot');
|
UI.rfb.showDotCursor = UI.getSetting('show_dot');
|
||||||
|
|
@ -1769,6 +1901,16 @@ const UI = {
|
||||||
document.title = e.detail.name + " - " + PAGE_TITLE;
|
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) {
|
bell(e) {
|
||||||
if (UI.getSetting('bell') === 'on') {
|
if (UI.getSetting('bell') === 'on') {
|
||||||
const promise = document.getElementById('noVNC_bell').play();
|
const promise = document.getElementById('noVNC_bell').play();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2025 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* Wrapper around the `navigator.wakeLock` api that handles reacquiring the
|
||||||
|
* lock on visiblility changes.
|
||||||
|
*
|
||||||
|
* The `acquire` and `release` methods may be called any number of times. The
|
||||||
|
* most recent call dictates the desired end-state (if `acquire` was most
|
||||||
|
* recently called, then we will try to acquire and hold the wake lock).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Log from '../core/util/logging.js';
|
||||||
|
|
||||||
|
const _STATES = {
|
||||||
|
/* No wake lock.
|
||||||
|
*
|
||||||
|
* Can transition to:
|
||||||
|
* - AWAITING_VISIBLE: `acquire` called when document is hidden.
|
||||||
|
* - ACQUIRING: `acquire` called.
|
||||||
|
* - ERROR: `acquired` called when the api is not available.
|
||||||
|
*/
|
||||||
|
RELEASED: 'released',
|
||||||
|
/* Wake lock requested, waiting for browser.
|
||||||
|
*
|
||||||
|
* Can transition to:
|
||||||
|
* - ACQUIRED: success
|
||||||
|
* - ACQUIRING_WANT_RELEASE: `release` called while waiting
|
||||||
|
* - ERROR
|
||||||
|
*/
|
||||||
|
ACQUIRING: 'acquiring',
|
||||||
|
/* Wake lock requested, release called, still waiting for browser.
|
||||||
|
*
|
||||||
|
* Can transition to:
|
||||||
|
* - ACQUIRING: `acquire` called (but promise has not resolved yet)
|
||||||
|
* - RELEASED: success
|
||||||
|
*/
|
||||||
|
ACQUIRING_WANT_RELEASE: 'releasing',
|
||||||
|
/* Wake lock held.
|
||||||
|
*
|
||||||
|
* Can transition to:
|
||||||
|
* - AWAITING_VISIBLE: wakelock lost due to visibility change
|
||||||
|
* - RELEASED: success
|
||||||
|
*/
|
||||||
|
ACQUIRED: 'acquired',
|
||||||
|
/* Caller wants wakelock, but we can not get it due to visibility.
|
||||||
|
*
|
||||||
|
* Can transition to:
|
||||||
|
* - ACQUIRING: document is now visible, attempting to get wakelock.
|
||||||
|
* - RELEASED: when release is called.
|
||||||
|
*/
|
||||||
|
AWAITING_VISIBLE: 'awaiting_visible',
|
||||||
|
/* An error has occurred.
|
||||||
|
*
|
||||||
|
* Can transition to:
|
||||||
|
* - RELEASED: will happen immediately.
|
||||||
|
*/
|
||||||
|
ERROR: 'error',
|
||||||
|
};
|
||||||
|
|
||||||
|
class TestOnlyWakeLockManagerStateChangeEvent extends Event {
|
||||||
|
constructor(oldState, newState) {
|
||||||
|
super("testOnlyStateChange");
|
||||||
|
this.oldState = oldState;
|
||||||
|
this.newState = newState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class WakeLockManager extends EventTarget {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this._state = _STATES.RELEASED;
|
||||||
|
this._wakelock = null;
|
||||||
|
|
||||||
|
this._eventHandlers = {
|
||||||
|
wakelockAcquired: this._wakelockAcquired.bind(this),
|
||||||
|
wakelockReleased: this._wakelockReleased.bind(this),
|
||||||
|
documentVisibilityChange: this._documentVisibilityChange.bind(this),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
acquire() {
|
||||||
|
switch (this._state) {
|
||||||
|
case _STATES.ACQUIRING_WANT_RELEASE:
|
||||||
|
// We are currently waiting to acquire the wakelock. While
|
||||||
|
// waiting, `release()` was called. By transitioning back to
|
||||||
|
// ACQUIRING, we will keep the lock after we receive it.
|
||||||
|
this._transitionTo(_STATES.ACQUIRING);
|
||||||
|
break;
|
||||||
|
case _STATES.AWAITING_VISIBLE:
|
||||||
|
case _STATES.ACQUIRING:
|
||||||
|
case _STATES.ACQUIRED:
|
||||||
|
break;
|
||||||
|
case _STATES.ERROR:
|
||||||
|
case _STATES.RELEASED:
|
||||||
|
if (document.hidden) {
|
||||||
|
// We can not acquire the wakelock while the document is
|
||||||
|
// hidden (eg, not the active tab). Wait until it is
|
||||||
|
// visible, then acquire the wakelock.
|
||||||
|
this._awaitVisible();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this._acquireWakelockNow();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
release() {
|
||||||
|
switch (this._state) {
|
||||||
|
case _STATES.ERROR:
|
||||||
|
case _STATES.RELEASED:
|
||||||
|
case _STATES.ACQUIRING_WANT_RELEASE:
|
||||||
|
break;
|
||||||
|
case _STATES.ACQUIRING:
|
||||||
|
// We are have requested (but not yet received) the wakelock.
|
||||||
|
// Give it up as soon as we acquire it.
|
||||||
|
this._transitionTo(_STATES.ACQUIRING_WANT_RELEASE);
|
||||||
|
break;
|
||||||
|
case _STATES.ACQUIRED:
|
||||||
|
// We remove the event listener first, as we don't want to be
|
||||||
|
// notified about this release (it is expected).
|
||||||
|
this._wakelock.removeEventListener("release", this._eventHandlers.wakelockReleased);
|
||||||
|
this._wakelock.release();
|
||||||
|
this._wakelock = null;
|
||||||
|
this._transitionTo(_STATES.RELEASED);
|
||||||
|
break;
|
||||||
|
case _STATES.AWAITING_VISIBLE:
|
||||||
|
// We don't currently have the lock, but are waiting for the
|
||||||
|
// document to become visible. By removing the event listener,
|
||||||
|
// we will not attempt to get the wakelock in the future.
|
||||||
|
document.removeEventListener("visibilitychange", this._eventHandlers.documentVisibilityChange);
|
||||||
|
this._transitionTo(_STATES.RELEASED);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_transitionTo(newState) {
|
||||||
|
let oldState = this._state;
|
||||||
|
Log.Debug(`WakelockManager transitioning ${oldState} -> ${newState}`);
|
||||||
|
this._state = newState;
|
||||||
|
this.dispatchEvent(new TestOnlyWakeLockManagerStateChangeEvent(oldState, newState));
|
||||||
|
}
|
||||||
|
|
||||||
|
_awaitVisible() {
|
||||||
|
document.addEventListener("visibilitychange", this._eventHandlers.documentVisibilityChange);
|
||||||
|
this._transitionTo(_STATES.AWAITING_VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
_acquireWakelockNow() {
|
||||||
|
if (!("wakeLock" in navigator)) {
|
||||||
|
Log.Warn("Unable to request wakeLock, Browser does not have wakeLock api");
|
||||||
|
this._transitionTo(_STATES.ERROR);
|
||||||
|
this._transitionTo(_STATES.RELEASED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigator.wakeLock.request("screen")
|
||||||
|
.then(this._eventHandlers.wakelockAcquired)
|
||||||
|
.catch((err) => {
|
||||||
|
Log.Warn("Error occurred while acquiring wakelock: " + err);
|
||||||
|
this._transitionTo(_STATES.ERROR);
|
||||||
|
this._transitionTo(_STATES.RELEASED);
|
||||||
|
});
|
||||||
|
this._transitionTo(_STATES.ACQUIRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_wakelockAcquired(wakelock) {
|
||||||
|
if (this._state === _STATES.ACQUIRING_WANT_RELEASE) {
|
||||||
|
// We were requested to release the wakelock while we were trying to
|
||||||
|
// acquire it. Now that we have acquired it, immediately release it.
|
||||||
|
wakelock.release();
|
||||||
|
this._transitionTo(_STATES.RELEASED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._wakelock = wakelock;
|
||||||
|
this._wakelock.addEventListener("release", this._eventHandlers.wakelockReleased);
|
||||||
|
this._transitionTo(_STATES.ACQUIRED);
|
||||||
|
}
|
||||||
|
|
||||||
|
_wakelockReleased(event) {
|
||||||
|
this._wakelock = null;
|
||||||
|
if (document.visibilityState === "visible") {
|
||||||
|
Log.Warn("Lost wakelock, but document is still visible. Not reacquiring");
|
||||||
|
this._transitionTo(_STATES.RELEASED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._awaitVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
_documentVisibilityChange(event) {
|
||||||
|
if (document.visibilityState !== "visible") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.removeEventListener("visibilitychange", this._eventHandlers.documentVisibilityChange);
|
||||||
|
this._acquireWakelockNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (c) 2025 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Log from './util/logging.js';
|
||||||
|
import { browserAsyncClipboardSupport } from './util/browser.js';
|
||||||
|
|
||||||
|
export default class AsyncClipboard {
|
||||||
|
constructor(target) {
|
||||||
|
this._target = target || null;
|
||||||
|
|
||||||
|
this._isAvailable = null;
|
||||||
|
|
||||||
|
this._eventHandlers = {
|
||||||
|
'focus': this._handleFocus.bind(this),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== EVENT HANDLERS =====
|
||||||
|
|
||||||
|
this.onpaste = () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== PRIVATE METHODS =====
|
||||||
|
|
||||||
|
async _ensureAvailable() {
|
||||||
|
if (this._isAvailable !== null) return this._isAvailable;
|
||||||
|
try {
|
||||||
|
const status = await browserAsyncClipboardSupport();
|
||||||
|
this._isAvailable = (status === 'available');
|
||||||
|
} catch {
|
||||||
|
this._isAvailable = false;
|
||||||
|
}
|
||||||
|
return this._isAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _handleFocus(event) {
|
||||||
|
if (!(await this._ensureAvailable())) return;
|
||||||
|
try {
|
||||||
|
const text = await navigator.clipboard.readText();
|
||||||
|
this.onpaste(text);
|
||||||
|
} catch (error) {
|
||||||
|
Log.Error("Clipboard read failed: ", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== PUBLIC METHODS =====
|
||||||
|
|
||||||
|
writeClipboard(text) {
|
||||||
|
// Can lazily check cached availability
|
||||||
|
if (!this._isAvailable) return false;
|
||||||
|
navigator.clipboard.writeText(text)
|
||||||
|
.catch(error => Log.Error("Clipboard write failed: ", error));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
grab() {
|
||||||
|
if (!this._target) return;
|
||||||
|
this._ensureAvailable()
|
||||||
|
.then((isAvailable) => {
|
||||||
|
if (isAvailable) {
|
||||||
|
this._target.addEventListener('focus', this._eventHandlers.focus);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ungrab() {
|
||||||
|
if (!this._target) return;
|
||||||
|
this._target.removeEventListener('focus', this._eventHandlers.focus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -181,11 +181,11 @@ class DES {
|
||||||
// Encrypt 8 bytes of text
|
// Encrypt 8 bytes of text
|
||||||
enc8(text) {
|
enc8(text) {
|
||||||
const b = text.slice();
|
const b = text.slice();
|
||||||
let i = 0, l, r, x; // left, right, accumulator
|
let l, r, x; // left, right, accumulator
|
||||||
|
|
||||||
// Squash 8 bytes to 2 ints
|
// Squash 8 bytes to 2 ints
|
||||||
l = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++];
|
l = b[0]<<24 | b[1]<<16 | b[2]<<8 | b[3];
|
||||||
r = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++];
|
r = b[4]<<24 | b[5]<<16 | b[6]<<8 | b[7];
|
||||||
|
|
||||||
x = ((l >>> 4) ^ r) & 0x0f0f0f0f;
|
x = ((l >>> 4) ^ r) & 0x0f0f0f0f;
|
||||||
r ^= x;
|
r ^= x;
|
||||||
|
|
@ -252,7 +252,7 @@ class DES {
|
||||||
|
|
||||||
// Spread ints to bytes
|
// Spread ints to bytes
|
||||||
x = [r, l];
|
x = [r, l];
|
||||||
for (i = 0; i < 8; i++) {
|
for (let i = 0; i < 8; i++) {
|
||||||
b[i] = (x[i>>>2] >>> (8 * (3 - (i % 4)))) % 256;
|
b[i] = (x[i>>>2] >>> (8 * (3 - (i % 4)))) % 256;
|
||||||
if (b[i] < 0) { b[i] += 256; } // unsigned
|
if (b[i] < 0) { b[i] += 256; } // unsigned
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@ export default class ZRLEDecoder {
|
||||||
|
|
||||||
_readRLELength() {
|
_readRLELength() {
|
||||||
let length = 0;
|
let length = 0;
|
||||||
let current = 0;
|
let current;
|
||||||
do {
|
do {
|
||||||
current = this._inflator.inflate(1)[0];
|
current = this._inflator.inflate(1)[0];
|
||||||
length += current;
|
length += current;
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export default class Display {
|
||||||
constructor(target) {
|
constructor(target) {
|
||||||
this._drawCtx = null;
|
this._drawCtx = null;
|
||||||
|
|
||||||
this._renderQ = []; // queue drawing actions for in-oder rendering
|
this._renderQ = []; // queue drawing actions for in-order rendering
|
||||||
this._flushPromise = null;
|
this._flushPromise = null;
|
||||||
|
|
||||||
// the full frame buffer (logical canvas) size
|
// the full frame buffer (logical canvas) size
|
||||||
|
|
|
||||||
31
core/rfb.js
31
core/rfb.js
|
|
@ -15,6 +15,7 @@ import { clientToElement } from './util/element.js';
|
||||||
import { setCapture } from './util/events.js';
|
import { setCapture } from './util/events.js';
|
||||||
import EventTargetMixin from './util/eventtarget.js';
|
import EventTargetMixin from './util/eventtarget.js';
|
||||||
import Display from "./display.js";
|
import Display from "./display.js";
|
||||||
|
import AsyncClipboard from "./clipboard.js";
|
||||||
import Inflator from "./inflator.js";
|
import Inflator from "./inflator.js";
|
||||||
import Deflator from "./deflator.js";
|
import Deflator from "./deflator.js";
|
||||||
import Keyboard from "./input/keyboard.js";
|
import Keyboard from "./input/keyboard.js";
|
||||||
|
|
@ -164,6 +165,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._sock = null; // Websock object
|
this._sock = null; // Websock object
|
||||||
this._display = null; // Display object
|
this._display = null; // Display object
|
||||||
this._flushing = false; // Display flushing state
|
this._flushing = false; // Display flushing state
|
||||||
|
this._asyncClipboard = null; // Async clipboard object
|
||||||
this._keyboard = null; // Keyboard input handler object
|
this._keyboard = null; // Keyboard input handler object
|
||||||
this._gestures = null; // Gesture input handler object
|
this._gestures = null; // Gesture input handler object
|
||||||
this._resizeObserver = null; // Resize observer object
|
this._resizeObserver = null; // Resize observer object
|
||||||
|
|
@ -266,6 +268,9 @@ export default class RFB extends EventTargetMixin {
|
||||||
throw exc;
|
throw exc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._asyncClipboard = new AsyncClipboard(this._canvas);
|
||||||
|
this._asyncClipboard.onpaste = this.clipboardPasteFrom.bind(this);
|
||||||
|
|
||||||
this._keyboard = new Keyboard(this._canvas);
|
this._keyboard = new Keyboard(this._canvas);
|
||||||
this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
|
this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
|
||||||
this._remoteCapsLock = null; // Null indicates unknown or irrelevant
|
this._remoteCapsLock = null; // Null indicates unknown or irrelevant
|
||||||
|
|
@ -315,8 +320,10 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._rfbConnectionState === "connected") {
|
this._rfbConnectionState === "connected") {
|
||||||
if (viewOnly) {
|
if (viewOnly) {
|
||||||
this._keyboard.ungrab();
|
this._keyboard.ungrab();
|
||||||
|
this._asyncClipboard.ungrab();
|
||||||
} else {
|
} else {
|
||||||
this._keyboard.grab();
|
this._keyboard.grab();
|
||||||
|
this._asyncClipboard.grab();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2208,7 +2215,10 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._setDesktopName(name);
|
this._setDesktopName(name);
|
||||||
this._resize(width, height);
|
this._resize(width, height);
|
||||||
|
|
||||||
if (!this._viewOnly) { this._keyboard.grab(); }
|
if (!this._viewOnly) {
|
||||||
|
this._keyboard.grab();
|
||||||
|
this._asyncClipboard.grab();
|
||||||
|
}
|
||||||
|
|
||||||
this._fbDepth = 24;
|
this._fbDepth = 24;
|
||||||
|
|
||||||
|
|
@ -2323,6 +2333,15 @@ export default class RFB extends EventTargetMixin {
|
||||||
return this._fail("Unexpected SetColorMapEntries message");
|
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() {
|
_handleServerCutText() {
|
||||||
Log.Debug("ServerCutText");
|
Log.Debug("ServerCutText");
|
||||||
|
|
||||||
|
|
@ -2342,9 +2361,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dispatchEvent(new CustomEvent(
|
this._writeClipboard(text);
|
||||||
"clipboard",
|
|
||||||
{ detail: { text: text } }));
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
//Extended msg.
|
//Extended msg.
|
||||||
|
|
@ -2480,9 +2497,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
|
|
||||||
textData = textData.replaceAll("\r\n", "\n");
|
textData = textData.replaceAll("\r\n", "\n");
|
||||||
|
|
||||||
this.dispatchEvent(new CustomEvent(
|
this._writeClipboard(textData);
|
||||||
"clipboard",
|
|
||||||
{ detail: { text: textData } }));
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return this._fail("Unexpected action in extended clipboard message: " + actions);
|
return this._fail("Unexpected action in extended clipboard message: " + actions);
|
||||||
|
|
@ -2934,7 +2949,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
|
|
||||||
// We need to handle errors when we requested the resize.
|
// We need to handle errors when we requested the resize.
|
||||||
if (this._FBU.x === 1 && this._FBU.y !== 0) {
|
if (this._FBU.x === 1 && this._FBU.y !== 0) {
|
||||||
let msg = "";
|
let msg;
|
||||||
// The y-position indicates the status code from the server
|
// The y-position indicates the status code from the server
|
||||||
switch (this._FBU.y) {
|
switch (this._FBU.y) {
|
||||||
case 1:
|
case 1:
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,39 @@
|
||||||
import * as Log from './logging.js';
|
import * as Log from './logging.js';
|
||||||
import Base64 from '../base64.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
|
// Touch detection
|
||||||
export let isTouchDevice = ('ontouchstart' in document.documentElement) ||
|
export let isTouchDevice = ('ontouchstart' in document.documentElement) ||
|
||||||
// required for Chrome debugger
|
// required for Chrome debugger
|
||||||
|
|
@ -149,7 +182,12 @@ async function _checkWebCodecsH264DecodeSupport() {
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
supportsWebCodecsH264Decode = await _checkWebCodecsH264DecodeSupport();
|
|
||||||
|
// FIXME: Avoid top-level await due to a Chromium bug where Decoder.flush()
|
||||||
|
// can hang indefinitely on some Android devices, blocking module evaluation.
|
||||||
|
_checkWebCodecsH264DecodeSupport().then((result) => {
|
||||||
|
supportsWebCodecsH264Decode = result;
|
||||||
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The functions for detection of platforms and browsers below are exported
|
* The functions for detection of platforms and browsers below are exported
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ keysym values.
|
||||||
* __Display__ (core/display.js): Efficient 2D rendering abstraction
|
* __Display__ (core/display.js): Efficient 2D rendering abstraction
|
||||||
layered on the HTML5 canvas element.
|
layered on the HTML5 canvas element.
|
||||||
|
|
||||||
|
* __Clipboard__ (core/clipboard.js): Clipboard event handler.
|
||||||
|
|
||||||
* __Websock__ (core/websock.js): Websock client from websockify
|
* __Websock__ (core/websock.js): Websock client from websockify
|
||||||
with transparent binary data support.
|
with transparent binary data support.
|
||||||
[Websock API](https://github.com/novnc/websockify-js/wiki/websock.js) wiki page.
|
[Websock API](https://github.com/novnc/websockify-js/wiki/websock.js) wiki page.
|
||||||
|
|
@ -25,10 +27,10 @@ with transparent binary data support.
|
||||||
|
|
||||||
## 1.2 Callbacks
|
## 1.2 Callbacks
|
||||||
|
|
||||||
For the Mouse, Keyboard and Display objects the callback functions are
|
For the Mouse, Keyboard, Display, and Clipboard objects, the callback
|
||||||
assigned to configuration attributes, just as for the RFB object. The
|
functions are assigned to configuration attributes, just as for the RFB
|
||||||
WebSock module has a method named 'on' that takes two parameters: the
|
object. The WebSock module has a method named 'on' that takes two
|
||||||
callback event name, and the callback function.
|
parameters: the callback event name, and the callback function.
|
||||||
|
|
||||||
## 2. Modules
|
## 2. Modules
|
||||||
|
|
||||||
|
|
@ -81,3 +83,23 @@ None
|
||||||
| blitImage | (x, y, width, height, arr, offset, from_queue) | Blit pixels (of R,G,B,A) to the display
|
| 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
|
| drawImage | (img, x, y) | Draw image and track damage
|
||||||
| autoscale | (containerWidth, containerHeight) | Scale the display
|
| 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
|
||||||
|
|
|
||||||
|
|
@ -77,11 +77,6 @@ protocol stream.
|
||||||
if the remote session is smaller than its container, or handled
|
if the remote session is smaller than its container, or handled
|
||||||
according to `clipViewport` if it is larger. Disabled by default.
|
according to `clipViewport` if it is larger. Disabled by default.
|
||||||
|
|
||||||
`showDotCursor`
|
|
||||||
- Is a `boolean` indicating whether a dot cursor should be shown
|
|
||||||
instead of a zero-sized or fully-transparent cursor if the server
|
|
||||||
sets such invisible cursor. Disabled by default.
|
|
||||||
|
|
||||||
`viewOnly`
|
`viewOnly`
|
||||||
- Is a `boolean` indicating if any events (e.g. key presses or mouse
|
- Is a `boolean` indicating if any events (e.g. key presses or mouse
|
||||||
movement) should be prevented from being sent to the server.
|
movement) should be prevented from being sent to the server.
|
||||||
|
|
|
||||||
|
|
@ -89,12 +89,13 @@ Currently, the following options are available:
|
||||||
|
|
||||||
* `compression` - The session compression level. Can be `0` to `9`.
|
* `compression` - The session compression level. Can be `0` to `9`.
|
||||||
|
|
||||||
* `show_dot` - If a dot cursor should be shown when the remote server provides
|
|
||||||
no local cursor, or provides a fully-transparent (invisible) cursor.
|
|
||||||
|
|
||||||
* `logging` - The console log level. Can be one of `error`, `warn`, `info` or
|
* `logging` - The console log level. Can be one of `error`, `warn`, `info` or
|
||||||
`debug`.
|
`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
|
## HTTP serving considerations
|
||||||
### Browser cache issue
|
### Browser cache issue
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import globals from "globals";
|
import globals from "globals";
|
||||||
|
import { defineConfig } from "eslint/config";
|
||||||
import js from "@eslint/js";
|
import js from "@eslint/js";
|
||||||
|
|
||||||
export default [
|
export default defineConfig([
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
{
|
{
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
|
|
@ -99,4 +100,4 @@ export default [
|
||||||
"no-console": 0,
|
"no-console": 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
]);
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ module.exports = (config) => {
|
||||||
{ pattern: 'node_modules/sinon-chai/**', included: false },
|
{ pattern: 'node_modules/sinon-chai/**', included: false },
|
||||||
// modules to test
|
// modules to test
|
||||||
{ pattern: 'app/localization.js', included: false, type: 'module' },
|
{ 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: 'app/webutil.js', included: false, type: 'module' },
|
||||||
{ pattern: 'core/**/*.js', included: false, type: 'module' },
|
{ pattern: 'core/**/*.js', included: false, type: 'module' },
|
||||||
{ pattern: 'vendor/pako/**/*.js', included: false, type: 'module' },
|
{ pattern: 'vendor/pako/**/*.js', included: false, type: 'module' },
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@novnc/novnc",
|
"name": "@novnc/novnc",
|
||||||
"version": "1.6.0",
|
"version": "1.7.0",
|
||||||
"description": "An HTML5 VNC client",
|
"description": "An HTML5 VNC client",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
|
|
@ -39,6 +39,7 @@
|
||||||
"chai": "latest",
|
"chai": "latest",
|
||||||
"commander": "latest",
|
"commander": "latest",
|
||||||
"eslint": "latest",
|
"eslint": "latest",
|
||||||
|
"@eslint/js": "latest",
|
||||||
"fs-extra": "latest",
|
"fs-extra": "latest",
|
||||||
"globals": "latest",
|
"globals": "latest",
|
||||||
"jsdom": "latest",
|
"jsdom": "latest",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ all:
|
||||||
.PHONY: update-po update-js update-pot
|
.PHONY: update-po update-js update-pot
|
||||||
.PHONY: FORCE
|
.PHONY: FORCE
|
||||||
|
|
||||||
LINGUAS := cs de el es fr hr it ja ko nl pl pt_BR ru sv tr zh_CN zh_TW
|
LINGUAS := cs de el es fr hr hu it ja ko nl pl pt_BR ru sv tr uk zh_CN zh_TW
|
||||||
|
|
||||||
VERSION := $(shell grep '"version"' ../package.json | cut -d '"' -f 4)
|
VERSION := $(shell grep '"version"' ../package.json | cut -d '"' -f 4)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,332 @@
|
||||||
|
# Hungarian translations for noVNC package.
|
||||||
|
# Copyright (C) 2025 The noVNC authors
|
||||||
|
# This file is distributed under the same license as the noVNC package.
|
||||||
|
# Daniel Felso <danielfelso@protonmail.com>, 2025.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: noVNC 1.6.0\n"
|
||||||
|
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
|
||||||
|
"POT-Creation-Date: 2025-02-14 10:14+0100\n"
|
||||||
|
"PO-Revision-Date: 2025-10-06 14:38+0200\n"
|
||||||
|
"Last-Translator: Daniel Felso <danielfelso@protonmail.com>\n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"Language: hu\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
|
#: ../app/ui.js:84
|
||||||
|
msgid ""
|
||||||
|
"Running without HTTPS is not recommended, crashes or other issues are likely."
|
||||||
|
msgstr "HTTPS nélkül futtatni nem ajánlott, összeomlások vagy más problémák várhatók."
|
||||||
|
|
||||||
|
#: ../app/ui.js:413
|
||||||
|
msgid "Connecting..."
|
||||||
|
msgstr "Kapcsolódás..."
|
||||||
|
|
||||||
|
#: ../app/ui.js:420
|
||||||
|
msgid "Disconnecting..."
|
||||||
|
msgstr "Kapcsolat bontása..."
|
||||||
|
|
||||||
|
#: ../app/ui.js:426
|
||||||
|
msgid "Reconnecting..."
|
||||||
|
msgstr "Újrakapcsolódás..."
|
||||||
|
|
||||||
|
#: ../app/ui.js:431
|
||||||
|
msgid "Internal error"
|
||||||
|
msgstr "Belső hiba"
|
||||||
|
|
||||||
|
#: ../app/ui.js:1079
|
||||||
|
msgid "Failed to connect to server: "
|
||||||
|
msgstr "Nem sikerült csatlakozni a szerverhez: "
|
||||||
|
|
||||||
|
#: ../app/ui.js:1145
|
||||||
|
msgid "Connected (encrypted) to "
|
||||||
|
msgstr "Kapcsolódva (titkosítva) ehhez: "
|
||||||
|
|
||||||
|
#: ../app/ui.js:1147
|
||||||
|
msgid "Connected (unencrypted) to "
|
||||||
|
msgstr "Kapcsolódva (titkosítatlanul) ehhez: "
|
||||||
|
|
||||||
|
#: ../app/ui.js:1170
|
||||||
|
msgid "Something went wrong, connection is closed"
|
||||||
|
msgstr "Valami hiba történt, a kapcsolat lezárult"
|
||||||
|
|
||||||
|
#: ../app/ui.js:1173
|
||||||
|
msgid "Failed to connect to server"
|
||||||
|
msgstr "Nem sikerült csatlakozni a szerverhez"
|
||||||
|
|
||||||
|
#: ../app/ui.js:1185
|
||||||
|
msgid "Disconnected"
|
||||||
|
msgstr "Kapcsolat bontva"
|
||||||
|
|
||||||
|
#: ../app/ui.js:1200
|
||||||
|
msgid "New connection has been rejected with reason: "
|
||||||
|
msgstr "Az új kapcsolat elutasítva, indok: "
|
||||||
|
|
||||||
|
#: ../app/ui.js:1203
|
||||||
|
msgid "New connection has been rejected"
|
||||||
|
msgstr "Az új kapcsolat elutasítva"
|
||||||
|
|
||||||
|
#: ../app/ui.js:1269
|
||||||
|
msgid "Credentials are required"
|
||||||
|
msgstr "Hitelesítő adatok szükségesek"
|
||||||
|
|
||||||
|
#: ../vnc.html:106
|
||||||
|
msgid "noVNC encountered an error:"
|
||||||
|
msgstr "A noVNC hibát észlelt:"
|
||||||
|
|
||||||
|
#: ../vnc.html:116
|
||||||
|
msgid "Hide/Show the control bar"
|
||||||
|
msgstr "Vezérlősáv elrejtése/megjelenítése"
|
||||||
|
|
||||||
|
#: ../vnc.html:125
|
||||||
|
msgid "Drag"
|
||||||
|
msgstr "Húzás"
|
||||||
|
|
||||||
|
#: ../vnc.html:125
|
||||||
|
msgid "Move/Drag viewport"
|
||||||
|
msgstr "Nézet mozgatása/húzása"
|
||||||
|
|
||||||
|
#: ../vnc.html:131
|
||||||
|
msgid "Keyboard"
|
||||||
|
msgstr "Billentyűzet"
|
||||||
|
|
||||||
|
#: ../vnc.html:131
|
||||||
|
msgid "Show keyboard"
|
||||||
|
msgstr "Billentyűzet megjelenítése"
|
||||||
|
|
||||||
|
#: ../vnc.html:136
|
||||||
|
msgid "Extra keys"
|
||||||
|
msgstr "Extra billentyűk"
|
||||||
|
|
||||||
|
#: ../vnc.html:136
|
||||||
|
msgid "Show extra keys"
|
||||||
|
msgstr "Extra billentyűk megjelenítése"
|
||||||
|
|
||||||
|
#: ../vnc.html:141
|
||||||
|
msgid "Ctrl"
|
||||||
|
msgstr "Ctrl"
|
||||||
|
|
||||||
|
#: ../vnc.html:141
|
||||||
|
msgid "Toggle Ctrl"
|
||||||
|
msgstr "Ctrl lenyomása/felengedése"
|
||||||
|
|
||||||
|
#: ../vnc.html:144
|
||||||
|
msgid "Alt"
|
||||||
|
msgstr "Alt"
|
||||||
|
|
||||||
|
#: ../vnc.html:144
|
||||||
|
msgid "Toggle Alt"
|
||||||
|
msgstr "Alt lenyomása/felengedése"
|
||||||
|
|
||||||
|
#: ../vnc.html:147
|
||||||
|
msgid "Toggle Windows"
|
||||||
|
msgstr "Windows lenyomása/felengedése"
|
||||||
|
|
||||||
|
#: ../vnc.html:147
|
||||||
|
msgid "Windows"
|
||||||
|
msgstr "Windows"
|
||||||
|
|
||||||
|
#: ../vnc.html:150
|
||||||
|
msgid "Send Tab"
|
||||||
|
msgstr "Tab küldése"
|
||||||
|
|
||||||
|
#: ../vnc.html:150
|
||||||
|
msgid "Tab"
|
||||||
|
msgstr "Tab"
|
||||||
|
|
||||||
|
#: ../vnc.html:153
|
||||||
|
msgid "Esc"
|
||||||
|
msgstr "Esc"
|
||||||
|
|
||||||
|
#: ../vnc.html:153
|
||||||
|
msgid "Send Escape"
|
||||||
|
msgstr "Escape küldése"
|
||||||
|
|
||||||
|
#: ../vnc.html:156
|
||||||
|
msgid "Ctrl+Alt+Del"
|
||||||
|
msgstr "Ctrl+Alt+Del"
|
||||||
|
|
||||||
|
#: ../vnc.html:156
|
||||||
|
msgid "Send Ctrl-Alt-Del"
|
||||||
|
msgstr "Ctrl-Alt-Del küldése"
|
||||||
|
|
||||||
|
#: ../vnc.html:163
|
||||||
|
msgid "Shutdown/Reboot"
|
||||||
|
msgstr "Leállítás/Újraindítás"
|
||||||
|
|
||||||
|
#: ../vnc.html:163
|
||||||
|
msgid "Shutdown/Reboot..."
|
||||||
|
msgstr "Leállítás/Újraindítás..."
|
||||||
|
|
||||||
|
#: ../vnc.html:169
|
||||||
|
msgid "Power"
|
||||||
|
msgstr "Bekapcsolás"
|
||||||
|
|
||||||
|
#: ../vnc.html:171
|
||||||
|
msgid "Shutdown"
|
||||||
|
msgstr "Leállítás"
|
||||||
|
|
||||||
|
#: ../vnc.html:172
|
||||||
|
msgid "Reboot"
|
||||||
|
msgstr "Újraindítás"
|
||||||
|
|
||||||
|
#: ../vnc.html:173
|
||||||
|
msgid "Reset"
|
||||||
|
msgstr "Reset"
|
||||||
|
|
||||||
|
#: ../vnc.html:178 ../vnc.html:184
|
||||||
|
msgid "Clipboard"
|
||||||
|
msgstr "Vágólap"
|
||||||
|
|
||||||
|
#: ../vnc.html:186
|
||||||
|
msgid "Edit clipboard content in the textarea below."
|
||||||
|
msgstr "Itt tudod módosítani a vágólap tartalmát."
|
||||||
|
|
||||||
|
#: ../vnc.html:194
|
||||||
|
msgid "Full screen"
|
||||||
|
msgstr "Teljes képernyő"
|
||||||
|
|
||||||
|
#: ../vnc.html:199 ../vnc.html:205
|
||||||
|
msgid "Settings"
|
||||||
|
msgstr "Beállítások"
|
||||||
|
|
||||||
|
#: ../vnc.html:211
|
||||||
|
msgid "Shared mode"
|
||||||
|
msgstr "Megosztott mód"
|
||||||
|
|
||||||
|
#: ../vnc.html:218
|
||||||
|
msgid "View only"
|
||||||
|
msgstr "Csak megtekintés"
|
||||||
|
|
||||||
|
#: ../vnc.html:226
|
||||||
|
msgid "Clip to window"
|
||||||
|
msgstr "Ablakhoz igazítás"
|
||||||
|
|
||||||
|
#: ../vnc.html:231
|
||||||
|
msgid "Scaling mode:"
|
||||||
|
msgstr "Méretezési mód:"
|
||||||
|
|
||||||
|
#: ../vnc.html:233
|
||||||
|
msgid "None"
|
||||||
|
msgstr "Nincs"
|
||||||
|
|
||||||
|
#: ../vnc.html:234
|
||||||
|
msgid "Local scaling"
|
||||||
|
msgstr "Helyi méretezés"
|
||||||
|
|
||||||
|
#: ../vnc.html:235
|
||||||
|
msgid "Remote resizing"
|
||||||
|
msgstr "Távoli átméretezés"
|
||||||
|
|
||||||
|
#: ../vnc.html:240
|
||||||
|
msgid "Advanced"
|
||||||
|
msgstr "Speciális"
|
||||||
|
|
||||||
|
#: ../vnc.html:243
|
||||||
|
msgid "Quality:"
|
||||||
|
msgstr "Minőség:"
|
||||||
|
|
||||||
|
#: ../vnc.html:247
|
||||||
|
msgid "Compression level:"
|
||||||
|
msgstr "Tömörítési szint:"
|
||||||
|
|
||||||
|
#: ../vnc.html:252
|
||||||
|
msgid "Repeater ID:"
|
||||||
|
msgstr "Ismétlő azonosító:"
|
||||||
|
|
||||||
|
#: ../vnc.html:256
|
||||||
|
msgid "WebSocket"
|
||||||
|
msgstr "WebSocket"
|
||||||
|
|
||||||
|
#: ../vnc.html:261
|
||||||
|
msgid "Encrypt"
|
||||||
|
msgstr "Titkosítás"
|
||||||
|
|
||||||
|
#: ../vnc.html:266
|
||||||
|
msgid "Host:"
|
||||||
|
msgstr "Hoszt:"
|
||||||
|
|
||||||
|
#: ../vnc.html:270
|
||||||
|
msgid "Port:"
|
||||||
|
msgstr "Port:"
|
||||||
|
|
||||||
|
#: ../vnc.html:274
|
||||||
|
msgid "Path:"
|
||||||
|
msgstr "Útvonal:"
|
||||||
|
|
||||||
|
#: ../vnc.html:283
|
||||||
|
msgid "Automatic reconnect"
|
||||||
|
msgstr "Automatikus újracsatlakozás"
|
||||||
|
|
||||||
|
#: ../vnc.html:288
|
||||||
|
msgid "Reconnect delay (ms):"
|
||||||
|
msgstr "Újracsatlakozás késleltetése (ms):"
|
||||||
|
|
||||||
|
#: ../vnc.html:295
|
||||||
|
msgid "Show dot when no cursor"
|
||||||
|
msgstr "Kurzor hiányában pont mutatása"
|
||||||
|
|
||||||
|
#: ../vnc.html:302
|
||||||
|
msgid "Logging:"
|
||||||
|
msgstr "Naplózás:"
|
||||||
|
|
||||||
|
#: ../vnc.html:311
|
||||||
|
msgid "Version:"
|
||||||
|
msgstr "Verzió:"
|
||||||
|
|
||||||
|
#: ../vnc.html:319
|
||||||
|
msgid "Disconnect"
|
||||||
|
msgstr "Kapcsolat bontása"
|
||||||
|
|
||||||
|
#: ../vnc.html:342
|
||||||
|
msgid "Connect"
|
||||||
|
msgstr "Csatlakozás"
|
||||||
|
|
||||||
|
#: ../vnc.html:351
|
||||||
|
msgid "Server identity"
|
||||||
|
msgstr "Szerver azonosító"
|
||||||
|
|
||||||
|
#: ../vnc.html:354
|
||||||
|
msgid "The server has provided the following identifying information:"
|
||||||
|
msgstr "A szerver a következő azonosító információt adta meg:"
|
||||||
|
|
||||||
|
#: ../vnc.html:357
|
||||||
|
msgid "Fingerprint:"
|
||||||
|
msgstr "Ujjlenyomat:"
|
||||||
|
|
||||||
|
#: ../vnc.html:361
|
||||||
|
msgid ""
|
||||||
|
"Please verify that the information is correct and press \"Approve\". "
|
||||||
|
"Otherwise press \"Reject\"."
|
||||||
|
msgstr "Ellenőrizze, hogy az információ helyes-e és nyomja meg a \"Jóváhagyás\" gombot. Ellenkező esetben nyomja meg az \"Elutasítás\" gombot."
|
||||||
|
|
||||||
|
#: ../vnc.html:366
|
||||||
|
msgid "Approve"
|
||||||
|
msgstr "Jóváhagyás"
|
||||||
|
|
||||||
|
#: ../vnc.html:367
|
||||||
|
msgid "Reject"
|
||||||
|
msgstr "Elutasítás"
|
||||||
|
|
||||||
|
#: ../vnc.html:375
|
||||||
|
msgid "Credentials"
|
||||||
|
msgstr "Hitelesítő adatok"
|
||||||
|
|
||||||
|
#: ../vnc.html:379
|
||||||
|
msgid "Username:"
|
||||||
|
msgstr "Felhasználónév:"
|
||||||
|
|
||||||
|
#: ../vnc.html:383
|
||||||
|
msgid "Password:"
|
||||||
|
msgstr "Jelszó:"
|
||||||
|
|
||||||
|
#: ../vnc.html:387
|
||||||
|
msgid "Send credentials"
|
||||||
|
msgstr "Hitelesítő adatok küldése"
|
||||||
|
|
||||||
|
#: ../vnc.html:396
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr "Mégse"
|
||||||
24
po/noVNC.pot
24
po/noVNC.pot
|
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: noVNC 1.6.0\n"
|
"Project-Id-Version: noVNC 1.6.0\n"
|
||||||
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
|
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
|
||||||
"POT-Creation-Date: 2025-02-14 10:14+0100\n"
|
"POT-Creation-Date: 2025-10-31 09:17+0100\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
|
@ -38,39 +38,43 @@ msgstr ""
|
||||||
msgid "Internal error"
|
msgid "Internal error"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: ../app/ui.js:1079
|
#: ../app/ui.js:1084
|
||||||
msgid "Failed to connect to server: "
|
msgid "Failed to connect to server: "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: ../app/ui.js:1145
|
#: ../app/ui.js:1151
|
||||||
msgid "Connected (encrypted) to "
|
msgid "Connected (encrypted) to "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: ../app/ui.js:1147
|
#: ../app/ui.js:1153
|
||||||
msgid "Connected (unencrypted) to "
|
msgid "Connected (unencrypted) to "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: ../app/ui.js:1170
|
#: ../app/ui.js:1178
|
||||||
msgid "Something went wrong, connection is closed"
|
msgid "Something went wrong, connection is closed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: ../app/ui.js:1173
|
#: ../app/ui.js:1181
|
||||||
msgid "Failed to connect to server"
|
msgid "Failed to connect to server"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: ../app/ui.js:1185
|
#: ../app/ui.js:1193
|
||||||
msgid "Disconnected"
|
msgid "Disconnected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: ../app/ui.js:1200
|
#: ../app/ui.js:1210
|
||||||
msgid "New connection has been rejected with reason: "
|
msgid "New connection has been rejected with reason: "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: ../app/ui.js:1203
|
#: ../app/ui.js:1213
|
||||||
msgid "New connection has been rejected"
|
msgid "New connection has been rejected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: ../app/ui.js:1269
|
#: ../app/ui.js:1225
|
||||||
|
msgid "Are you sure you want to disconnect the session?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: ../app/ui.js:1297
|
||||||
msgid "Credentials are required"
|
msgid "Credentials are required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
||||||
44
po/sv.po
44
po/sv.po
|
|
@ -8,8 +8,8 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: noVNC 1.6.0\n"
|
"Project-Id-Version: noVNC 1.6.0\n"
|
||||||
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
|
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
|
||||||
"POT-Creation-Date: 2025-02-14 10:14+0100\n"
|
"POT-Creation-Date: 2025-10-31 09:17+0100\n"
|
||||||
"PO-Revision-Date: 2025-02-14 10:29+0100\n"
|
"PO-Revision-Date: 2025-10-31 10:48+0100\n"
|
||||||
"Last-Translator: Alexander Zeijlon <aleze@cendio.com>\n"
|
"Last-Translator: Alexander Zeijlon <aleze@cendio.com>\n"
|
||||||
"Language-Team: none\n"
|
"Language-Team: none\n"
|
||||||
"Language: sv\n"
|
"Language: sv\n"
|
||||||
|
|
@ -17,7 +17,7 @@ msgstr ""
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
"X-Generator: Poedit 3.5\n"
|
"X-Generator: Poedit 3.7\n"
|
||||||
|
|
||||||
#: ../app/ui.js:84
|
#: ../app/ui.js:84
|
||||||
msgid ""
|
msgid ""
|
||||||
|
|
@ -32,7 +32,7 @@ msgstr "Ansluter..."
|
||||||
|
|
||||||
#: ../app/ui.js:420
|
#: ../app/ui.js:420
|
||||||
msgid "Disconnecting..."
|
msgid "Disconnecting..."
|
||||||
msgstr "Kopplar ner..."
|
msgstr "Kopplar ifrån..."
|
||||||
|
|
||||||
#: ../app/ui.js:426
|
#: ../app/ui.js:426
|
||||||
msgid "Reconnecting..."
|
msgid "Reconnecting..."
|
||||||
|
|
@ -42,39 +42,43 @@ msgstr "Återansluter..."
|
||||||
msgid "Internal error"
|
msgid "Internal error"
|
||||||
msgstr "Internt fel"
|
msgstr "Internt fel"
|
||||||
|
|
||||||
#: ../app/ui.js:1079
|
#: ../app/ui.js:1084
|
||||||
msgid "Failed to connect to server: "
|
msgid "Failed to connect to server: "
|
||||||
msgstr "Misslyckades att ansluta till servern: "
|
msgstr "Misslyckades att ansluta till servern: "
|
||||||
|
|
||||||
#: ../app/ui.js:1145
|
#: ../app/ui.js:1151
|
||||||
msgid "Connected (encrypted) to "
|
msgid "Connected (encrypted) to "
|
||||||
msgstr "Ansluten (krypterat) till "
|
msgstr "Ansluten (krypterat) till "
|
||||||
|
|
||||||
#: ../app/ui.js:1147
|
#: ../app/ui.js:1153
|
||||||
msgid "Connected (unencrypted) to "
|
msgid "Connected (unencrypted) to "
|
||||||
msgstr "Ansluten (okrypterat) till "
|
msgstr "Ansluten (okrypterat) till "
|
||||||
|
|
||||||
#: ../app/ui.js:1170
|
#: ../app/ui.js:1178
|
||||||
msgid "Something went wrong, connection is closed"
|
msgid "Something went wrong, connection is closed"
|
||||||
msgstr "Något gick fel, anslutningen avslutades"
|
msgstr "Något gick fel, anslutningen avslutades"
|
||||||
|
|
||||||
#: ../app/ui.js:1173
|
#: ../app/ui.js:1181
|
||||||
msgid "Failed to connect to server"
|
msgid "Failed to connect to server"
|
||||||
msgstr "Misslyckades att ansluta till servern"
|
msgstr "Misslyckades att ansluta till servern"
|
||||||
|
|
||||||
#: ../app/ui.js:1185
|
#: ../app/ui.js:1193
|
||||||
msgid "Disconnected"
|
msgid "Disconnected"
|
||||||
msgstr "Frånkopplad"
|
msgstr "Frånkopplad"
|
||||||
|
|
||||||
#: ../app/ui.js:1200
|
#: ../app/ui.js:1210
|
||||||
msgid "New connection has been rejected with reason: "
|
msgid "New connection has been rejected with reason: "
|
||||||
msgstr "Ny anslutning har blivit nekad med följande skäl: "
|
msgstr "Ny anslutning har blivit nekad med följande skäl: "
|
||||||
|
|
||||||
#: ../app/ui.js:1203
|
#: ../app/ui.js:1213
|
||||||
msgid "New connection has been rejected"
|
msgid "New connection has been rejected"
|
||||||
msgstr "Ny anslutning har blivit nekad"
|
msgstr "Ny anslutning har blivit nekad"
|
||||||
|
|
||||||
#: ../app/ui.js:1269
|
#: ../app/ui.js:1225
|
||||||
|
msgid "Are you sure you want to disconnect the session?"
|
||||||
|
msgstr "Är du säker på att du vill koppla ifrån sessionen?"
|
||||||
|
|
||||||
|
#: ../app/ui.js:1297
|
||||||
msgid "Credentials are required"
|
msgid "Credentials are required"
|
||||||
msgstr "Användaruppgifter krävs"
|
msgstr "Användaruppgifter krävs"
|
||||||
|
|
||||||
|
|
@ -160,11 +164,11 @@ msgstr "Skicka Ctrl-Alt-Del"
|
||||||
|
|
||||||
#: ../vnc.html:163
|
#: ../vnc.html:163
|
||||||
msgid "Shutdown/Reboot"
|
msgid "Shutdown/Reboot"
|
||||||
msgstr "Stäng av/Boota om"
|
msgstr "Stäng av/Starta om"
|
||||||
|
|
||||||
#: ../vnc.html:163
|
#: ../vnc.html:163
|
||||||
msgid "Shutdown/Reboot..."
|
msgid "Shutdown/Reboot..."
|
||||||
msgstr "Stäng av/Boota om..."
|
msgstr "Stäng av/Starta om..."
|
||||||
|
|
||||||
#: ../vnc.html:169
|
#: ../vnc.html:169
|
||||||
msgid "Power"
|
msgid "Power"
|
||||||
|
|
@ -176,7 +180,7 @@ msgstr "Stäng av"
|
||||||
|
|
||||||
#: ../vnc.html:172
|
#: ../vnc.html:172
|
||||||
msgid "Reboot"
|
msgid "Reboot"
|
||||||
msgstr "Boota om"
|
msgstr "Starta om"
|
||||||
|
|
||||||
#: ../vnc.html:173
|
#: ../vnc.html:173
|
||||||
msgid "Reset"
|
msgid "Reset"
|
||||||
|
|
@ -284,7 +288,7 @@ msgstr "Version:"
|
||||||
|
|
||||||
#: ../vnc.html:319
|
#: ../vnc.html:319
|
||||||
msgid "Disconnect"
|
msgid "Disconnect"
|
||||||
msgstr "Koppla från"
|
msgstr "Koppla ifrån"
|
||||||
|
|
||||||
#: ../vnc.html:342
|
#: ../vnc.html:342
|
||||||
msgid "Connect"
|
msgid "Connect"
|
||||||
|
|
@ -292,7 +296,7 @@ msgstr "Anslut"
|
||||||
|
|
||||||
#: ../vnc.html:351
|
#: ../vnc.html:351
|
||||||
msgid "Server identity"
|
msgid "Server identity"
|
||||||
msgstr "Server-identitet"
|
msgstr "Serveridentitet"
|
||||||
|
|
||||||
#: ../vnc.html:354
|
#: ../vnc.html:354
|
||||||
msgid "The server has provided the following identifying information:"
|
msgid "The server has provided the following identifying information:"
|
||||||
|
|
@ -308,7 +312,7 @@ msgid ""
|
||||||
"Otherwise press \"Reject\"."
|
"Otherwise press \"Reject\"."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Kontrollera att informationen är korrekt och tryck sedan \"Godkänn\". Tryck "
|
"Kontrollera att informationen är korrekt och tryck sedan \"Godkänn\". Tryck "
|
||||||
"annars \"Neka\"."
|
"annars \"Avvisa\"."
|
||||||
|
|
||||||
#: ../vnc.html:366
|
#: ../vnc.html:366
|
||||||
msgid "Approve"
|
msgid "Approve"
|
||||||
|
|
@ -316,7 +320,7 @@ msgstr "Godkänn"
|
||||||
|
|
||||||
#: ../vnc.html:367
|
#: ../vnc.html:367
|
||||||
msgid "Reject"
|
msgid "Reject"
|
||||||
msgstr "Neka"
|
msgstr "Avvisa"
|
||||||
|
|
||||||
#: ../vnc.html:375
|
#: ../vnc.html:375
|
||||||
msgid "Credentials"
|
msgid "Credentials"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,341 @@
|
||||||
|
# Ukrainian translation of noVNC.
|
||||||
|
# Copyright (C) 2025 The noVNC authors
|
||||||
|
# This file is distributed under the same license as the noVNC package.
|
||||||
|
# Denys Nykula <nykula@ukr.net>, 2025.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: noVNC 1.6.0\n"
|
||||||
|
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
|
||||||
|
"POT-Creation-Date: 2025-10-31 09:17+0100\n"
|
||||||
|
"PO-Revision-Date: 2025-11-22 20:21+0200\n"
|
||||||
|
"Last-Translator: Denys Nykula <nykula@ukr.net>\n"
|
||||||
|
"Language-Team: Ukrainian <uk@li.org>\n"
|
||||||
|
"Language: uk\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
|
||||||
|
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||||
|
|
||||||
|
#: ../app/ui.js:84
|
||||||
|
msgid ""
|
||||||
|
"Running without HTTPS is not recommended, crashes or other issues are likely."
|
||||||
|
msgstr ""
|
||||||
|
"Робота без HTTPS не рекомендується, ймовірні збої чи інші проблеми."
|
||||||
|
|
||||||
|
#: ../app/ui.js:413
|
||||||
|
msgid "Connecting..."
|
||||||
|
msgstr "З'єднання..."
|
||||||
|
|
||||||
|
#: ../app/ui.js:420
|
||||||
|
msgid "Disconnecting..."
|
||||||
|
msgstr "Від'єднання..."
|
||||||
|
|
||||||
|
#: ../app/ui.js:426
|
||||||
|
msgid "Reconnecting..."
|
||||||
|
msgstr "Перез'єднання..."
|
||||||
|
|
||||||
|
#: ../app/ui.js:431
|
||||||
|
msgid "Internal error"
|
||||||
|
msgstr "Внутрішня помилка"
|
||||||
|
|
||||||
|
#: ../app/ui.js:1084
|
||||||
|
msgid "Failed to connect to server: "
|
||||||
|
msgstr "Не вдалося з'єднатися з сервером: "
|
||||||
|
|
||||||
|
#: ../app/ui.js:1151
|
||||||
|
msgid "Connected (encrypted) to "
|
||||||
|
msgstr "З'єднано (з шифруванням) з "
|
||||||
|
|
||||||
|
#: ../app/ui.js:1153
|
||||||
|
msgid "Connected (unencrypted) to "
|
||||||
|
msgstr "З'єднано (без шифрування) з "
|
||||||
|
|
||||||
|
#: ../app/ui.js:1178
|
||||||
|
msgid "Something went wrong, connection is closed"
|
||||||
|
msgstr "Щось пішло не так, з'єднання закрито"
|
||||||
|
|
||||||
|
#: ../app/ui.js:1181
|
||||||
|
msgid "Failed to connect to server"
|
||||||
|
msgstr "Не вдалося з'єднатися з сервером"
|
||||||
|
|
||||||
|
#: ../app/ui.js:1193
|
||||||
|
msgid "Disconnected"
|
||||||
|
msgstr "Від'єднано"
|
||||||
|
|
||||||
|
#: ../app/ui.js:1210
|
||||||
|
msgid "New connection has been rejected with reason: "
|
||||||
|
msgstr "Нове з'єднання відхилено. Причина: "
|
||||||
|
|
||||||
|
#: ../app/ui.js:1213
|
||||||
|
msgid "New connection has been rejected"
|
||||||
|
msgstr "Нове з'єднання відхилено"
|
||||||
|
|
||||||
|
#: ../app/ui.js:1225
|
||||||
|
msgid "Are you sure you want to disconnect the session?"
|
||||||
|
msgstr "Точно від'єднати сеанс?"
|
||||||
|
|
||||||
|
#: ../app/ui.js:1297
|
||||||
|
msgid "Credentials are required"
|
||||||
|
msgstr "Треба особові дані"
|
||||||
|
|
||||||
|
#: ../vnc.html:106
|
||||||
|
msgid "noVNC encountered an error:"
|
||||||
|
msgstr "Помилка noVNC:"
|
||||||
|
|
||||||
|
#: ../vnc.html:116
|
||||||
|
msgid "Hide/Show the control bar"
|
||||||
|
msgstr "Сховати/показати панель керування"
|
||||||
|
|
||||||
|
#: ../vnc.html:125
|
||||||
|
msgid "Drag"
|
||||||
|
msgstr "Посунути"
|
||||||
|
|
||||||
|
#: ../vnc.html:125
|
||||||
|
msgid "Move/Drag viewport"
|
||||||
|
msgstr "Змістити область огляду"
|
||||||
|
|
||||||
|
#: ../vnc.html:131
|
||||||
|
msgid "Keyboard"
|
||||||
|
msgstr "Клавіатура"
|
||||||
|
|
||||||
|
#: ../vnc.html:131
|
||||||
|
msgid "Show keyboard"
|
||||||
|
msgstr "Показати клавіатуру"
|
||||||
|
|
||||||
|
#: ../vnc.html:136
|
||||||
|
msgid "Extra keys"
|
||||||
|
msgstr "Додаткові клавіші"
|
||||||
|
|
||||||
|
#: ../vnc.html:136
|
||||||
|
msgid "Show extra keys"
|
||||||
|
msgstr "Показати додаткові клавіші"
|
||||||
|
|
||||||
|
#: ../vnc.html:141
|
||||||
|
msgid "Ctrl"
|
||||||
|
msgstr "Ctrl"
|
||||||
|
|
||||||
|
#: ../vnc.html:141
|
||||||
|
msgid "Toggle Ctrl"
|
||||||
|
msgstr "Затиснути Ctrl"
|
||||||
|
|
||||||
|
#: ../vnc.html:144
|
||||||
|
msgid "Alt"
|
||||||
|
msgstr "Alt"
|
||||||
|
|
||||||
|
#: ../vnc.html:144
|
||||||
|
msgid "Toggle Alt"
|
||||||
|
msgstr "Затиснути Alt"
|
||||||
|
|
||||||
|
#: ../vnc.html:147
|
||||||
|
msgid "Toggle Windows"
|
||||||
|
msgstr "Затиснути Windows"
|
||||||
|
|
||||||
|
#: ../vnc.html:147
|
||||||
|
msgid "Windows"
|
||||||
|
msgstr "Windows"
|
||||||
|
|
||||||
|
#: ../vnc.html:150
|
||||||
|
msgid "Send Tab"
|
||||||
|
msgstr "Натиснути Tab"
|
||||||
|
|
||||||
|
#: ../vnc.html:150
|
||||||
|
msgid "Tab"
|
||||||
|
msgstr "Tab"
|
||||||
|
|
||||||
|
#: ../vnc.html:153
|
||||||
|
msgid "Esc"
|
||||||
|
msgstr "Esc"
|
||||||
|
|
||||||
|
#: ../vnc.html:153
|
||||||
|
msgid "Send Escape"
|
||||||
|
msgstr "Натиснути Escape"
|
||||||
|
|
||||||
|
#: ../vnc.html:156
|
||||||
|
msgid "Ctrl+Alt+Del"
|
||||||
|
msgstr "Ctrl+Alt+Del"
|
||||||
|
|
||||||
|
#: ../vnc.html:156
|
||||||
|
msgid "Send Ctrl-Alt-Del"
|
||||||
|
msgstr "Натиснути Ctrl+Alt+Del"
|
||||||
|
|
||||||
|
#: ../vnc.html:163
|
||||||
|
msgid "Shutdown/Reboot"
|
||||||
|
msgstr "Вимкнути/перезавантажити"
|
||||||
|
|
||||||
|
#: ../vnc.html:163
|
||||||
|
msgid "Shutdown/Reboot..."
|
||||||
|
msgstr "Вимкнути/перезавантажити..."
|
||||||
|
|
||||||
|
#: ../vnc.html:169
|
||||||
|
msgid "Power"
|
||||||
|
msgstr "Живлення"
|
||||||
|
|
||||||
|
#: ../vnc.html:171
|
||||||
|
msgid "Shutdown"
|
||||||
|
msgstr "Вимкнути"
|
||||||
|
|
||||||
|
#: ../vnc.html:172
|
||||||
|
msgid "Reboot"
|
||||||
|
msgstr "Перезавантажити"
|
||||||
|
|
||||||
|
#: ../vnc.html:173
|
||||||
|
msgid "Reset"
|
||||||
|
msgstr "Скинути"
|
||||||
|
|
||||||
|
#: ../vnc.html:178 ../vnc.html:184
|
||||||
|
msgid "Clipboard"
|
||||||
|
msgstr "Буфер обміну"
|
||||||
|
|
||||||
|
#: ../vnc.html:186
|
||||||
|
msgid "Edit clipboard content in the textarea below."
|
||||||
|
msgstr "Редагуйте вміст буфера обміну в текстовій зоні внизу."
|
||||||
|
|
||||||
|
#: ../vnc.html:194
|
||||||
|
msgid "Full screen"
|
||||||
|
msgstr "Повний екран"
|
||||||
|
|
||||||
|
#: ../vnc.html:199 ../vnc.html:205
|
||||||
|
msgid "Settings"
|
||||||
|
msgstr "Параметри"
|
||||||
|
|
||||||
|
#: ../vnc.html:211
|
||||||
|
msgid "Shared mode"
|
||||||
|
msgstr "Спільний режим"
|
||||||
|
|
||||||
|
#: ../vnc.html:218
|
||||||
|
msgid "View only"
|
||||||
|
msgstr "Лише перегляд"
|
||||||
|
|
||||||
|
#: ../vnc.html:226
|
||||||
|
msgid "Clip to window"
|
||||||
|
msgstr "До розмірів вікна"
|
||||||
|
|
||||||
|
#: ../vnc.html:231
|
||||||
|
msgid "Scaling mode:"
|
||||||
|
msgstr "Режим масштабування:"
|
||||||
|
|
||||||
|
#: ../vnc.html:233
|
||||||
|
msgid "None"
|
||||||
|
msgstr "Вимкнено"
|
||||||
|
|
||||||
|
#: ../vnc.html:234
|
||||||
|
msgid "Local scaling"
|
||||||
|
msgstr "Локальне масштабування"
|
||||||
|
|
||||||
|
#: ../vnc.html:235
|
||||||
|
msgid "Remote resizing"
|
||||||
|
msgstr "Віддалене масштабування"
|
||||||
|
|
||||||
|
#: ../vnc.html:240
|
||||||
|
msgid "Advanced"
|
||||||
|
msgstr "Додатково"
|
||||||
|
|
||||||
|
#: ../vnc.html:243
|
||||||
|
msgid "Quality:"
|
||||||
|
msgstr "Якість:"
|
||||||
|
|
||||||
|
#: ../vnc.html:247
|
||||||
|
msgid "Compression level:"
|
||||||
|
msgstr "Рівень стиснення:"
|
||||||
|
|
||||||
|
#: ../vnc.html:252
|
||||||
|
msgid "Repeater ID:"
|
||||||
|
msgstr "Ідентифікатор репітера:"
|
||||||
|
|
||||||
|
#: ../vnc.html:256
|
||||||
|
msgid "WebSocket"
|
||||||
|
msgstr "WebSocket"
|
||||||
|
|
||||||
|
#: ../vnc.html:261
|
||||||
|
msgid "Encrypt"
|
||||||
|
msgstr "Шифрування"
|
||||||
|
|
||||||
|
#: ../vnc.html:266
|
||||||
|
msgid "Host:"
|
||||||
|
msgstr "Сервер:"
|
||||||
|
|
||||||
|
#: ../vnc.html:270
|
||||||
|
msgid "Port:"
|
||||||
|
msgstr "Порт:"
|
||||||
|
|
||||||
|
#: ../vnc.html:274
|
||||||
|
msgid "Path:"
|
||||||
|
msgstr "Шлях:"
|
||||||
|
|
||||||
|
#: ../vnc.html:283
|
||||||
|
msgid "Automatic reconnect"
|
||||||
|
msgstr "Автоматичне перез'єднання"
|
||||||
|
|
||||||
|
#: ../vnc.html:288
|
||||||
|
msgid "Reconnect delay (ms):"
|
||||||
|
msgstr "Затримка перез'єднання (мс):"
|
||||||
|
|
||||||
|
#: ../vnc.html:295
|
||||||
|
msgid "Show dot when no cursor"
|
||||||
|
msgstr "Показувати крапку, коли нема курсора"
|
||||||
|
|
||||||
|
#: ../vnc.html:302
|
||||||
|
msgid "Logging:"
|
||||||
|
msgstr "Журнал:"
|
||||||
|
|
||||||
|
#: ../vnc.html:311
|
||||||
|
msgid "Version:"
|
||||||
|
msgstr "Версія:"
|
||||||
|
|
||||||
|
#: ../vnc.html:319
|
||||||
|
msgid "Disconnect"
|
||||||
|
msgstr "Від'єднати"
|
||||||
|
|
||||||
|
#: ../vnc.html:342
|
||||||
|
msgid "Connect"
|
||||||
|
msgstr "З'єднати"
|
||||||
|
|
||||||
|
#: ../vnc.html:351
|
||||||
|
msgid "Server identity"
|
||||||
|
msgstr "Ідентифікація сервера"
|
||||||
|
|
||||||
|
#: ../vnc.html:354
|
||||||
|
msgid "The server has provided the following identifying information:"
|
||||||
|
msgstr "Сервер надає такі ідентифікаційні дані:"
|
||||||
|
|
||||||
|
#: ../vnc.html:357
|
||||||
|
msgid "Fingerprint:"
|
||||||
|
msgstr "Відбиток:"
|
||||||
|
|
||||||
|
#: ../vnc.html:361
|
||||||
|
msgid ""
|
||||||
|
"Please verify that the information is correct and press \"Approve\". "
|
||||||
|
"Otherwise press \"Reject\"."
|
||||||
|
msgstr ""
|
||||||
|
"Перевірте, чи дані коректні, й натисніть «Схвалити». "
|
||||||
|
"Інакше натисніть «Відхилити»."
|
||||||
|
|
||||||
|
#: ../vnc.html:366
|
||||||
|
msgid "Approve"
|
||||||
|
msgstr "Схвалити"
|
||||||
|
|
||||||
|
#: ../vnc.html:367
|
||||||
|
msgid "Reject"
|
||||||
|
msgstr "Відхилити"
|
||||||
|
|
||||||
|
#: ../vnc.html:375
|
||||||
|
msgid "Credentials"
|
||||||
|
msgstr "Особові дані"
|
||||||
|
|
||||||
|
#: ../vnc.html:379
|
||||||
|
msgid "Username:"
|
||||||
|
msgstr "Користувацьке ім'я:"
|
||||||
|
|
||||||
|
#: ../vnc.html:383
|
||||||
|
msgid "Password:"
|
||||||
|
msgstr "Пароль:"
|
||||||
|
|
||||||
|
#: ../vnc.html:387
|
||||||
|
msgid "Send credentials"
|
||||||
|
msgstr "Надіслати особові дані"
|
||||||
|
|
||||||
|
#: ../vnc.html:396
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr "Скасувати"
|
||||||
|
|
@ -1,6 +1,74 @@
|
||||||
import { isMac, isWindows, isIOS, isAndroid, isChromeOS,
|
import { isMac, isWindows, isIOS, isAndroid, isChromeOS,
|
||||||
isSafari, isFirefox, isChrome, isChromium, isOpera, isEdge,
|
isSafari, isFirefox, isChrome, isChromium, isOpera, isEdge,
|
||||||
isGecko, isWebKit, isBlink } from '../core/util/browser.js';
|
isGecko, isWebKit, isBlink,
|
||||||
|
browserAsyncClipboardSupport } from '../core/util/browser.js';
|
||||||
|
|
||||||
|
describe('Async clipboard', function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
sinon.stub(navigator, "clipboard").value({
|
||||||
|
writeText: sinon.stub(),
|
||||||
|
readText: sinon.stub(),
|
||||||
|
});
|
||||||
|
sinon.stub(navigator, "permissions").value({
|
||||||
|
query: sinon.stub().resolves({ state: "granted" })
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
sinon.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("queries permissions with correct parameters", async function () {
|
||||||
|
const queryStub = navigator.permissions.query;
|
||||||
|
await browserAsyncClipboardSupport();
|
||||||
|
expect(queryStub.firstCall).to.have.been.calledWithExactly({
|
||||||
|
name: "clipboard-write",
|
||||||
|
allowWithoutGesture: true
|
||||||
|
});
|
||||||
|
expect(queryStub.secondCall).to.have.been.calledWithExactly({
|
||||||
|
name: "clipboard-read",
|
||||||
|
allowWithoutGesture: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is available when API present and permissions granted", async function () {
|
||||||
|
navigator.permissions.query.resolves({ state: "granted" });
|
||||||
|
const result = await browserAsyncClipboardSupport();
|
||||||
|
expect(result).to.equal('available');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is available when API present and permissions yield 'prompt'", async function () {
|
||||||
|
navigator.permissions.query.resolves({ state: "prompt" });
|
||||||
|
const result = await browserAsyncClipboardSupport();
|
||||||
|
expect(result).to.equal('available');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is unavailable when permissions denied", async function () {
|
||||||
|
navigator.permissions.query.resolves({ state: "denied" });
|
||||||
|
const result = await browserAsyncClipboardSupport();
|
||||||
|
expect(result).to.equal('denied');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is unavailable when permissions API fails", async function () {
|
||||||
|
navigator.permissions.query.rejects(new Error("fail"));
|
||||||
|
const result = await browserAsyncClipboardSupport();
|
||||||
|
expect(result).to.equal('unsupported');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is unavailable when write text API missing", async function () {
|
||||||
|
navigator.clipboard.writeText = undefined;
|
||||||
|
const result = await browserAsyncClipboardSupport();
|
||||||
|
expect(result).to.equal('unsupported');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is unavailable when read text API missing", async function () {
|
||||||
|
navigator.clipboard.readText = undefined;
|
||||||
|
const result = await browserAsyncClipboardSupport();
|
||||||
|
expect(result).to.equal('unsupported');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('OS detection', function () {
|
describe('OS detection', function () {
|
||||||
let origNavigator;
|
let origNavigator;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
import AsyncClipboard from '../core/clipboard.js';
|
||||||
|
|
||||||
|
describe('Async Clipboard', function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
let targetMock;
|
||||||
|
let clipboard;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
sinon.stub(navigator, "clipboard").value({
|
||||||
|
writeText: sinon.stub().resolves(),
|
||||||
|
readText: sinon.stub().resolves(),
|
||||||
|
});
|
||||||
|
|
||||||
|
sinon.stub(navigator, "permissions").value({
|
||||||
|
query: sinon.stub(),
|
||||||
|
});
|
||||||
|
|
||||||
|
targetMock = document.createElement("canvas");
|
||||||
|
clipboard = new AsyncClipboard(targetMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
sinon.restore();
|
||||||
|
targetMock = null;
|
||||||
|
clipboard = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
function stubClipboardPermissions(state) {
|
||||||
|
navigator.permissions.query
|
||||||
|
.withArgs({ name: 'clipboard-write', allowWithoutGesture: true })
|
||||||
|
.resolves({ state: state });
|
||||||
|
navigator.permissions.query
|
||||||
|
.withArgs({ name: 'clipboard-read', allowWithoutGesture: false })
|
||||||
|
.resolves({ state: state });
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextTick() {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
it('grab() adds listener if permissions granted', async function () {
|
||||||
|
stubClipboardPermissions('granted');
|
||||||
|
|
||||||
|
const addListenerSpy = sinon.spy(targetMock, 'addEventListener');
|
||||||
|
clipboard.grab();
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(addListenerSpy.calledWith('focus')).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('grab() does not add listener if permissions denied', async function () {
|
||||||
|
stubClipboardPermissions('denied');
|
||||||
|
|
||||||
|
const addListenerSpy = sinon.spy(targetMock, 'addEventListener');
|
||||||
|
clipboard.grab();
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(addListenerSpy.calledWith('focus')).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('focus event triggers onpaste() if permissions granted', async function () {
|
||||||
|
stubClipboardPermissions('granted');
|
||||||
|
|
||||||
|
const text = 'hello clipboard world';
|
||||||
|
navigator.clipboard.readText.resolves(text);
|
||||||
|
|
||||||
|
const spyPromise = new Promise(resolve => clipboard.onpaste = resolve);
|
||||||
|
|
||||||
|
clipboard.grab();
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
targetMock.dispatchEvent(new Event('focus'));
|
||||||
|
|
||||||
|
const res = await spyPromise;
|
||||||
|
expect(res).to.equal(text);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('focus event does not trigger onpaste() if permissions denied', async function () {
|
||||||
|
stubClipboardPermissions('denied');
|
||||||
|
|
||||||
|
const text = 'should not read';
|
||||||
|
navigator.clipboard.readText.resolves(text);
|
||||||
|
|
||||||
|
clipboard.onpaste = sinon.spy();
|
||||||
|
|
||||||
|
clipboard.grab();
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
targetMock.dispatchEvent(new Event('focus'));
|
||||||
|
|
||||||
|
expect(clipboard.onpaste.called).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('writeClipboard() calls navigator.clipboard.writeText() if permissions granted', async function () {
|
||||||
|
stubClipboardPermissions('granted');
|
||||||
|
clipboard._isAvailable = true;
|
||||||
|
|
||||||
|
const text = 'writing to clipboard';
|
||||||
|
const result = clipboard.writeClipboard(text);
|
||||||
|
|
||||||
|
expect(navigator.clipboard.writeText.calledWith(text)).to.be.true;
|
||||||
|
expect(result).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('writeClipboard() does not call navigator.clipboard.writeText() if permissions denied', async function () {
|
||||||
|
stubClipboardPermissions('denied');
|
||||||
|
clipboard._isAvailable = false;
|
||||||
|
|
||||||
|
const text = 'should not write';
|
||||||
|
const result = clipboard.writeClipboard(text);
|
||||||
|
|
||||||
|
expect(navigator.clipboard.writeText.called).to.be.false;
|
||||||
|
expect(result).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
@ -3467,17 +3467,47 @@ describe('Remote Frame Buffer protocol client', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Normal clipboard handling receive', function () {
|
describe('Normal clipboard handling receive', function () {
|
||||||
it('should fire the clipboard callback with the retrieved text on ServerCutText', function () {
|
it('should not dispatch a clipboard event following successful async write clipboard', async function () {
|
||||||
|
client._viewOnly = false;
|
||||||
|
client._asyncClipboard = {
|
||||||
|
writeClipboard: sinon.stub().returns(true),
|
||||||
|
};
|
||||||
const expectedStr = 'cheese!';
|
const expectedStr = 'cheese!';
|
||||||
const data = [3, 0, 0, 0];
|
const data = [3, 0, 0, 0];
|
||||||
push32(data, expectedStr.length);
|
push32(data, expectedStr.length);
|
||||||
for (let i = 0; i < expectedStr.length; i++) { data.push(expectedStr.charCodeAt(i)); }
|
for (let i = 0; i < expectedStr.length; i++) { data.push(expectedStr.charCodeAt(i)); }
|
||||||
const spy = sinon.spy();
|
|
||||||
client.addEventListener("clipboard", spy);
|
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
|
||||||
|
|
||||||
client._sock._websocket._receiveData(new Uint8Array(data));
|
client._sock._websocket._receiveData(new Uint8Array(data));
|
||||||
expect(spy).to.have.been.calledOnce;
|
|
||||||
expect(spy.args[0][0].detail.text).to.equal(expectedStr);
|
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
|
||||||
|
expectedStr
|
||||||
|
)).to.be.true;
|
||||||
|
expect(dispatchEventSpy.calledWith(
|
||||||
|
new CustomEvent("clipboard", {detail: {text: expectedStr}})
|
||||||
|
)).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dispatch a clipboard event following unsuccessful async write clipboard', async function () {
|
||||||
|
client._viewOnly = false;
|
||||||
|
client._asyncClipboard = {
|
||||||
|
writeClipboard: sinon.stub().returns(false),
|
||||||
|
};
|
||||||
|
const expectedStr = 'cheese!';
|
||||||
|
const data = [3, 0, 0, 0];
|
||||||
|
push32(data, expectedStr.length);
|
||||||
|
for (let i = 0; i < expectedStr.length; i++) { data.push(expectedStr.charCodeAt(i)); }
|
||||||
|
|
||||||
|
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
|
||||||
|
|
||||||
|
client._sock._websocket._receiveData(new Uint8Array(data));
|
||||||
|
|
||||||
|
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
|
||||||
|
expectedStr
|
||||||
|
)).to.be.true;
|
||||||
|
expect(dispatchEventSpy.args[0][0].type).to.equal("clipboard");
|
||||||
|
expect(dispatchEventSpy.args[0][0].detail.text).to.equal(expectedStr);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -3530,8 +3560,70 @@ describe('Remote Frame Buffer protocol client', function () {
|
||||||
client._sock._websocket._receiveData(new Uint8Array(data));
|
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 () {
|
describe('Handle Provide', function () {
|
||||||
it('should update clipboard with correct Unicode data from a Provide message', function () {
|
it('should update clipboard with correct Unicode data from a Provide message', async function () {
|
||||||
|
client._viewOnly = false;
|
||||||
|
client._asyncClipboard = {
|
||||||
|
writeClipboard: sinon.stub().returns(false),
|
||||||
|
};
|
||||||
let expectedData = "Aå漢字!";
|
let expectedData = "Aå漢字!";
|
||||||
let data = [3, 0, 0, 0];
|
let data = [3, 0, 0, 0];
|
||||||
const flags = [0x10, 0x00, 0x00, 0x01];
|
const flags = [0x10, 0x00, 0x00, 0x01];
|
||||||
|
|
@ -3545,16 +3637,22 @@ describe('Remote Frame Buffer protocol client', function () {
|
||||||
data = data.concat(flags);
|
data = data.concat(flags);
|
||||||
data = data.concat(Array.from(deflatedText));
|
data = data.concat(Array.from(deflatedText));
|
||||||
|
|
||||||
const spy = sinon.spy();
|
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
|
||||||
client.addEventListener("clipboard", spy);
|
|
||||||
|
|
||||||
client._sock._websocket._receiveData(new Uint8Array(data));
|
client._sock._websocket._receiveData(new Uint8Array(data));
|
||||||
expect(spy).to.have.been.calledOnce;
|
|
||||||
expect(spy.args[0][0].detail.text).to.equal(expectedData);
|
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
|
||||||
client.removeEventListener("clipboard", spy);
|
expectedData
|
||||||
|
)).to.be.true;
|
||||||
|
expect(dispatchEventSpy.args[0][0].type).to.equal("clipboard");
|
||||||
|
expect(dispatchEventSpy.args[0][0].detail.text).to.equal(expectedData);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update clipboard with correct escape characters from a Provide message ', function () {
|
it('should update clipboard with correct escape characters from a Provide message ', async function () {
|
||||||
|
client._viewOnly = false;
|
||||||
|
client._asyncClipboard = {
|
||||||
|
writeClipboard: sinon.stub().returns(false),
|
||||||
|
};
|
||||||
let expectedData = "Oh\nmy\n!";
|
let expectedData = "Oh\nmy\n!";
|
||||||
let data = [3, 0, 0, 0];
|
let data = [3, 0, 0, 0];
|
||||||
const flags = [0x10, 0x00, 0x00, 0x01];
|
const flags = [0x10, 0x00, 0x00, 0x01];
|
||||||
|
|
@ -3569,16 +3667,22 @@ describe('Remote Frame Buffer protocol client', function () {
|
||||||
data = data.concat(flags);
|
data = data.concat(flags);
|
||||||
data = data.concat(Array.from(deflatedText));
|
data = data.concat(Array.from(deflatedText));
|
||||||
|
|
||||||
const spy = sinon.spy();
|
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
|
||||||
client.addEventListener("clipboard", spy);
|
|
||||||
|
|
||||||
client._sock._websocket._receiveData(new Uint8Array(data));
|
client._sock._websocket._receiveData(new Uint8Array(data));
|
||||||
expect(spy).to.have.been.calledOnce;
|
|
||||||
expect(spy.args[0][0].detail.text).to.equal(expectedData);
|
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
|
||||||
client.removeEventListener("clipboard", spy);
|
expectedData
|
||||||
|
)).to.be.true;
|
||||||
|
expect(dispatchEventSpy.args[0][0].type).to.equal("clipboard");
|
||||||
|
expect(dispatchEventSpy.args[0][0].detail.text).to.equal(expectedData);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to handle large Provide messages', function () {
|
it('should be able to handle large Provide messages', async function () {
|
||||||
|
client._viewOnly = false;
|
||||||
|
client._asyncClipboard = {
|
||||||
|
writeClipboard: sinon.stub().returns(false),
|
||||||
|
};
|
||||||
let expectedData = "hello".repeat(100000);
|
let expectedData = "hello".repeat(100000);
|
||||||
let data = [3, 0, 0, 0];
|
let data = [3, 0, 0, 0];
|
||||||
const flags = [0x10, 0x00, 0x00, 0x01];
|
const flags = [0x10, 0x00, 0x00, 0x01];
|
||||||
|
|
@ -3593,13 +3697,15 @@ describe('Remote Frame Buffer protocol client', function () {
|
||||||
data = data.concat(flags);
|
data = data.concat(flags);
|
||||||
data = data.concat(Array.from(deflatedText));
|
data = data.concat(Array.from(deflatedText));
|
||||||
|
|
||||||
const spy = sinon.spy();
|
const dispatchEventSpy = sinon.spy(client, 'dispatchEvent');
|
||||||
client.addEventListener("clipboard", spy);
|
|
||||||
|
|
||||||
client._sock._websocket._receiveData(new Uint8Array(data));
|
client._sock._websocket._receiveData(new Uint8Array(data));
|
||||||
expect(spy).to.have.been.calledOnce;
|
|
||||||
expect(spy.args[0][0].detail.text).to.equal(expectedData);
|
expect(client._asyncClipboard.writeClipboard.calledOnceWith(
|
||||||
client.removeEventListener("clipboard", spy);
|
expectedData
|
||||||
|
)).to.be.true;
|
||||||
|
expect(dispatchEventSpy.args[0][0].type).to.equal("clipboard");
|
||||||
|
expect(dispatchEventSpy.args[0][0].detail.text).to.equal(expectedData);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
/* jshint expr: true */
|
||||||
|
|
||||||
|
import WakeLockManager from '../app/wakelock.js';
|
||||||
|
|
||||||
|
class FakeWakeLockSentinal extends EventTarget {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.released = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async release() {
|
||||||
|
if (this.released) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.released = true;
|
||||||
|
this.dispatchEvent(new Event("release"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForStateTransition(wakelockManager, newState) {
|
||||||
|
const {promise, resolve} = Promise.withResolvers();
|
||||||
|
|
||||||
|
const eventListener = (event) => {
|
||||||
|
if (event.newState !== newState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wakelockManager.removeEventListener("testOnlyStateChange", eventListener);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
wakelockManager.addEventListener("testOnlyStateChange", eventListener);
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('WakeLockManager', function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
let wakelockRequest;
|
||||||
|
beforeEach(function () {
|
||||||
|
wakelockRequest = sinon.stub(navigator.wakeLock, 'request');
|
||||||
|
});
|
||||||
|
afterEach(function () {
|
||||||
|
wakelockRequest.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can acquire and release lock', async function () {
|
||||||
|
let wakeLockSentinal = new FakeWakeLockSentinal();
|
||||||
|
wakelockRequest.onFirstCall().resolves(wakeLockSentinal);
|
||||||
|
|
||||||
|
let wlm = new WakeLockManager();
|
||||||
|
expect(wakelockRequest).to.not.have.been.called;
|
||||||
|
|
||||||
|
let done = waitForStateTransition(wlm, 'acquired');
|
||||||
|
wlm.acquire();
|
||||||
|
await done;
|
||||||
|
expect(wakelockRequest).to.have.been.calledOnce;
|
||||||
|
expect(wakeLockSentinal.released).to.be.false;
|
||||||
|
|
||||||
|
done = waitForStateTransition(wlm, 'released');
|
||||||
|
wlm.release();
|
||||||
|
await done;
|
||||||
|
expect(wakelockRequest).to.have.been.calledOnce;
|
||||||
|
expect(wakeLockSentinal.released).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can release without holding wakelock', async function () {
|
||||||
|
let wlm = new WakeLockManager();
|
||||||
|
wlm.release();
|
||||||
|
expect(wakelockRequest).to.not.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can release while waiting for wakelock', async function () {
|
||||||
|
let wakeLockSentinal = new FakeWakeLockSentinal();
|
||||||
|
let {promise, resolve} = Promise.withResolvers();
|
||||||
|
|
||||||
|
wakelockRequest.onFirstCall().returns(promise);
|
||||||
|
|
||||||
|
let wlm = new WakeLockManager();
|
||||||
|
expect(wakelockRequest).to.not.have.been.called;
|
||||||
|
|
||||||
|
let seenAcquiring = waitForStateTransition(wlm, 'acquiring');
|
||||||
|
let seenReleasing = waitForStateTransition(wlm, 'releasing');
|
||||||
|
let seenReleased = waitForStateTransition(wlm, 'released');
|
||||||
|
|
||||||
|
wlm.acquire();
|
||||||
|
await seenAcquiring;
|
||||||
|
expect(wakelockRequest).to.have.been.calledOnce;
|
||||||
|
|
||||||
|
// We can call acquire multiple times, while waiting for the promise
|
||||||
|
// to resolve.
|
||||||
|
wlm.acquire();
|
||||||
|
// It should not request a second wakelock.
|
||||||
|
expect(wakelockRequest).to.have.been.calledOnce;
|
||||||
|
|
||||||
|
wlm.release();
|
||||||
|
await seenReleasing;
|
||||||
|
|
||||||
|
expect(wakeLockSentinal.released).to.be.false;
|
||||||
|
|
||||||
|
// Now return the wake lock, we should immediately release it.
|
||||||
|
resolve(wakeLockSentinal);
|
||||||
|
await seenReleased;
|
||||||
|
expect(wakeLockSentinal.released).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles visibility loss', async function () {
|
||||||
|
let documentHidden = sinon.stub(document, 'hidden');
|
||||||
|
let documentVisibility = sinon.stub(document, 'visibilityState');
|
||||||
|
afterEach(function () {
|
||||||
|
documentHidden.restore();
|
||||||
|
documentVisibility.restore();
|
||||||
|
});
|
||||||
|
documentHidden.value(false);
|
||||||
|
documentVisibility.value('visible');
|
||||||
|
|
||||||
|
let wakeLockSentinal1 = new FakeWakeLockSentinal();
|
||||||
|
let wakeLockSentinal2 = new FakeWakeLockSentinal();
|
||||||
|
wakelockRequest.onFirstCall().resolves(wakeLockSentinal1);
|
||||||
|
wakelockRequest.onSecondCall().resolves(wakeLockSentinal2);
|
||||||
|
|
||||||
|
let wlm = new WakeLockManager();
|
||||||
|
let seenAcquired = waitForStateTransition(wlm, 'acquired');
|
||||||
|
let seenAwaitingVisible = waitForStateTransition(wlm, 'awaiting_visible');
|
||||||
|
|
||||||
|
wlm.acquire();
|
||||||
|
await seenAcquired;
|
||||||
|
expect(wakelockRequest).to.have.been.calledOnce;
|
||||||
|
|
||||||
|
// Fake a visibility change.
|
||||||
|
documentHidden.value(true);
|
||||||
|
documentVisibility.value('hidden');
|
||||||
|
wakeLockSentinal1.release();
|
||||||
|
|
||||||
|
await seenAwaitingVisible;
|
||||||
|
seenAcquired = waitForStateTransition(wlm, 'acquired');
|
||||||
|
|
||||||
|
// Fake a visibility change back
|
||||||
|
documentHidden.value(false);
|
||||||
|
documentVisibility.value('visible');
|
||||||
|
document.dispatchEvent(new Event('visibilitychange'));
|
||||||
|
await seenAcquired;
|
||||||
|
|
||||||
|
expect(wakelockRequest).to.have.been.calledTwice;
|
||||||
|
expect(wakeLockSentinal2.released).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can start hidden', async function () {
|
||||||
|
let documentHidden = sinon.stub(document, 'hidden');
|
||||||
|
let documentVisibility = sinon.stub(document, 'visibilityState');
|
||||||
|
afterEach(function () {
|
||||||
|
documentHidden.restore();
|
||||||
|
documentVisibility.restore();
|
||||||
|
});
|
||||||
|
documentHidden.value(true);
|
||||||
|
documentVisibility.value('hidden');
|
||||||
|
|
||||||
|
let wakeLockSentinal = new FakeWakeLockSentinal();
|
||||||
|
wakelockRequest.onFirstCall().resolves(wakeLockSentinal);
|
||||||
|
|
||||||
|
let wlm = new WakeLockManager();
|
||||||
|
let seenAwaitingVisible = waitForStateTransition(wlm, 'awaiting_visible');
|
||||||
|
let seenAcquired = waitForStateTransition(wlm, 'acquired');
|
||||||
|
|
||||||
|
wlm.acquire();
|
||||||
|
await seenAwaitingVisible;
|
||||||
|
expect(wakelockRequest).to.not.have.been.called;
|
||||||
|
|
||||||
|
// Fake a visibility change.
|
||||||
|
documentHidden.value(false);
|
||||||
|
documentVisibility.value('visible');
|
||||||
|
document.dispatchEvent(new Event('visibilitychange'));
|
||||||
|
await seenAcquired;
|
||||||
|
|
||||||
|
expect(wakelockRequest).to.have.been.calledOnce;
|
||||||
|
expect(wakeLockSentinal.released).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles acquire errors', async function () {
|
||||||
|
wakelockRequest.onFirstCall().rejects('WakeLockError');
|
||||||
|
let wakeLockSentinal = new FakeWakeLockSentinal();
|
||||||
|
wakelockRequest.onSecondCall().resolves(wakeLockSentinal);
|
||||||
|
|
||||||
|
let wlm = new WakeLockManager();
|
||||||
|
|
||||||
|
let seenError = waitForStateTransition(wlm, 'error');
|
||||||
|
wlm.acquire();
|
||||||
|
await seenError;
|
||||||
|
expect(wakelockRequest).to.have.been.calledOnce;
|
||||||
|
|
||||||
|
// Even though we saw an error previously, it will retry when
|
||||||
|
// requested.
|
||||||
|
let seenAcquired = waitForStateTransition(wlm, 'acquired');
|
||||||
|
wlm.acquire();
|
||||||
|
await seenAcquired;
|
||||||
|
expect(wakelockRequest).to.have.been.calledTwice;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -222,9 +222,9 @@ fi
|
||||||
|
|
||||||
echo -e "\n\nNavigate to this URL:\n"
|
echo -e "\n\nNavigate to this URL:\n"
|
||||||
if [ "x$SSLONLY" == "x" ]; then
|
if [ "x$SSLONLY" == "x" ]; then
|
||||||
echo -e " http://${HOST}:${PORT}/vnc.html?host=${HOST}&port=${PORT}\n"
|
echo -e " http://${HOST}:${PORT}/vnc.html\n"
|
||||||
else
|
else
|
||||||
echo -e " https://${HOST}:${PORT}/vnc.html?host=${HOST}&port=${PORT}\n"
|
echo -e " https://${HOST}:${PORT}/vnc.html\n"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "Press Ctrl-C to exit\n\n"
|
echo -e "Press Ctrl-C to exit\n\n"
|
||||||
|
|
|
||||||
|
|
@ -50,14 +50,6 @@ for fn in "$@"; do
|
||||||
error=$(echo $line | cut -d ":" -f 4-)
|
error=$(echo $line | cut -d ":" -f 4-)
|
||||||
|
|
||||||
case $error in
|
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("*)
|
*"\"clip-path\": \"path("*)
|
||||||
# FIXME: https://github.com/validator/validator/issues/1786
|
# FIXME: https://github.com/validator/validator/issues/1786
|
||||||
echo "Ignoring below error on line ${line_start}," \
|
echo "Ignoring below error on line ${line_start}," \
|
||||||
|
|
|
||||||
39
vnc.html
39
vnc.html
|
|
@ -110,14 +110,14 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- noVNC control bar -->
|
<!-- noVNC control bar -->
|
||||||
<div id="noVNC_control_bar_anchor" class="noVNC_vcenter">
|
<div id="noVNC_control_bar_anchor" class="noVNC_crosscenter">
|
||||||
|
|
||||||
<div id="noVNC_control_bar">
|
<div id="noVNC_control_bar">
|
||||||
<div id="noVNC_control_bar_handle" title="Hide/Show the control bar"><div></div></div>
|
<div id="noVNC_control_bar_handle" title="Hide/Show the control bar"><div></div></div>
|
||||||
|
|
||||||
<div class="noVNC_scroll">
|
<div class="noVNC_scroll">
|
||||||
|
|
||||||
<h1 class="noVNC_logo" translate="no"><span>no</span><br>VNC</h1>
|
<img class="noVNC_logo" src="app/images/icons/novnc-icon-35x21.svg"
|
||||||
|
alt="noVNC">
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
|
@ -127,16 +127,14 @@
|
||||||
title="Move/Drag viewport">
|
title="Move/Drag viewport">
|
||||||
|
|
||||||
<!--noVNC touch device only buttons-->
|
<!--noVNC touch device only buttons-->
|
||||||
<div id="noVNC_mobile_buttons">
|
|
||||||
<input type="image" alt="Keyboard" src="app/images/keyboard.svg"
|
<input type="image" alt="Keyboard" src="app/images/keyboard.svg"
|
||||||
id="noVNC_keyboard_button" class="noVNC_button" title="Show keyboard">
|
id="noVNC_keyboard_button" class="noVNC_button" title="Show keyboard">
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Extra manual keys -->
|
<!-- Extra manual keys -->
|
||||||
<input type="image" alt="Extra keys" src="app/images/toggleextrakeys.svg"
|
<input type="image" alt="Extra keys" src="app/images/toggleextrakeys.svg"
|
||||||
id="noVNC_toggle_extra_keys_button" class="noVNC_button"
|
id="noVNC_toggle_extra_keys_button" class="noVNC_button"
|
||||||
title="Show extra keys">
|
title="Show extra keys">
|
||||||
<div class="noVNC_vcenter">
|
<div class="noVNC_crosscenter">
|
||||||
<div id="noVNC_modifiers" class="noVNC_panel">
|
<div id="noVNC_modifiers" class="noVNC_panel">
|
||||||
<input type="image" alt="Ctrl" src="app/images/ctrl.svg"
|
<input type="image" alt="Ctrl" src="app/images/ctrl.svg"
|
||||||
id="noVNC_toggle_ctrl_button" class="noVNC_button"
|
id="noVNC_toggle_ctrl_button" class="noVNC_button"
|
||||||
|
|
@ -163,7 +161,7 @@
|
||||||
<input type="image" alt="Shutdown/Reboot" src="app/images/power.svg"
|
<input type="image" alt="Shutdown/Reboot" src="app/images/power.svg"
|
||||||
id="noVNC_power_button" class="noVNC_button"
|
id="noVNC_power_button" class="noVNC_button"
|
||||||
title="Shutdown/Reboot...">
|
title="Shutdown/Reboot...">
|
||||||
<div class="noVNC_vcenter">
|
<div class="noVNC_crosscenter">
|
||||||
<div id="noVNC_power" class="noVNC_panel">
|
<div id="noVNC_power" class="noVNC_panel">
|
||||||
<div class="noVNC_heading">
|
<div class="noVNC_heading">
|
||||||
<img alt="" src="app/images/power.svg"> Power
|
<img alt="" src="app/images/power.svg"> Power
|
||||||
|
|
@ -178,7 +176,7 @@
|
||||||
<input type="image" alt="Clipboard" src="app/images/clipboard.svg"
|
<input type="image" alt="Clipboard" src="app/images/clipboard.svg"
|
||||||
id="noVNC_clipboard_button" class="noVNC_button"
|
id="noVNC_clipboard_button" class="noVNC_button"
|
||||||
title="Clipboard">
|
title="Clipboard">
|
||||||
<div class="noVNC_vcenter">
|
<div class="noVNC_crosscenter">
|
||||||
<div id="noVNC_clipboard" class="noVNC_panel">
|
<div id="noVNC_clipboard" class="noVNC_panel">
|
||||||
<div class="noVNC_heading">
|
<div class="noVNC_heading">
|
||||||
<img alt="" src="app/images/clipboard.svg"> Clipboard
|
<img alt="" src="app/images/clipboard.svg"> Clipboard
|
||||||
|
|
@ -199,7 +197,7 @@
|
||||||
<input type="image" alt="Settings" src="app/images/settings.svg"
|
<input type="image" alt="Settings" src="app/images/settings.svg"
|
||||||
id="noVNC_settings_button" class="noVNC_button"
|
id="noVNC_settings_button" class="noVNC_button"
|
||||||
title="Settings">
|
title="Settings">
|
||||||
<div class="noVNC_vcenter">
|
<div class="noVNC_crosscenter">
|
||||||
<div id="noVNC_settings" class="noVNC_panel">
|
<div id="noVNC_settings" class="noVNC_panel">
|
||||||
<div class="noVNC_heading">
|
<div class="noVNC_heading">
|
||||||
<img alt="" src="app/images/settings.svg"> Settings
|
<img alt="" src="app/images/settings.svg"> Settings
|
||||||
|
|
@ -296,6 +294,13 @@
|
||||||
Show dot when no cursor
|
Show dot when no cursor
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</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>
|
<li><hr></li>
|
||||||
<!-- Logging selection dropdown -->
|
<!-- Logging selection dropdown -->
|
||||||
<li>
|
<li>
|
||||||
|
|
@ -325,8 +330,20 @@
|
||||||
|
|
||||||
</div> <!-- End of noVNC_control_bar -->
|
</div> <!-- End of noVNC_control_bar -->
|
||||||
|
|
||||||
<div id="noVNC_hint_anchor" class="noVNC_vcenter">
|
<div class="noVNC_hint_anchor noVNC_crosscenter">
|
||||||
<div id="noVNC_control_bar_hint">
|
<div class="noVNC_control_bar_hint">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="noVNC_hint_anchor noVNC_right noVNC_crosscenter">
|
||||||
|
<div class="noVNC_control_bar_hint">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="noVNC_hint_anchor noVNC_top noVNC_crosscenter">
|
||||||
|
<div class="noVNC_control_bar_hint">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="noVNC_hint_anchor noVNC_bottom noVNC_crosscenter">
|
||||||
|
<div class="noVNC_control_bar_hint">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue