Merge branch 'master' into reconnect_timeout

This commit is contained in:
Ankit Kumar 2025-11-19 10:36:32 +05:30 committed by GitHub
commit 5d7e66bd87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 1715 additions and 581 deletions

View File

@ -9,6 +9,9 @@ on:
jobs:
npm:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- run: |
@ -18,6 +21,9 @@ jobs:
if: github.event_name != 'release'
- uses: actions/setup-node@v4
with:
# Node 24 is needed to get npm > 11.5.1, which is a requirement for
# OIDC auth.
node-version: 24
# Needs to be explicitly specified for auth to work
registry-url: 'https://registry.npmjs.org'
- run: npm install
@ -26,22 +32,16 @@ jobs:
name: npm
path: lib
- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
if: |
github.repository == 'novnc/noVNC' &&
github.event_name == 'release' &&
!github.event.release.prerelease
- run: npm publish --access public --tag beta
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
if: |
github.repository == 'novnc/noVNC' &&
github.event_name == 'release' &&
github.event.release.prerelease
- run: npm publish --access public --tag dev
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
if: |
github.repository == 'novnc/noVNC' &&
github.event_name == 'push' &&

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

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

View File

@ -9,7 +9,7 @@
import * as Log from '../core/util/logging.js';
import _, { l10n } from './localization.js';
import { isTouchDevice, isMac, isIOS, isAndroid, isChromeOS, isSafari,
hasScrollbarGutter, dragThreshold }
hasScrollbarGutter, dragThreshold, browserAsyncClipboardSupport }
from '../core/util/browser.js';
import { setCapture, getPointerEvent } from '../core/util/events.js';
import KeyTable from "../core/input/keysym.js";
@ -20,7 +20,7 @@ import * as WebUtil from "./webutil.js";
const PAGE_TITLE = "noVNC";
const LINGUAS = ["cs", "de", "el", "es", "fr", "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", "zh_CN", "zh_TW"];
const UI = {
@ -1107,6 +1107,7 @@ const UI = {
UI.rfb.showDotCursor = UI.getSetting('show_dot');
UI.updateViewOnly(); // requires UI.rfb
UI.updateClipboard();
},
disconnect() {
@ -1175,6 +1176,9 @@ const UI = {
UI.updateVisualState('connected');
// Here we can reset the retry count
UI.resetFirstReconnection();
UI.updateBeforeUnload();
// Do this last because it can only be used on rendered elements
UI.rfb.focus();
},
@ -1211,6 +1215,8 @@ const UI = {
UI.showStatus(_("Disconnected"), 'normal');
}
UI.updateBeforeUnload();
document.title = PAGE_TITLE;
UI.openControlbar();
@ -1231,6 +1237,24 @@ const UI = {
UI.showStatus(msg, 'error');
},
handleBeforeUnload(e) {
// Trigger a "Leave site?" warning prompt before closing the
// page. Modern browsers (Oct 2025) accept either (or both)
// preventDefault() or a nonempty returnValue, though the latter is
// considered legacy. The custom string is ignored by modern browsers,
// which display a native message, but older browsers will show it.
e.preventDefault();
e.returnValue = _("Are you sure you want to disconnect the session?");
},
updateBeforeUnload() {
// Remove first to avoid adding duplicates
window.removeEventListener("beforeunload", UI.handleBeforeUnload);
if (!UI.rfb?.viewOnly && UI.connected) {
window.addEventListener("beforeunload", UI.handleBeforeUnload);
}
},
/* ------^-------
* /CONNECTION
* ==============
@ -1757,6 +1781,8 @@ const UI = {
if (!UI.rfb) return;
UI.rfb.viewOnly = UI.getSetting('view_only');
UI.updateBeforeUnload();
// Hide input related buttons in view only mode
if (UI.rfb.viewOnly) {
document.getElementById('noVNC_keyboard_button')
@ -1775,6 +1801,31 @@ const UI = {
}
},
updateClipboard() {
browserAsyncClipboardSupport()
.then((support) => {
if (support === 'unsupported') {
// Use fallback clipboard panel
return;
}
if (support === 'denied' || support === 'available') {
UI.closeClipboardPanel();
document.getElementById('noVNC_clipboard_button')
.classList.add('noVNC_hidden');
document.getElementById('noVNC_clipboard_button')
.removeEventListener('click', UI.toggleClipboardPanel);
document.getElementById('noVNC_clipboard_text')
.removeEventListener('change', UI.clipboardSend);
if (UI.rfb) {
UI.rfb.removeEventListener('clipboard', UI.clipboardReceive);
}
}
})
.catch(() => {
// Treat as unsupported
});
},
updateShowDotCursor() {
if (!UI.rfb) return;
UI.rfb.showDotCursor = UI.getSetting('show_dot');

72
core/clipboard.js Normal file
View File

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

View File

@ -119,18 +119,33 @@ export default class JPEGDecoder {
let extra = 0;
if (type === 0xDA) {
// start of scan
extra += 2;
while (true) {
if (sock.rQwait("JPEG", length-2+extra, 4)) {
if (sock.rQwait("JPEG", length-2 + 2, 4)) {
return null;
}
let data = sock.rQpeekBytes(length-2+extra, false);
if (data.at(-2) === 0xFF && data.at(-1) !== 0x00 &&
!(data.at(-1) >= 0xD0 && data.at(-1) <= 0xD7)) {
extra -= 2;
break;
let len = sock.rQlen();
let data = sock.rQpeekBytes(len, false);
while (true) {
let idx = data.indexOf(0xFF, length-2+extra);
if (idx === -1) {
sock.rQwait("JPEG", Infinity, 4);
return null;
}
extra++;
if (idx === len-1) {
sock.rQwait("JPEG", Infinity, 4);
return null;
}
if (data.at(idx+1) === 0x00 ||
(data.at(idx+1) >= 0xD0 && data.at(idx+1) <= 0xD7)) {
extra = idx+2 - (length-2);
continue;
}
extra = idx - (length-2);
break;
}
}

View File

@ -521,6 +521,9 @@ export default class Display {
return;
}
this.drawImage(a.img, a.x, a.y);
// This helps the browser free the memory right
// away, rather than ballooning
a.img.src = "";
} else {
a.img._noVNCDisplay = this;
a.img.addEventListener('load', this._resumeRenderQ);

View File

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

View File

@ -11,6 +11,39 @@
import * as Log from './logging.js';
import Base64 from '../base64.js';
// Async clipboard detection
/* Evaluates if there is browser support for the async clipboard API and
* relevant clipboard permissions. Returns 'unsupported' if permission states
* cannot be resolved. On the other hand, detecting 'granted' or 'prompt'
* permission states for both read and write indicates full API support with no
* imposed native browser paste prompt. Conversely, detecting 'denied' indicates
* the user elected to disable clipboard.
*/
export async function browserAsyncClipboardSupport() {
if (!(navigator?.permissions?.query &&
navigator?.clipboard?.writeText &&
navigator?.clipboard?.readText)) {
return 'unsupported';
}
try {
const writePerm = await navigator.permissions.query(
{name: "clipboard-write", allowWithoutGesture: true});
const readPerm = await navigator.permissions.query(
{name: "clipboard-read", allowWithoutGesture: false});
if (writePerm.state === "denied" || readPerm.state === "denied") {
return 'denied';
}
if ((writePerm.state === "granted" || writePerm.state === "prompt") &&
(readPerm.state === "granted" || readPerm.state === "prompt")) {
return 'available';
}
} catch {
return 'unsupported';
}
return 'unsupported';
}
// Touch detection
export let isTouchDevice = ('ontouchstart' in document.documentElement) ||
// required for Chrome debugger

View File

@ -124,6 +124,10 @@ export default class Websock {
return res >>> 0;
}
rQlen() {
return this._rQlen - this._rQi;
}
rQshiftStr(len) {
let str = "";
// Handle large arrays in steps to avoid long strings on the stack

View File

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

View File

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

View File

@ -89,9 +89,6 @@ Currently, the following options are available:
* `compression` - The session compression level. Can be `0` to `9`.
* `show_dot` - If a dot cursor should be shown when the remote server provides
no local cursor, or provides a fully-transparent (invisible) cursor.
* `logging` - The console log level. Can be one of `error`, `warn`, `info` or
`debug`.

View File

@ -2,24 +2,20 @@
"name": "@novnc/novnc",
"version": "1.6.0",
"description": "An HTML5 VNC client",
"browser": "lib/rfb",
"directories": {
"lib": "lib",
"doc": "docs",
"test": "tests"
},
"type": "module",
"files": [
"lib",
"core",
"vendor",
"AUTHORS",
"VERSION",
"docs/API.md",
"docs/LIBRARY.md",
"docs/LICENSE*"
],
"exports": "./core/rfb.js",
"scripts": {
"lint": "eslint app core po/po2js po/xgettext-html tests utils",
"test": "karma start karma.conf.js",
"prepublish": "node ./utils/convert.js --clean"
"test": "karma start karma.conf.cjs"
},
"repository": {
"type": "git",

View File

@ -2,7 +2,7 @@ all:
.PHONY: update-po update-js update-pot
.PHONY: FORCE
LINGUAS := cs de el es fr 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 zh_CN zh_TW
VERSION := $(shell grep '"version"' ../package.json | cut -d '"' -f 4)

338
po/hr.po Normal file
View File

@ -0,0 +1,338 @@
# Croatian translations for noVNC package
# Hrvatski prijevod za noVNC paket
# Copyright (C) 2025 The noVNC authors
# This file is distributed under the same license as the noVNC package.
# Milo Ivir <mail@milotype.de>, 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-08-25 18:24+0200\n"
"Last-Translator: Milo Ivir <mail@mivirtype.de>\n"
"Language-Team: \n"
"Language: hr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.7\n"
#: ../app/ui.js:84
msgid ""
"Running without HTTPS is not recommended, crashes or other issues are likely."
msgstr ""
"Pokretanje bez HTTPS-a se ne preporučuje, vjerojatno će se dogoditi prekidi "
"rada ili drugi problemi."
#: ../app/ui.js:413
msgid "Connecting..."
msgstr "Povezivanje …"
#: ../app/ui.js:420
msgid "Disconnecting..."
msgstr "Odspajanje …"
#: ../app/ui.js:426
msgid "Reconnecting..."
msgstr "Ponovno povezivanje …"
#: ../app/ui.js:431
msgid "Internal error"
msgstr "Interna greška"
#: ../app/ui.js:1079
msgid "Failed to connect to server: "
msgstr "Povezivanje sa serverom nije uspjelo: "
#: ../app/ui.js:1145
msgid "Connected (encrypted) to "
msgstr "Povezano (šifrirano) na "
#: ../app/ui.js:1147
msgid "Connected (unencrypted) to "
msgstr "Povezano (nešifrirano) na "
#: ../app/ui.js:1170
msgid "Something went wrong, connection is closed"
msgstr "Nešto nije u redu, veza je zatvorena"
#: ../app/ui.js:1173
msgid "Failed to connect to server"
msgstr "Povezivanje sa serverom nije uspjelo"
#: ../app/ui.js:1185
msgid "Disconnected"
msgstr "Odspojeno"
#: ../app/ui.js:1200
msgid "New connection has been rejected with reason: "
msgstr "Nova veza je odbijena s razlogom: "
#: ../app/ui.js:1203
msgid "New connection has been rejected"
msgstr "Nova veza je odbijena"
#: ../app/ui.js:1269
msgid "Credentials are required"
msgstr "Podaci za prijavu su obavezni"
#: ../vnc.html:106
msgid "noVNC encountered an error:"
msgstr "noVNC je naišao na grešku:"
#: ../vnc.html:116
msgid "Hide/Show the control bar"
msgstr "Sakrij/Prikaži traku kontrola"
#: ../vnc.html:125
msgid "Drag"
msgstr "Povuci"
#: ../vnc.html:125
msgid "Move/Drag viewport"
msgstr "Pomakni/Povuci vidljivo područje"
#: ../vnc.html:131
msgid "Keyboard"
msgstr "Tipkovnica"
#: ../vnc.html:131
msgid "Show keyboard"
msgstr "Prikaži tipkovnicu"
#: ../vnc.html:136
msgid "Extra keys"
msgstr "Dodatne tipke"
#: ../vnc.html:136
msgid "Show extra keys"
msgstr "Prikaži dodatne tipke"
#: ../vnc.html:141
msgid "Ctrl"
msgstr "Ctrl"
#: ../vnc.html:141
msgid "Toggle Ctrl"
msgstr "Uključi/Isključi Ctrl"
#: ../vnc.html:144
msgid "Alt"
msgstr "Alt"
#: ../vnc.html:144
msgid "Toggle Alt"
msgstr "Uključi/Isključi Alt"
#: ../vnc.html:147
msgid "Toggle Windows"
msgstr "Uključi/Isključi Windows"
#: ../vnc.html:147
msgid "Windows"
msgstr "Windows"
#: ../vnc.html:150
msgid "Send Tab"
msgstr "Pošalji tabulator"
#: ../vnc.html:150
msgid "Tab"
msgstr "Tabulator"
#: ../vnc.html:153
msgid "Esc"
msgstr "Esc"
#: ../vnc.html:153
msgid "Send Escape"
msgstr "Pošalji Escape"
#: ../vnc.html:156
msgid "Ctrl+Alt+Del"
msgstr "Ctrl + Alt + Del"
#: ../vnc.html:156
msgid "Send Ctrl-Alt-Del"
msgstr "Pošalji Ctrl+Alt+Del"
#: ../vnc.html:163
msgid "Shutdown/Reboot"
msgstr "Isključi/Ponovo pokreni"
#: ../vnc.html:163
msgid "Shutdown/Reboot..."
msgstr "Isključi/Ponovo pokreni …"
#: ../vnc.html:169
msgid "Power"
msgstr "Napajanje"
#: ../vnc.html:171
msgid "Shutdown"
msgstr "Isključi"
#: ../vnc.html:172
msgid "Reboot"
msgstr "Ponovo pokreni"
#: ../vnc.html:173
msgid "Reset"
msgstr "Resetiraj"
#: ../vnc.html:178 ../vnc.html:184
msgid "Clipboard"
msgstr "Međuspremnik"
#: ../vnc.html:186
msgid "Edit clipboard content in the textarea below."
msgstr "Uredi sadržaj međuspremnika u donjem području teksta."
#: ../vnc.html:194
msgid "Full screen"
msgstr "Cjeloekranski prikaz"
#: ../vnc.html:199 ../vnc.html:205
msgid "Settings"
msgstr "Postavke"
#: ../vnc.html:211
msgid "Shared mode"
msgstr "Dijeljeni modus"
#: ../vnc.html:218
msgid "View only"
msgstr "Samo prikaz"
#: ../vnc.html:226
msgid "Clip to window"
msgstr "Isijeci na veličinu prozora"
#: ../vnc.html:231
msgid "Scaling mode:"
msgstr "Modus skaliranja:"
#: ../vnc.html:233
msgid "None"
msgstr "Bez"
#: ../vnc.html:234
msgid "Local scaling"
msgstr "Lokalno skaliranje"
#: ../vnc.html:235
msgid "Remote resizing"
msgstr "Daljinsko mijenjanje veličine"
#: ../vnc.html:240
msgid "Advanced"
msgstr "Napredno"
#: ../vnc.html:243
msgid "Quality:"
msgstr "Kvaliteta:"
#: ../vnc.html:247
msgid "Compression level:"
msgstr "Razina kompresije:"
#: ../vnc.html:252
msgid "Repeater ID:"
msgstr "ID repetitora:"
#: ../vnc.html:256
msgid "WebSocket"
msgstr "WebSocket"
#: ../vnc.html:261
msgid "Encrypt"
msgstr "Šifriraj"
#: ../vnc.html:266
msgid "Host:"
msgstr "Host:"
#: ../vnc.html:270
msgid "Port:"
msgstr "Priključak:"
#: ../vnc.html:274
msgid "Path:"
msgstr "Putanja:"
#: ../vnc.html:283
msgid "Automatic reconnect"
msgstr "Automatsko ponovno povezivanje"
#: ../vnc.html:288
msgid "Reconnect delay (ms):"
msgstr "Kašnjenje ponovnog povezivanja (ms):"
#: ../vnc.html:295
msgid "Show dot when no cursor"
msgstr "Prikaži točku kada nema pokazivača"
#: ../vnc.html:302
msgid "Logging:"
msgstr "Zapisivanje:"
#: ../vnc.html:311
msgid "Version:"
msgstr "Verzija:"
#: ../vnc.html:319
msgid "Disconnect"
msgstr "Odspoji"
#: ../vnc.html:342
msgid "Connect"
msgstr "Poveži"
#: ../vnc.html:351
msgid "Server identity"
msgstr "Identitet servera"
#: ../vnc.html:354
msgid "The server has provided the following identifying information:"
msgstr "Server je pružio sljedeće identifikacijske podatke:"
#: ../vnc.html:357
msgid "Fingerprint:"
msgstr "Otisak:"
#: ../vnc.html:361
msgid ""
"Please verify that the information is correct and press \"Approve\". "
"Otherwise press \"Reject\"."
msgstr ""
"Provjeri jesu li podaci točni i pritisni „Odobri“. U suprotnom pritisni "
"„Odbaci“."
#: ../vnc.html:366
msgid "Approve"
msgstr "Odobri"
#: ../vnc.html:367
msgid "Reject"
msgstr "Odbij"
#: ../vnc.html:375
msgid "Credentials"
msgstr "Podaci za prijavu"
#: ../vnc.html:379
msgid "Username:"
msgstr "Korisničko ime:"
#: ../vnc.html:383
msgid "Password:"
msgstr "Lozinka:"
#: ../vnc.html:387
msgid "Send credentials"
msgstr "Pošalji podatke za prijavu"
#: ../vnc.html:396
msgid "Cancel"
msgstr "Odustani"

332
po/hu.po Normal file
View File

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

View File

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

View File

@ -17,9 +17,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const { program } = require('commander');
const fs = require('fs');
const pofile = require("pofile");
import { program } from 'commander';
import fs from 'fs';
import pofile from "pofile";
program
.argument('<input>')

View File

@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: noVNC 1.6.0\n"
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
"POT-Creation-Date: 2025-02-14 10:14+0100\n"
"PO-Revision-Date: 2025-02-14 10:29+0100\n"
"POT-Creation-Date: 2025-10-31 09:17+0100\n"
"PO-Revision-Date: 2025-10-31 10:48+0100\n"
"Last-Translator: Alexander Zeijlon <aleze@cendio.com>\n"
"Language-Team: none\n"
"Language: sv\n"
@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.5\n"
"X-Generator: Poedit 3.7\n"
#: ../app/ui.js:84
msgid ""
@ -32,7 +32,7 @@ msgstr "Ansluter..."
#: ../app/ui.js:420
msgid "Disconnecting..."
msgstr "Kopplar ner..."
msgstr "Kopplar ifrån..."
#: ../app/ui.js:426
msgid "Reconnecting..."
@ -42,39 +42,43 @@ msgstr "Återansluter..."
msgid "Internal error"
msgstr "Internt fel"
#: ../app/ui.js:1079
#: ../app/ui.js:1084
msgid "Failed to connect to server: "
msgstr "Misslyckades att ansluta till servern: "
#: ../app/ui.js:1145
#: ../app/ui.js:1151
msgid "Connected (encrypted) to "
msgstr "Ansluten (krypterat) till "
#: ../app/ui.js:1147
#: ../app/ui.js:1153
msgid "Connected (unencrypted) to "
msgstr "Ansluten (okrypterat) till "
#: ../app/ui.js:1170
#: ../app/ui.js:1178
msgid "Something went wrong, connection is closed"
msgstr "Något gick fel, anslutningen avslutades"
#: ../app/ui.js:1173
#: ../app/ui.js:1181
msgid "Failed to connect to server"
msgstr "Misslyckades att ansluta till servern"
#: ../app/ui.js:1185
#: ../app/ui.js:1193
msgid "Disconnected"
msgstr "Frånkopplad"
#: ../app/ui.js:1200
#: ../app/ui.js:1210
msgid "New connection has been rejected with reason: "
msgstr "Ny anslutning har blivit nekad med följande skäl: "
#: ../app/ui.js:1203
#: ../app/ui.js:1213
msgid "New connection has been rejected"
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"
msgstr "Användaruppgifter krävs"
@ -160,11 +164,11 @@ msgstr "Skicka Ctrl-Alt-Del"
#: ../vnc.html:163
msgid "Shutdown/Reboot"
msgstr "Stäng av/Boota om"
msgstr "Stäng av/Starta om"
#: ../vnc.html:163
msgid "Shutdown/Reboot..."
msgstr "Stäng av/Boota om..."
msgstr "Stäng av/Starta om..."
#: ../vnc.html:169
msgid "Power"
@ -176,7 +180,7 @@ msgstr "Stäng av"
#: ../vnc.html:172
msgid "Reboot"
msgstr "Boota om"
msgstr "Starta om"
#: ../vnc.html:173
msgid "Reset"
@ -284,7 +288,7 @@ msgstr "Version:"
#: ../vnc.html:319
msgid "Disconnect"
msgstr "Koppla från"
msgstr "Koppla ifrån"
#: ../vnc.html:342
msgid "Connect"
@ -292,7 +296,7 @@ msgstr "Anslut"
#: ../vnc.html:351
msgid "Server identity"
msgstr "Server-identitet"
msgstr "Serveridentitet"
#: ../vnc.html:354
msgid "The server has provided the following identifying information:"
@ -308,7 +312,7 @@ msgid ""
"Otherwise press \"Reject\"."
msgstr ""
"Kontrollera att informationen är korrekt och tryck sedan \"Godkänn\". Tryck "
"annars \"Neka\"."
"annars \"Avvisa\"."
#: ../vnc.html:366
msgid "Approve"
@ -316,7 +320,7 @@ msgstr "Godkänn"
#: ../vnc.html:367
msgid "Reject"
msgstr "Neka"
msgstr "Avvisa"
#: ../vnc.html:375
msgid "Credentials"

View File

@ -5,9 +5,9 @@
* Licensed under MPL 2.0 (see LICENSE.txt)
*/
const { program } = require('commander');
const jsdom = require("jsdom");
const fs = require("fs");
import { program } from 'commander';
import jsdom from 'jsdom';
import fs from 'fs';
program
.argument('<INPUT...>')
@ -106,7 +106,7 @@ let output = "";
for (let str in strings) {
output += "#:";
for (location in strings[str]) {
for (let location in strings[str]) {
output += " " + location;
}
output += "\n";

View File

@ -1,4 +1,4 @@
import * as chai from '../node_modules/chai/chai.js';
import * as chai from '../node_modules/chai/index.js';
import sinon from '../node_modules/sinon/pkg/sinon-esm.js';
import sinonChai from '../node_modules/sinon-chai/lib/sinon-chai.js';

View File

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

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

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

View File

@ -384,10 +384,11 @@ describe('Display/Canvas helper', function () {
});
it('should draw an image from an image object on type "img" (if complete)', function () {
const img = { complete: true };
display.drawImage = sinon.spy();
display._renderQPush({ type: 'img', x: 3, y: 4, img: { complete: true } });
display._renderQPush({ type: 'img', x: 3, y: 4, img: img });
expect(display.drawImage).to.have.been.calledOnce;
expect(display.drawImage).to.have.been.calledWith({ complete: true }, 3, 4);
expect(display.drawImage).to.have.been.calledWith(img, 3, 4);
});
});
});

View File

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

View File

@ -47,6 +47,20 @@ describe('Websock', function () {
});
});
describe('rQlen())', function () {
it('should return the number of buffered bytes in the receive queue', function () {
websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,
0x88, 0xee, 0x11, 0x33]));
expect(sock.rQlen()).to.equal(8);
sock.rQshift8();
expect(sock.rQlen()).to.equal(7);
sock.rQshift16();
expect(sock.rQlen()).to.equal(5);
sock.rQshift32();
expect(sock.rQlen()).to.equal(1);
});
});
describe('rQshiftStr', function () {
it('should shift the given number of bytes off of the receive queue and return a string', function () {
websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,

View File

@ -1,140 +0,0 @@
#!/usr/bin/env node
const path = require('path');
const { program } = require('commander');
const fs = require('fs');
const fse = require('fs-extra');
const babel = require('@babel/core');
program
.option('-m, --with-source-maps [type]', 'output source maps when not generating a bundled app (type may be empty for external source maps, inline for inline source maps, or both) ')
.option('--clean', 'clear the lib folder before building')
.parse(process.argv);
// the various important paths
const paths = {
main: path.resolve(__dirname, '..'),
core: path.resolve(__dirname, '..', 'core'),
vendor: path.resolve(__dirname, '..', 'vendor'),
libDirBase: path.resolve(__dirname, '..', 'lib'),
};
// util.promisify requires Node.js 8.x, so we have our own
function promisify(original) {
return function promiseWrap() {
const args = Array.prototype.slice.call(arguments);
return new Promise((resolve, reject) => {
original.apply(this, args.concat((err, value) => {
if (err) return reject(err);
resolve(value);
}));
});
};
}
const writeFile = promisify(fs.writeFile);
const readdir = promisify(fs.readdir);
const lstat = promisify(fs.lstat);
const ensureDir = promisify(fse.ensureDir);
const babelTransformFile = promisify(babel.transformFile);
// walkDir *recursively* walks directories trees,
// calling the callback for all normal files found.
function walkDir(basePath, cb, filter) {
return readdir(basePath)
.then((files) => {
const paths = files.map(filename => path.join(basePath, filename));
return Promise.all(paths.map(filepath => lstat(filepath)
.then((stats) => {
if (filter !== undefined && !filter(filepath, stats)) return;
if (stats.isSymbolicLink()) return;
if (stats.isFile()) return cb(filepath);
if (stats.isDirectory()) return walkDir(filepath, cb, filter);
})));
});
}
function makeLibFiles(sourceMaps) {
// NB: we need to make a copy of babelOpts, since babel sets some defaults on it
const babelOpts = () => ({
plugins: [],
presets: [
[ '@babel/preset-env',
{ modules: 'commonjs' } ]
],
ast: false,
sourceMaps: sourceMaps,
});
fse.ensureDirSync(paths.libDirBase);
const outFiles = [];
const handleDir = (vendorRewrite, inPathBase, filename) => Promise.resolve()
.then(() => {
const outPath = path.join(paths.libDirBase, path.relative(inPathBase, filename));
if (path.extname(filename) !== '.js') {
return; // skip non-javascript files
}
return Promise.resolve()
.then(() => ensureDir(path.dirname(outPath)))
.then(() => {
const opts = babelOpts();
// Adjust for the fact that we move the core files relative
// to the vendor directory
if (vendorRewrite) {
opts.plugins.push(["import-redirect",
{"root": paths.libDirBase,
"redirect": { "vendor/(.+)": "./vendor/$1"}}]);
}
return babelTransformFile(filename, opts)
.then((res) => {
console.log(`Writing ${outPath}`);
const {map} = res;
let {code} = res;
if (sourceMaps === true) {
// append URL for external source map
code += `\n//# sourceMappingURL=${path.basename(outPath)}.map\n`;
}
outFiles.push(`${outPath}`);
return writeFile(outPath, code)
.then(() => {
if (sourceMaps === true || sourceMaps === 'both') {
console.log(` and ${outPath}.map`);
outFiles.push(`${outPath}.map`);
return writeFile(`${outPath}.map`, JSON.stringify(map));
}
});
});
});
});
Promise.resolve()
.then(() => {
const handler = handleDir.bind(null, false, paths.main);
return walkDir(paths.vendor, handler);
})
.then(() => {
const handler = handleDir.bind(null, true, paths.core);
return walkDir(paths.core, handler);
})
.catch((err) => {
console.error(`Failure converting modules: ${err}`);
process.exit(1);
});
}
let options = program.opts();
if (options.clean) {
console.log(`Removing ${paths.libDirBase}`);
fse.removeSync(paths.libDirBase);
}
makeLibFiles(options.withSourceMaps);

View File

@ -7,7 +7,7 @@
"use strict";
const fs = require('fs');
import fs from 'fs';
let showHelp = process.argv.length === 2;
let filename;

View File

@ -181,10 +181,8 @@ if [[ -d ${HERE}/websockify ]]; then
echo "Using local websockify at $WEBSOCKIFY"
else
WEBSOCKIFY_FROMSYSTEM=$(which websockify 2>/dev/null)
WEBSOCKIFY_FROMSNAP=${HERE}/../usr/bin/python2-websockify
WEBSOCKIFY_FROMSYSTEM=$(type -P websockify 2>/dev/null)
[ -f $WEBSOCKIFY_FROMSYSTEM ] && WEBSOCKIFY=$WEBSOCKIFY_FROMSYSTEM
[ -f $WEBSOCKIFY_FROMSNAP ] && WEBSOCKIFY=$WEBSOCKIFY_FROMSNAP
if [ ! -f "$WEBSOCKIFY" ]; then
echo "No installed websockify, attempting to clone websockify..."