From e4def7f715477dedb89f7505ef75c5cff222cf28 Mon Sep 17 00:00:00 2001 From: Milo Ivir <43657314+milotype@users.noreply.github.com> Date: Mon, 25 Aug 2025 18:36:38 +0200 Subject: [PATCH 01/25] Add Croatian translation --- po/hr.po | 338 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 po/hr.po diff --git a/po/hr.po b/po/hr.po new file mode 100644 index 00000000..a2165c66 --- /dev/null +++ b/po/hr.po @@ -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 , 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 \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" From d49d2b366a35e734f0910a5e649e463c1952bee9 Mon Sep 17 00:00:00 2001 From: Alexander Zeijlon Date: Fri, 5 Sep 2025 10:29:28 +0200 Subject: [PATCH 02/25] Use Croatian translations --- app/ui.js | 2 +- po/Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/ui.js b/app/ui.js index 51e57bd3..2542e059 100644 --- a/app/ui.js +++ b/app/ui.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", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"]; const UI = { diff --git a/po/Makefile b/po/Makefile index dcf5ba44..5572c940 100644 --- a/po/Makefile +++ b/po/Makefile @@ -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 it ja ko nl pl pt_BR ru sv tr zh_CN zh_TW VERSION := $(shell grep '"version"' ../package.json | cut -d '"' -f 4) From 8ebd9ddef9b5b8ab5636a40854fb59fcb327fb39 Mon Sep 17 00:00:00 2001 From: Alexander Zeijlon Date: Mon, 8 Sep 2025 16:42:30 +0200 Subject: [PATCH 03/25] Fix broken Chai import Chai v6.0.0 introduced a breaking change where file imports now need to point at 'chai/index.js'. See the corresponding release note. --- tests/assertions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/assertions.js b/tests/assertions.js index a7012271..70c8a2c7 100644 --- a/tests/assertions.js +++ b/tests/assertions.js @@ -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'; From 23b7219a5d39577f752be939ee25db2484d465b6 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Wed, 10 Sep 2025 09:58:09 +0200 Subject: [PATCH 04/25] Drop Image data once rendered Helps the browser to free up the memory right away, rather than waiting until some later cleanup process. At least Firefox can start consuming gigabytes of memory without this. --- core/display.js | 3 +++ tests/test.display.js | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/core/display.js b/core/display.js index ef42ac66..4efd6f4b 100644 --- a/core/display.js +++ b/core/display.js @@ -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); diff --git a/tests/test.display.js b/tests/test.display.js index 5844ce17..528b1906 100644 --- a/tests/test.display.js +++ b/tests/test.display.js @@ -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); }); }); }); From d5b18a84abc89a7e2c5db30639f8978eb5e8847d Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Wed, 3 Sep 2025 11:24:34 +0200 Subject: [PATCH 05/25] Expose length of buffered WebSocket data Some encodings don't know how much data they need, rather they must probe the data stream until they find an end marker. Expose how much data is buffered in order to make this search efficient. --- core/websock.js | 4 ++++ tests/test.websock.js | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/core/websock.js b/core/websock.js index ae17a440..ee8a4bc4 100644 --- a/core/websock.js +++ b/core/websock.js @@ -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 diff --git a/tests/test.websock.js b/tests/test.websock.js index 62bcbfa5..110e6ad0 100644 --- a/tests/test.websock.js +++ b/tests/test.websock.js @@ -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, From 356eab4f4d259b6db7d1c7b84d43408b2e7acb09 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Wed, 3 Sep 2025 11:24:34 +0200 Subject: [PATCH 06/25] Scan all buffered data looking for JPEG end This is much more efficient than looking at two bytes at a time. --- core/decoders/jpeg.js | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/core/decoders/jpeg.js b/core/decoders/jpeg.js index 5fd1e056..f7984603 100644 --- a/core/decoders/jpeg.js +++ b/core/decoders/jpeg.js @@ -119,18 +119,33 @@ export default class JPEGDecoder { let extra = 0; if (type === 0xDA) { // start of scan - extra += 2; + if (sock.rQwait("JPEG", length-2 + 2, 4)) { + return null; + } + + let len = sock.rQlen(); + let data = sock.rQpeekBytes(len, false); + while (true) { - if (sock.rQwait("JPEG", length-2+extra, 4)) { + let idx = data.indexOf(0xFF, length-2+extra); + if (idx === -1) { + sock.rQwait("JPEG", Infinity, 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; + + if (idx === len-1) { + sock.rQwait("JPEG", Infinity, 4); + return null; } - extra++; + + 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; } } From 6cf02042ded96f3fdaf59064afda05df69ccc0a5 Mon Sep 17 00:00:00 2001 From: Eli Kogan-Wang Date: Tue, 6 May 2025 12:40:30 +0200 Subject: [PATCH 07/25] Remove deprecated "directories" entry --- package.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/package.json b/package.json index bfe16a54..19d0b51c 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,6 @@ "version": "1.6.0", "description": "An HTML5 VNC client", "browser": "lib/rfb", - "directories": { - "lib": "lib", - "doc": "docs", - "test": "tests" - }, "files": [ "lib", "AUTHORS", From fe29dc650958fa31453ee66b2bcb8dbe0cf0708b Mon Sep 17 00:00:00 2001 From: Eli Kogan-Wang Date: Tue, 6 May 2025 12:40:30 +0200 Subject: [PATCH 08/25] Convert NPM bundle to ES-Module format --- karma.conf.js => karma.conf.cjs | 0 package.json | 9 +- po/po2js | 6 +- po/xgettext-html | 8 +- utils/convert.js | 140 -------------------------------- utils/genkeysymdef.js | 2 +- 6 files changed, 13 insertions(+), 152 deletions(-) rename karma.conf.js => karma.conf.cjs (100%) delete mode 100755 utils/convert.js diff --git a/karma.conf.js b/karma.conf.cjs similarity index 100% rename from karma.conf.js rename to karma.conf.cjs diff --git a/package.json b/package.json index 19d0b51c..3e60b63e 100644 --- a/package.json +++ b/package.json @@ -2,19 +2,20 @@ "name": "@novnc/novnc", "version": "1.6.0", "description": "An HTML5 VNC client", - "browser": "lib/rfb", + "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", diff --git a/po/po2js b/po/po2js index 6347e1ea..38effd30 100755 --- a/po/po2js +++ b/po/po2js @@ -17,9 +17,9 @@ * along with this program. If not, see . */ -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('') diff --git a/po/xgettext-html b/po/xgettext-html index f5ba57cc..3fcaacd4 100755 --- a/po/xgettext-html +++ b/po/xgettext-html @@ -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('') @@ -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"; diff --git a/utils/convert.js b/utils/convert.js deleted file mode 100755 index 617f4ed6..00000000 --- a/utils/convert.js +++ /dev/null @@ -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); diff --git a/utils/genkeysymdef.js b/utils/genkeysymdef.js index b10240ec..b41c40eb 100755 --- a/utils/genkeysymdef.js +++ b/utils/genkeysymdef.js @@ -7,7 +7,7 @@ "use strict"; -const fs = require('fs'); +import fs from 'fs'; let showHelp = process.argv.length === 2; let filename; From cf6fe1063c3648320b956dee6b4dbf87186eb067 Mon Sep 17 00:00:00 2001 From: "Felso Daniel (McP/MFE4)" Date: Wed, 8 Oct 2025 13:04:27 +0200 Subject: [PATCH 09/25] Add Hungarian translation --- app/locale/hu.json | 80 +++++++++++ app/ui.js | 2 +- po/Makefile | 2 +- po/hu.po | 332 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 414 insertions(+), 2 deletions(-) create mode 100644 app/locale/hu.json create mode 100644 po/hu.po diff --git a/app/locale/hu.json b/app/locale/hu.json new file mode 100644 index 00000000..ba700b5f --- /dev/null +++ b/app/locale/hu.json @@ -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" +} \ No newline at end of file diff --git a/app/ui.js b/app/ui.js index 2542e059..6cf2dba4 100644 --- a/app/ui.js +++ b/app/ui.js @@ -20,7 +20,7 @@ import * as WebUtil from "./webutil.js"; const PAGE_TITLE = "noVNC"; -const LINGUAS = ["cs", "de", "el", "es", "fr", "hr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"]; +const LINGUAS = ["cs", "de", "el", "es", "fr", "hr", "hu", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"]; const UI = { diff --git a/po/Makefile b/po/Makefile index 5572c940..28b03585 100644 --- a/po/Makefile +++ b/po/Makefile @@ -2,7 +2,7 @@ all: .PHONY: update-po update-js update-pot .PHONY: FORCE -LINGUAS := cs de el es fr hr it ja ko nl pl pt_BR ru sv tr zh_CN zh_TW +LINGUAS := cs de el es fr hr hu it ja ko nl pl pt_BR ru sv tr zh_CN zh_TW VERSION := $(shell grep '"version"' ../package.json | cut -d '"' -f 4) diff --git a/po/hu.po b/po/hu.po new file mode 100644 index 00000000..d2158dcf --- /dev/null +++ b/po/hu.po @@ -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 , 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 \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" From eaf5b704d8ba2cf2e934c915916a38fed6569e9e Mon Sep 17 00:00:00 2001 From: Alexander Zeijlon Date: Mon, 20 Oct 2025 14:20:04 +0200 Subject: [PATCH 10/25] Remove obsolete checks in novnc_proxy Our snap packages no longer contain python2-websockify. It's now called websockify and is found in snap by the WEBSOCKIFY_FROMSYSTEM check. --- utils/novnc_proxy | 2 -- 1 file changed, 2 deletions(-) diff --git a/utils/novnc_proxy b/utils/novnc_proxy index 6b55504a..6a4b11a5 100755 --- a/utils/novnc_proxy +++ b/utils/novnc_proxy @@ -182,9 +182,7 @@ 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 [ -f $WEBSOCKIFY_FROMSYSTEM ] && WEBSOCKIFY=$WEBSOCKIFY_FROMSYSTEM - [ -f $WEBSOCKIFY_FROMSNAP ] && WEBSOCKIFY=$WEBSOCKIFY_FROMSNAP if [ ! -f "$WEBSOCKIFY" ]; then echo "No installed websockify, attempting to clone websockify..." From 71d0bfaccd7ca69df4607d6cb2571c59fd4023cd Mon Sep 17 00:00:00 2001 From: Alexander Zeijlon Date: Mon, 20 Oct 2025 14:23:17 +0200 Subject: [PATCH 11/25] Use bash's type command to find websockify Our snap package requires bash to run novnc_proxy, but it doesn't explicitly set a requirement for the which command to be installed. Let's therefore use a bash built-in when looking for the websockify binary. --- utils/novnc_proxy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/novnc_proxy b/utils/novnc_proxy index 6a4b11a5..2f29a772 100755 --- a/utils/novnc_proxy +++ b/utils/novnc_proxy @@ -181,7 +181,7 @@ if [[ -d ${HERE}/websockify ]]; then echo "Using local websockify at $WEBSOCKIFY" else - WEBSOCKIFY_FROMSYSTEM=$(which websockify 2>/dev/null) + WEBSOCKIFY_FROMSYSTEM=$(type -P websockify 2>/dev/null) [ -f $WEBSOCKIFY_FROMSYSTEM ] && WEBSOCKIFY=$WEBSOCKIFY_FROMSYSTEM if [ ! -f "$WEBSOCKIFY" ]; then From f5a4eedcea749f82b7cab05cb78a4eb8a92b2c32 Mon Sep 17 00:00:00 2001 From: Tobias Date: Tue, 10 Jun 2025 16:40:58 +0200 Subject: [PATCH 12/25] Add permissions-exclusive async clipboard Clipboard permissions must be supported, with states "granted" or "prompt" for both write and read. --- core/clipboard.js | 72 ++++++++++++++++++ core/rfb.js | 29 ++++++-- core/util/browser.js | 33 +++++++++ tests/test.browser.js | 70 +++++++++++++++++- tests/test.clipboard.js | 121 +++++++++++++++++++++++++++++++ tests/test.rfb.js | 157 ++++++++++++++++++++++++++++++++++------ 6 files changed, 451 insertions(+), 31 deletions(-) create mode 100644 core/clipboard.js create mode 100644 tests/test.clipboard.js diff --git a/core/clipboard.js b/core/clipboard.js new file mode 100644 index 00000000..ae3cad15 --- /dev/null +++ b/core/clipboard.js @@ -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); + } +} diff --git a/core/rfb.js b/core/rfb.js index 80011e4a..1073a878 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -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); diff --git a/core/util/browser.js b/core/util/browser.js index 63596d21..12f47a76 100644 --- a/core/util/browser.js +++ b/core/util/browser.js @@ -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 diff --git a/tests/test.browser.js b/tests/test.browser.js index 692cc23b..6c9bc568 100644 --- a/tests/test.browser.js +++ b/tests/test.browser.js @@ -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; diff --git a/tests/test.clipboard.js b/tests/test.clipboard.js new file mode 100644 index 00000000..1c173a8d --- /dev/null +++ b/tests/test.clipboard.js @@ -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; + }); + +}); diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 2a7bbeaa..7aa54cd0 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -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; }); }); From d9b45d390be3c26b2d0938283fa3182556553b98 Mon Sep 17 00:00:00 2001 From: Tobias Date: Sat, 13 Sep 2025 00:00:13 +0200 Subject: [PATCH 13/25] Disable clipboard button with async clipboard With async clipboard available, the fallback clipboard textarea adds mostly confusion. If async clipboard is out right denied, users most likely don't want to see any clipboard activity. --- app/ui.js | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/app/ui.js b/app/ui.js index 2542e059..2b936c02 100644 --- a/app/ui.js +++ b/app/ui.js @@ -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"; @@ -1103,6 +1103,7 @@ const UI = { UI.rfb.showDotCursor = UI.getSetting('show_dot'); UI.updateViewOnly(); // requires UI.rfb + UI.updateClipboard(); }, disconnect() { @@ -1754,6 +1755,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'); From 3d5698c71c398c5b9085aa5f993a05091182681f Mon Sep 17 00:00:00 2001 From: Tobias Date: Wed, 11 Jun 2025 15:48:44 +0200 Subject: [PATCH 14/25] Add async clipboard module to internal API docs --- docs/API-internal.md | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/docs/API-internal.md b/docs/API-internal.md index 5b41548e..1d0a1104 100644 --- a/docs/API-internal.md +++ b/docs/API-internal.md @@ -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 From 63c2c14a50d60ac60686c28fd938a1d6e36cbf71 Mon Sep 17 00:00:00 2001 From: Tobias Date: Mon, 27 Oct 2025 17:22:59 +0100 Subject: [PATCH 15/25] Display warning prompt before closing tab The warning prompt is only displayed if there is an active connected session, when viewOnly is disabled. --- app/ui.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/app/ui.js b/app/ui.js index bf72c9a4..bbdea60d 100644 --- a/app/ui.js +++ b/app/ui.js @@ -1155,6 +1155,8 @@ const UI = { UI.showStatus(msg); UI.updateVisualState('connected'); + UI.updateBeforeUnload(); + // Do this last because it can only be used on rendered elements UI.rfb.focus(); }, @@ -1191,6 +1193,8 @@ const UI = { UI.showStatus(_("Disconnected"), 'normal'); } + UI.updateBeforeUnload(); + document.title = PAGE_TITLE; UI.openControlbar(); @@ -1211,6 +1215,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 * ============== @@ -1737,6 +1759,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') From 9d77c806fe9130027a68c2c29272ef78c967ba2f Mon Sep 17 00:00:00 2001 From: Alexander Zeijlon Date: Fri, 31 Oct 2025 09:18:04 +0100 Subject: [PATCH 16/25] Update translation template file --- po/noVNC.pot | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/po/noVNC.pot b/po/noVNC.pot index 7c32a3ed..0f69b9fd 100644 --- a/po/noVNC.pot +++ b/po/noVNC.pot @@ -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 \n" "Language-Team: LANGUAGE \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 "" From c2d89730dd90223bccb972465835228c3e8f8760 Mon Sep 17 00:00:00 2001 From: Alexander Zeijlon Date: Fri, 31 Oct 2025 10:53:45 +0100 Subject: [PATCH 17/25] Update Swedish translation --- po/sv.po | 700 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 352 insertions(+), 348 deletions(-) diff --git a/po/sv.po b/po/sv.po index aff531ad..20fd1dc7 100644 --- a/po/sv.po +++ b/po/sv.po @@ -1,348 +1,352 @@ -# Swedish translations for noVNC package -# Svenska översättningar för paketet noVNC. -# Copyright (C) 2025 The noVNC authors -# This file is distributed under the same license as the noVNC package. -# Samuel Mannehed , 2020. -# -msgid "" -msgstr "" -"Project-Id-Version: noVNC 1.6.0\n" -"Report-Msgid-Bugs-To: novnc@googlegroups.com\n" -"POT-Creation-Date: 2025-02-14 10:14+0100\n" -"PO-Revision-Date: 2025-02-14 10:29+0100\n" -"Last-Translator: Alexander Zeijlon \n" -"Language-Team: none\n" -"Language: sv\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Poedit 3.5\n" - -#: ../app/ui.js:84 -msgid "" -"Running without HTTPS is not recommended, crashes or other issues are likely." -msgstr "" -"Det är ej rekommenderat att köra utan HTTPS, krascher och andra problem är " -"troliga." - -#: ../app/ui.js:413 -msgid "Connecting..." -msgstr "Ansluter..." - -#: ../app/ui.js:420 -msgid "Disconnecting..." -msgstr "Kopplar ner..." - -#: ../app/ui.js:426 -msgid "Reconnecting..." -msgstr "Återansluter..." - -#: ../app/ui.js:431 -msgid "Internal error" -msgstr "Internt fel" - -#: ../app/ui.js:1079 -msgid "Failed to connect to server: " -msgstr "Misslyckades att ansluta till servern: " - -#: ../app/ui.js:1145 -msgid "Connected (encrypted) to " -msgstr "Ansluten (krypterat) till " - -#: ../app/ui.js:1147 -msgid "Connected (unencrypted) to " -msgstr "Ansluten (okrypterat) till " - -#: ../app/ui.js:1170 -msgid "Something went wrong, connection is closed" -msgstr "Något gick fel, anslutningen avslutades" - -#: ../app/ui.js:1173 -msgid "Failed to connect to server" -msgstr "Misslyckades att ansluta till servern" - -#: ../app/ui.js:1185 -msgid "Disconnected" -msgstr "Frånkopplad" - -#: ../app/ui.js:1200 -msgid "New connection has been rejected with reason: " -msgstr "Ny anslutning har blivit nekad med följande skäl: " - -#: ../app/ui.js:1203 -msgid "New connection has been rejected" -msgstr "Ny anslutning har blivit nekad" - -#: ../app/ui.js:1269 -msgid "Credentials are required" -msgstr "Användaruppgifter krävs" - -#: ../vnc.html:106 -msgid "noVNC encountered an error:" -msgstr "noVNC stötte på ett problem:" - -#: ../vnc.html:116 -msgid "Hide/Show the control bar" -msgstr "Göm/Visa kontrollbaren" - -#: ../vnc.html:125 -msgid "Drag" -msgstr "Dra" - -#: ../vnc.html:125 -msgid "Move/Drag viewport" -msgstr "Flytta/Dra vyn" - -#: ../vnc.html:131 -msgid "Keyboard" -msgstr "Tangentbord" - -#: ../vnc.html:131 -msgid "Show keyboard" -msgstr "Visa tangentbord" - -#: ../vnc.html:136 -msgid "Extra keys" -msgstr "Extraknappar" - -#: ../vnc.html:136 -msgid "Show extra keys" -msgstr "Visa extraknappar" - -#: ../vnc.html:141 -msgid "Ctrl" -msgstr "Ctrl" - -#: ../vnc.html:141 -msgid "Toggle Ctrl" -msgstr "Växla Ctrl" - -#: ../vnc.html:144 -msgid "Alt" -msgstr "Alt" - -#: ../vnc.html:144 -msgid "Toggle Alt" -msgstr "Växla Alt" - -#: ../vnc.html:147 -msgid "Toggle Windows" -msgstr "Växla Windows" - -#: ../vnc.html:147 -msgid "Windows" -msgstr "Windows" - -#: ../vnc.html:150 -msgid "Send Tab" -msgstr "Skicka Tab" - -#: ../vnc.html:150 -msgid "Tab" -msgstr "Tab" - -#: ../vnc.html:153 -msgid "Esc" -msgstr "Esc" - -#: ../vnc.html:153 -msgid "Send Escape" -msgstr "Skicka Escape" - -#: ../vnc.html:156 -msgid "Ctrl+Alt+Del" -msgstr "Ctrl+Alt+Del" - -#: ../vnc.html:156 -msgid "Send Ctrl-Alt-Del" -msgstr "Skicka Ctrl-Alt-Del" - -#: ../vnc.html:163 -msgid "Shutdown/Reboot" -msgstr "Stäng av/Boota om" - -#: ../vnc.html:163 -msgid "Shutdown/Reboot..." -msgstr "Stäng av/Boota om..." - -#: ../vnc.html:169 -msgid "Power" -msgstr "Ström" - -#: ../vnc.html:171 -msgid "Shutdown" -msgstr "Stäng av" - -#: ../vnc.html:172 -msgid "Reboot" -msgstr "Boota om" - -#: ../vnc.html:173 -msgid "Reset" -msgstr "Återställ" - -#: ../vnc.html:178 ../vnc.html:184 -msgid "Clipboard" -msgstr "Urklipp" - -#: ../vnc.html:186 -msgid "Edit clipboard content in the textarea below." -msgstr "Redigera urklippets innehåll i fältet nedan." - -#: ../vnc.html:194 -msgid "Full screen" -msgstr "Fullskärm" - -#: ../vnc.html:199 ../vnc.html:205 -msgid "Settings" -msgstr "Inställningar" - -#: ../vnc.html:211 -msgid "Shared mode" -msgstr "Delat läge" - -#: ../vnc.html:218 -msgid "View only" -msgstr "Endast visning" - -#: ../vnc.html:226 -msgid "Clip to window" -msgstr "Begränsa till fönster" - -#: ../vnc.html:231 -msgid "Scaling mode:" -msgstr "Skalningsläge:" - -#: ../vnc.html:233 -msgid "None" -msgstr "Ingen" - -#: ../vnc.html:234 -msgid "Local scaling" -msgstr "Lokal skalning" - -#: ../vnc.html:235 -msgid "Remote resizing" -msgstr "Ändra storlek" - -#: ../vnc.html:240 -msgid "Advanced" -msgstr "Avancerat" - -#: ../vnc.html:243 -msgid "Quality:" -msgstr "Kvalitet:" - -#: ../vnc.html:247 -msgid "Compression level:" -msgstr "Kompressionsnivå:" - -#: ../vnc.html:252 -msgid "Repeater ID:" -msgstr "Repeater-ID:" - -#: ../vnc.html:256 -msgid "WebSocket" -msgstr "WebSocket" - -#: ../vnc.html:261 -msgid "Encrypt" -msgstr "Kryptera" - -#: ../vnc.html:266 -msgid "Host:" -msgstr "Värd:" - -#: ../vnc.html:270 -msgid "Port:" -msgstr "Port:" - -#: ../vnc.html:274 -msgid "Path:" -msgstr "Sökväg:" - -#: ../vnc.html:283 -msgid "Automatic reconnect" -msgstr "Automatisk återanslutning" - -#: ../vnc.html:288 -msgid "Reconnect delay (ms):" -msgstr "Fördröjning (ms):" - -#: ../vnc.html:295 -msgid "Show dot when no cursor" -msgstr "Visa prick när ingen muspekare finns" - -#: ../vnc.html:302 -msgid "Logging:" -msgstr "Loggning:" - -#: ../vnc.html:311 -msgid "Version:" -msgstr "Version:" - -#: ../vnc.html:319 -msgid "Disconnect" -msgstr "Koppla från" - -#: ../vnc.html:342 -msgid "Connect" -msgstr "Anslut" - -#: ../vnc.html:351 -msgid "Server identity" -msgstr "Server-identitet" - -#: ../vnc.html:354 -msgid "The server has provided the following identifying information:" -msgstr "Servern har gett följande identifierande information:" - -#: ../vnc.html:357 -msgid "Fingerprint:" -msgstr "Fingeravtryck:" - -#: ../vnc.html:361 -msgid "" -"Please verify that the information is correct and press \"Approve\". " -"Otherwise press \"Reject\"." -msgstr "" -"Kontrollera att informationen är korrekt och tryck sedan \"Godkänn\". Tryck " -"annars \"Neka\"." - -#: ../vnc.html:366 -msgid "Approve" -msgstr "Godkänn" - -#: ../vnc.html:367 -msgid "Reject" -msgstr "Neka" - -#: ../vnc.html:375 -msgid "Credentials" -msgstr "Användaruppgifter" - -#: ../vnc.html:379 -msgid "Username:" -msgstr "Användarnamn:" - -#: ../vnc.html:383 -msgid "Password:" -msgstr "Lösenord:" - -#: ../vnc.html:387 -msgid "Send credentials" -msgstr "Skicka användaruppgifter" - -#: ../vnc.html:396 -msgid "Cancel" -msgstr "Avbryt" - -#~ msgid "Must set host" -#~ msgstr "Du måste specifiera en värd" - -#~ msgid "HTTPS is required for full functionality" -#~ msgstr "HTTPS krävs för full funktionalitet" - -#~ msgid "Clear" -#~ msgstr "Rensa" +# Swedish translations for noVNC package +# Svenska översättningar för paketet noVNC. +# Copyright (C) 2025 The noVNC authors +# This file is distributed under the same license as the noVNC package. +# Samuel Mannehed , 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: noVNC 1.6.0\n" +"Report-Msgid-Bugs-To: novnc@googlegroups.com\n" +"POT-Creation-Date: 2025-10-31 09:17+0100\n" +"PO-Revision-Date: 2025-10-31 10:48+0100\n" +"Last-Translator: Alexander Zeijlon \n" +"Language-Team: none\n" +"Language: sv\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.7\n" + +#: ../app/ui.js:84 +msgid "" +"Running without HTTPS is not recommended, crashes or other issues are likely." +msgstr "" +"Det är ej rekommenderat att köra utan HTTPS, krascher och andra problem är " +"troliga." + +#: ../app/ui.js:413 +msgid "Connecting..." +msgstr "Ansluter..." + +#: ../app/ui.js:420 +msgid "Disconnecting..." +msgstr "Kopplar ifrån..." + +#: ../app/ui.js:426 +msgid "Reconnecting..." +msgstr "Återansluter..." + +#: ../app/ui.js:431 +msgid "Internal error" +msgstr "Internt fel" + +#: ../app/ui.js:1084 +msgid "Failed to connect to server: " +msgstr "Misslyckades att ansluta till servern: " + +#: ../app/ui.js:1151 +msgid "Connected (encrypted) to " +msgstr "Ansluten (krypterat) till " + +#: ../app/ui.js:1153 +msgid "Connected (unencrypted) to " +msgstr "Ansluten (okrypterat) till " + +#: ../app/ui.js:1178 +msgid "Something went wrong, connection is closed" +msgstr "Något gick fel, anslutningen avslutades" + +#: ../app/ui.js:1181 +msgid "Failed to connect to server" +msgstr "Misslyckades att ansluta till servern" + +#: ../app/ui.js:1193 +msgid "Disconnected" +msgstr "Frånkopplad" + +#: ../app/ui.js:1210 +msgid "New connection has been rejected with reason: " +msgstr "Ny anslutning har blivit nekad med följande skäl: " + +#: ../app/ui.js:1213 +msgid "New connection has been rejected" +msgstr "Ny anslutning har blivit nekad" + +#: ../app/ui.js:1225 +msgid "Are you sure you want to disconnect the session?" +msgstr "Är du säker på att du vill koppla ifrån sessionen?" + +#: ../app/ui.js:1297 +msgid "Credentials are required" +msgstr "Användaruppgifter krävs" + +#: ../vnc.html:106 +msgid "noVNC encountered an error:" +msgstr "noVNC stötte på ett problem:" + +#: ../vnc.html:116 +msgid "Hide/Show the control bar" +msgstr "Göm/Visa kontrollbaren" + +#: ../vnc.html:125 +msgid "Drag" +msgstr "Dra" + +#: ../vnc.html:125 +msgid "Move/Drag viewport" +msgstr "Flytta/Dra vyn" + +#: ../vnc.html:131 +msgid "Keyboard" +msgstr "Tangentbord" + +#: ../vnc.html:131 +msgid "Show keyboard" +msgstr "Visa tangentbord" + +#: ../vnc.html:136 +msgid "Extra keys" +msgstr "Extraknappar" + +#: ../vnc.html:136 +msgid "Show extra keys" +msgstr "Visa extraknappar" + +#: ../vnc.html:141 +msgid "Ctrl" +msgstr "Ctrl" + +#: ../vnc.html:141 +msgid "Toggle Ctrl" +msgstr "Växla Ctrl" + +#: ../vnc.html:144 +msgid "Alt" +msgstr "Alt" + +#: ../vnc.html:144 +msgid "Toggle Alt" +msgstr "Växla Alt" + +#: ../vnc.html:147 +msgid "Toggle Windows" +msgstr "Växla Windows" + +#: ../vnc.html:147 +msgid "Windows" +msgstr "Windows" + +#: ../vnc.html:150 +msgid "Send Tab" +msgstr "Skicka Tab" + +#: ../vnc.html:150 +msgid "Tab" +msgstr "Tab" + +#: ../vnc.html:153 +msgid "Esc" +msgstr "Esc" + +#: ../vnc.html:153 +msgid "Send Escape" +msgstr "Skicka Escape" + +#: ../vnc.html:156 +msgid "Ctrl+Alt+Del" +msgstr "Ctrl+Alt+Del" + +#: ../vnc.html:156 +msgid "Send Ctrl-Alt-Del" +msgstr "Skicka Ctrl-Alt-Del" + +#: ../vnc.html:163 +msgid "Shutdown/Reboot" +msgstr "Stäng av/Starta om" + +#: ../vnc.html:163 +msgid "Shutdown/Reboot..." +msgstr "Stäng av/Starta om..." + +#: ../vnc.html:169 +msgid "Power" +msgstr "Ström" + +#: ../vnc.html:171 +msgid "Shutdown" +msgstr "Stäng av" + +#: ../vnc.html:172 +msgid "Reboot" +msgstr "Starta om" + +#: ../vnc.html:173 +msgid "Reset" +msgstr "Återställ" + +#: ../vnc.html:178 ../vnc.html:184 +msgid "Clipboard" +msgstr "Urklipp" + +#: ../vnc.html:186 +msgid "Edit clipboard content in the textarea below." +msgstr "Redigera urklippets innehåll i fältet nedan." + +#: ../vnc.html:194 +msgid "Full screen" +msgstr "Fullskärm" + +#: ../vnc.html:199 ../vnc.html:205 +msgid "Settings" +msgstr "Inställningar" + +#: ../vnc.html:211 +msgid "Shared mode" +msgstr "Delat läge" + +#: ../vnc.html:218 +msgid "View only" +msgstr "Endast visning" + +#: ../vnc.html:226 +msgid "Clip to window" +msgstr "Begränsa till fönster" + +#: ../vnc.html:231 +msgid "Scaling mode:" +msgstr "Skalningsläge:" + +#: ../vnc.html:233 +msgid "None" +msgstr "Ingen" + +#: ../vnc.html:234 +msgid "Local scaling" +msgstr "Lokal skalning" + +#: ../vnc.html:235 +msgid "Remote resizing" +msgstr "Ändra storlek" + +#: ../vnc.html:240 +msgid "Advanced" +msgstr "Avancerat" + +#: ../vnc.html:243 +msgid "Quality:" +msgstr "Kvalitet:" + +#: ../vnc.html:247 +msgid "Compression level:" +msgstr "Kompressionsnivå:" + +#: ../vnc.html:252 +msgid "Repeater ID:" +msgstr "Repeater-ID:" + +#: ../vnc.html:256 +msgid "WebSocket" +msgstr "WebSocket" + +#: ../vnc.html:261 +msgid "Encrypt" +msgstr "Kryptera" + +#: ../vnc.html:266 +msgid "Host:" +msgstr "Värd:" + +#: ../vnc.html:270 +msgid "Port:" +msgstr "Port:" + +#: ../vnc.html:274 +msgid "Path:" +msgstr "Sökväg:" + +#: ../vnc.html:283 +msgid "Automatic reconnect" +msgstr "Automatisk återanslutning" + +#: ../vnc.html:288 +msgid "Reconnect delay (ms):" +msgstr "Fördröjning (ms):" + +#: ../vnc.html:295 +msgid "Show dot when no cursor" +msgstr "Visa prick när ingen muspekare finns" + +#: ../vnc.html:302 +msgid "Logging:" +msgstr "Loggning:" + +#: ../vnc.html:311 +msgid "Version:" +msgstr "Version:" + +#: ../vnc.html:319 +msgid "Disconnect" +msgstr "Koppla ifrån" + +#: ../vnc.html:342 +msgid "Connect" +msgstr "Anslut" + +#: ../vnc.html:351 +msgid "Server identity" +msgstr "Serveridentitet" + +#: ../vnc.html:354 +msgid "The server has provided the following identifying information:" +msgstr "Servern har gett följande identifierande information:" + +#: ../vnc.html:357 +msgid "Fingerprint:" +msgstr "Fingeravtryck:" + +#: ../vnc.html:361 +msgid "" +"Please verify that the information is correct and press \"Approve\". " +"Otherwise press \"Reject\"." +msgstr "" +"Kontrollera att informationen är korrekt och tryck sedan \"Godkänn\". Tryck " +"annars \"Avvisa\"." + +#: ../vnc.html:366 +msgid "Approve" +msgstr "Godkänn" + +#: ../vnc.html:367 +msgid "Reject" +msgstr "Avvisa" + +#: ../vnc.html:375 +msgid "Credentials" +msgstr "Användaruppgifter" + +#: ../vnc.html:379 +msgid "Username:" +msgstr "Användarnamn:" + +#: ../vnc.html:383 +msgid "Password:" +msgstr "Lösenord:" + +#: ../vnc.html:387 +msgid "Send credentials" +msgstr "Skicka användaruppgifter" + +#: ../vnc.html:396 +msgid "Cancel" +msgstr "Avbryt" + +#~ msgid "Must set host" +#~ msgstr "Du måste specifiera en värd" + +#~ msgid "HTTPS is required for full functionality" +#~ msgstr "HTTPS krävs för full funktionalitet" + +#~ msgid "Clear" +#~ msgstr "Rensa" From b29cc6493d52d3bf419ccb38900f227f198bdb97 Mon Sep 17 00:00:00 2001 From: Alexander Zeijlon Date: Mon, 3 Nov 2025 12:37:45 +0100 Subject: [PATCH 18/25] Allow publishing to npmjs.com with OIDC --- .github/workflows/deploy.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a11d3d0a..1b9c5c9a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -9,6 +9,9 @@ on: jobs: npm: runs-on: ubuntu-latest + permissions: + id-token: write + contents: read steps: - uses: actions/checkout@v4 - run: | From 99865e5aba68372d837b18fdd2a8edf7cea55901 Mon Sep 17 00:00:00 2001 From: Alexander Zeijlon Date: Mon, 3 Nov 2025 13:44:26 +0100 Subject: [PATCH 19/25] Stop using access tokens when publishing to npmjs We are using OIDC now instead of the old style access tokens. --- .github/workflows/deploy.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1b9c5c9a..1de3f2f0 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -29,22 +29,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' && From e03a54e12ae9a525a16d446be11d2fab5bec2908 Mon Sep 17 00:00:00 2001 From: Alexander Zeijlon Date: Mon, 3 Nov 2025 13:58:03 +0100 Subject: [PATCH 20/25] Publish with latest npm version Apparently publishing with OIDC as an auth method only works with npm 11.5.1 and onward. --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1de3f2f0..40d35ebf 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -23,7 +23,7 @@ jobs: with: # Needs to be explicitly specified for auth to work registry-url: 'https://registry.npmjs.org' - - run: npm install + - run: npm install -g npm@latest - uses: actions/upload-artifact@v4 with: name: npm From d3d69a811832b2b207ed47575e5ec58eeeaa933c Mon Sep 17 00:00:00 2001 From: Alexander Zeijlon Date: Mon, 3 Nov 2025 14:52:08 +0100 Subject: [PATCH 21/25] Use Node version 24 when publishing to npmjs Node 24 comes with npm > 11.5.1 which we need in order to publish with OIDC auth. This also reverts the change in commit e03a54e, which broke the publishing step. --- .github/workflows/deploy.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 40d35ebf..1a18e936 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -21,9 +21,12 @@ 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 -g npm@latest + - run: npm install - uses: actions/upload-artifact@v4 with: name: npm From fb97e0f1e2362f6bc68b31ecce1977fac728fa65 Mon Sep 17 00:00:00 2001 From: Alexander Zeijlon Date: Tue, 4 Nov 2025 09:37:09 +0100 Subject: [PATCH 22/25] Remove showDotCursor from docs/API.md This should have been a part of commit 243d7fd --- docs/API.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/API.md b/docs/API.md index eb3ec333..c1f6fac9 100644 --- a/docs/API.md +++ b/docs/API.md @@ -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. From d44f7e04fc456844836c7c5ac911d0f4e8dd06e6 Mon Sep 17 00:00:00 2001 From: Alexander Zeijlon Date: Tue, 4 Nov 2025 09:44:21 +0100 Subject: [PATCH 23/25] Remove show_dot from docs/EMBEDDING.md This should have been a part of commit 243d7fd --- docs/EMBEDDING.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/EMBEDDING.md b/docs/EMBEDDING.md index 9e927d0d..26bcce3f 100644 --- a/docs/EMBEDDING.md +++ b/docs/EMBEDDING.md @@ -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`. From 817c1dee5f0bc8ea1e01706ee1ed226458aa85b0 Mon Sep 17 00:00:00 2001 From: Daniel Hammerschmidt Date: Tue, 11 Nov 2025 23:26:18 +0100 Subject: [PATCH 24/25] Add 'RFB::clipboardreceived' event --- core/rfb.js | 12 +++++++----- docs/API.md | 6 ++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index 1073a878..cb9aa8f1 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -2335,11 +2335,13 @@ export default class RFB extends EventTargetMixin { _writeClipboard(text) { if (this._viewOnly) return; - if (this._asyncClipboard.writeClipboard(text)) return; - // Fallback clipboard - this.dispatchEvent( - new CustomEvent("clipboard", {detail: {text: text}}) - ); + if (!(this._asyncClipboard.writeClipboard(text))) { + // Fallback clipboard + this.dispatchEvent( + new CustomEvent("clipboard", {detail: {text: text}}) + ); + } + this.dispatchEvent(new CustomEvent("clipboardreceived")); } _handleServerCutText() { diff --git a/docs/API.md b/docs/API.md index c1f6fac9..ea80f89a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -96,6 +96,12 @@ protocol stream. - The `clipboard` event is fired when clipboard data is received from the server. +['clipboardreceived'](#clipboardreceived) + - The `clipboardreceived` event is fired after the `clipboard` event + or after the clipboard has been updated through the + [Clipboard module](API-internal.md#11-module-list). The copied text + has already been written to the system clipboard or the clipboard panel. + [`clippingviewport`](#clippingviewport) - The `clippingviewport` event is fired when `RFB.clippingViewport` is updated. From 537600d93bbd05dc3fb74fc555552829c741d70a Mon Sep 17 00:00:00 2001 From: Daniel Hammerschmidt Date: Tue, 11 Nov 2025 23:51:20 +0100 Subject: [PATCH 25/25] Update notification when clipboard is updated - use existing UI/API - add 'noVNC_setting_notify_clipboard_received' - add support for Clipboard module introduced with fb7e891841e2d1f8dd2d07e43a38de97b49ade94 (PR #1993) --- app/styles/base.css | 24 ------------------------ app/ui.js | 31 ++++++++++++++++++------------- vnc.html | 7 +++++++ 3 files changed, 25 insertions(+), 37 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index 009532b6..33f0f359 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -902,30 +902,6 @@ html { #noVNC_bell { display: none; } -/* ---------------------------------------- - * - * Styles for Showing the Clipboard updated -*/ -.notification { - position: fixed; - top: 20px; - right: 20px; - background-color: grey; - color: white; - padding: 10px 20px; - border-radius: 8px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); - font-family: sans-serif; - font-size: 16px; - opacity: 1; - transition: opacity 0.5s ease; - z-index: 9999; -} - -.fade-out { - opacity: 0; -} - /* ---------------------------------------- * Media sizing diff --git a/app/ui.js b/app/ui.js index 623c4d3b..1afba879 100644 --- a/app/ui.js +++ b/app/ui.js @@ -185,6 +185,7 @@ const UI = { UI.initSetting('bell', 'on'); UI.initSetting('view_only', false); UI.initSetting('show_dot', false); + UI.initSetting('notify_clipboard_received', false); UI.initSetting('path', 'websockify'); UI.initSetting('repeaterID', ''); UI.initSetting('reconnect', false); @@ -371,6 +372,8 @@ const UI = { UI.addSettingChangeHandler('view_only', UI.updateViewOnly); UI.addSettingChangeHandler('show_dot'); UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor); + UI.addSettingChangeHandler('notify_clipboard_received'); + UI.addSettingChangeHandler('notify_clipboard_received', UI.updateNotifyClipboardReceived); UI.addSettingChangeHandler('host'); UI.addSettingChangeHandler('port'); UI.addSettingChangeHandler('path'); @@ -892,6 +895,7 @@ const UI = { UI.updateSetting('logging'); UI.updateSetting('reconnect'); UI.updateSetting('reconnect_delay'); + UI.updateSetting('notify_clipboard_received'); document.getElementById('noVNC_settings') .classList.add("noVNC_open"); @@ -993,25 +997,17 @@ const UI = { UI.openClipboardPanel(); } }, - showCopiedNotification() { - // This is responsible for showing the notification when text is updated in the clipboard. - const notificationBox = document.createElement("div"); - notificationBox.className = "notification"; - notificationBox.innerText = "Clipboard updated"; - document.body.appendChild(notificationBox); - setTimeout(() => { - notificationBox.classList.add("fade-out"); - setTimeout(() => { - notificationBox.remove(); - }, 500); // Wait for fade-out transition - }, 3000); // Display for 3 seconds + notifyClipboardReceived() { + // When enabled with setting 'notify_clipboard_received', shows + // notification when a 'clipboard-received'-event is fired by + // the RFB instance. + UI.showStatus(_('Clipboard updated'), 3000); }, clipboardReceive(e) { Log.Debug(">> UI.clipboardReceive: " + e.detail.text.substr(0, 40) + "..."); document.getElementById('noVNC_clipboard_text').value = e.detail.text; - UI.showCopiedNotification(); Log.Debug("<< UI.clipboardReceive"); }, @@ -1119,6 +1115,7 @@ const UI = { UI.updateViewOnly(); // requires UI.rfb UI.updateClipboard(); + UI.updateNotifyClipboardReceived(); }, disconnect() { @@ -1824,6 +1821,14 @@ const UI = { UI.rfb.showDotCursor = UI.getSetting('show_dot'); }, + updateNotifyClipboardReceived() { + if (!UI.rfb) return; + UI.rfb.removeEventListener('clipboardreceived', UI.notifyClipboardReceived); + if (UI.getSetting('notify_clipboard_received')) { + UI.rfb.addEventListener('clipboardreceived', UI.notifyClipboardReceived); + } + }, + updateLogging() { WebUtil.initLogging(UI.getSetting('logging')); }, diff --git a/vnc.html b/vnc.html index 82cacd58..080f8adf 100644 --- a/vnc.html +++ b/vnc.html @@ -296,6 +296,13 @@ Show dot when no cursor +
  • + +