From a30f609de48227df08f633bc0443e16678b20d32 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Wed, 2 Nov 2022 10:23:36 +0100 Subject: [PATCH 001/166] Don't crash if we can't use localStorage Our settings are not a fatal requirement, we can fall back on the default values if they can't be accessed. A scenario where we've seen this happen is when cookies are disabled in the browser. It seems localStorage is disabled along with cookies in these settings. So, lets log a message about the failure and otherwise silently continue in this case. Fixes issue #1577. --- app/webutil.js | 74 ++++++++++++++++++++++++++++++++++++++++--- tests/test.webutil.js | 15 +++++++++ 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/app/webutil.js b/app/webutil.js index b94f035d3..6011442cb 100644 --- a/app/webutil.js +++ b/app/webutil.js @@ -6,16 +6,16 @@ * See README.md for usage and integration instructions. */ -import { initLogging as mainInitLogging } from '../core/util/logging.js'; +import * as Log from '../core/util/logging.js'; // init log level reading the logging HTTP param export function initLogging(level) { "use strict"; if (typeof level !== "undefined") { - mainInitLogging(level); + Log.initLogging(level); } else { const param = document.location.href.match(/logging=([A-Za-z0-9._-]*)/); - mainInitLogging(param || undefined); + Log.initLogging(param || undefined); } } @@ -146,7 +146,7 @@ export function writeSetting(name, value) { if (window.chrome && window.chrome.storage) { window.chrome.storage.sync.set(settings); } else { - localStorage.setItem(name, value); + localStorageSet(name, value); } } @@ -156,7 +156,7 @@ export function readSetting(name, defaultValue) { if ((name in settings) || (window.chrome && window.chrome.storage)) { value = settings[name]; } else { - value = localStorage.getItem(name); + value = localStorageGet(name); settings[name] = value; } if (typeof value === "undefined") { @@ -181,6 +181,70 @@ export function eraseSetting(name) { if (window.chrome && window.chrome.storage) { window.chrome.storage.sync.remove(name); } else { + localStorageRemove(name); + } +} + +let loggedMsgs = []; +function logOnce(msg, level = "warn") { + if (!loggedMsgs.includes(msg)) { + switch (level) { + case "error": + Log.Error(msg); + break; + case "warn": + Log.Warn(msg); + break; + case "debug": + Log.Debug(msg); + break; + default: + Log.Info(msg); + } + loggedMsgs.push(msg); + } +} + +let cookiesMsg = "Couldn't access noVNC settings, are cookies disabled?"; + +function localStorageGet(name) { + let r; + try { + r = localStorage.getItem(name); + } catch (e) { + if (e instanceof DOMException) { + logOnce(cookiesMsg); + logOnce("'localStorage.getItem(" + name + ")' failed: " + e, + "debug"); + } else { + throw e; + } + } + return r; +} +function localStorageSet(name, value) { + try { + localStorage.setItem(name, value); + } catch (e) { + if (e instanceof DOMException) { + logOnce(cookiesMsg); + logOnce("'localStorage.setItem(" + name + "," + value + + ")' failed: " + e, "debug"); + } else { + throw e; + } + } +} +function localStorageRemove(name) { + try { localStorage.removeItem(name); + } catch (e) { + if (e instanceof DOMException) { + logOnce(cookiesMsg); + logOnce("'localStorage.removeItem(" + name + ")' failed: " + e, + "debug"); + } else { + throw e; + } } } diff --git a/tests/test.webutil.js b/tests/test.webutil.js index 6f460a3fc..df8227aef 100644 --- a/tests/test.webutil.js +++ b/tests/test.webutil.js @@ -92,6 +92,11 @@ describe('WebUtil', function () { expect(window.localStorage.setItem).to.have.been.calledWithExactly('test', 'value'); expect(WebUtil.readSetting('test')).to.equal('value'); }); + + it('should not crash when local storage save fails', function () { + localStorage.setItem.throws(new DOMException()); + expect(WebUtil.writeSetting('test', 'value')).to.not.throw; + }); }); describe('setSetting', function () { @@ -137,6 +142,11 @@ describe('WebUtil', function () { WebUtil.writeSetting('test', 'something else'); expect(WebUtil.readSetting('test')).to.equal('something else'); }); + + it('should not crash when local storage read fails', function () { + localStorage.getItem.throws(new DOMException()); + expect(WebUtil.readSetting('test')).to.not.throw; + }); }); // this doesn't appear to be used anywhere @@ -145,6 +155,11 @@ describe('WebUtil', function () { WebUtil.eraseSetting('test'); expect(window.localStorage.removeItem).to.have.been.calledWithExactly('test'); }); + + it('should not crash when local storage remove fails', function () { + localStorage.removeItem.throws(new DOMException()); + expect(WebUtil.eraseSetting('test')).to.not.throw; + }); }); }); From 01bb36d43171c340cb10028e938d2c3afadb2049 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 29 Aug 2023 17:28:54 +0200 Subject: [PATCH 002/166] Use proper argument to deflateInit() This was an accidental copy error from inflator.js. The second argument to deflateInit() is the compression level, not the window bits. We have not strong opinions on an appropriate level, so stick to the default. --- core/deflator.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/deflator.js b/core/deflator.js index fe2a8f703..22f6770b3 100644 --- a/core/deflator.js +++ b/core/deflator.js @@ -7,7 +7,7 @@ */ import { deflateInit, deflate } from "../vendor/pako/lib/zlib/deflate.js"; -import { Z_FULL_FLUSH } from "../vendor/pako/lib/zlib/deflate.js"; +import { Z_FULL_FLUSH, Z_DEFAULT_COMPRESSION } from "../vendor/pako/lib/zlib/deflate.js"; import ZStream from "../vendor/pako/lib/zlib/zstream.js"; export default class Deflator { @@ -15,9 +15,8 @@ export default class Deflator { this.strm = new ZStream(); this.chunkSize = 1024 * 10 * 10; this.outputBuffer = new Uint8Array(this.chunkSize); - this.windowBits = 5; - deflateInit(this.strm, this.windowBits); + deflateInit(this.strm, Z_DEFAULT_COMPRESSION); } deflate(inData) { From b40a45a11b98c2a2dac24f9b6151722f1e3115be Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 29 Aug 2023 17:30:00 +0200 Subject: [PATCH 003/166] Remove unused argument to inflateInit() There is just one argument to inflateInit(). It is inflateInit2() that takes two arguments. Since this argument was never used, let's just remove it and keep the existing behaviour. --- core/inflator.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/inflator.js b/core/inflator.js index 4b337607b..f851f2a7d 100644 --- a/core/inflator.js +++ b/core/inflator.js @@ -14,9 +14,8 @@ export default class Inflate { this.strm = new ZStream(); this.chunkSize = 1024 * 10 * 10; this.strm.output = new Uint8Array(this.chunkSize); - this.windowBits = 5; - inflateInit(this.strm, this.windowBits); + inflateInit(this.strm); } setInput(data) { From e81602d705982d29f7b46ce47c3af108865b03e7 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 29 Aug 2023 17:38:44 +0200 Subject: [PATCH 004/166] Fix zlib level change in clipboard tests The compression level got changed in 01bb36d4, but the tests weren't updated to follow this change. --- tests/test.rfb.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index bf12a4600..fd1563404 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -3,7 +3,7 @@ const expect = chai.expect; import RFB from '../core/rfb.js'; import Websock from '../core/websock.js'; import ZStream from "../vendor/pako/lib/zlib/zstream.js"; -import { deflateInit, deflate } from "../vendor/pako/lib/zlib/deflate.js"; +import { deflateInit, deflate, Z_DEFAULT_COMPRESSION } from "../vendor/pako/lib/zlib/deflate.js"; import { encodings } from '../core/encodings.js'; import { toUnsigned32bit } from '../core/util/int.js'; import { encodeUTF8 } from '../core/util/strings.js'; @@ -54,7 +54,7 @@ function deflateWithSize(data) { let strm = new ZStream(); let chunkSize = 1024 * 10 * 10; strm.output = new Uint8Array(chunkSize); - deflateInit(strm, 5); + deflateInit(strm, Z_DEFAULT_COMPRESSION); /* eslint-disable camelcase */ strm.input = unCompData; From 72f6810797f7bc70b94dea62de2c8256ccb944d9 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 7 Sep 2023 14:38:04 +0200 Subject: [PATCH 005/166] Correctly handle "none" auth on old servers There is no security result for the "none" authentication until RFB 3.8. This got broken by mistake in 5671072. --- core/rfb.js | 6 +++++- tests/test.rfb.js | 19 +++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index fb9df0b9c..477b30f5e 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -1925,7 +1925,11 @@ export default class RFB extends EventTargetMixin { _negotiateAuthentication() { switch (this._rfbAuthScheme) { case securityTypeNone: - this._rfbInitState = 'SecurityResult'; + if (this._rfbVersion >= 3.8) { + this._rfbInitState = 'SecurityResult'; + } else { + this._rfbInitState = 'ClientInitialisation'; + } return true; case securityTypeXVP: diff --git a/tests/test.rfb.js b/tests/test.rfb.js index fd1563404..d1313713e 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1257,7 +1257,15 @@ describe('Remote Frame Buffer Protocol Client', function () { client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 1])); expect(client._rfbInitState).to.equal('ServerInitialisation'); }); - }); + + it('should transition straight to ServerInitialisation on "no auth" for versions < 3.8', function () { + sendVer('003.007\n', client); + client._sock._websocket._getSentData(); + + sendSecurity(1, client); + expect(client._rfbInitState).to.equal('ServerInitialisation'); + }); + }); describe('Authentication', function () { beforeEach(function () { @@ -2231,10 +2239,17 @@ describe('Remote Frame Buffer Protocol Client', function () { describe('Legacy SecurityResult', function () { beforeEach(function () { + client.addEventListener("credentialsrequired", () => { + client.sendCredentials({ password: 'passwd' }); + }); sendVer('003.007\n', client); client._sock._websocket._getSentData(); - sendSecurity(1, client); + sendSecurity(2, client); + const challenge = []; + for (let i = 0; i < 16; i++) { challenge[i] = i; } + client._sock._websocket._receiveData(new Uint8Array(challenge)); client._sock._websocket._getSentData(); + clock.tick(); }); it('should not include reason in securityfailure event', function () { From 370f21b11723196b67f0acebe85d45ca7fc7a34e Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 7 Sep 2023 14:59:36 +0200 Subject: [PATCH 006/166] Correctly handle legacy security rejections The code comment of this code was entirely incorrect, but the commit message for 5671072 when it was added was correct. I.e. there is a result, but not a reason. Adjust the unit tests to make sure this doesn't regress again. --- core/rfb.js | 7 ------- tests/test.rfb.js | 27 ++++++++++++++++++++------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index 477b30f5e..6bce48fd6 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -1966,13 +1966,6 @@ export default class RFB extends EventTargetMixin { } _handleSecurityResult() { - // There is no security choice, and hence no security result - // until RFB 3.7 - if (this._rfbVersion < 3.7) { - this._rfbInitState = 'ClientInitialisation'; - return true; - } - if (this._sock.rQwait('VNC auth response ', 4)) { return false; } const status = this._sock.rQshift32(); diff --git a/tests/test.rfb.js b/tests/test.rfb.js index d1313713e..fa7f14023 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -2238,23 +2238,36 @@ describe('Remote Frame Buffer Protocol Client', function () { }); describe('Legacy SecurityResult', function () { - beforeEach(function () { + it('should not include reason in securityfailure event for versions < 3.7', function () { client.addEventListener("credentialsrequired", () => { client.sendCredentials({ password: 'passwd' }); }); - sendVer('003.007\n', client); - client._sock._websocket._getSentData(); - sendSecurity(2, client); + const spy = sinon.spy(); + client.addEventListener("securityfailure", spy); + sendVer('003.006\n', client); + client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 2])); const challenge = []; for (let i = 0; i < 16; i++) { challenge[i] = i; } client._sock._websocket._receiveData(new Uint8Array(challenge)); - client._sock._websocket._getSentData(); - clock.tick(); + + client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 2])); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][0].detail.status).to.equal(2); + expect('reason' in spy.args[0][0].detail).to.be.false; }); - it('should not include reason in securityfailure event', function () { + it('should not include reason in securityfailure event for versions < 3.8', function () { + client.addEventListener("credentialsrequired", () => { + client.sendCredentials({ password: 'passwd' }); + }); const spy = sinon.spy(); client.addEventListener("securityfailure", spy); + sendVer('003.007\n', client); + sendSecurity(2, client); + const challenge = []; + for (let i = 0; i < 16; i++) { challenge[i] = i; } + client._sock._websocket._receiveData(new Uint8Array(challenge)); + client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 2])); expect(spy).to.have.been.calledOnce; expect(spy.args[0][0].detail.status).to.equal(2); From bf12c24f4c88d2ba3ecd9c7a27d923fbf05f0632 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 7 Sep 2023 15:35:20 +0200 Subject: [PATCH 007/166] Fix bad indentation --- tests/test.rfb.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index fa7f14023..c0e85c5b9 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1265,7 +1265,7 @@ describe('Remote Frame Buffer Protocol Client', function () { sendSecurity(1, client); expect(client._rfbInitState).to.equal('ServerInitialisation'); }); - }); + }); describe('Authentication', function () { beforeEach(function () { From a0b7c0dac5359e4002e7f1d946e60e2eb9b4a54e Mon Sep 17 00:00:00 2001 From: Otto van Houten Date: Wed, 26 Jul 2023 14:38:31 +0200 Subject: [PATCH 008/166] Add QEMU Led Pseudo encoding support Previously, num-lock and caps-lock syncing was performed on a best effort basis by qemu. Now, the syncing is performed by no-vnc instead. This allows the led state syncing to work in cases where it previously couldn't, since no-vnc has with this extension knowledge of both the remote and local capslock and numlock status, which QEMU doesn't have. --- core/encodings.js | 1 + core/input/keyboard.js | 34 ++++++---- core/rfb.js | 51 ++++++++++++++- tests/test.keyboard.js | 48 ++++++++++++++ tests/test.rfb.js | 143 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 263 insertions(+), 14 deletions(-) diff --git a/core/encodings.js b/core/encodings.js index 2041b6e02..1a79989d1 100644 --- a/core/encodings.js +++ b/core/encodings.js @@ -22,6 +22,7 @@ export const encodings = { pseudoEncodingLastRect: -224, pseudoEncodingCursor: -239, pseudoEncodingQEMUExtendedKeyEvent: -258, + pseudoEncodingQEMULedEvent: -261, pseudoEncodingDesktopName: -307, pseudoEncodingExtendedDesktopSize: -308, pseudoEncodingXvp: -309, diff --git a/core/input/keyboard.js b/core/input/keyboard.js index ddb5ce099..9068e9e9f 100644 --- a/core/input/keyboard.js +++ b/core/input/keyboard.js @@ -36,7 +36,7 @@ export default class Keyboard { // ===== PRIVATE METHODS ===== - _sendKeyEvent(keysym, code, down) { + _sendKeyEvent(keysym, code, down, numlock = null, capslock = null) { if (down) { this._keyDownList[code] = keysym; } else { @@ -48,8 +48,8 @@ export default class Keyboard { } Log.Debug("onkeyevent " + (down ? "down" : "up") + - ", keysym: " + keysym, ", code: " + code); - this.onkeyevent(keysym, code, down); + ", keysym: " + keysym, ", code: " + code, + ", numlock: " + numlock + ", capslock: " + capslock); + this.onkeyevent(keysym, code, down, numlock, capslock); } _getKeyCode(e) { @@ -86,6 +86,14 @@ export default class Keyboard { _handleKeyDown(e) { const code = this._getKeyCode(e); let keysym = KeyboardUtil.getKeysym(e); + let numlock = e.getModifierState('NumLock'); + let capslock = e.getModifierState('CapsLock'); + + // getModifierState for NumLock is not supported on mac and ios and always returns false. + // Set to null to indicate unknown/unsupported instead. + if (browser.isMac() || browser.isIOS()) { + numlock = null; + } // Windows doesn't have a proper AltGr, but handles it using // fake Ctrl+Alt. However the remote end might not be Windows, @@ -107,7 +115,7 @@ export default class Keyboard { // key to "AltGraph". keysym = KeyTable.XK_ISO_Level3_Shift; } else { - this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); + this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true, numlock, capslock); } } @@ -118,8 +126,8 @@ export default class Keyboard { // If it's a virtual keyboard then it should be // sufficient to just send press and release right // after each other - this._sendKeyEvent(keysym, code, true); - this._sendKeyEvent(keysym, code, false); + this._sendKeyEvent(keysym, code, true, numlock, capslock); + this._sendKeyEvent(keysym, code, false, numlock, capslock); } stopEvent(e); @@ -157,8 +165,8 @@ export default class Keyboard { // while meta is held down if ((browser.isMac() || browser.isIOS()) && (e.metaKey && code !== 'MetaLeft' && code !== 'MetaRight')) { - this._sendKeyEvent(keysym, code, true); - this._sendKeyEvent(keysym, code, false); + this._sendKeyEvent(keysym, code, true, numlock, capslock); + this._sendKeyEvent(keysym, code, false, numlock, capslock); stopEvent(e); return; } @@ -168,8 +176,8 @@ export default class Keyboard { // which toggles on each press, but not on release. So pretend // it was a quick press and release of the button. if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) { - this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true); - this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false); + this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true, numlock, capslock); + this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false, numlock, capslock); stopEvent(e); return; } @@ -182,8 +190,8 @@ export default class Keyboard { KeyTable.XK_Hiragana, KeyTable.XK_Romaji ]; if (browser.isWindows() && jpBadKeys.includes(keysym)) { - this._sendKeyEvent(keysym, code, true); - this._sendKeyEvent(keysym, code, false); + this._sendKeyEvent(keysym, code, true, numlock, capslock); + this._sendKeyEvent(keysym, code, false, numlock, capslock); stopEvent(e); return; } @@ -199,7 +207,7 @@ export default class Keyboard { return; } - this._sendKeyEvent(keysym, code, true); + this._sendKeyEvent(keysym, code, true, numlock, capslock); } _handleKeyUp(e) { diff --git a/core/rfb.js b/core/rfb.js index fb9df0b9c..cb9726001 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -260,6 +260,8 @@ export default class RFB extends EventTargetMixin { this._keyboard = new Keyboard(this._canvas); this._keyboard.onkeyevent = this._handleKeyEvent.bind(this); + this._remoteCapsLock = null; // Null indicates unknown or irrelevant + this._remoteNumLock = null; this._gestures = new GestureHandler(); @@ -993,7 +995,35 @@ export default class RFB extends EventTargetMixin { } } - _handleKeyEvent(keysym, code, down) { + _handleKeyEvent(keysym, code, down, numlock, capslock) { + // If remote state of capslock is known, and it doesn't match the local led state of + // the keyboard, we send a capslock keypress first to bring it into sync. + // If we just pressed CapsLock, or we toggled it remotely due to it being out of sync + // we clear the remote state so that we don't send duplicate or spurious fixes, + // since it may take some time to receive the new remote CapsLock state. + if (code == 'CapsLock' && down) { + this._remoteCapsLock = null; + } + if (this._remoteCapsLock !== null && capslock !== null && this._remoteCapsLock !== capslock && down) { + Log.Debug("Fixing remote caps lock"); + + this.sendKey(KeyTable.XK_Caps_Lock, 'CapsLock', true); + this.sendKey(KeyTable.XK_Caps_Lock, 'CapsLock', false); + // We clear the remote capsLock state when we do this to prevent issues with doing this twice + // before we receive an update of the the remote state. + this._remoteCapsLock = null; + } + + // Logic for numlock is exactly the same. + if (code == 'NumLock' && down) { + this._remoteNumLock = null; + } + if (this._remoteNumLock !== null && numlock !== null && this._remoteNumLock !== numlock && down) { + Log.Debug("Fixing remote num lock"); + this.sendKey(KeyTable.XK_Num_Lock, 'NumLock', true); + this.sendKey(KeyTable.XK_Num_Lock, 'NumLock', false); + this._remoteNumLock = null; + } this.sendKey(keysym, code, down); } @@ -2104,6 +2134,7 @@ export default class RFB extends EventTargetMixin { encs.push(encodings.pseudoEncodingDesktopSize); encs.push(encodings.pseudoEncodingLastRect); encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent); + encs.push(encodings.pseudoEncodingQEMULedEvent); encs.push(encodings.pseudoEncodingExtendedDesktopSize); encs.push(encodings.pseudoEncodingXvp); encs.push(encodings.pseudoEncodingFence); @@ -2539,6 +2570,9 @@ export default class RFB extends EventTargetMixin { case encodings.pseudoEncodingExtendedDesktopSize: return this._handleExtendedDesktopSize(); + case encodings.pseudoEncodingQEMULedEvent: + return this._handleLedEvent(); + default: return this._handleDataRect(); } @@ -2716,6 +2750,21 @@ export default class RFB extends EventTargetMixin { return true; } + _handleLedEvent() { + if (this._sock.rQwait("LED Status", 1)) { + return false; + } + + let data = this._sock.rQshift8(); + // ScrollLock state can be retrieved with data & 1. This is currently not needed. + let numLock = data & 2 ? true : false; + let capsLock = data & 4 ? true : false; + this._remoteCapsLock = capsLock; + this._remoteNumLock = numLock; + + return true; + } + _handleExtendedDesktopSize() { if (this._sock.rQwait("ExtendedDesktopSize", 4)) { return false; diff --git a/tests/test.keyboard.js b/tests/test.keyboard.js index 0d8cac60e..efc84c306 100644 --- a/tests/test.keyboard.js +++ b/tests/test.keyboard.js @@ -14,6 +14,10 @@ describe('Key Event Handling', function () { } e.stopPropagation = sinon.spy(); e.preventDefault = sinon.spy(); + e.getModifierState = function (key) { + return e[key]; + }; + return e; } @@ -310,6 +314,50 @@ describe('Key Event Handling', function () { }); }); + describe('Modifier status info', function () { + let origNavigator; + beforeEach(function () { + // window.navigator is a protected read-only property in many + // environments, so we need to redefine it whilst running these + // tests. + origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); + + Object.defineProperty(window, "navigator", {value: {}}); + }); + + afterEach(function () { + Object.defineProperty(window, "navigator", origNavigator); + }); + + it('should provide caps lock state', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'A', NumLock: false, CapsLock: true})); + + expect(kbd.onkeyevent).to.have.been.calledOnce; + expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0x41, "KeyA", true, false, true); + }); + + it('should provide num lock state', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'A', NumLock: true, CapsLock: false})); + + expect(kbd.onkeyevent).to.have.been.calledOnce; + expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0x41, "KeyA", true, true, false); + }); + + it('should have no num lock state on mac', function () { + window.navigator.platform = "Mac"; + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'A', NumLock: false, CapsLock: true})); + + expect(kbd.onkeyevent).to.have.been.calledOnce; + expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0x41, "KeyA", true, null, true); + }); + }); + describe('Japanese IM keys on Windows', function () { let origNavigator; beforeEach(function () { diff --git a/tests/test.rfb.js b/tests/test.rfb.js index bf12a4600..ec3b66a33 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -2979,6 +2979,149 @@ describe('Remote Frame Buffer Protocol Client', function () { expect(spy.args[0][0].detail.name).to.equal('som€ nam€'); }); }); + + describe('Caps Lock and Num Lock remote fixup', function () { + function sendLedStateUpdate(state) { + let data = []; + push8(data, state); + sendFbuMsg([{ x: 0, y: 0, width: 0, height: 0, encoding: -261 }], [data], client); + } + + let client; + beforeEach(function () { + client = makeRFB(); + sinon.stub(client, 'sendKey'); + }); + + it('should toggle caps lock if remote caps lock is on and local is off', function () { + sendLedStateUpdate(0b100); + client._handleKeyEvent(0x61, 'KeyA', true, null, false); + + expect(client.sendKey).to.have.been.calledThrice; + expect(client.sendKey.firstCall).to.have.been.calledWith(0xFFE5, "CapsLock", true); + expect(client.sendKey.secondCall).to.have.been.calledWith(0xFFE5, "CapsLock", false); + expect(client.sendKey.thirdCall).to.have.been.calledWith(0x61, "KeyA", true); + }); + + it('should toggle caps lock if remote caps lock is off and local is on', function () { + sendLedStateUpdate(0b011); + client._handleKeyEvent(0x41, 'KeyA', true, null, true); + + expect(client.sendKey).to.have.been.calledThrice; + expect(client.sendKey.firstCall).to.have.been.calledWith(0xFFE5, "CapsLock", true); + expect(client.sendKey.secondCall).to.have.been.calledWith(0xFFE5, "CapsLock", false); + expect(client.sendKey.thirdCall).to.have.been.calledWith(0x41, "KeyA", true); + }); + + it('should not toggle caps lock if remote caps lock is on and local is on', function () { + sendLedStateUpdate(0b100); + client._handleKeyEvent(0x41, 'KeyA', true, null, true); + + expect(client.sendKey).to.have.been.calledOnce; + expect(client.sendKey.firstCall).to.have.been.calledWith(0x41, "KeyA", true); + }); + + it('should not toggle caps lock if remote caps lock is off and local is off', function () { + sendLedStateUpdate(0b011); + client._handleKeyEvent(0x61, 'KeyA', true, null, false); + + expect(client.sendKey).to.have.been.calledOnce; + expect(client.sendKey.firstCall).to.have.been.calledWith(0x61, "KeyA", true); + }); + + it('should not toggle caps lock if the key is caps lock', function () { + sendLedStateUpdate(0b011); + client._handleKeyEvent(0xFFE5, 'CapsLock', true, null, true); + + expect(client.sendKey).to.have.been.calledOnce; + expect(client.sendKey.firstCall).to.have.been.calledWith(0xFFE5, "CapsLock", true); + }); + + it('should toggle caps lock only once', function () { + sendLedStateUpdate(0b100); + client._handleKeyEvent(0x61, 'KeyA', true, null, false); + client._handleKeyEvent(0x61, 'KeyA', true, null, false); + + expect(client.sendKey).to.have.callCount(4); + expect(client.sendKey.firstCall).to.have.been.calledWith(0xFFE5, "CapsLock", true); + expect(client.sendKey.secondCall).to.have.been.calledWith(0xFFE5, "CapsLock", false); + expect(client.sendKey.thirdCall).to.have.been.calledWith(0x61, "KeyA", true); + expect(client.sendKey.lastCall).to.have.been.calledWith(0x61, "KeyA", true); + }); + + it('should retain remote caps lock state on capslock key up', function () { + sendLedStateUpdate(0b100); + client._handleKeyEvent(0xFFE5, 'CapsLock', false, null, true); + + expect(client.sendKey).to.have.been.calledOnce; + expect(client.sendKey.firstCall).to.have.been.calledWith(0xFFE5, "CapsLock", false); + expect(client._remoteCapsLock).to.equal(true); + }); + + it('should toggle num lock if remote num lock is on and local is off', function () { + sendLedStateUpdate(0b010); + client._handleKeyEvent(0xFF9C, 'NumPad1', true, false, null); + + expect(client.sendKey).to.have.been.calledThrice; + expect(client.sendKey.firstCall).to.have.been.calledWith(0xFF7F, "NumLock", true); + expect(client.sendKey.secondCall).to.have.been.calledWith(0xFF7F, "NumLock", false); + expect(client.sendKey.thirdCall).to.have.been.calledWith(0xFF9C, "NumPad1", true); + }); + + it('should toggle num lock if remote num lock is off and local is on', function () { + sendLedStateUpdate(0b101); + client._handleKeyEvent(0xFFB1, 'NumPad1', true, true, null); + + expect(client.sendKey).to.have.been.calledThrice; + expect(client.sendKey.firstCall).to.have.been.calledWith(0xFF7F, "NumLock", true); + expect(client.sendKey.secondCall).to.have.been.calledWith(0xFF7F, "NumLock", false); + expect(client.sendKey.thirdCall).to.have.been.calledWith(0xFFB1, "NumPad1", true); + }); + + it('should not toggle num lock if remote num lock is on and local is on', function () { + sendLedStateUpdate(0b010); + client._handleKeyEvent(0xFFB1, 'NumPad1', true, true, null); + + expect(client.sendKey).to.have.been.calledOnce; + expect(client.sendKey.firstCall).to.have.been.calledWith(0xFFB1, "NumPad1", true); + }); + + it('should not toggle num lock if remote num lock is off and local is off', function () { + sendLedStateUpdate(0b101); + client._handleKeyEvent(0xFF9C, 'NumPad1', true, false, null); + + expect(client.sendKey).to.have.been.calledOnce; + expect(client.sendKey.firstCall).to.have.been.calledWith(0xFF9C, "NumPad1", true); + }); + + it('should not toggle num lock if the key is num lock', function () { + sendLedStateUpdate(0b101); + client._handleKeyEvent(0xFF7F, 'NumLock', true, true, null); + + expect(client.sendKey).to.have.been.calledOnce; + expect(client.sendKey.firstCall).to.have.been.calledWith(0xFF7F, "NumLock", true); + }); + + it('should not toggle num lock if local state is unknown', function () { + sendLedStateUpdate(0b010); + client._handleKeyEvent(0xFFB1, 'NumPad1', true, null, null); + + expect(client.sendKey).to.have.been.calledOnce; + expect(client.sendKey.firstCall).to.have.been.calledWith(0xFFB1, "NumPad1", true); + }); + + it('should toggle num lock only once', function () { + sendLedStateUpdate(0b010); + client._handleKeyEvent(0xFF9C, 'NumPad1', true, false, null); + client._handleKeyEvent(0xFF9C, 'NumPad1', true, false, null); + + expect(client.sendKey).to.have.callCount(4); + expect(client.sendKey.firstCall).to.have.been.calledWith(0xFF7F, "NumLock", true); + expect(client.sendKey.secondCall).to.have.been.calledWith(0xFF7F, "NumLock", false); + expect(client.sendKey.thirdCall).to.have.been.calledWith(0xFF9C, "NumPad1", true); + expect(client.sendKey.lastCall).to.have.been.calledWith(0xFF9C, "NumPad1", true); + }); + }); }); describe('XVP Message Handling', function () { From f1174023c1b15f65991bbfca41cc8182c466b7e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Wed, 11 Oct 2023 12:28:39 +0200 Subject: [PATCH 009/166] Add the possibility to listen on a specific host For instance, for listening only on "localhost" That is, bind on 127.0.0.1 instead of 0.0.0.0 --- docs/novnc_proxy.1 | 4 ++-- utils/novnc_proxy | 45 ++++++++++++++++++++++++++++++--------------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/docs/novnc_proxy.1 b/docs/novnc_proxy.1 index 11a003b3a..78f06355e 100644 --- a/docs/novnc_proxy.1 +++ b/docs/novnc_proxy.1 @@ -3,12 +3,12 @@ .SH NAME novnc_proxy - noVNC proxy server .SH SYNOPSIS -.B novnc_proxy [--listen PORT] [--vnc VNC_HOST:PORT] [--cert CERT] [--ssl-only] +.B novnc_proxy [--listen [HOST:]PORT] [--vnc VNC_HOST:PORT] [--cert CERT] [--ssl-only] Starts the WebSockets proxy and a mini-webserver and provides a cut-and-paste URL to go to. - --listen PORT Port for proxy/webserver to listen on + --listen [HOST:]PORT Port for proxy/webserver to listen on Default: 6080 --vnc VNC_HOST:PORT VNC server host:port proxy target Default: localhost:5900 diff --git a/utils/novnc_proxy b/utils/novnc_proxy index ea3ea706c..d5b17dd00 100755 --- a/utils/novnc_proxy +++ b/utils/novnc_proxy @@ -8,12 +8,12 @@ usage() { echo "$*" echo fi - echo "Usage: ${NAME} [--listen PORT] [--vnc VNC_HOST:PORT] [--cert CERT] [--ssl-only]" + echo "Usage: ${NAME} [--listen [HOST:]PORT] [--vnc VNC_HOST:PORT] [--cert CERT] [--ssl-only]" echo echo "Starts the WebSockets proxy and a mini-webserver and " echo "provides a cut-and-paste URL to go to." echo - echo " --listen PORT Port for proxy/webserver to listen on" + echo " --listen [HOST:]PORT Port for proxy/webserver to listen on" echo " Default: 6080" echo " --vnc VNC_HOST:PORT VNC server host:port proxy target" echo " Default: localhost:5900" @@ -47,7 +47,9 @@ usage() { NAME="$(basename $0)" REAL_NAME="$(readlink -f $0)" HERE="$(cd "$(dirname "$REAL_NAME")" && pwd)" +HOST="" PORT="6080" +LISTEN="$PORT" VNC_DEST="localhost:5900" CERT="" KEY="" @@ -86,7 +88,7 @@ cleanup() { while [ "$*" ]; do param=$1; shift; OPTARG=$1 case $param in - --listen) PORT="${OPTARG}"; shift ;; + --listen) LISTEN="${OPTARG}"; shift ;; --vnc) VNC_DEST="${OPTARG}"; shift ;; --cert) CERT="${OPTARG}"; shift ;; --key) KEY="${OPTARG}"; shift ;; @@ -107,14 +109,23 @@ while [ "$*" ]; do esac done +if [ "$LISTEN" != "$PORT" ]; then + HOST=${LISTEN%:*} + PORT=${LISTEN##*:} + # if no host was given, restore + [ "$HOST" = "$PORT" ] && HOST="" +fi + # Sanity checks -if bash -c "exec 7<>/dev/tcp/localhost/${PORT}" &> /dev/null; then - exec 7<&- - exec 7>&- - die "Port ${PORT} in use. Try --listen PORT" -else - exec 7<&- - exec 7>&- +if [ -z "${HOST}" ]; then + if bash -c "exec 7<>/dev/tcp/localhost/${PORT}" &> /dev/null; then + exec 7<&- + exec 7>&- + die "Port ${PORT} in use. Try --listen PORT" + else + exec 7<&- + exec 7>&- + fi fi trap "cleanup" TERM QUIT INT EXIT @@ -191,9 +202,9 @@ else fi fi -echo "Starting webserver and WebSockets proxy on port ${PORT}" -#${HERE}/websockify --web ${WEB} ${CERT:+--cert ${CERT}} ${PORT} ${VNC_DEST} & -${WEBSOCKIFY} ${SYSLOG_ARG} ${SSLONLY} ${FILEONLY_ARG} --web ${WEB} ${CERT:+--cert ${CERT}} ${KEY:+--key ${KEY}} ${PORT} ${VNC_DEST} ${HEARTBEAT_ARG} ${IDLETIMEOUT_ARG} ${RECORD_ARG} ${TIMEOUT_ARG} ${WEBAUTH_ARG} ${AUTHPLUGIN_ARG} ${AUTHSOURCE_ARG} & +echo "Starting webserver and WebSockets proxy on${HOST:+ host ${HOST}} port ${PORT}" +#${HERE}/websockify --web ${WEB} ${CERT:+--cert ${CERT}} ${LISTEN} ${VNC_DEST} & +${WEBSOCKIFY} ${SYSLOG_ARG} ${SSLONLY} ${FILEONLY_ARG} --web ${WEB} ${CERT:+--cert ${CERT}} ${KEY:+--key ${KEY}} ${LISTEN} ${VNC_DEST} ${HEARTBEAT_ARG} ${IDLETIMEOUT_ARG} ${RECORD_ARG} ${TIMEOUT_ARG} ${WEBAUTH_ARG} ${AUTHPLUGIN_ARG} ${AUTHSOURCE_ARG} & proxy_pid="$!" sleep 1 if [ -z "$proxy_pid" ] || ! ps -eo pid= | grep -w "$proxy_pid" > /dev/null; then @@ -202,11 +213,15 @@ if [ -z "$proxy_pid" ] || ! ps -eo pid= | grep -w "$proxy_pid" > /dev/null; then exit 1 fi +if [ -z "$HOST" ]; then + HOST=$(hostname) +fi + echo -e "\n\nNavigate to this URL:\n" if [ "x$SSLONLY" == "x" ]; then - echo -e " http://$(hostname):${PORT}/vnc.html?host=$(hostname)&port=${PORT}\n" + echo -e " http://${HOST}:${PORT}/vnc.html?host=${HOST}&port=${PORT}\n" else - echo -e " https://$(hostname):${PORT}/vnc.html?host=$(hostname)&port=${PORT}\n" + echo -e " https://${HOST}:${PORT}/vnc.html?host=${HOST}&port=${PORT}\n" fi echo -e "Press Ctrl-C to exit\n\n" From a792b7f39e8d2c429655bcff2e54a35b3cf9f049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Sat, 14 Oct 2023 09:51:29 +0200 Subject: [PATCH 010/166] Document default port applies to all interfaces --- docs/novnc_proxy.1 | 2 +- utils/novnc_proxy | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/novnc_proxy.1 b/docs/novnc_proxy.1 index 78f06355e..259e1b413 100644 --- a/docs/novnc_proxy.1 +++ b/docs/novnc_proxy.1 @@ -9,7 +9,7 @@ Starts the WebSockets proxy and a mini-webserver and provides a cut-and-paste URL to go to. --listen [HOST:]PORT Port for proxy/webserver to listen on - Default: 6080 + Default: 6080 (on all interfaces) --vnc VNC_HOST:PORT VNC server host:port proxy target Default: localhost:5900 --cert CERT Path to combined cert/key file, or just diff --git a/utils/novnc_proxy b/utils/novnc_proxy index d5b17dd00..9d2ae0025 100755 --- a/utils/novnc_proxy +++ b/utils/novnc_proxy @@ -14,7 +14,7 @@ usage() { echo "provides a cut-and-paste URL to go to." echo echo " --listen [HOST:]PORT Port for proxy/webserver to listen on" - echo " Default: 6080" + echo " Default: 6080 (on all interfaces)" echo " --vnc VNC_HOST:PORT VNC server host:port proxy target" echo " Default: localhost:5900" echo " --cert CERT Path to combined cert/key file, or just" From 5ebc297164a3c8a194200e141332f3a0a2bc7cc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Sat, 14 Oct 2023 09:52:13 +0200 Subject: [PATCH 011/166] Remove comment about websockify command arguments --- utils/novnc_proxy | 1 - 1 file changed, 1 deletion(-) diff --git a/utils/novnc_proxy b/utils/novnc_proxy index 9d2ae0025..f805db236 100755 --- a/utils/novnc_proxy +++ b/utils/novnc_proxy @@ -203,7 +203,6 @@ else fi echo "Starting webserver and WebSockets proxy on${HOST:+ host ${HOST}} port ${PORT}" -#${HERE}/websockify --web ${WEB} ${CERT:+--cert ${CERT}} ${LISTEN} ${VNC_DEST} & ${WEBSOCKIFY} ${SYSLOG_ARG} ${SSLONLY} ${FILEONLY_ARG} --web ${WEB} ${CERT:+--cert ${CERT}} ${KEY:+--key ${KEY}} ${LISTEN} ${VNC_DEST} ${HEARTBEAT_ARG} ${IDLETIMEOUT_ARG} ${RECORD_ARG} ${TIMEOUT_ARG} ${WEBAUTH_ARG} ${AUTHPLUGIN_ARG} ${AUTHSOURCE_ARG} & proxy_pid="$!" sleep 1 From 9ac632deee0410be07187dcbb3d40fbf00ede0c3 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 5 Dec 2023 11:30:30 +0100 Subject: [PATCH 012/166] Handle immediate connection errors The browser might throw an exception right away if there is something it doesn't like with our connect attempt. E.g. using a non-TLS WebSocket from a TLS web page. --- app/ui.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/ui.js b/app/ui.js index 85695ca2e..00628c721 100644 --- a/app/ui.js +++ b/app/ui.js @@ -1041,10 +1041,18 @@ const UI = { } url += '/' + path; - UI.rfb = new RFB(document.getElementById('noVNC_container'), url, - { shared: UI.getSetting('shared'), - repeaterID: UI.getSetting('repeaterID'), - credentials: { password: password } }); + try { + UI.rfb = new RFB(document.getElementById('noVNC_container'), url, + { shared: UI.getSetting('shared'), + repeaterID: UI.getSetting('repeaterID'), + credentials: { password: password } }); + } catch (exc) { + Log.Error("Failed to connect to server: " + exc); + UI.updateVisualState('disconnected'); + UI.showStatus(_("Failed to connect to server: ") + exc, 'error'); + return; + } + UI.rfb.addEventListener("connect", UI.connectFinished); UI.rfb.addEventListener("disconnect", UI.disconnectFinished); UI.rfb.addEventListener("serververification", UI.serverVerify); From 829725b30e3d3486991e34db0c86406e556ebf98 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 5 Dec 2023 11:33:15 +0100 Subject: [PATCH 013/166] Handle relative paths in novnc_proxy websockify changes the working directory before it starts looking for files, so we must give it relative paths for things to work reliably. --- utils/novnc_proxy | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/utils/novnc_proxy b/utils/novnc_proxy index f805db236..4b2e3032d 100755 --- a/utils/novnc_proxy +++ b/utils/novnc_proxy @@ -56,7 +56,7 @@ KEY="" WEB="" proxy_pid="" SSLONLY="" -RECORD_ARG="" +RECORD="" SYSLOG_ARG="" HEARTBEAT_ARG="" IDLETIMEOUT_ARG="" @@ -95,7 +95,7 @@ while [ "$*" ]; do --web) WEB="${OPTARG}"; shift ;; --ssl-only) SSLONLY="--ssl-only" ;; --file-only) FILEONLY_ARG="--file-only" ;; - --record) RECORD_ARG="--record ${OPTARG}"; shift ;; + --record) RECORD="${OPTARG}"; shift ;; --syslog) SYSLOG_ARG="--syslog ${OPTARG}"; shift ;; --heartbeat) HEARTBEAT_ARG="--heartbeat ${OPTARG}"; shift ;; --idle-timeout) IDLETIMEOUT_ARG="--idle-timeout ${OPTARG}"; shift ;; @@ -202,8 +202,14 @@ else fi fi +# Make all file paths absolute as websockify changes working directory +WEB=`realpath "${WEB}"` +[ -n "${CERT}" ] && CERT=`realpath "${CERT}"` +[ -n "${KEY}" ] && KEY=`realpath "${KEY}"` +[ -n "${RECORD}" ] && RECORD=`realpath "${RECORD}"` + echo "Starting webserver and WebSockets proxy on${HOST:+ host ${HOST}} port ${PORT}" -${WEBSOCKIFY} ${SYSLOG_ARG} ${SSLONLY} ${FILEONLY_ARG} --web ${WEB} ${CERT:+--cert ${CERT}} ${KEY:+--key ${KEY}} ${LISTEN} ${VNC_DEST} ${HEARTBEAT_ARG} ${IDLETIMEOUT_ARG} ${RECORD_ARG} ${TIMEOUT_ARG} ${WEBAUTH_ARG} ${AUTHPLUGIN_ARG} ${AUTHSOURCE_ARG} & +${WEBSOCKIFY} ${SYSLOG_ARG} ${SSLONLY} ${FILEONLY_ARG} --web ${WEB} ${CERT:+--cert ${CERT}} ${KEY:+--key ${KEY}} ${LISTEN} ${VNC_DEST} ${HEARTBEAT_ARG} ${IDLETIMEOUT_ARG} ${RECORD:+--record ${RECORD}} ${TIMEOUT_ARG} ${WEBAUTH_ARG} ${AUTHPLUGIN_ARG} ${AUTHSOURCE_ARG} & proxy_pid="$!" sleep 1 if [ -z "$proxy_pid" ] || ! ps -eo pid= | grep -w "$proxy_pid" > /dev/null; then From 796e924e477fca35c10e8d344fe38351a55565b6 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Wed, 10 Jan 2024 07:56:13 +0100 Subject: [PATCH 014/166] Remove unused npm dependencies These should have been removed as part of 890cff9. --- package.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/package.json b/package.json index 5847887f1..740f0d21e 100644 --- a/package.json +++ b/package.json @@ -39,17 +39,11 @@ "homepage": "https://github.com/novnc/noVNC", "devDependencies": { "@babel/core": "latest", - "@babel/plugin-syntax-dynamic-import": "latest", - "@babel/plugin-transform-modules-commonjs": "latest", "@babel/preset-env": "latest", - "@babel/cli": "latest", "babel-plugin-import-redirect": "latest", "browserify": "latest", - "babelify": "latest", - "core-js": "latest", "chai": "latest", "commander": "latest", - "es-module-loader": "latest", "eslint": "latest", "fs-extra": "latest", "jsdom": "latest", From d3aaf4d5b3f33b80bb6fe21ae71a63c8a331da9b Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Wed, 10 Jan 2024 14:44:44 +0100 Subject: [PATCH 015/166] Upgrade base snap to Ubuntu 22.04 Ubuntu 18.04 base snap is no longer supported, so switch to the currently newest one. --- snap/snapcraft.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 02094820d..ccf618930 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: novnc -base: core18 # the base snap is the execution environment for this snap +base: core22 # the base snap is the execution environment for this snap version: git summary: Open Source VNC client using HTML5 (WebSockets, Canvas) description: | From b35cf6dd1253142267f68f052986d0560f7a495c Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Wed, 17 Jan 2024 16:19:16 +0100 Subject: [PATCH 016/166] Don't include ES6 module versions in npm package The npm package is supposed to be for CommonJS usage, so only package that to avoid confusion. This has become an issue now that nodejs supports ES6 modules, where users are accidentally trying to import the wrong files and get errors. --- package.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/package.json b/package.json index 740f0d21e..482b86eb3 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,7 @@ "VERSION", "docs/API.md", "docs/LIBRARY.md", - "docs/LICENSE*", - "core", - "vendor/pako" + "docs/LICENSE*" ], "scripts": { "lint": "eslint app core po/po2js po/xgettext-html tests utils", From fca48df85d394b2c1e9d26ed6e9d10d33db9f4f4 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 19 Jan 2024 15:58:48 +0100 Subject: [PATCH 017/166] Increase test timeout for Chrome on Windows There is some bug in Chrome 119+ on some systems, where it takes forever for the first readback from a canvas, timing out the first test that does this. Work around the issue by increasing the timeout on that platform until Chrome manages to resolve the issue. --- karma.conf.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/karma.conf.js b/karma.conf.js index 1ea17475a..faa8beea2 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -81,5 +81,12 @@ module.exports = (config) => { singleRun: true, }; + if (process.env.TEST_BROWSER_NAME === 'ChromeHeadless') { + let os = require('os'); + if (os.platform() === 'win32') { + my_conf.client.mocha['timeout'] = 5000; + } + } + config.set(my_conf); }; From ab2fd4169348e968ddf60b3334dcbe3b377e7cbc Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 23 Jan 2024 12:51:35 +0100 Subject: [PATCH 018/166] Handle broken Oculus browser keyboard events It sets KeyboardEvent.key to "Unidentified" for all non-character keys, which means we must ignore it and use the legacy handling to figure out the key pressed. --- core/input/util.js | 2 +- tests/test.helper.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/input/util.js b/core/input/util.js index 58f84e558..36b698176 100644 --- a/core/input/util.js +++ b/core/input/util.js @@ -67,7 +67,7 @@ export function getKeycode(evt) { // Get 'KeyboardEvent.key', handling legacy browsers export function getKey(evt) { // Are we getting a proper key value? - if (evt.key !== undefined) { + if ((evt.key !== undefined) && (evt.key !== 'Unidentified')) { // Mozilla isn't fully in sync with the spec yet switch (evt.key) { case 'OS': return 'Meta'; diff --git a/tests/test.helper.js b/tests/test.helper.js index ff83c5392..9995973fd 100644 --- a/tests/test.helper.js +++ b/tests/test.helper.js @@ -108,6 +108,8 @@ describe('Helpers', function () { }); it('should use charCode if no key', function () { expect(KeyboardUtil.getKey({charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.be.equal('Š'); + // Broken Oculus browser + expect(KeyboardUtil.getKey({charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43, key: 'Unidentified'})).to.be.equal('Š'); }); it('should return Unidentified when it cannot map the key', function () { expect(KeyboardUtil.getKey({keycode: 0x42})).to.be.equal('Unidentified'); From 48c8e41877a98dbf6c65e2a8086160c82b01e7dd Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 23 Jan 2024 12:54:18 +0100 Subject: [PATCH 019/166] Fix key event debug output Fix for a0b7c0dac5359e4002e7f1d946e60e2eb9b4a54e. --- core/input/keyboard.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/input/keyboard.js b/core/input/keyboard.js index 9068e9e9f..68da2312b 100644 --- a/core/input/keyboard.js +++ b/core/input/keyboard.js @@ -48,7 +48,8 @@ export default class Keyboard { } Log.Debug("onkeyevent " + (down ? "down" : "up") + - ", keysym: " + keysym, ", code: " + code, + ", numlock: " + numlock + ", capslock: " + capslock); + ", keysym: " + keysym, ", code: " + code + + ", numlock: " + numlock + ", capslock: " + capslock); this.onkeyevent(keysym, code, down, numlock, capslock); } From bd32922ff8f6209bdb20dc0b75c034d1b304dc7a Mon Sep 17 00:00:00 2001 From: Simon Bungartz Date: Wed, 31 Jan 2024 16:16:01 +0000 Subject: [PATCH 020/166] Avoid exception when cursor was removed from DOM already --- core/util/cursor.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/util/cursor.js b/core/util/cursor.js index 3000cf0e6..20e75f1b2 100644 --- a/core/util/cursor.js +++ b/core/util/cursor.js @@ -69,7 +69,9 @@ export default class Cursor { this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options); this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options); - document.body.removeChild(this._canvas); + if (document.contains(this._canvas)) { + document.body.removeChild(this._canvas); + } } this._target = null; From e75938bebcf88b678f24ed35b78be9eaf982d213 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Fri, 2 Feb 2024 16:51:21 +0100 Subject: [PATCH 021/166] Make non-HTTPS message more harsh As browsers are placing more and more new functionality as secure-context only, we need to prepare users for more problems. I find it likely that we will disable non-HTTPS connections in the future. --- app/ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/ui.js b/app/ui.js index 00628c721..f27dfe28e 100644 --- a/app/ui.js +++ b/app/ui.js @@ -66,7 +66,7 @@ const UI = { // insecure context if (!window.isSecureContext) { // FIXME: This gets hidden when connecting - UI.showStatus(_("HTTPS is required for full functionality"), 'error'); + UI.showStatus(_("Running without HTTPS is not recommended, crashes or other issues are likely."), 'error'); } // Try to fetch version number From 60643fe6955f107faecdd0d6af8a213d65fdc68b Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 5 Feb 2024 16:34:47 +0100 Subject: [PATCH 022/166] Update github actions to latest versions Primarily to avoid the versions that are now deprecated, but also update actions/upload-artifact to keep us up to date. --- .github/workflows/deploy.yml | 10 +++++----- .github/workflows/lint.yml | 8 ++++---- .github/workflows/test.yml | 4 ++-- .github/workflows/translate.yml | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 84e634d2f..a11d3d0a5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -10,18 +10,18 @@ jobs: npm: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: | GITREV=$(git rev-parse --short HEAD) echo $GITREV sed -i "s/^\(.*\"version\".*\)\"\([^\"]\+\)\"\(.*\)\$/\1\"\2-g$GITREV\"\3/" package.json if: github.event_name != 'release' - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: # Needs to be explicitly specified for auth to work registry-url: 'https://registry.npmjs.org' - run: npm install - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: npm path: lib @@ -49,7 +49,7 @@ jobs: snap: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: | GITREV=$(git rev-parse --short HEAD) echo $GITREV @@ -61,7 +61,7 @@ jobs: sed -i "s/^version:.*/version: '$VERSION'/" snap/snapcraft.yaml - uses: snapcore/action-build@v1 id: snapcraft - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: snap path: ${{ steps.snapcraft.outputs.snap }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7cd5b2156..540bb9907 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,14 +6,14 @@ jobs: eslint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 - run: npm update - run: npm run lint html: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 - run: npm update - run: git ls-tree --name-only -r HEAD | grep -E "[.](html|css)$" | xargs ./utils/validate diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1bc1728a3..b72195b52 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,8 +20,8 @@ jobs: fail-fast: false runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 - run: npm update - run: npm run test env: diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml index ea6e6bb36..a4da9cbfa 100644 --- a/.github/workflows/translate.yml +++ b/.github/workflows/translate.yml @@ -6,8 +6,8 @@ jobs: translate: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 - run: npm update - run: sudo apt-get install gettext - run: make -C po update-pot From cd927723bc98bd56f23119b8bcc011139a382702 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 5 Feb 2024 16:43:29 +0100 Subject: [PATCH 023/166] Fix import of "commander" The default import was deprecated ages ago, and in v12 it has now finally been changed in a breaking way. Change the code to import things the proper way. --- utils/convert.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/convert.js b/utils/convert.js index aeba49d9f..617f4ed65 100755 --- a/utils/convert.js +++ b/utils/convert.js @@ -1,7 +1,7 @@ #!/usr/bin/env node const path = require('path'); -const program = require('commander'); +const { program } = require('commander'); const fs = require('fs'); const fse = require('fs-extra'); const babel = require('@babel/core'); From 9a1b1f0d06567739251314a3b67ac5c432ecad47 Mon Sep 17 00:00:00 2001 From: Kostiantyn Syrykh Date: Mon, 25 Mar 2024 17:35:28 +0200 Subject: [PATCH 024/166] Clipboard: handle multiple CR+LF --- core/rfb.js | 2 +- tests/test.rfb.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index c71d6b88f..f2deb0e7b 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -2356,7 +2356,7 @@ export default class RFB extends EventTargetMixin { textData = textData.slice(0, -1); } - textData = textData.replace("\r\n", "\n"); + textData = textData.replaceAll("\r\n", "\n"); this.dispatchEvent(new CustomEvent( "clipboard", diff --git a/tests/test.rfb.js b/tests/test.rfb.js index c1ee8b16d..62b80ca3f 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -3263,11 +3263,11 @@ describe('Remote Frame Buffer Protocol Client', function () { }); it('should update clipboard with correct escape characters from a Provide message ', function () { - let expectedData = "Oh\nmy!"; + let expectedData = "Oh\nmy\n!"; let data = [3, 0, 0, 0]; const flags = [0x10, 0x00, 0x00, 0x01]; - let text = encodeUTF8("Oh\r\nmy!\0"); + let text = encodeUTF8("Oh\r\nmy\r\n!\0"); let deflatedText = deflateWithSize(text); From 92c8a91964313e51008c6c9bd73234d508d2584b Mon Sep 17 00:00:00 2001 From: Bubble Date: Wed, 24 Apr 2024 22:54:24 +0800 Subject: [PATCH 025/166] Update zh_CN.po (#1851) Update Chinese translation --- po/zh_CN.po | 132 ++++++++++++++++++++++++++-------------------------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/po/zh_CN.po b/po/zh_CN.po index ede9d4414..caae28504 100644 --- a/po/zh_CN.po +++ b/po/zh_CN.po @@ -15,58 +15,42 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: ../app/ui.js:395 +#: ../app/ui.js:430 msgid "Connecting..." msgstr "连接中..." -#: ../app/ui.js:402 -msgid "Disconnecting..." -msgstr "正在断开连接..." - -#: ../app/ui.js:408 -msgid "Reconnecting..." -msgstr "重新连接中..." - -#: ../app/ui.js:413 -msgid "Internal error" -msgstr "内部错误" - -#: ../app/ui.js:1015 -msgid "Must set host" -msgstr "请提供主机名" - -#: ../app/ui.js:1097 +#: ../app/ui.js:438 msgid "Connected (encrypted) to " -msgstr "已连接到(加密)" +msgstr "已连接(已加密)到" -#: ../app/ui.js:1099 +#: ../app/ui.js:440 msgid "Connected (unencrypted) to " -msgstr "已连接到(未加密)" +msgstr "已连接(未加密)到" -#: ../app/ui.js:1120 -msgid "Something went wrong, connection is closed" -msgstr "发生错误,连接已关闭" - -#: ../app/ui.js:1123 -msgid "Failed to connect to server" -msgstr "无法连接到服务器" +#: ../app/ui.js:446 +msgid "Disconnecting..." +msgstr "正在断开连接..." -#: ../app/ui.js:1133 +#: ../app/ui.js:450 msgid "Disconnected" msgstr "已断开连接" -#: ../app/ui.js:1146 -msgid "New connection has been rejected with reason: " -msgstr "连接被拒绝,原因:" +#: ../app/ui.js:1052 ../core/rfb.js:248 +msgid "Must set host" +msgstr "必须设置主机" -#: ../app/ui.js:1149 -msgid "New connection has been rejected" -msgstr "连接被拒绝" +#: ../app/ui.js:1101 +msgid "Reconnecting..." +msgstr "重新连接中..." -#: ../app/ui.js:1170 +#: ../app/ui.js:1140 msgid "Password is required" msgstr "请提供密码" +#: ../core/rfb.js:548 +msgid "Disconnect timeout" +msgstr "超时断开" + #: ../vnc.html:89 msgid "noVNC encountered an error:" msgstr "noVNC 遇到一个错误:" @@ -77,31 +61,31 @@ msgstr "显示/隐藏控制栏" #: ../vnc.html:106 msgid "Move/Drag Viewport" -msgstr "拖放显示范围" +msgstr "移动/拖动窗口" #: ../vnc.html:106 msgid "viewport drag" -msgstr "显示范围拖放" +msgstr "窗口拖动" #: ../vnc.html:112 ../vnc.html:115 ../vnc.html:118 ../vnc.html:121 msgid "Active Mouse Button" -msgstr "启动鼠标按鍵" +msgstr "启动鼠标按键" #: ../vnc.html:112 msgid "No mousebutton" -msgstr "禁用鼠标按鍵" +msgstr "禁用鼠标按键" #: ../vnc.html:115 msgid "Left mousebutton" -msgstr "鼠标左鍵" +msgstr "鼠标左键" #: ../vnc.html:118 msgid "Middle mousebutton" -msgstr "鼠标中鍵" +msgstr "鼠标中键" #: ../vnc.html:121 msgid "Right mousebutton" -msgstr "鼠标右鍵" +msgstr "鼠标右键" #: ../vnc.html:124 msgid "Keyboard" @@ -127,6 +111,10 @@ msgstr "Ctrl" msgid "Toggle Ctrl" msgstr "切换 Ctrl" +#: ../vnc.html:136 +msgid "Edit clipboard content in the textarea below." +msgstr "在下面的文本区域中编辑剪贴板内容。" + #: ../vnc.html:139 msgid "Alt" msgstr "Alt" @@ -153,19 +141,19 @@ msgstr "发送 Escape 键" #: ../vnc.html:148 msgid "Ctrl+Alt+Del" -msgstr "Ctrl-Alt-Del" +msgstr "Ctrl+Alt+Del" #: ../vnc.html:148 msgid "Send Ctrl-Alt-Del" -msgstr "发送 Ctrl-Alt-Del 键" +msgstr "发送 Ctrl+Alt+Del 键" #: ../vnc.html:156 msgid "Shutdown/Reboot" -msgstr "关机/重新启动" +msgstr "关机/重启" #: ../vnc.html:156 msgid "Shutdown/Reboot..." -msgstr "关机/重新启动..." +msgstr "关机/重启..." #: ../vnc.html:162 msgid "Power" @@ -177,7 +165,7 @@ msgstr "关机" #: ../vnc.html:165 msgid "Reboot" -msgstr "重新启动" +msgstr "重启" #: ../vnc.html:166 msgid "Reset" @@ -199,6 +187,10 @@ msgstr "全屏" msgid "Settings" msgstr "设置" +#: ../vnc.html:200 +msgid "Encrypt" +msgstr "加密" + #: ../vnc.html:202 msgid "Shared Mode" msgstr "分享模式" @@ -224,61 +216,69 @@ msgid "Local Scaling" msgstr "本地缩放" #: ../vnc.html:216 +msgid "Local Downscaling" +msgstr "降低本地尺寸" + +#: ../vnc.html:217 msgid "Remote Resizing" msgstr "远程调整大小" -#: ../vnc.html:221 +#: ../vnc.html:222 msgid "Advanced" msgstr "高级" -#: ../vnc.html:224 +#: ../vnc.html:225 +msgid "Local Cursor" +msgstr "本地光标" + +#: ../vnc.html:229 msgid "Repeater ID:" msgstr "中继站 ID" -#: ../vnc.html:228 +#: ../vnc.html:233 msgid "WebSocket" msgstr "WebSocket" -#: ../vnc.html:231 -msgid "Encrypt" -msgstr "加密" - -#: ../vnc.html:234 +#: ../vnc.html:239 msgid "Host:" msgstr "主机:" -#: ../vnc.html:238 +#: ../vnc.html:243 msgid "Port:" msgstr "端口:" -#: ../vnc.html:242 +#: ../vnc.html:247 msgid "Path:" msgstr "路径:" -#: ../vnc.html:249 +#: ../vnc.html:254 msgid "Automatic Reconnect" msgstr "自动重新连接" -#: ../vnc.html:252 +#: ../vnc.html:257 msgid "Reconnect Delay (ms):" msgstr "重新连接间隔 (ms):" -#: ../vnc.html:258 +#: ../vnc.html:263 msgid "Logging:" msgstr "日志级别:" -#: ../vnc.html:270 +#: ../vnc.html:275 msgid "Disconnect" -msgstr "中断连接" +msgstr "断开连接" -#: ../vnc.html:289 +#: ../vnc.html:294 msgid "Connect" msgstr "连接" -#: ../vnc.html:299 +#: ../vnc.html:304 msgid "Password:" msgstr "密码:" -#: ../vnc.html:313 +#: ../vnc.html:318 msgid "Cancel" msgstr "取消" + +#: ../vnc.html:334 +msgid "Canvas not supported." +msgstr "不支持 Canvas。" \ No newline at end of file From 9d293f1ababd710d4ebc225300ef075b78d637c5 Mon Sep 17 00:00:00 2001 From: Giannis Kosmas Date: Wed, 24 Apr 2024 19:53:49 +0300 Subject: [PATCH 026/166] Updated el.po --- po/el.po | 262 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 169 insertions(+), 93 deletions(-) diff --git a/po/el.po b/po/el.po index 5213ae542..de690fe93 100644 --- a/po/el.po +++ b/po/el.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: noVNC 0.6.1\n" "Report-Msgid-Bugs-To: novnc@googlegroups.com\n" -"POT-Creation-Date: 2017-11-17 21:40+0200\n" +"POT-Creation-Date: 2022-12-27 15:24+0100\n" "PO-Revision-Date: 2017-10-11 16:16+0200\n" "Last-Translator: Giannis Kosmas \n" "Language-Team: none\n" @@ -17,273 +17,349 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: ../app/ui.js:404 +#: ../app/ui.js:69 +msgid "HTTPS is required for full functionality" +msgstr "Το HTTPS είναι απαιτούμενο για πλήρη λειτουργικότητα" + +#: ../app/ui.js:410 msgid "Connecting..." msgstr "Συνδέεται..." -#: ../app/ui.js:411 +#: ../app/ui.js:417 msgid "Disconnecting..." msgstr "Aποσυνδέεται..." -#: ../app/ui.js:417 +#: ../app/ui.js:423 msgid "Reconnecting..." msgstr "Επανασυνδέεται..." -#: ../app/ui.js:422 +#: ../app/ui.js:428 msgid "Internal error" msgstr "Εσωτερικό σφάλμα" -#: ../app/ui.js:1019 +#: ../app/ui.js:1026 msgid "Must set host" msgstr "Πρέπει να οριστεί ο διακομιστής" -#: ../app/ui.js:1099 +#: ../app/ui.js:1110 msgid "Connected (encrypted) to " msgstr "Συνδέθηκε (κρυπτογραφημένα) με το " -#: ../app/ui.js:1101 +#: ../app/ui.js:1112 msgid "Connected (unencrypted) to " msgstr "Συνδέθηκε (μη κρυπτογραφημένα) με το " -#: ../app/ui.js:1119 +#: ../app/ui.js:1135 msgid "Something went wrong, connection is closed" msgstr "Κάτι πήγε στραβά, η σύνδεση διακόπηκε" -#: ../app/ui.js:1129 +#: ../app/ui.js:1138 +msgid "Failed to connect to server" +msgstr "Αποτυχία στη σύνδεση με το διακομιστή" + +#: ../app/ui.js:1150 msgid "Disconnected" msgstr "Αποσυνδέθηκε" -#: ../app/ui.js:1142 +#: ../app/ui.js:1165 msgid "New connection has been rejected with reason: " msgstr "Η νέα σύνδεση απορρίφθηκε διότι: " -#: ../app/ui.js:1145 +#: ../app/ui.js:1168 msgid "New connection has been rejected" msgstr "Η νέα σύνδεση απορρίφθηκε " -#: ../app/ui.js:1166 -msgid "Password is required" -msgstr "Απαιτείται ο κωδικός πρόσβασης" +#: ../app/ui.js:1234 +msgid "Credentials are required" +msgstr "Απαιτούνται διαπιστευτήρια" -#: ../vnc.html:89 +#: ../vnc.html:57 msgid "noVNC encountered an error:" msgstr "το noVNC αντιμετώπισε ένα σφάλμα:" -#: ../vnc.html:99 +#: ../vnc.html:67 msgid "Hide/Show the control bar" msgstr "Απόκρυψη/Εμφάνιση γραμμής ελέγχου" -#: ../vnc.html:106 +#: ../vnc.html:76 +msgid "Drag" +msgstr "Σύρσιμο" + +#: ../vnc.html:76 msgid "Move/Drag Viewport" msgstr "Μετακίνηση/Σύρσιμο Θεατού πεδίου" -#: ../vnc.html:106 -msgid "viewport drag" -msgstr "σύρσιμο θεατού πεδίου" - -#: ../vnc.html:112 ../vnc.html:115 ../vnc.html:118 ../vnc.html:121 -msgid "Active Mouse Button" -msgstr "Ενεργό Πλήκτρο Ποντικιού" - -#: ../vnc.html:112 -msgid "No mousebutton" -msgstr "Χωρίς Πλήκτρο Ποντικιού" - -#: ../vnc.html:115 -msgid "Left mousebutton" -msgstr "Αριστερό Πλήκτρο Ποντικιού" - -#: ../vnc.html:118 -msgid "Middle mousebutton" -msgstr "Μεσαίο Πλήκτρο Ποντικιού" - -#: ../vnc.html:121 -msgid "Right mousebutton" -msgstr "Δεξί Πλήκτρο Ποντικιού" - -#: ../vnc.html:124 +#: ../vnc.html:82 msgid "Keyboard" msgstr "Πληκτρολόγιο" -#: ../vnc.html:124 +#: ../vnc.html:82 msgid "Show Keyboard" msgstr "Εμφάνιση Πληκτρολογίου" -#: ../vnc.html:131 +#: ../vnc.html:87 msgid "Extra keys" msgstr "Επιπλέον πλήκτρα" -#: ../vnc.html:131 +#: ../vnc.html:87 msgid "Show Extra Keys" msgstr "Εμφάνιση Επιπλέον Πλήκτρων" -#: ../vnc.html:136 +#: ../vnc.html:92 msgid "Ctrl" msgstr "Ctrl" -#: ../vnc.html:136 +#: ../vnc.html:92 msgid "Toggle Ctrl" msgstr "Εναλλαγή Ctrl" -#: ../vnc.html:139 +#: ../vnc.html:95 msgid "Alt" msgstr "Alt" -#: ../vnc.html:139 +#: ../vnc.html:95 msgid "Toggle Alt" msgstr "Εναλλαγή Alt" -#: ../vnc.html:142 +#: ../vnc.html:98 +msgid "Toggle Windows" +msgstr "Εναλλαγή Παράθυρων" + +#: ../vnc.html:98 +msgid "Windows" +msgstr "Παράθυρα" + +#: ../vnc.html:101 msgid "Send Tab" msgstr "Αποστολή Tab" -#: ../vnc.html:142 +#: ../vnc.html:101 msgid "Tab" msgstr "Tab" -#: ../vnc.html:145 +#: ../vnc.html:104 msgid "Esc" msgstr "Esc" -#: ../vnc.html:145 +#: ../vnc.html:104 msgid "Send Escape" msgstr "Αποστολή Escape" -#: ../vnc.html:148 +#: ../vnc.html:107 msgid "Ctrl+Alt+Del" msgstr "Ctrl+Alt+Del" -#: ../vnc.html:148 +#: ../vnc.html:107 msgid "Send Ctrl-Alt-Del" msgstr "Αποστολή Ctrl-Alt-Del" -#: ../vnc.html:156 +#: ../vnc.html:114 msgid "Shutdown/Reboot" msgstr "Κλείσιμο/Επανεκκίνηση" -#: ../vnc.html:156 +#: ../vnc.html:114 msgid "Shutdown/Reboot..." msgstr "Κλείσιμο/Επανεκκίνηση..." -#: ../vnc.html:162 +#: ../vnc.html:120 msgid "Power" msgstr "Απενεργοποίηση" -#: ../vnc.html:164 +#: ../vnc.html:122 msgid "Shutdown" msgstr "Κλείσιμο" -#: ../vnc.html:165 +#: ../vnc.html:123 msgid "Reboot" msgstr "Επανεκκίνηση" -#: ../vnc.html:166 +#: ../vnc.html:124 msgid "Reset" msgstr "Επαναφορά" -#: ../vnc.html:171 ../vnc.html:177 +#: ../vnc.html:129 ../vnc.html:135 msgid "Clipboard" msgstr "Πρόχειρο" -#: ../vnc.html:181 -msgid "Clear" -msgstr "Καθάρισμα" +#: ../vnc.html:137 +msgid "Edit clipboard content in the textarea below." +msgstr "Επεξεργαστείτε το περιεχόμενο του πρόχειρου στην περιοχή κειμένου παρακάτω." -#: ../vnc.html:187 -msgid "Fullscreen" +#: ../vnc.html:145 +#, fuzzy +msgid "Full Screen" msgstr "Πλήρης Οθόνη" -#: ../vnc.html:192 ../vnc.html:199 +#: ../vnc.html:150 ../vnc.html:156 msgid "Settings" msgstr "Ρυθμίσεις" -#: ../vnc.html:202 +#: ../vnc.html:160 msgid "Shared Mode" msgstr "Κοινόχρηστη Λειτουργία" -#: ../vnc.html:205 +#: ../vnc.html:163 msgid "View Only" msgstr "Μόνο Θέαση" -#: ../vnc.html:209 +#: ../vnc.html:167 msgid "Clip to Window" msgstr "Αποκοπή στο όριο του Παράθυρου" -#: ../vnc.html:212 +#: ../vnc.html:170 msgid "Scaling Mode:" msgstr "Λειτουργία Κλιμάκωσης:" -#: ../vnc.html:214 +#: ../vnc.html:172 msgid "None" msgstr "Καμία" -#: ../vnc.html:215 +#: ../vnc.html:173 msgid "Local Scaling" msgstr "Τοπική Κλιμάκωση" -#: ../vnc.html:216 +#: ../vnc.html:174 msgid "Remote Resizing" msgstr "Απομακρυσμένη Αλλαγή μεγέθους" -#: ../vnc.html:221 +#: ../vnc.html:179 msgid "Advanced" msgstr "Για προχωρημένους" -#: ../vnc.html:224 +#: ../vnc.html:182 +msgid "Quality:" +msgstr "Ποιότητα:" + +#: ../vnc.html:186 +msgid "Compression level:" +msgstr "Επίπεδο συμπίεσης:" + +#: ../vnc.html:191 msgid "Repeater ID:" msgstr "Repeater ID:" -#: ../vnc.html:228 +#: ../vnc.html:195 msgid "WebSocket" msgstr "WebSocket" -#: ../vnc.html:231 +#: ../vnc.html:198 msgid "Encrypt" msgstr "Κρυπτογράφηση" -#: ../vnc.html:234 +#: ../vnc.html:201 msgid "Host:" msgstr "Όνομα διακομιστή:" -#: ../vnc.html:238 +#: ../vnc.html:205 msgid "Port:" msgstr "Πόρτα διακομιστή:" -#: ../vnc.html:242 +#: ../vnc.html:209 msgid "Path:" msgstr "Διαδρομή:" -#: ../vnc.html:249 +#: ../vnc.html:216 msgid "Automatic Reconnect" msgstr "Αυτόματη επανασύνδεση" -#: ../vnc.html:252 +#: ../vnc.html:219 msgid "Reconnect Delay (ms):" msgstr "Καθυστέρηση επανασύνδεσης (ms):" -#: ../vnc.html:258 +#: ../vnc.html:224 +msgid "Show Dot when No Cursor" +msgstr "Εμφάνιση Τελείας όταν δεν υπάρχει Δρομέας" + +#: ../vnc.html:229 msgid "Logging:" msgstr "Καταγραφή:" -#: ../vnc.html:270 +#: ../vnc.html:238 +msgid "Version:" +msgstr "Έκδοση:" + +#: ../vnc.html:246 msgid "Disconnect" msgstr "Αποσύνδεση" -#: ../vnc.html:289 +#: ../vnc.html:269 msgid "Connect" msgstr "Σύνδεση" -#: ../vnc.html:299 +#: ../vnc.html:278 +msgid "Server identity" +msgstr "Ταυτότητα Διακομιστή" + +#: ../vnc.html:281 +msgid "The server has provided the following identifying information:" +msgstr "Ο διακομιστής παρείχε την ακόλουθη πληροφορία ταυτοποίησης:" + +#: ../vnc.html:285 +msgid "Fingerprint:" +msgstr "Δακτυλικό αποτύπωμα:" + +#: ../vnc.html:288 +msgid "" +"Please verify that the information is correct and press \"Approve\". " +"Otherwise press \"Reject\"." +msgstr "" +"Παρακαλώ επαληθεύσετε ότι η πληροφορία είναι σωστή και πιέστε \"Αποδοχή\". " +"Αλλιώς πιέστε \"Απόρριψη\"." + +#: ../vnc.html:293 +msgid "Approve" +msgstr "Αποδοχή" + +#: ../vnc.html:294 +msgid "Reject" +msgstr "Απόρριψη" + +#: ../vnc.html:302 +msgid "Credentials" +msgstr "Διαπιστευτήρια" + +#: ../vnc.html:306 +msgid "Username:" +msgstr "Κωδικός Χρήστη:" + +#: ../vnc.html:310 msgid "Password:" msgstr "Κωδικός Πρόσβασης:" -#: ../vnc.html:313 +#: ../vnc.html:314 +msgid "Send Credentials" +msgstr "Αποστολή Διαπιστευτηρίων" + +#: ../vnc.html:323 msgid "Cancel" msgstr "Ακύρωση" -#: ../vnc.html:329 -msgid "Canvas not supported." -msgstr "Δεν υποστηρίζεται το στοιχείο Canvas" +#~ msgid "Password is required" +#~ msgstr "Απαιτείται ο κωδικός πρόσβασης" + +#~ msgid "viewport drag" +#~ msgstr "σύρσιμο θεατού πεδίου" + +#~ msgid "Active Mouse Button" +#~ msgstr "Ενεργό Πλήκτρο Ποντικιού" + +#~ msgid "No mousebutton" +#~ msgstr "Χωρίς Πλήκτρο Ποντικιού" + +#~ msgid "Left mousebutton" +#~ msgstr "Αριστερό Πλήκτρο Ποντικιού" + +#~ msgid "Middle mousebutton" +#~ msgstr "Μεσαίο Πλήκτρο Ποντικιού" + +#~ msgid "Right mousebutton" +#~ msgstr "Δεξί Πλήκτρο Ποντικιού" + +#~ msgid "Clear" +#~ msgstr "Καθάρισμα" + +#~ msgid "Canvas not supported." +#~ msgstr "Δεν υποστηρίζεται το στοιχείο Canvas" #~ msgid "Disconnect timeout" #~ msgstr "Παρέλευση χρονικού ορίου αποσύνδεσης" From 8d1b665808ce3da8307c5597d821f31eb925cb99 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Mon, 22 Apr 2024 08:49:30 +0200 Subject: [PATCH 027/166] Migrate deprecated eslint config to to new format The .eslintrc and .eslintignore formats are deprecated. The new format uses a single eslint.config.js (or .mjs) file at the top. --- .eslintignore | 1 - .eslintrc | 54 -------------------------- eslint.config.mjs | 98 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + po/.eslintrc | 5 --- tests/.eslintrc | 15 -------- utils/.eslintrc | 8 ---- 7 files changed, 99 insertions(+), 83 deletions(-) delete mode 100644 .eslintignore delete mode 100644 .eslintrc create mode 100644 eslint.config.mjs delete mode 100644 po/.eslintrc delete mode 100644 tests/.eslintrc delete mode 100644 utils/.eslintrc diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index d38162800..000000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -**/xtscancodes.js diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 10e15cec3..000000000 --- a/.eslintrc +++ /dev/null @@ -1,54 +0,0 @@ -{ - "env": { - "browser": true, - "es2020": true - }, - "parserOptions": { - "sourceType": "module", - "ecmaVersion": 2020 - }, - "extends": "eslint:recommended", - "rules": { - // Unsafe or confusing stuff that we forbid - - "no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": true }], - "no-constant-condition": ["error", { "checkLoops": false }], - "no-var": "error", - "no-useless-constructor": "error", - "object-shorthand": ["error", "methods", { "avoidQuotes": true }], - "prefer-arrow-callback": "error", - "arrow-body-style": ["error", "as-needed", { "requireReturnForObjectLiteral": false } ], - "arrow-parens": ["error", "as-needed", { "requireForBlockBody": true }], - "arrow-spacing": ["error"], - "no-confusing-arrow": ["error", { "allowParens": true }], - - // Enforced coding style - - "brace-style": ["error", "1tbs", { "allowSingleLine": true }], - "indent": ["error", 4, { "SwitchCase": 1, - "VariableDeclarator": "first", - "FunctionDeclaration": { "parameters": "first" }, - "FunctionExpression": { "parameters": "first" }, - "CallExpression": { "arguments": "first" }, - "ArrayExpression": "first", - "ObjectExpression": "first", - "ImportDeclaration": "first", - "ignoreComments": true }], - "comma-spacing": ["error"], - "comma-style": ["error"], - "curly": ["error", "multi-line"], - "func-call-spacing": ["error"], - "func-names": ["error"], - "func-style": ["error", "declaration", { "allowArrowFunctions": true }], - "key-spacing": ["error"], - "keyword-spacing": ["error"], - "no-trailing-spaces": ["error"], - "semi": ["error"], - "space-before-blocks": ["error"], - "space-before-function-paren": ["error", { "anonymous": "always", - "named": "never", - "asyncArrow": "always" }], - "switch-colon-spacing": ["error"], - "camelcase": ["error", { allow: ["^XK_", "^XF86XK_"] }], - } -} diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 000000000..e3bfcd78a --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,98 @@ +import globals from "globals"; +import js from "@eslint/js"; + +export default [ + js.configs.recommended, + { + languageOptions: { + ecmaVersion: 2020, + sourceType: "module", + globals: { + ...globals.browser, + ...globals.es2020, + } + }, + ignores: ["**/xtscancodes.js"], + rules: { + // Unsafe or confusing stuff that we forbid + + "no-unused-vars": ["error", { "vars": "all", + "args": "none", + "ignoreRestSiblings": true, + "caughtErrors": "none" }], + "no-constant-condition": ["error", { "checkLoops": false }], + "no-var": "error", + "no-useless-constructor": "error", + "object-shorthand": ["error", "methods", { "avoidQuotes": true }], + "prefer-arrow-callback": "error", + "arrow-body-style": ["error", "as-needed", { "requireReturnForObjectLiteral": false } ], + "arrow-parens": ["error", "as-needed", { "requireForBlockBody": true }], + "arrow-spacing": ["error"], + "no-confusing-arrow": ["error", { "allowParens": true }], + + // Enforced coding style + + "brace-style": ["error", "1tbs", { "allowSingleLine": true }], + "indent": ["error", 4, { "SwitchCase": 1, + "VariableDeclarator": "first", + "FunctionDeclaration": { "parameters": "first" }, + "FunctionExpression": { "parameters": "first" }, + "CallExpression": { "arguments": "first" }, + "ArrayExpression": "first", + "ObjectExpression": "first", + "ImportDeclaration": "first", + "ignoreComments": true }], + "comma-spacing": ["error"], + "comma-style": ["error"], + "curly": ["error", "multi-line"], + "func-call-spacing": ["error"], + "func-names": ["error"], + "func-style": ["error", "declaration", { "allowArrowFunctions": true }], + "key-spacing": ["error"], + "keyword-spacing": ["error"], + "no-trailing-spaces": ["error"], + "semi": ["error"], + "space-before-blocks": ["error"], + "space-before-function-paren": ["error", { "anonymous": "always", + "named": "never", + "asyncArrow": "always" }], + "switch-colon-spacing": ["error"], + "camelcase": ["error", { "allow": ["^XK_", "^XF86XK_"] }], + } + }, + { + files: ["po/po2js", "po/xgettext-html"], + languageOptions: { + globals: { + ...globals.node, + } + }, + }, + { + files: ["tests/*"], + languageOptions: { + globals: { + ...globals.node, + ...globals.mocha, + sinon: false, + chai: false, + } + }, + rules: { + "prefer-arrow-callback": 0, + // Too many anonymous callbacks + "func-names": "off", + }, + }, + { + files: ["utils/*"], + languageOptions: { + globals: { + ...globals.node, + } + }, + rules: { + "no-console": 0, + }, + }, +]; diff --git a/package.json b/package.json index 482b86eb3..9f22165bc 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "commander": "latest", "eslint": "latest", "fs-extra": "latest", + "globals": "latest", "jsdom": "latest", "karma": "latest", "karma-mocha": "latest", diff --git a/po/.eslintrc b/po/.eslintrc deleted file mode 100644 index a0157e2a8..000000000 --- a/po/.eslintrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "env": { - "node": true, - }, -} diff --git a/tests/.eslintrc b/tests/.eslintrc deleted file mode 100644 index 545fa2ed2..000000000 --- a/tests/.eslintrc +++ /dev/null @@ -1,15 +0,0 @@ -{ - "env": { - "node": true, - "mocha": true - }, - "globals": { - "chai": false, - "sinon": false - }, - "rules": { - "prefer-arrow-callback": 0, - // Too many anonymous callbacks - "func-names": "off", - } -} diff --git a/utils/.eslintrc b/utils/.eslintrc deleted file mode 100644 index b7dc129f1..000000000 --- a/utils/.eslintrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "env": { - "node": true - }, - "rules": { - "no-console": 0 - } -} \ No newline at end of file From 10ee10ce56d5b7aaf642457bfb99b56a13d56ad6 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Tue, 30 Apr 2024 15:25:03 +0200 Subject: [PATCH 028/166] Cleanup "no-console" eslint rules Removes unexpected exceptions and clarifies where we want to avoid console calls. --- eslint.config.mjs | 4 ++++ tests/test.browser.js | 1 - tests/test.deflator.js | 1 - tests/test.inflator.js | 1 - tests/test.int.js | 1 - 5 files changed, 4 insertions(+), 4 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index e3bfcd78a..c88e7b758 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -58,6 +58,7 @@ export default [ "asyncArrow": "always" }], "switch-colon-spacing": ["error"], "camelcase": ["error", { "allow": ["^XK_", "^XF86XK_"] }], + "no-console": ["error"], } }, { @@ -67,6 +68,9 @@ export default [ ...globals.node, } }, + rules: { + "no-console": 0, + }, }, { files: ["tests/*"], diff --git a/tests/test.browser.js b/tests/test.browser.js index 3b2299f63..1beeb48d5 100644 --- a/tests/test.browser.js +++ b/tests/test.browser.js @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ const expect = chai.expect; import { isMac, isWindows, isIOS, isAndroid, isChromeOS, diff --git a/tests/test.deflator.js b/tests/test.deflator.js index 12e8a46bf..a7e972ec0 100644 --- a/tests/test.deflator.js +++ b/tests/test.deflator.js @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ const expect = chai.expect; import { inflateInit, inflate } from "../vendor/pako/lib/zlib/inflate.js"; diff --git a/tests/test.inflator.js b/tests/test.inflator.js index 533bcd865..304e7a0fd 100644 --- a/tests/test.inflator.js +++ b/tests/test.inflator.js @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ const expect = chai.expect; import { deflateInit, deflate, Z_FULL_FLUSH } from "../vendor/pako/lib/zlib/deflate.js"; diff --git a/tests/test.int.js b/tests/test.int.js index 954fd279f..084d68abd 100644 --- a/tests/test.int.js +++ b/tests/test.int.js @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ const expect = chai.expect; import { toUnsigned32bit, toSigned32bit } from '../core/util/int.js'; From c187b2e5e0e44b4157f5e6220b263940f600bbca Mon Sep 17 00:00:00 2001 From: Jiang XueQian Date: Thu, 2 May 2024 20:41:38 +0800 Subject: [PATCH 029/166] Implement gradient filter of tight decoder, fixing issue #1767 This commit is a basic implementation of the gradient filter required by qemu `lossy` option. --- core/decoders/tight.js | 68 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/core/decoders/tight.js b/core/decoders/tight.js index 45622080f..8bc977a79 100644 --- a/core/decoders/tight.js +++ b/core/decoders/tight.js @@ -285,7 +285,73 @@ export default class TightDecoder { } _gradientFilter(streamId, x, y, width, height, sock, display, depth) { - throw new Error("Gradient filter not implemented"); + // assume the TPIXEL is 3 bytes long + const uncompressedSize = width * height * 3; + let data; + + if (uncompressedSize === 0) { + return true; + } + + if (uncompressedSize < 12) { + if (sock.rQwait("TIGHT", uncompressedSize)) { + return false; + } + + data = sock.rQshiftBytes(uncompressedSize); + } else { + data = this._readData(sock); + if (data === null) { + return false; + } + + this._zlibs[streamId].setInput(data); + data = this._zlibs[streamId].inflate(uncompressedSize); + this._zlibs[streamId].setInput(null); + } + + let rgbx = new Uint8Array(4 * width * height); + + let rgbxIndex = 0, dataIndex = 0; + let left = new Uint8Array(3); + for (let x = 0; x < width; x++) { + for (let c = 0; c < 3; c++) { + const prediction = left[c]; + const value = data[dataIndex++] + prediction; + rgbx[rgbxIndex++] = value; + left[c] = value; + } + rgbx[rgbxIndex++] = 255; + } + + let upperIndex = 0; + let upper = new Uint8Array(3), + upperleft = new Uint8Array(3); + for (let y = 1; y < height; y++) { + left.fill(0); + upperleft.fill(0); + for (let x = 0; x < width; x++) { + for (let c = 0; c < 3; c++) { + upper[c] = rgbx[upperIndex++]; + let prediction = left[c] + upper[c] - upperleft[c]; + if (prediction < 0) { + prediction = 0; + } else if (prediction > 255) { + prediction = 255; + } + const value = data[dataIndex++] + prediction; + rgbx[rgbxIndex++] = value; + upperleft[c] = upper[c]; + left[c] = value; + } + rgbx[rgbxIndex++] = 255; + upperIndex++; + } + } + + display.blitImage(x, y, width, height, rgbx, 0, false); + + return true; } _readData(sock) { From d80e3bfa2f372e2b210f46b8c46796a9754abf27 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 16 May 2024 16:53:49 +0200 Subject: [PATCH 030/166] Add unit tests for Tight gradient filter --- tests/test.tight.js | 74 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/tests/test.tight.js b/tests/test.tight.js index b3457a88b..141d7b6e2 100644 --- a/tests/test.tight.js +++ b/tests/test.tight.js @@ -228,12 +228,59 @@ describe('Tight Decoder', function () { expect(display).to.have.displayed(targetData); }); - it.skip('should handle uncompressed gradient rects', function () { - // Not implemented yet + it('should handle uncompressed gradient rects', function () { + let done; + let blueData = [ 0x40, 0x02, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00 ]; + let greenData = [ 0x40, 0x02, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00 ]; + + done = testDecodeRect(decoder, 0, 0, 2, 1, blueData, display, 24); + expect(done).to.be.true; + done = testDecodeRect(decoder, 0, 1, 2, 1, blueData, display, 24); + expect(done).to.be.true; + done = testDecodeRect(decoder, 2, 0, 2, 1, greenData, display, 24); + expect(done).to.be.true; + done = testDecodeRect(decoder, 2, 1, 2, 1, greenData, display, 24); + expect(done).to.be.true; + done = testDecodeRect(decoder, 0, 2, 2, 1, greenData, display, 24); + expect(done).to.be.true; + done = testDecodeRect(decoder, 0, 3, 2, 1, greenData, display, 24); + expect(done).to.be.true; + done = testDecodeRect(decoder, 2, 2, 2, 1, blueData, display, 24); + expect(done).to.be.true; + done = testDecodeRect(decoder, 2, 3, 2, 1, blueData, display, 24); + expect(done).to.be.true; + + let targetData = new Uint8Array([ + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 + ]); + + expect(display).to.have.displayed(targetData); }); - it.skip('should handle compressed gradient rects', function () { - // Not implemented yet + it('should handle compressed gradient rects', function () { + let data = [ + // Control byte + 0x40, 0x02, + // Pixels (compressed) + 0x18, + 0x78, 0x9c, 0x62, 0x60, 0xf8, 0xcf, 0x00, 0x04, + 0xff, 0x19, 0x19, 0xd0, 0x00, 0x44, 0x84, 0xf1, + 0x3f, 0x9a, 0x30, 0x00, 0x00, 0x00, 0xff, 0xff ]; + + let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24); + + let targetData = new Uint8Array([ + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 + ]); + + expect(done).to.be.true; + expect(display).to.have.displayed(targetData); }); it('should handle empty copy rects', function () { @@ -275,6 +322,25 @@ describe('Tight Decoder', function () { expect(display).to.have.displayed(targetData); }); + it('should handle empty gradient rects', function () { + display.fillRect(0, 0, 4, 4, [ 0x00, 0x00, 0xff ]); + display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]); + display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]); + + let done = testDecodeRect(decoder, 1, 2, 0, 0, + [ 0x40, 0x02 ], display, 24); + + let targetData = new Uint8Array([ + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 + ]); + + expect(done).to.be.true; + expect(display).to.have.displayed(targetData); + }); + it('should handle empty fill rects', function () { display.fillRect(0, 0, 4, 4, [ 0x00, 0x00, 0xff ]); display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]); From fc11b9d2b095b0dd0db106ca0f80a47d6e262f6c Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 3 Jun 2024 14:09:00 +0200 Subject: [PATCH 031/166] Remove Twitter links These are not updated anymore as they are not under the control of the current team. --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index a771cb438..b95d15e65 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,6 @@ for a more complete list with additional info and links. ### News/help/contact The project website is found at [novnc.com](http://novnc.com). -Notable commits, announcements and news are posted to -[@noVNC](http://www.twitter.com/noVNC). If you are a noVNC developer/integrator/user (or want to be) please join the [noVNC discussion group](https://groups.google.com/forum/?fromgroups#!forum/novnc). @@ -59,7 +57,6 @@ profits such as: [Electronic Frontier Foundation](https://www.eff.org/), [Against Malaria Foundation](http://www.againstmalaria.com/), [Nothing But Nets](http://www.nothingbutnets.net/), etc. -Please tweet [@noVNC](http://www.twitter.com/noVNC) if you do. ### Features From f28e9daec387c681a70b10b1328f1a365ef51363 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 3 Jun 2024 14:10:47 +0200 Subject: [PATCH 032/166] Update translation template file --- po/noVNC.pot | 155 ++++++++++++++++++++++++++------------------------- 1 file changed, 80 insertions(+), 75 deletions(-) diff --git a/po/noVNC.pot b/po/noVNC.pot index 3641e3bc9..0f85a82e5 100644 --- a/po/noVNC.pot +++ b/po/noVNC.pot @@ -6,9 +6,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: noVNC 1.4.0\n" +"Project-Id-Version: noVNC 1.5.0\n" "Report-Msgid-Bugs-To: novnc@googlegroups.com\n" -"POT-Creation-Date: 2022-12-27 15:24+0100\n" +"POT-Creation-Date: 2024-06-03 14:10+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,7 +18,8 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" #: ../app/ui.js:69 -msgid "HTTPS is required for full functionality" +msgid "" +"Running without HTTPS is not recommended, crashes or other issues are likely." msgstr "" #: ../app/ui.js:410 @@ -41,292 +42,296 @@ msgstr "" msgid "Must set host" msgstr "" -#: ../app/ui.js:1110 +#: ../app/ui.js:1052 +msgid "Failed to connect to server: " +msgstr "" + +#: ../app/ui.js:1118 msgid "Connected (encrypted) to " msgstr "" -#: ../app/ui.js:1112 +#: ../app/ui.js:1120 msgid "Connected (unencrypted) to " msgstr "" -#: ../app/ui.js:1135 +#: ../app/ui.js:1143 msgid "Something went wrong, connection is closed" msgstr "" -#: ../app/ui.js:1138 +#: ../app/ui.js:1146 msgid "Failed to connect to server" msgstr "" -#: ../app/ui.js:1150 +#: ../app/ui.js:1158 msgid "Disconnected" msgstr "" -#: ../app/ui.js:1165 +#: ../app/ui.js:1173 msgid "New connection has been rejected with reason: " msgstr "" -#: ../app/ui.js:1168 +#: ../app/ui.js:1176 msgid "New connection has been rejected" msgstr "" -#: ../app/ui.js:1234 +#: ../app/ui.js:1242 msgid "Credentials are required" msgstr "" -#: ../vnc.html:57 +#: ../vnc.html:55 msgid "noVNC encountered an error:" msgstr "" -#: ../vnc.html:67 +#: ../vnc.html:65 msgid "Hide/Show the control bar" msgstr "" -#: ../vnc.html:76 +#: ../vnc.html:74 msgid "Drag" msgstr "" -#: ../vnc.html:76 +#: ../vnc.html:74 msgid "Move/Drag Viewport" msgstr "" -#: ../vnc.html:82 +#: ../vnc.html:80 msgid "Keyboard" msgstr "" -#: ../vnc.html:82 +#: ../vnc.html:80 msgid "Show Keyboard" msgstr "" -#: ../vnc.html:87 +#: ../vnc.html:85 msgid "Extra keys" msgstr "" -#: ../vnc.html:87 +#: ../vnc.html:85 msgid "Show Extra Keys" msgstr "" -#: ../vnc.html:92 +#: ../vnc.html:90 msgid "Ctrl" msgstr "" -#: ../vnc.html:92 +#: ../vnc.html:90 msgid "Toggle Ctrl" msgstr "" -#: ../vnc.html:95 +#: ../vnc.html:93 msgid "Alt" msgstr "" -#: ../vnc.html:95 +#: ../vnc.html:93 msgid "Toggle Alt" msgstr "" -#: ../vnc.html:98 +#: ../vnc.html:96 msgid "Toggle Windows" msgstr "" -#: ../vnc.html:98 +#: ../vnc.html:96 msgid "Windows" msgstr "" -#: ../vnc.html:101 +#: ../vnc.html:99 msgid "Send Tab" msgstr "" -#: ../vnc.html:101 +#: ../vnc.html:99 msgid "Tab" msgstr "" -#: ../vnc.html:104 +#: ../vnc.html:102 msgid "Esc" msgstr "" -#: ../vnc.html:104 +#: ../vnc.html:102 msgid "Send Escape" msgstr "" -#: ../vnc.html:107 +#: ../vnc.html:105 msgid "Ctrl+Alt+Del" msgstr "" -#: ../vnc.html:107 +#: ../vnc.html:105 msgid "Send Ctrl-Alt-Del" msgstr "" -#: ../vnc.html:114 +#: ../vnc.html:112 msgid "Shutdown/Reboot" msgstr "" -#: ../vnc.html:114 +#: ../vnc.html:112 msgid "Shutdown/Reboot..." msgstr "" -#: ../vnc.html:120 +#: ../vnc.html:118 msgid "Power" msgstr "" -#: ../vnc.html:122 +#: ../vnc.html:120 msgid "Shutdown" msgstr "" -#: ../vnc.html:123 +#: ../vnc.html:121 msgid "Reboot" msgstr "" -#: ../vnc.html:124 +#: ../vnc.html:122 msgid "Reset" msgstr "" -#: ../vnc.html:129 ../vnc.html:135 +#: ../vnc.html:127 ../vnc.html:133 msgid "Clipboard" msgstr "" -#: ../vnc.html:137 +#: ../vnc.html:135 msgid "Edit clipboard content in the textarea below." msgstr "" -#: ../vnc.html:145 +#: ../vnc.html:143 msgid "Full Screen" msgstr "" -#: ../vnc.html:150 ../vnc.html:156 +#: ../vnc.html:148 ../vnc.html:154 msgid "Settings" msgstr "" -#: ../vnc.html:160 +#: ../vnc.html:158 msgid "Shared Mode" msgstr "" -#: ../vnc.html:163 +#: ../vnc.html:161 msgid "View Only" msgstr "" -#: ../vnc.html:167 +#: ../vnc.html:165 msgid "Clip to Window" msgstr "" -#: ../vnc.html:170 +#: ../vnc.html:168 msgid "Scaling Mode:" msgstr "" -#: ../vnc.html:172 +#: ../vnc.html:170 msgid "None" msgstr "" -#: ../vnc.html:173 +#: ../vnc.html:171 msgid "Local Scaling" msgstr "" -#: ../vnc.html:174 +#: ../vnc.html:172 msgid "Remote Resizing" msgstr "" -#: ../vnc.html:179 +#: ../vnc.html:177 msgid "Advanced" msgstr "" -#: ../vnc.html:182 +#: ../vnc.html:180 msgid "Quality:" msgstr "" -#: ../vnc.html:186 +#: ../vnc.html:184 msgid "Compression level:" msgstr "" -#: ../vnc.html:191 +#: ../vnc.html:189 msgid "Repeater ID:" msgstr "" -#: ../vnc.html:195 +#: ../vnc.html:193 msgid "WebSocket" msgstr "" -#: ../vnc.html:198 +#: ../vnc.html:196 msgid "Encrypt" msgstr "" -#: ../vnc.html:201 +#: ../vnc.html:199 msgid "Host:" msgstr "" -#: ../vnc.html:205 +#: ../vnc.html:203 msgid "Port:" msgstr "" -#: ../vnc.html:209 +#: ../vnc.html:207 msgid "Path:" msgstr "" -#: ../vnc.html:216 +#: ../vnc.html:214 msgid "Automatic Reconnect" msgstr "" -#: ../vnc.html:219 +#: ../vnc.html:217 msgid "Reconnect Delay (ms):" msgstr "" -#: ../vnc.html:224 +#: ../vnc.html:222 msgid "Show Dot when No Cursor" msgstr "" -#: ../vnc.html:229 +#: ../vnc.html:227 msgid "Logging:" msgstr "" -#: ../vnc.html:238 +#: ../vnc.html:236 msgid "Version:" msgstr "" -#: ../vnc.html:246 +#: ../vnc.html:244 msgid "Disconnect" msgstr "" -#: ../vnc.html:269 +#: ../vnc.html:267 msgid "Connect" msgstr "" -#: ../vnc.html:278 +#: ../vnc.html:276 msgid "Server identity" msgstr "" -#: ../vnc.html:281 +#: ../vnc.html:279 msgid "The server has provided the following identifying information:" msgstr "" -#: ../vnc.html:285 +#: ../vnc.html:283 msgid "Fingerprint:" msgstr "" -#: ../vnc.html:288 +#: ../vnc.html:286 msgid "" "Please verify that the information is correct and press \"Approve\". " "Otherwise press \"Reject\"." msgstr "" -#: ../vnc.html:293 +#: ../vnc.html:291 msgid "Approve" msgstr "" -#: ../vnc.html:294 +#: ../vnc.html:292 msgid "Reject" msgstr "" -#: ../vnc.html:302 +#: ../vnc.html:300 msgid "Credentials" msgstr "" -#: ../vnc.html:306 +#: ../vnc.html:304 msgid "Username:" msgstr "" -#: ../vnc.html:310 +#: ../vnc.html:308 msgid "Password:" msgstr "" -#: ../vnc.html:314 +#: ../vnc.html:312 msgid "Send Credentials" msgstr "" -#: ../vnc.html:323 +#: ../vnc.html:321 msgid "Cancel" msgstr "" From 68e09ee8b3f06dac37a08725b9ccc2a64b2fda4f Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 3 Jun 2024 14:45:11 +0200 Subject: [PATCH 033/166] Upgrade to websockify 0.12.0 in snap package --- snap/snapcraft.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index ccf618930..82d52de4d 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -42,7 +42,7 @@ parts: - jq websockify: - source: https://github.com/novnc/websockify/archive/v0.11.0.tar.gz + source: https://github.com/novnc/websockify/archive/v0.12.0.tar.gz plugin: python stage-packages: - python3-numpy From aead0b2f891732687b970225350419ae62f1942c Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 3 Jun 2024 14:14:24 +0200 Subject: [PATCH 034/166] noVNC 1.5.0 beta --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9f22165bc..af6cfc742 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@novnc/novnc", - "version": "1.4.0", + "version": "1.5.0-beta", "description": "An HTML5 VNC client", "browser": "lib/rfb", "directories": { From fb1817c99fdf71180e8d05bdb6f9595be2b5541b Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 13 Jun 2024 08:38:00 +0200 Subject: [PATCH 035/166] Remove Chrome timeout workaround This is a revert of fca48df85d394b2c1e9d26ed6e9d10d33db9f4f4. The issue seems to be fixed in the current version of Chrome, so let's keep things simple again. --- karma.conf.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/karma.conf.js b/karma.conf.js index faa8beea2..1ea17475a 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -81,12 +81,5 @@ module.exports = (config) => { singleRun: true, }; - if (process.env.TEST_BROWSER_NAME === 'ChromeHeadless') { - let os = require('os'); - if (os.platform() === 'win32') { - my_conf.client.mocha['timeout'] = 5000; - } - } - config.set(my_conf); }; From 1a62eb7d3e769e21bf9894e7808ee8143c07080f Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 18 Jun 2024 14:01:40 +0200 Subject: [PATCH 036/166] Don't include missing translation in .js It just adds size and confusion. Instead, omit any lines where no translation is available. --- app/locale/fr.json | 8 -------- app/locale/it.json | 4 ---- po/po2js | 12 +++++++----- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/app/locale/fr.json b/app/locale/fr.json index 22531f73b..e25f34ece 100644 --- a/app/locale/fr.json +++ b/app/locale/fr.json @@ -1,5 +1,4 @@ { - "HTTPS is required for full functionality": "", "Connecting...": "En cours de connexion...", "Disconnecting...": "Déconnexion en cours...", "Reconnecting...": "Reconnexion en cours...", @@ -40,7 +39,6 @@ "Reboot": "Redémarrer", "Reset": "Réinitialiser", "Clipboard": "Presse-papiers", - "Edit clipboard content in the textarea below.": "", "Settings": "Paramètres", "Shared Mode": "Mode partagé", "View Only": "Afficher uniquement", @@ -65,12 +63,6 @@ "Version:": "Version :", "Disconnect": "Déconnecter", "Connect": "Connecter", - "Server identity": "", - "The server has provided the following identifying information:": "", - "Fingerprint:": "", - "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "", - "Approve": "", - "Reject": "", "Username:": "Nom d'utilisateur :", "Password:": "Mot de passe :", "Send Credentials": "Envoyer les identifiants", diff --git a/app/locale/it.json b/app/locale/it.json index 6fd25702b..18a7f7447 100644 --- a/app/locale/it.json +++ b/app/locale/it.json @@ -14,8 +14,6 @@ "Credentials are required": "Le credenziali sono obbligatorie", "noVNC encountered an error:": "noVNC ha riscontrato un errore:", "Hide/Show the control bar": "Nascondi/Mostra la barra di controllo", - "Drag": "", - "Move/Drag Viewport": "", "Keyboard": "Tastiera", "Show Keyboard": "Mostra tastiera", "Extra keys": "Tasti Aggiuntivi", @@ -44,7 +42,6 @@ "Settings": "Impostazioni", "Shared Mode": "Modalità condivisa", "View Only": "Sola Visualizzazione", - "Clip to Window": "", "Scaling Mode:": "Modalità di ridimensionamento:", "None": "Nessuna", "Local Scaling": "Ridimensionamento Locale", @@ -61,7 +58,6 @@ "Automatic Reconnect": "Riconnessione Automatica", "Reconnect Delay (ms):": "Ritardo Riconnessione (ms):", "Show Dot when No Cursor": "Mostra Punto quando Nessun Cursore", - "Logging:": "", "Version:": "Versione:", "Disconnect": "Disconnetti", "Connect": "Connetti", diff --git a/po/po2js b/po/po2js index fc6e88103..e293deda6 100755 --- a/po/po2js +++ b/po/po2js @@ -32,11 +32,13 @@ if (opt.argv.length != 2) { const data = po2json.parseFileSync(opt.argv[0]); -const bodyPart = Object.keys(data).filter(msgid => msgid !== "").map((msgid) => { - if (msgid === "") return; - const msgstr = data[msgid][1]; - return " " + JSON.stringify(msgid) + ": " + JSON.stringify(msgstr); -}).join(",\n"); +const bodyPart = Object.keys(data) + .filter(msgid => msgid !== "") + .filter(msgid => data[msgid][1] !== "") + .map((msgid) => { + const msgstr = data[msgid][1]; + return " " + JSON.stringify(msgid) + ": " + JSON.stringify(msgstr); + }).join(",\n"); const output = "{\n" + bodyPart + "\n}"; From 7f364a173d1875ba029b6f7be1bd41f89d8ca00c Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 18 Jun 2024 14:02:34 +0200 Subject: [PATCH 037/166] Update Swedish translation --- app/locale/sv.json | 3 ++- po/sv.po | 42 ++++++++++++++++++++++++++---------------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/app/locale/sv.json b/app/locale/sv.json index 077ef42c8..80a400bfa 100644 --- a/app/locale/sv.json +++ b/app/locale/sv.json @@ -1,10 +1,11 @@ { - "HTTPS is required for full functionality": "HTTPS krävs för full funktionalitet", + "Running without HTTPS is not recommended, crashes or other issues are likely.": "Det är ej rekommenderat att köra utan HTTPS, krascher och andra problem är troliga.", "Connecting...": "Ansluter...", "Disconnecting...": "Kopplar ner...", "Reconnecting...": "Återansluter...", "Internal error": "Internt fel", "Must set host": "Du måste specifiera en värd", + "Failed to connect to server: ": "Misslyckades att ansluta till servern: ", "Connected (encrypted) to ": "Ansluten (krypterat) till ", "Connected (unencrypted) to ": "Ansluten (okrypterat) till ", "Something went wrong, connection is closed": "Något gick fel, anslutningen avslutades", diff --git a/po/sv.po b/po/sv.po index 972e40004..85c4e3054 100644 --- a/po/sv.po +++ b/po/sv.po @@ -8,20 +8,23 @@ msgid "" msgstr "" "Project-Id-Version: noVNC 1.3.0\n" "Report-Msgid-Bugs-To: novnc@googlegroups.com\n" -"POT-Creation-Date: 2023-01-20 12:54+0100\n" -"PO-Revision-Date: 2023-01-20 12:58+0100\n" -"Last-Translator: Samuel Mannehed \n" +"POT-Creation-Date: 2024-06-03 14:10+0200\n" +"PO-Revision-Date: 2024-06-18 13:52+0200\n" +"Last-Translator: Pierre Ossman \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.2.2\n" +"X-Generator: Poedit 3.4.4\n" #: ../app/ui.js:69 -msgid "HTTPS is required for full functionality" -msgstr "HTTPS krävs för full funktionalitet" +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:410 msgid "Connecting..." @@ -43,35 +46,39 @@ msgstr "Internt fel" msgid "Must set host" msgstr "Du måste specifiera en värd" -#: ../app/ui.js:1110 +#: ../app/ui.js:1052 +msgid "Failed to connect to server: " +msgstr "Misslyckades att ansluta till servern: " + +#: ../app/ui.js:1118 msgid "Connected (encrypted) to " msgstr "Ansluten (krypterat) till " -#: ../app/ui.js:1112 +#: ../app/ui.js:1120 msgid "Connected (unencrypted) to " msgstr "Ansluten (okrypterat) till " -#: ../app/ui.js:1135 +#: ../app/ui.js:1143 msgid "Something went wrong, connection is closed" msgstr "Något gick fel, anslutningen avslutades" -#: ../app/ui.js:1138 +#: ../app/ui.js:1146 msgid "Failed to connect to server" msgstr "Misslyckades att ansluta till servern" -#: ../app/ui.js:1150 +#: ../app/ui.js:1158 msgid "Disconnected" msgstr "Frånkopplad" -#: ../app/ui.js:1165 +#: ../app/ui.js:1173 msgid "New connection has been rejected with reason: " msgstr "Ny anslutning har blivit nekad med följande skäl: " -#: ../app/ui.js:1168 +#: ../app/ui.js:1176 msgid "New connection has been rejected" msgstr "Ny anslutning har blivit nekad" -#: ../app/ui.js:1234 +#: ../app/ui.js:1242 msgid "Credentials are required" msgstr "Användaruppgifter krävs" @@ -304,8 +311,8 @@ 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\"." +"Kontrollera att informationen är korrekt och tryck sedan \"Godkänn\". Tryck " +"annars \"Neka\"." #: ../vnc.html:291 msgid "Approve" @@ -335,5 +342,8 @@ msgstr "Skicka Användaruppgifter" msgid "Cancel" msgstr "Avbryt" +#~ msgid "HTTPS is required for full functionality" +#~ msgstr "HTTPS krävs för full funktionalitet" + #~ msgid "Clear" #~ msgstr "Rensa" From aaadec4f1343e73a3bc9766305544ab896c04ced Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 18 Jun 2024 14:03:30 +0200 Subject: [PATCH 038/166] Update json files for new translations --- app/locale/el.json | 32 +++++++++++++++++++---------- app/locale/fr.json | 2 ++ app/locale/ja.json | 22 +++++++++++++------- app/locale/zh_CN.json | 48 +++++++++++++++++++++---------------------- 4 files changed, 62 insertions(+), 42 deletions(-) diff --git a/app/locale/el.json b/app/locale/el.json index f801251c5..4df3e03c4 100644 --- a/app/locale/el.json +++ b/app/locale/el.json @@ -1,4 +1,5 @@ { + "HTTPS is required for full functionality": "Το HTTPS είναι απαιτούμενο για πλήρη λειτουργικότητα", "Connecting...": "Συνδέεται...", "Disconnecting...": "Aποσυνδέεται...", "Reconnecting...": "Επανασυνδέεται...", @@ -7,19 +8,15 @@ "Connected (encrypted) to ": "Συνδέθηκε (κρυπτογραφημένα) με το ", "Connected (unencrypted) to ": "Συνδέθηκε (μη κρυπτογραφημένα) με το ", "Something went wrong, connection is closed": "Κάτι πήγε στραβά, η σύνδεση διακόπηκε", + "Failed to connect to server": "Αποτυχία στη σύνδεση με το διακομιστή", "Disconnected": "Αποσυνδέθηκε", "New connection has been rejected with reason: ": "Η νέα σύνδεση απορρίφθηκε διότι: ", "New connection has been rejected": "Η νέα σύνδεση απορρίφθηκε ", - "Password is required": "Απαιτείται ο κωδικός πρόσβασης", + "Credentials are required": "Απαιτούνται διαπιστευτήρια", "noVNC encountered an error:": "το noVNC αντιμετώπισε ένα σφάλμα:", "Hide/Show the control bar": "Απόκρυψη/Εμφάνιση γραμμής ελέγχου", + "Drag": "Σύρσιμο", "Move/Drag Viewport": "Μετακίνηση/Σύρσιμο Θεατού πεδίου", - "viewport drag": "σύρσιμο θεατού πεδίου", - "Active Mouse Button": "Ενεργό Πλήκτρο Ποντικιού", - "No mousebutton": "Χωρίς Πλήκτρο Ποντικιού", - "Left mousebutton": "Αριστερό Πλήκτρο Ποντικιού", - "Middle mousebutton": "Μεσαίο Πλήκτρο Ποντικιού", - "Right mousebutton": "Δεξί Πλήκτρο Ποντικιού", "Keyboard": "Πληκτρολόγιο", "Show Keyboard": "Εμφάνιση Πληκτρολογίου", "Extra keys": "Επιπλέον πλήκτρα", @@ -28,6 +25,8 @@ "Toggle Ctrl": "Εναλλαγή Ctrl", "Alt": "Alt", "Toggle Alt": "Εναλλαγή Alt", + "Toggle Windows": "Εναλλαγή Παράθυρων", + "Windows": "Παράθυρα", "Send Tab": "Αποστολή Tab", "Tab": "Tab", "Esc": "Esc", @@ -41,8 +40,7 @@ "Reboot": "Επανεκκίνηση", "Reset": "Επαναφορά", "Clipboard": "Πρόχειρο", - "Clear": "Καθάρισμα", - "Fullscreen": "Πλήρης Οθόνη", + "Edit clipboard content in the textarea below.": "Επεξεργαστείτε το περιεχόμενο του πρόχειρου στην περιοχή κειμένου παρακάτω.", "Settings": "Ρυθμίσεις", "Shared Mode": "Κοινόχρηστη Λειτουργία", "View Only": "Μόνο Θέαση", @@ -52,6 +50,8 @@ "Local Scaling": "Τοπική Κλιμάκωση", "Remote Resizing": "Απομακρυσμένη Αλλαγή μεγέθους", "Advanced": "Για προχωρημένους", + "Quality:": "Ποιότητα:", + "Compression level:": "Επίπεδο συμπίεσης:", "Repeater ID:": "Repeater ID:", "WebSocket": "WebSocket", "Encrypt": "Κρυπτογράφηση", @@ -60,10 +60,20 @@ "Path:": "Διαδρομή:", "Automatic Reconnect": "Αυτόματη επανασύνδεση", "Reconnect Delay (ms):": "Καθυστέρηση επανασύνδεσης (ms):", + "Show Dot when No Cursor": "Εμφάνιση Τελείας όταν δεν υπάρχει Δρομέας", "Logging:": "Καταγραφή:", + "Version:": "Έκδοση:", "Disconnect": "Αποσύνδεση", "Connect": "Σύνδεση", + "Server identity": "Ταυτότητα Διακομιστή", + "The server has provided the following identifying information:": "Ο διακομιστής παρείχε την ακόλουθη πληροφορία ταυτοποίησης:", + "Fingerprint:": "Δακτυλικό αποτύπωμα:", + "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Παρακαλώ επαληθεύσετε ότι η πληροφορία είναι σωστή και πιέστε \"Αποδοχή\". Αλλιώς πιέστε \"Απόρριψη\".", + "Approve": "Αποδοχή", + "Reject": "Απόρριψη", + "Credentials": "Διαπιστευτήρια", + "Username:": "Κωδικός Χρήστη:", "Password:": "Κωδικός Πρόσβασης:", - "Cancel": "Ακύρωση", - "Canvas not supported.": "Δεν υποστηρίζεται το στοιχείο Canvas" + "Send Credentials": "Αποστολή Διαπιστευτηρίων", + "Cancel": "Ακύρωση" } \ No newline at end of file diff --git a/app/locale/fr.json b/app/locale/fr.json index e25f34ece..c0eeec7d3 100644 --- a/app/locale/fr.json +++ b/app/locale/fr.json @@ -39,6 +39,8 @@ "Reboot": "Redémarrer", "Reset": "Réinitialiser", "Clipboard": "Presse-papiers", + "Clear": "Effacer", + "Fullscreen": "Plein écran", "Settings": "Paramètres", "Shared Mode": "Mode partagé", "View Only": "Afficher uniquement", diff --git a/app/locale/ja.json b/app/locale/ja.json index 43fc5bf38..70fd7a5d1 100644 --- a/app/locale/ja.json +++ b/app/locale/ja.json @@ -1,4 +1,5 @@ { + "HTTPS is required for full functionality": "すべての機能を使用するにはHTTPS接続が必要です", "Connecting...": "接続しています...", "Disconnecting...": "切断しています...", "Reconnecting...": "再接続しています...", @@ -21,10 +22,10 @@ "Extra keys": "追加キー", "Show Extra Keys": "追加キーを表示", "Ctrl": "Ctrl", - "Toggle Ctrl": "Ctrl キーを切り替え", + "Toggle Ctrl": "Ctrl キーをトグル", "Alt": "Alt", - "Toggle Alt": "Alt キーを切り替え", - "Toggle Windows": "Windows キーを切り替え", + "Toggle Alt": "Alt キーをトグル", + "Toggle Windows": "Windows キーをトグル", "Windows": "Windows", "Send Tab": "Tab キーを送信", "Tab": "Tab", @@ -39,11 +40,11 @@ "Reboot": "再起動", "Reset": "リセット", "Clipboard": "クリップボード", - "Clear": "クリア", - "Fullscreen": "全画面表示", + "Edit clipboard content in the textarea below.": "以下の入力欄からクリップボードの内容を編集できます。", + "Full Screen": "全画面表示", "Settings": "設定", "Shared Mode": "共有モード", - "View Only": "表示のみ", + "View Only": "表示専用", "Clip to Window": "ウィンドウにクリップ", "Scaling Mode:": "スケーリングモード:", "None": "なし", @@ -60,11 +61,18 @@ "Path:": "パス:", "Automatic Reconnect": "自動再接続", "Reconnect Delay (ms):": "再接続する遅延 (ミリ秒):", - "Show Dot when No Cursor": "カーソルがないときにドットを表示", + "Show Dot when No Cursor": "カーソルがないときにドットを表示する", "Logging:": "ロギング:", "Version:": "バージョン:", "Disconnect": "切断", "Connect": "接続", + "Server identity": "サーバーの識別情報", + "The server has provided the following identifying information:": "サーバーは以下の識別情報を提供しています:", + "Fingerprint:": "フィンガープリント:", + "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "この情報が正しい場合は「承認」を、そうでない場合は「拒否」を押してください。", + "Approve": "承認", + "Reject": "拒否", + "Credentials": "資格情報", "Username:": "ユーザー名:", "Password:": "パスワード:", "Send Credentials": "資格情報を送信", diff --git a/app/locale/zh_CN.json b/app/locale/zh_CN.json index f0aea9af3..3679eaddd 100644 --- a/app/locale/zh_CN.json +++ b/app/locale/zh_CN.json @@ -1,69 +1,69 @@ { "Connecting...": "连接中...", + "Connected (encrypted) to ": "已连接(已加密)到", + "Connected (unencrypted) to ": "已连接(未加密)到", "Disconnecting...": "正在断开连接...", - "Reconnecting...": "重新连接中...", - "Internal error": "内部错误", - "Must set host": "请提供主机名", - "Connected (encrypted) to ": "已连接到(加密)", - "Connected (unencrypted) to ": "已连接到(未加密)", - "Something went wrong, connection is closed": "发生错误,连接已关闭", - "Failed to connect to server": "无法连接到服务器", "Disconnected": "已断开连接", - "New connection has been rejected with reason: ": "连接被拒绝,原因:", - "New connection has been rejected": "连接被拒绝", + "Must set host": "必须设置主机", + "Reconnecting...": "重新连接中...", "Password is required": "请提供密码", + "Disconnect timeout": "超时断开", "noVNC encountered an error:": "noVNC 遇到一个错误:", "Hide/Show the control bar": "显示/隐藏控制栏", - "Move/Drag Viewport": "拖放显示范围", - "viewport drag": "显示范围拖放", - "Active Mouse Button": "启动鼠标按鍵", - "No mousebutton": "禁用鼠标按鍵", - "Left mousebutton": "鼠标左鍵", - "Middle mousebutton": "鼠标中鍵", - "Right mousebutton": "鼠标右鍵", + "Move/Drag Viewport": "移动/拖动窗口", + "viewport drag": "窗口拖动", + "Active Mouse Button": "启动鼠标按键", + "No mousebutton": "禁用鼠标按键", + "Left mousebutton": "鼠标左键", + "Middle mousebutton": "鼠标中键", + "Right mousebutton": "鼠标右键", "Keyboard": "键盘", "Show Keyboard": "显示键盘", "Extra keys": "额外按键", "Show Extra Keys": "显示额外按键", "Ctrl": "Ctrl", "Toggle Ctrl": "切换 Ctrl", + "Edit clipboard content in the textarea below.": "在下面的文本区域中编辑剪贴板内容。", "Alt": "Alt", "Toggle Alt": "切换 Alt", "Send Tab": "发送 Tab 键", "Tab": "Tab", "Esc": "Esc", "Send Escape": "发送 Escape 键", - "Ctrl+Alt+Del": "Ctrl-Alt-Del", - "Send Ctrl-Alt-Del": "发送 Ctrl-Alt-Del 键", - "Shutdown/Reboot": "关机/重新启动", - "Shutdown/Reboot...": "关机/重新启动...", + "Ctrl+Alt+Del": "Ctrl+Alt+Del", + "Send Ctrl-Alt-Del": "发送 Ctrl+Alt+Del 键", + "Shutdown/Reboot": "关机/重启", + "Shutdown/Reboot...": "关机/重启...", "Power": "电源", "Shutdown": "关机", - "Reboot": "重新启动", + "Reboot": "重启", "Reset": "重置", "Clipboard": "剪贴板", "Clear": "清除", "Fullscreen": "全屏", "Settings": "设置", + "Encrypt": "加密", "Shared Mode": "分享模式", "View Only": "仅查看", "Clip to Window": "限制/裁切窗口大小", "Scaling Mode:": "缩放模式:", "None": "无", "Local Scaling": "本地缩放", + "Local Downscaling": "降低本地尺寸", "Remote Resizing": "远程调整大小", "Advanced": "高级", + "Local Cursor": "本地光标", "Repeater ID:": "中继站 ID", "WebSocket": "WebSocket", - "Encrypt": "加密", "Host:": "主机:", "Port:": "端口:", "Path:": "路径:", "Automatic Reconnect": "自动重新连接", "Reconnect Delay (ms):": "重新连接间隔 (ms):", "Logging:": "日志级别:", - "Disconnect": "中断连接", + "Disconnect": "断开连接", "Connect": "连接", "Password:": "密码:", - "Cancel": "取消" + "Cancel": "取消", + "Canvas not supported.": "不支持 Canvas。" } \ No newline at end of file From 7fcf9dcfe0cc5b14e3841a4429dc091a6ffca861 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 18 Jun 2024 14:05:35 +0200 Subject: [PATCH 039/166] noVNC 1.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index af6cfc742..9fa8c312d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@novnc/novnc", - "version": "1.5.0-beta", + "version": "1.5.0", "description": "An HTML5 VNC client", "browser": "lib/rfb", "directories": { From 1230a4ce738ebccce80fc08ee6172bc3a19fadba Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Tue, 23 Jul 2024 00:19:26 +0200 Subject: [PATCH 040/166] Use theme-color to color address bar in browsers This makes the address bar on mobile browsers match the background. Note that it requires a valid certificate and a non-dark mode set on the device. Not supported on desktop browsers. --- vnc.html | 1 + 1 file changed, 1 insertion(+) diff --git a/vnc.html b/vnc.html index 24a118dbd..89ee11e36 100644 --- a/vnc.html +++ b/vnc.html @@ -16,6 +16,7 @@ noVNC + From 6c07136169830d70a1db2d67f7b955b6bc834d6f Mon Sep 17 00:00:00 2001 From: leedagee <61650578+leedagee@users.noreply.github.com> Date: Thu, 1 Aug 2024 01:31:47 +0800 Subject: [PATCH 041/166] Interrupt AltGr sequence detection on focus lost, fixes #1880 --- core/input/keyboard.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/core/input/keyboard.js b/core/input/keyboard.js index 68da2312b..ff14de849 100644 --- a/core/input/keyboard.js +++ b/core/input/keyboard.js @@ -203,7 +203,7 @@ export default class Keyboard { if ((code === "ControlLeft") && browser.isWindows() && !("ControlLeft" in this._keyDownList)) { this._altGrArmed = true; - this._altGrTimeout = setTimeout(this._handleAltGrTimeout.bind(this), 100); + this._altGrTimeout = setTimeout(this._interruptAltGrSequence.bind(this), 100); this._altGrCtrlTime = e.timeStamp; return; } @@ -218,11 +218,7 @@ export default class Keyboard { // We can't get a release in the middle of an AltGr sequence, so // abort that detection - if (this._altGrArmed) { - this._altGrArmed = false; - clearTimeout(this._altGrTimeout); - this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); - } + this._interruptAltGrSequence(); // See comment in _handleKeyDown() if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) { @@ -249,14 +245,20 @@ export default class Keyboard { } } - _handleAltGrTimeout() { - this._altGrArmed = false; - clearTimeout(this._altGrTimeout); - this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); + _interruptAltGrSequence() { + if (this._altGrArmed) { + this._altGrArmed = false; + clearTimeout(this._altGrTimeout); + this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); + } } _allKeysUp() { Log.Debug(">> Keyboard.allKeysUp"); + + // Prevent control key being processed after losing focus. + this._interruptAltGrSequence(); + for (let code in this._keyDownList) { this._sendKeyEvent(this._keyDownList[code], code, false); } From bc31e4e8a274c0218de010c0d37b5e684e739ac4 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 5 Aug 2024 15:44:07 +0200 Subject: [PATCH 042/166] Stop creating sinon sandbox early sinon might not be loaded at this point, which can cause tests to fail. We could create the sandbox in one of the hooks instead, but let's remove the sandbox completely to stay consistent with our other tests. --- tests/test.webutil.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/test.webutil.js b/tests/test.webutil.js index df8227aef..11d093092 100644 --- a/tests/test.webutil.js +++ b/tests/test.webutil.js @@ -182,16 +182,15 @@ describe('WebUtil', function () { window.chrome = chrome; }); - const csSandbox = sinon.createSandbox(); - beforeEach(function () { settings = {}; - csSandbox.spy(window.chrome.storage.sync, 'set'); - csSandbox.spy(window.chrome.storage.sync, 'remove'); + sinon.spy(window.chrome.storage.sync, 'set'); + sinon.spy(window.chrome.storage.sync, 'remove'); return WebUtil.initSettings(); }); afterEach(function () { - csSandbox.restore(); + window.chrome.storage.sync.set.restore(); + window.chrome.storage.sync.remove.restore(); }); describe('writeSetting', function () { From 1b2fe3321bface82604f2c5034cbc2ad8396560a Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 5 Aug 2024 14:23:54 +0200 Subject: [PATCH 043/166] Manually load sinon and chai karma-sinon-chai is not compatible with Chai 5+, and Karma is no longer being updated. Load sinon and chai manually instead, until we can have a long term plan in place. --- eslint.config.mjs | 2 +- karma.conf.js | 11 +++++++++-- package.json | 1 - tests/assertions.js | 9 +++++++++ tests/test.base64.js | 2 -- tests/test.browser.js | 2 -- tests/test.copyrect.js | 2 -- tests/test.deflator.js | 2 -- tests/test.display.js | 2 -- tests/test.gesturehandler.js | 2 -- tests/test.helper.js | 2 -- tests/test.hextile.js | 2 -- tests/test.inflator.js | 2 -- tests/test.int.js | 2 -- tests/test.jpeg.js | 2 -- tests/test.keyboard.js | 2 -- tests/test.localization.js | 1 - tests/test.raw.js | 2 -- tests/test.rfb.js | 2 -- tests/test.rre.js | 2 -- tests/test.tight.js | 2 -- tests/test.tightpng.js | 2 -- tests/test.util.js | 2 -- tests/test.websock.js | 2 -- tests/test.webutil.js | 2 -- tests/test.zrle.js | 2 -- 26 files changed, 19 insertions(+), 47 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index c88e7b758..13b1a32a4 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -79,7 +79,7 @@ export default [ ...globals.node, ...globals.mocha, sinon: false, - chai: false, + expect: false, } }, rules: { diff --git a/karma.conf.js b/karma.conf.js index 1ea17475a..54380ebd2 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -27,15 +27,22 @@ module.exports = (config) => { // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['mocha', 'sinon-chai'], + frameworks: ['mocha'], - // list of files / patterns to load in the browser (loaded in order) + // list of files / patterns to load in the browser files: [ + // node modules + { pattern: 'node_modules/chai/**', included: false }, + { pattern: 'node_modules/sinon/**', included: false }, + { pattern: 'node_modules/sinon-chai/**', included: false }, + // modules to test { pattern: 'app/localization.js', included: false, type: 'module' }, { pattern: 'app/webutil.js', included: false, type: 'module' }, { pattern: 'core/**/*.js', included: false, type: 'module' }, { pattern: 'vendor/pako/**/*.js', included: false, type: 'module' }, + // tests { pattern: 'tests/test.*.js', type: 'module' }, + // test support files { pattern: 'tests/fake.*.js', included: false, type: 'module' }, { pattern: 'tests/assertions.js', type: 'module' }, ], diff --git a/package.json b/package.json index 9fa8c312d..e28850a8d 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,6 @@ "karma-mocha-reporter": "latest", "karma-safari-launcher": "latest", "karma-script-launcher": "latest", - "karma-sinon-chai": "latest", "mocha": "latest", "node-getopt": "latest", "po2json": "latest", diff --git a/tests/assertions.js b/tests/assertions.js index 739f63753..a70122717 100644 --- a/tests/assertions.js +++ b/tests/assertions.js @@ -1,3 +1,12 @@ +import * as chai from '../node_modules/chai/chai.js'; +import sinon from '../node_modules/sinon/pkg/sinon-esm.js'; +import sinonChai from '../node_modules/sinon-chai/lib/sinon-chai.js'; + +window.expect = chai.expect; + +window.sinon = sinon; +chai.use(sinonChai); + // noVNC specific assertions chai.use(function (_chai, utils) { function _equal(a, b) { diff --git a/tests/test.base64.js b/tests/test.base64.js index 04bd207b7..e5644dcdb 100644 --- a/tests/test.base64.js +++ b/tests/test.base64.js @@ -1,5 +1,3 @@ -const expect = chai.expect; - import Base64 from '../core/base64.js'; describe('Base64 Tools', function () { diff --git a/tests/test.browser.js b/tests/test.browser.js index 1beeb48d5..692cc23b2 100644 --- a/tests/test.browser.js +++ b/tests/test.browser.js @@ -1,5 +1,3 @@ -const expect = chai.expect; - import { isMac, isWindows, isIOS, isAndroid, isChromeOS, isSafari, isFirefox, isChrome, isChromium, isOpera, isEdge, isGecko, isWebKit, isBlink } from '../core/util/browser.js'; diff --git a/tests/test.copyrect.js b/tests/test.copyrect.js index a10cddce7..60c395287 100644 --- a/tests/test.copyrect.js +++ b/tests/test.copyrect.js @@ -1,5 +1,3 @@ -const expect = chai.expect; - import Websock from '../core/websock.js'; import Display from '../core/display.js'; diff --git a/tests/test.deflator.js b/tests/test.deflator.js index a7e972ec0..b565b9075 100644 --- a/tests/test.deflator.js +++ b/tests/test.deflator.js @@ -1,5 +1,3 @@ -const expect = chai.expect; - import { inflateInit, inflate } from "../vendor/pako/lib/zlib/inflate.js"; import ZStream from "../vendor/pako/lib/zlib/zstream.js"; import Deflator from "../core/deflator.js"; diff --git a/tests/test.display.js b/tests/test.display.js index e6c0406f9..d2c51793b 100644 --- a/tests/test.display.js +++ b/tests/test.display.js @@ -1,5 +1,3 @@ -const expect = chai.expect; - import Base64 from '../core/base64.js'; import Display from '../core/display.js'; diff --git a/tests/test.gesturehandler.js b/tests/test.gesturehandler.js index 73356be36..d2e27ed2a 100644 --- a/tests/test.gesturehandler.js +++ b/tests/test.gesturehandler.js @@ -1,5 +1,3 @@ -const expect = chai.expect; - import EventTargetMixin from '../core/util/eventtarget.js'; import GestureHandler from '../core/input/gesturehandler.js'; diff --git a/tests/test.helper.js b/tests/test.helper.js index 9995973fd..2c8720c77 100644 --- a/tests/test.helper.js +++ b/tests/test.helper.js @@ -1,5 +1,3 @@ -const expect = chai.expect; - import keysyms from '../core/input/keysymdef.js'; import * as KeyboardUtil from "../core/input/util.js"; diff --git a/tests/test.hextile.js b/tests/test.hextile.js index cbe6f7b5a..f788fd4dc 100644 --- a/tests/test.hextile.js +++ b/tests/test.hextile.js @@ -1,5 +1,3 @@ -const expect = chai.expect; - import Websock from '../core/websock.js'; import Display from '../core/display.js'; diff --git a/tests/test.inflator.js b/tests/test.inflator.js index 304e7a0fd..11a02f2f4 100644 --- a/tests/test.inflator.js +++ b/tests/test.inflator.js @@ -1,5 +1,3 @@ -const expect = chai.expect; - import { deflateInit, deflate, Z_FULL_FLUSH } from "../vendor/pako/lib/zlib/deflate.js"; import ZStream from "../vendor/pako/lib/zlib/zstream.js"; import Inflator from "../core/inflator.js"; diff --git a/tests/test.int.js b/tests/test.int.js index 084d68abd..378ebd589 100644 --- a/tests/test.int.js +++ b/tests/test.int.js @@ -1,5 +1,3 @@ -const expect = chai.expect; - import { toUnsigned32bit, toSigned32bit } from '../core/util/int.js'; describe('Integer casting', function () { diff --git a/tests/test.jpeg.js b/tests/test.jpeg.js index 8dee48912..5cc153f90 100644 --- a/tests/test.jpeg.js +++ b/tests/test.jpeg.js @@ -1,5 +1,3 @@ -const expect = chai.expect; - import Websock from '../core/websock.js'; import Display from '../core/display.js'; diff --git a/tests/test.keyboard.js b/tests/test.keyboard.js index efc84c306..135c5981b 100644 --- a/tests/test.keyboard.js +++ b/tests/test.keyboard.js @@ -1,5 +1,3 @@ -const expect = chai.expect; - import Keyboard from '../core/input/keyboard.js'; describe('Key Event Handling', function () { diff --git a/tests/test.localization.js b/tests/test.localization.js index 916ff8462..a1cb45474 100644 --- a/tests/test.localization.js +++ b/tests/test.localization.js @@ -1,4 +1,3 @@ -const expect = chai.expect; import _, { Localizer, l10n } from '../app/localization.js'; describe('Localization', function () { diff --git a/tests/test.raw.js b/tests/test.raw.js index 4a634ccd0..19b2377f7 100644 --- a/tests/test.raw.js +++ b/tests/test.raw.js @@ -1,5 +1,3 @@ -const expect = chai.expect; - import Websock from '../core/websock.js'; import Display from '../core/display.js'; diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 62b80ca3f..2be3bfbfc 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1,5 +1,3 @@ -const expect = chai.expect; - import RFB from '../core/rfb.js'; import Websock from '../core/websock.js'; import ZStream from "../vendor/pako/lib/zlib/zstream.js"; diff --git a/tests/test.rre.js b/tests/test.rre.js index c55d7f397..7b5f73d0e 100644 --- a/tests/test.rre.js +++ b/tests/test.rre.js @@ -1,5 +1,3 @@ -const expect = chai.expect; - import Websock from '../core/websock.js'; import Display from '../core/display.js'; diff --git a/tests/test.tight.js b/tests/test.tight.js index 141d7b6e2..3d6b555da 100644 --- a/tests/test.tight.js +++ b/tests/test.tight.js @@ -1,5 +1,3 @@ -const expect = chai.expect; - import Websock from '../core/websock.js'; import Display from '../core/display.js'; diff --git a/tests/test.tightpng.js b/tests/test.tightpng.js index 02c66d93b..e7edc8fa6 100644 --- a/tests/test.tightpng.js +++ b/tests/test.tightpng.js @@ -1,5 +1,3 @@ -const expect = chai.expect; - import Websock from '../core/websock.js'; import Display from '../core/display.js'; diff --git a/tests/test.util.js b/tests/test.util.js index cd61f248a..eb7240951 100644 --- a/tests/test.util.js +++ b/tests/test.util.js @@ -1,6 +1,4 @@ /* eslint-disable no-console */ -const expect = chai.expect; - import * as Log from '../core/util/logging.js'; import { encodeUTF8, decodeUTF8 } from '../core/util/strings.js'; diff --git a/tests/test.websock.js b/tests/test.websock.js index dc361b749..53145b360 100644 --- a/tests/test.websock.js +++ b/tests/test.websock.js @@ -1,5 +1,3 @@ -const expect = chai.expect; - import Websock from '../core/websock.js'; import FakeWebSocket from './fake.websocket.js'; diff --git a/tests/test.webutil.js b/tests/test.webutil.js index 11d093092..9151a0603 100644 --- a/tests/test.webutil.js +++ b/tests/test.webutil.js @@ -1,7 +1,5 @@ /* jshint expr: true */ -const expect = chai.expect; - import * as WebUtil from '../app/webutil.js'; describe('WebUtil', function () { diff --git a/tests/test.zrle.js b/tests/test.zrle.js index be0464093..f7c6089d5 100644 --- a/tests/test.zrle.js +++ b/tests/test.zrle.js @@ -1,5 +1,3 @@ -const expect = chai.expect; - import Websock from '../core/websock.js'; import Display from '../core/display.js'; From 06f14a5cd3c188c63ad2060b7532a914c0d8f7f6 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 5 Aug 2024 16:31:59 +0200 Subject: [PATCH 044/166] Add test for AltGr abort on blur --- tests/test.keyboard.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test.keyboard.js b/tests/test.keyboard.js index 135c5981b..11c8b6eb7 100644 --- a/tests/test.keyboard.js +++ b/tests/test.keyboard.js @@ -478,6 +478,22 @@ describe('Key Event Handling', function () { expect(kbd.onkeyevent).to.not.have.been.called; }); + it('should release ControlLeft on blur', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1})); + expect(kbd.onkeyevent).to.not.have.been.called; + kbd._allKeysUp(); + expect(kbd.onkeyevent).to.have.been.calledTwice; + expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xffe3, "ControlLeft", true); + expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xffe3, "ControlLeft", false); + + // Check that the timer is properly dead + kbd.onkeyevent.resetHistory(); + this.clock.tick(100); + expect(kbd.onkeyevent).to.not.have.been.called; + }); + it('should generate AltGraph for quick Ctrl+Alt sequence', function () { const kbd = new Keyboard(document); kbd.onkeyevent = sinon.spy(); From 074fa1a40f6e3f65bd61109de3f404796e266524 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 8 Aug 2024 14:40:04 +0200 Subject: [PATCH 045/166] Let browser construct URL string for us Likely a lot safer for corner cases than us trying to figure this out ourselves. --- app/ui.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/ui.js b/app/ui.js index f27dfe28e..1a9571dc2 100644 --- a/app/ui.js +++ b/app/ui.js @@ -1033,16 +1033,17 @@ const UI = { let url; - url = UI.getSetting('encrypt') ? 'wss' : 'ws'; + url = new URL("https://" + host); - url += '://' + host; + url.protocol = UI.getSetting('encrypt') ? 'wss:' : 'ws:'; if (port) { - url += ':' + port; + url.port = port; } - url += '/' + path; + url.pathname = '/' + path; try { - UI.rfb = new RFB(document.getElementById('noVNC_container'), url, + UI.rfb = new RFB(document.getElementById('noVNC_container'), + url.href, { shared: UI.getSetting('shared'), repeaterID: UI.getSetting('repeaterID'), credentials: { password: password } }); From 96c76f7709037956e760ae5f58dbde1bc1308bba Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 8 Aug 2024 14:53:42 +0200 Subject: [PATCH 046/166] Allow relative WebSocket URLs This can be very useful if you have multiple instances of noVNC, and you want to redirect them to different VNC servers. The new default settings will have the same behaviour as before for systems where noVNC is deployed in the root web folder. --- app/ui.js | 37 +++++++++++++------------------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/app/ui.js b/app/ui.js index 1a9571dc2..90dc71893 100644 --- a/app/ui.js +++ b/app/ui.js @@ -158,20 +158,7 @@ const UI = { UI.initSetting('logging', 'warn'); UI.updateLogging(); - // if port == 80 (or 443) then it won't be present and should be - // set manually - let port = window.location.port; - if (!port) { - if (window.location.protocol.substring(0, 5) == 'https') { - port = 443; - } else if (window.location.protocol.substring(0, 4) == 'http') { - port = 80; - } - } - /* Populate the controls if defaults are provided in the URL */ - UI.initSetting('host', window.location.hostname); - UI.initSetting('port', port); UI.initSetting('encrypt', (window.location.protocol === "https:")); UI.initSetting('view_clip', false); UI.initSetting('resize', 'off'); @@ -1021,25 +1008,27 @@ const UI = { UI.hideStatus(); - if (!host) { - Log.Error("Can't connect when host is: " + host); - UI.showStatus(_("Must set host"), 'error'); - return; - } - UI.closeConnectPanel(); UI.updateVisualState('connecting'); let url; - url = new URL("https://" + host); + if (host) { + url = new URL("https://" + host); - url.protocol = UI.getSetting('encrypt') ? 'wss:' : 'ws:'; - if (port) { - url.port = port; + url.protocol = UI.getSetting('encrypt') ? 'wss:' : 'ws:'; + if (port) { + url.port = port; + } + url.pathname = '/' + path; + } else { + // Current (May 2024) browsers support relative WebSocket + // URLs natively, but we need to support older browsers for + // some time. + url = new URL(path, location.href); + url.protocol = (window.location.protocol === "https:") ? 'wss:' : 'ws:'; } - url.pathname = '/' + path; try { UI.rfb = new RFB(document.getElementById('noVNC_container'), From 9334c6824156877551d21570fea46dae8a527cc2 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 26 Jun 2020 15:57:09 +0200 Subject: [PATCH 047/166] Handle all settings via UI.getSetting() Makes sure everything behaves the same way, even if there is no visible UI for a settings. --- app/ui.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/app/ui.js b/app/ui.js index 90dc71893..caba8e180 100644 --- a/app/ui.js +++ b/app/ui.js @@ -120,7 +120,7 @@ const UI = { document.documentElement.classList.remove("noVNC_loading"); - let autoconnect = WebUtil.getConfigVar('autoconnect', false); + let autoconnect = UI.getSetting('autoconnect'); if (autoconnect === 'true' || autoconnect == '1') { autoconnect = true; UI.connect(); @@ -160,11 +160,14 @@ const UI = { /* Populate the controls if defaults are provided in the URL */ UI.initSetting('encrypt', (window.location.protocol === "https:")); + UI.initSetting('password'); + UI.initSetting('autoconnect', false); UI.initSetting('view_clip', false); UI.initSetting('resize', 'off'); UI.initSetting('quality', 6); UI.initSetting('compression', 2); UI.initSetting('shared', true); + UI.initSetting('bell', 'on'); UI.initSetting('view_only', false); UI.initSetting('show_dot', false); UI.initSetting('path', 'websockify'); @@ -759,9 +762,12 @@ const UI = { let value = UI.getSetting(name); const ctrl = document.getElementById('noVNC_setting_' + name); + if (ctrl === null) { + return; + } + if (ctrl.type === 'checkbox') { ctrl.checked = value; - } else if (typeof ctrl.options !== 'undefined') { for (let i = 0; i < ctrl.options.length; i += 1) { if (ctrl.options[i].value === value) { @@ -794,7 +800,8 @@ const UI = { getSetting(name) { const ctrl = document.getElementById('noVNC_setting_' + name); let val = WebUtil.readSetting(name); - if (typeof val !== 'undefined' && val !== null && ctrl.type === 'checkbox') { + if (typeof val !== 'undefined' && val !== null && + ctrl !== null && ctrl.type === 'checkbox') { if (val.toString().toLowerCase() in {'0': 1, 'no': 1, 'false': 1}) { val = false; } else { @@ -998,7 +1005,7 @@ const UI = { const path = UI.getSetting('path'); if (typeof password === 'undefined') { - password = WebUtil.getConfigVar('password'); + password = UI.getSetting('password'); UI.reconnectPassword = password; } @@ -1728,7 +1735,7 @@ const UI = { }, bell(e) { - if (WebUtil.getConfigVar('bell', 'on') === 'on') { + if (UI.getSetting('bell') === 'on') { const promise = document.getElementById('noVNC_bell').play(); // The standards disagree on the return value here if (promise) { From c6606a5caf9b9895e89b580351201ece2f1a49e4 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 26 Jun 2020 15:59:00 +0200 Subject: [PATCH 048/166] Merge UI startup in to a single routine Makes it easier to see how things are connected. --- app/ui.js | 67 ++++++++++++++++++++++++++----------------------------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/app/ui.js b/app/ui.js index caba8e180..e4dfd5af8 100644 --- a/app/ui.js +++ b/app/ui.js @@ -20,6 +20,8 @@ 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 UI = { connected: false, @@ -42,20 +44,23 @@ const UI = { reconnectCallback: null, reconnectPassword: null, - prime() { - return WebUtil.initSettings().then(() => { - if (document.readyState === "interactive" || document.readyState === "complete") { - return UI.start(); - } + async start() { + // Set up translations + try { + await l10n.setup(LINGUAS, "app/locale/"); + } catch (err) { + Log.Error("Failed to load translations: " + err); + } - return new Promise((resolve, reject) => { - document.addEventListener('DOMContentLoaded', () => UI.start().then(resolve).catch(reject)); - }); - }); - }, + // Initialize setting storage + await WebUtil.initSettings(); - // Render default UI and initialize settings menu - start() { + // Wait for the page to load + if (document.readyState !== "interactive" && document.readyState !== "complete") { + await new Promise((resolve, reject) => { + document.addEventListener('DOMContentLoaded', resolve); + }); + } UI.initSettings(); @@ -70,22 +75,20 @@ const UI = { } // Try to fetch version number - fetch('./package.json') - .then((response) => { - if (!response.ok) { - throw Error("" + response.status + " " + response.statusText); - } - return response.json(); - }) - .then((packageInfo) => { - Array.from(document.getElementsByClassName('noVNC_version')).forEach(el => el.innerText = packageInfo.version); - }) - .catch((err) => { - Log.Error("Couldn't fetch package.json: " + err); - Array.from(document.getElementsByClassName('noVNC_version_wrapper')) - .concat(Array.from(document.getElementsByClassName('noVNC_version_separator'))) - .forEach(el => el.style.display = 'none'); - }); + try { + let response = await fetch('./package.json'); + if (!response.ok) { + throw Error("" + response.status + " " + response.statusText); + } + + let packageInfo = await response.json(); + Array.from(document.getElementsByClassName('noVNC_version')).forEach(el => el.innerText = packageInfo.version); + } catch (err) { + Log.Error("Couldn't fetch package.json: " + err); + Array.from(document.getElementsByClassName('noVNC_version_wrapper')) + .concat(Array.from(document.getElementsByClassName('noVNC_version_separator'))) + .forEach(el => el.style.display = 'none'); + } // Adapt the interface for touch screen devices if (isTouchDevice) { @@ -129,8 +132,6 @@ const UI = { // Show the connect panel on first load unless autoconnecting UI.openConnectPanel(); } - - return Promise.resolve(UI.rfb); }, initFullscreen() { @@ -1766,10 +1767,6 @@ const UI = { */ }; -// Set up translations -const LINGUAS = ["cs", "de", "el", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"]; -l10n.setup(LINGUAS, "app/locale/") - .catch(err => Log.Error("Failed to load translations: " + err)) - .then(UI.prime); +UI.start(); export default UI; From 84897fd1103d7e4603d7cadc4957111ee6bd66d0 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 26 Jun 2020 16:15:09 +0200 Subject: [PATCH 049/166] Handle disabling settings without label --- app/ui.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/ui.js b/app/ui.js index e4dfd5af8..316042911 100644 --- a/app/ui.js +++ b/app/ui.js @@ -818,13 +818,17 @@ const UI = { disableSetting(name) { const ctrl = document.getElementById('noVNC_setting_' + name); ctrl.disabled = true; - ctrl.label.classList.add('noVNC_disabled'); + if (ctrl.label !== undefined) { + ctrl.label.classList.add('noVNC_disabled'); + } }, enableSetting(name) { const ctrl = document.getElementById('noVNC_setting_' + name); ctrl.disabled = false; - ctrl.label.classList.remove('noVNC_disabled'); + if (ctrl.label !== undefined) { + ctrl.label.classList.remove('noVNC_disabled'); + } }, /* ------^------- From c6c8e5e51329001f778a72607bc68432b9d359f8 Mon Sep 17 00:00:00 2001 From: Mark Peek Date: Thu, 15 Aug 2024 09:39:05 -0700 Subject: [PATCH 050/166] Add Zlib encoding --- README.md | 2 +- core/decoders/zlib.js | 51 ++++++++++++++++++++++++++ core/encodings.js | 2 ++ core/rfb.js | 3 ++ tests/test.zlib.js | 84 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 core/decoders/zlib.js create mode 100644 tests/test.zlib.js diff --git a/README.md b/README.md index b95d15e65..305108588 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ profits such as: RSA-AES, Tight, VeNCrypt Plain, XVP, Apple's Diffie-Hellman, UltraVNC's MSLogonII * Supported VNC encodings: raw, copyrect, rre, hextile, tight, tightPNG, - ZRLE, JPEG + ZRLE, JPEG, Zlib * Supports scaling, clipping and resizing the desktop * Local cursor rendering * Clipboard copy/paste with full Unicode support diff --git a/core/decoders/zlib.js b/core/decoders/zlib.js new file mode 100644 index 000000000..d1e5d5c98 --- /dev/null +++ b/core/decoders/zlib.js @@ -0,0 +1,51 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2024 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + */ + +import Inflator from "../inflator.js"; + +export default class ZlibDecoder { + constructor() { + this._zlib = new Inflator(); + this._length = 0; + } + + decodeRect(x, y, width, height, sock, display, depth) { + if ((width === 0) || (height === 0)) { + return true; + } + + if (this._length === 0) { + if (sock.rQwait("ZLIB", 4)) { + return false; + } + + this._length = sock.rQshift32(); + } + + if (sock.rQwait("ZLIB", this._length)) { + return false; + } + + let data = new Uint8Array(sock.rQshiftBytes(this._length, false)); + this._length = 0; + + this._zlib.setInput(data); + data = this._zlib.inflate(width * height * 4); + this._zlib.setInput(null); + + // Max sure the image is fully opaque + for (let i = 0; i < width * height; i++) { + data[i * 4 + 3] = 255; + } + + display.blitImage(x, y, width, height, data, 0); + + return true; + } +} diff --git a/core/encodings.js b/core/encodings.js index 1a79989d1..d80536191 100644 --- a/core/encodings.js +++ b/core/encodings.js @@ -11,6 +11,7 @@ export const encodings = { encodingCopyRect: 1, encodingRRE: 2, encodingHextile: 5, + encodingZlib: 6, encodingTight: 7, encodingZRLE: 16, encodingTightPNG: -260, @@ -40,6 +41,7 @@ export function encodingName(num) { case encodings.encodingCopyRect: return "CopyRect"; case encodings.encodingRRE: return "RRE"; case encodings.encodingHextile: return "Hextile"; + case encodings.encodingZlib: return "Zlib"; case encodings.encodingTight: return "Tight"; case encodings.encodingZRLE: return "ZRLE"; case encodings.encodingTightPNG: return "TightPNG"; diff --git a/core/rfb.js b/core/rfb.js index f2deb0e7b..0bd2b07e9 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -31,6 +31,7 @@ import RawDecoder from "./decoders/raw.js"; import CopyRectDecoder from "./decoders/copyrect.js"; import RREDecoder from "./decoders/rre.js"; import HextileDecoder from "./decoders/hextile.js"; +import ZlibDecoder from './decoders/zlib.js'; import TightDecoder from "./decoders/tight.js"; import TightPNGDecoder from "./decoders/tightpng.js"; import ZRLEDecoder from "./decoders/zrle.js"; @@ -244,6 +245,7 @@ export default class RFB extends EventTargetMixin { this._decoders[encodings.encodingCopyRect] = new CopyRectDecoder(); this._decoders[encodings.encodingRRE] = new RREDecoder(); this._decoders[encodings.encodingHextile] = new HextileDecoder(); + this._decoders[encodings.encodingZlib] = new ZlibDecoder(); this._decoders[encodings.encodingTight] = new TightDecoder(); this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder(); this._decoders[encodings.encodingZRLE] = new ZRLEDecoder(); @@ -2121,6 +2123,7 @@ export default class RFB extends EventTargetMixin { encs.push(encodings.encodingJPEG); encs.push(encodings.encodingHextile); encs.push(encodings.encodingRRE); + encs.push(encodings.encodingZlib); } encs.push(encodings.encodingRaw); diff --git a/tests/test.zlib.js b/tests/test.zlib.js new file mode 100644 index 000000000..bc72137e5 --- /dev/null +++ b/tests/test.zlib.js @@ -0,0 +1,84 @@ +import Websock from '../core/websock.js'; +import Display from '../core/display.js'; + +import ZlibDecoder from '../core/decoders/zlib.js'; + +import FakeWebSocket from './fake.websocket.js'; + +function testDecodeRect(decoder, x, y, width, height, data, display, depth) { + let sock; + let done = false; + + sock = new Websock; + sock.open("ws://example.com"); + + sock.on('message', () => { + done = decoder.decodeRect(x, y, width, height, sock, display, depth); + }); + + // Empty messages are filtered at multiple layers, so we need to + // do a direct call + if (data.length === 0) { + done = decoder.decodeRect(x, y, width, height, sock, display, depth); + } else { + sock._websocket._receiveData(new Uint8Array(data)); + } + + display.flip(); + + return done; +} + +describe('Zlib Decoder', function () { + let decoder; + let display; + + before(FakeWebSocket.replace); + after(FakeWebSocket.restore); + + beforeEach(function () { + decoder = new ZlibDecoder(); + display = new Display(document.createElement('canvas')); + display.resize(4, 4); + }); + + it('should handle the Zlib encoding', function () { + let done; + + let zlibData = new Uint8Array([ + 0x00, 0x00, 0x00, 0x23, /* length */ + 0x78, 0x01, 0xfa, 0xcf, 0x00, 0x04, 0xff, 0x61, 0x04, 0x90, 0x01, 0x41, 0x50, 0xc1, 0xff, 0x0c, + 0xef, 0x40, 0x02, 0xef, 0xfe, 0x33, 0xac, 0x02, 0xe2, 0xd5, 0x40, 0x8c, 0xce, 0x07, 0x00, 0x00, + 0x00, 0xff, 0xff, + ]); + done = testDecodeRect(decoder, 0, 0, 4, 4, zlibData, display, 24); + expect(done).to.be.true; + + let targetData = new Uint8ClampedArray([ + 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255, + 0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255 + ]); + + expect(display).to.have.displayed(targetData); + }); + + it('should handle empty rects', function () { + display.fillRect(0, 0, 4, 4, [0x00, 0x00, 0xff]); + display.fillRect(2, 0, 2, 2, [0x00, 0xff, 0x00]); + display.fillRect(0, 2, 2, 2, [0x00, 0xff, 0x00]); + + let done = testDecodeRect(decoder, 1, 2, 0, 0, [], display, 24); + + let targetData = new Uint8Array([ + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 + ]); + + expect(done).to.be.true; + expect(display).to.have.displayed(targetData); + }); +}); From d106b7a6bba12feec81d475586157be0193a94f6 Mon Sep 17 00:00:00 2001 From: Andri Yngvason Date: Sat, 29 Jun 2024 13:41:50 +0000 Subject: [PATCH 051/166] Add H.264 decoder This adds an H.264 decoder based on WebCodecs. --- core/decoders/h264.js | 321 ++++++++++++++++++++++++++++++++++++++++++ core/display.js | 53 ++++++- core/encodings.js | 2 + core/rfb.js | 7 +- core/util/browser.js | 20 +++ 5 files changed, 399 insertions(+), 4 deletions(-) create mode 100644 core/decoders/h264.js diff --git a/core/decoders/h264.js b/core/decoders/h264.js new file mode 100644 index 000000000..db144fcd3 --- /dev/null +++ b/core/decoders/h264.js @@ -0,0 +1,321 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2024 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + */ + +import * as Log from '../util/logging.js'; + +class H264Parser { + constructor(data) { + this._data = data; + this._index = 0; + this.profileIdc = null; + this.constraintSet = null; + this.levelIdc = null; + } + + _getStartSequenceLen(index) { + let data = this._data; + if (data[index + 0] == 0 && data[index + 1] == 0 && data[index + 2] == 0 && data[index + 3] == 1) { + return 4; + } + if (data[index + 0] == 0 && data[index + 1] == 0 && data[index + 2] == 1) { + return 3; + } + return 0; + } + + _indexOfNextNalUnit(index) { + let data = this._data; + for (let i = index; i < data.length; ++i) { + if (this._getStartSequenceLen(i) != 0) { + return i; + } + } + return -1; + } + + _parseSps(index) { + this.profileIdc = this._data[index]; + this.constraintSet = this._data[index + 1]; + this.levelIdc = this._data[index + 2]; + } + + _parseNalUnit(index) { + const firstByte = this._data[index]; + if (firstByte & 0x80) { + throw new Error('H264 parsing sanity check failed, forbidden zero bit is set'); + } + const unitType = firstByte & 0x1f; + + switch (unitType) { + case 1: // coded slice, non-idr + return { slice: true }; + case 5: // coded slice, idr + return { slice: true, key: true }; + case 6: // sei + return {}; + case 7: // sps + this._parseSps(index + 1); + return {}; + case 8: // pps + return {}; + default: + Log.Warn("Unhandled unit type: ", unitType); + break; + } + return {}; + } + + parse() { + const startIndex = this._index; + let isKey = false; + + while (this._index < this._data.length) { + const startSequenceLen = this._getStartSequenceLen(this._index); + if (startSequenceLen == 0) { + throw new Error('Invalid start sequence in bit stream'); + } + + const { slice, key } = this._parseNalUnit(this._index + startSequenceLen); + + let nextIndex = this._indexOfNextNalUnit(this._index + startSequenceLen); + if (nextIndex == -1) { + this._index = this._data.length; + } else { + this._index = nextIndex; + } + + if (key) { + isKey = true; + } + if (slice) { + break; + } + } + + if (startIndex === this._index) { + return null; + } + + return { + frame: this._data.subarray(startIndex, this._index), + key: isKey, + }; + } +} + +class H264Context { + constructor(width, height) { + this.lastUsed = 0; + this._width = width; + this._height = height; + this._profileIdc = null; + this._constraintSet = null; + this._levelIdc = null; + this._decoder = null; + this._pendingFrames = []; + } + + _handleFrame(frame) { + let pending = this._pendingFrames.shift(); + if (pending === undefined) { + throw new Error("Pending frame queue empty when receiving frame from decoder"); + } + + if (pending.timestamp != frame.timestamp) { + throw new Error("Video frame timestamp mismatch. Expected " + + frame.timestamp + " but but got " + pending.timestamp); + } + + pending.frame = frame; + pending.ready = true; + pending.resolve(); + + if (!pending.keep) { + frame.close(); + } + } + + _handleError(e) { + throw new Error("Failed to decode frame: " + e.message); + } + + _configureDecoder(profileIdc, constraintSet, levelIdc) { + if (this._decoder === null || this._decoder.state === 'closed') { + this._decoder = new VideoDecoder({ + output: frame => this._handleFrame(frame), + error: e => this._handleError(e), + }); + } + const codec = 'avc1.' + + profileIdc.toString(16).padStart(2, '0') + + constraintSet.toString(16).padStart(2, '0') + + levelIdc.toString(16).padStart(2, '0'); + this._decoder.configure({ + codec: codec, + codedWidth: this._width, + codedHeight: this._height, + optimizeForLatency: true, + }); + } + + _preparePendingFrame(timestamp) { + let pending = { + timestamp: timestamp, + promise: null, + resolve: null, + frame: null, + ready: false, + keep: false, + }; + pending.promise = new Promise((resolve) => { + pending.resolve = resolve; + }); + this._pendingFrames.push(pending); + + return pending; + } + + decode(payload) { + let parser = new H264Parser(payload); + let result = null; + + // Ideally, this timestamp should come from the server, but we'll just + // approximate it instead. + let timestamp = Math.round(window.performance.now() * 1e3); + + while (true) { + let encodedFrame = parser.parse(); + if (encodedFrame === null) { + break; + } + + if (parser.profileIdc !== null) { + self._profileIdc = parser.profileIdc; + self._constraintSet = parser.constraintSet; + self._levelIdc = parser.levelIdc; + } + + if (this._decoder === null || this._decoder.state !== 'configured') { + if (!encodedFrame.key) { + Log.Warn("Missing key frame. Can't decode until one arrives"); + continue; + } + if (self._profileIdc === null) { + Log.Warn('Cannot config decoder. Have not received SPS and PPS yet.'); + continue; + } + this._configureDecoder(self._profileIdc, self._constraintSet, + self._levelIdc); + } + + result = this._preparePendingFrame(timestamp); + + const chunk = new EncodedVideoChunk({ + timestamp: timestamp, + type: encodedFrame.key ? 'key' : 'delta', + data: encodedFrame.frame, + }); + + try { + this._decoder.decode(chunk); + } catch (e) { + Log.Warn("Failed to decode:", e); + } + } + + // We only keep last frame of each payload + if (result !== null) { + result.keep = true; + } + + return result; + } +} + +export default class H264Decoder { + constructor() { + this._tick = 0; + this._contexts = {}; + } + + _contextId(x, y, width, height) { + return [x, y, width, height].join(','); + } + + _findOldestContextId() { + let oldestTick = Number.MAX_VALUE; + let oldestKey = undefined; + for (const [key, value] of Object.entries(this._contexts)) { + if (value.lastUsed < oldestTick) { + oldestTick = value.lastUsed; + oldestKey = key; + } + } + return oldestKey; + } + + _createContext(x, y, width, height) { + const maxContexts = 64; + if (Object.keys(this._contexts).length >= maxContexts) { + let oldestContextId = this._findOldestContextId(); + delete this._contexts[oldestContextId]; + } + let context = new H264Context(width, height); + this._contexts[this._contextId(x, y, width, height)] = context; + return context; + } + + _getContext(x, y, width, height) { + let context = this._contexts[this._contextId(x, y, width, height)]; + return context !== undefined ? context : this._createContext(x, y, width, height); + } + + _resetContext(x, y, width, height) { + delete this._contexts[this._contextId(x, y, width, height)]; + } + + _resetAllContexts() { + this._contexts = {}; + } + + decodeRect(x, y, width, height, sock, display, depth) { + const resetContextFlag = 1; + const resetAllContextsFlag = 2; + + if (sock.rQwait("h264 header", 8)) { + return false; + } + + const length = sock.rQshift32(); + const flags = sock.rQshift32(); + + if (sock.rQwait("h264 payload", length, 8)) { + return false; + } + + if (flags & resetAllContextsFlag) { + this._resetAllContexts(); + } else if (flags & resetContextFlag) { + this._resetContext(x, y, width, height); + } + + let context = this._getContext(x, y, width, height); + context.lastUsed = this._tick++; + + if (length !== 0) { + let payload = sock.rQshiftBytes(length, false); + let frame = context.decode(payload); + if (frame !== null) { + display.videoFrame(x, y, width, height, frame); + } + } + + return true; + } +} diff --git a/core/display.js b/core/display.js index fcd626999..bc0bf2190 100644 --- a/core/display.js +++ b/core/display.js @@ -380,6 +380,17 @@ export default class Display { }); } + videoFrame(x, y, width, height, frame) { + this._renderQPush({ + 'type': 'frame', + 'frame': frame, + 'x': x, + 'y': y, + 'width': width, + 'height': height + }); + } + blitImage(x, y, width, height, arr, offset, fromQueue) { if (this._renderQ.length !== 0 && !fromQueue) { // NB(directxman12): it's technically more performant here to use preallocated arrays, @@ -406,9 +417,16 @@ export default class Display { } } - drawImage(img, x, y) { - this._drawCtx.drawImage(img, x, y); - this._damage(x, y, img.width, img.height); + drawImage(img, ...args) { + this._drawCtx.drawImage(img, ...args); + + if (args.length <= 4) { + const [x, y] = args; + this._damage(x, y, img.width, img.height); + } else { + const [,, sw, sh, dx, dy] = args; + this._damage(dx, dy, sw, sh); + } } autoscale(containerWidth, containerHeight) { @@ -511,6 +529,35 @@ export default class Display { ready = false; } break; + case 'frame': + if (a.frame.ready) { + // The encoded frame may be larger than the rect due to + // limitations of the encoder, so we need to crop the + // frame. + let frame = a.frame.frame; + if (frame.codedWidth < a.width || frame.codedHeight < a.height) { + Log.Warn("Decoded video frame does not cover its full rectangle area. Expecting at least " + + a.width + "x" + a.height + " but got " + + frame.codedWidth + "x" + frame.codedHeight); + } + const sx = 0; + const sy = 0; + const sw = a.width; + const sh = a.height; + const dx = a.x; + const dy = a.y; + const dw = sw; + const dh = sh; + this.drawImage(frame, sx, sy, sw, sh, dx, dy, dw, dh); + frame.close(); + } else { + let display = this; + a.frame.promise.then(() => { + display._scanRenderQ(); + }); + ready = false; + } + break; } if (ready) { diff --git a/core/encodings.js b/core/encodings.js index 1a79989d1..aa1fd4bbc 100644 --- a/core/encodings.js +++ b/core/encodings.js @@ -15,6 +15,7 @@ export const encodings = { encodingZRLE: 16, encodingTightPNG: -260, encodingJPEG: 21, + encodingH264: 50, pseudoEncodingQualityLevel9: -23, pseudoEncodingQualityLevel0: -32, @@ -44,6 +45,7 @@ export function encodingName(num) { case encodings.encodingZRLE: return "ZRLE"; case encodings.encodingTightPNG: return "TightPNG"; case encodings.encodingJPEG: return "JPEG"; + case encodings.encodingH264: return "H.264"; default: return "[unknown encoding " + num + "]"; } } diff --git a/core/rfb.js b/core/rfb.js index f2deb0e7b..9225cb464 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -10,7 +10,7 @@ import { toUnsigned32bit, toSigned32bit } from './util/int.js'; import * as Log from './util/logging.js'; import { encodeUTF8, decodeUTF8 } from './util/strings.js'; -import { dragThreshold } from './util/browser.js'; +import { dragThreshold, supportsWebCodecsH264Decode } from './util/browser.js'; import { clientToElement } from './util/element.js'; import { setCapture } from './util/events.js'; import EventTargetMixin from './util/eventtarget.js'; @@ -35,6 +35,7 @@ import TightDecoder from "./decoders/tight.js"; import TightPNGDecoder from "./decoders/tightpng.js"; import ZRLEDecoder from "./decoders/zrle.js"; import JPEGDecoder from "./decoders/jpeg.js"; +import H264Decoder from "./decoders/h264.js"; // How many seconds to wait for a disconnect to finish const DISCONNECT_TIMEOUT = 3; @@ -248,6 +249,7 @@ export default class RFB extends EventTargetMixin { this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder(); this._decoders[encodings.encodingZRLE] = new ZRLEDecoder(); this._decoders[encodings.encodingJPEG] = new JPEGDecoder(); + this._decoders[encodings.encodingH264] = new H264Decoder(); // NB: nothing that needs explicit teardown should be done // before this point, since this can throw an exception @@ -2115,6 +2117,9 @@ export default class RFB extends EventTargetMixin { encs.push(encodings.encodingCopyRect); // Only supported with full depth support if (this._fbDepth == 24) { + if (supportsWebCodecsH264Decode) { + encs.push(encodings.encodingH264); + } encs.push(encodings.encodingTight); encs.push(encodings.encodingTightPNG); encs.push(encodings.encodingZRLE); diff --git a/core/util/browser.js b/core/util/browser.js index bbc9f5c1e..1ecded662 100644 --- a/core/util/browser.js +++ b/core/util/browser.js @@ -70,6 +70,26 @@ try { } export const hasScrollbarGutter = _hasScrollbarGutter; +export let supportsWebCodecsH264Decode = false; + +async function _checkWebCodecsH264DecodeSupport() { + if (!('VideoDecoder' in window)) { + return; + } + + // We'll need to make do with some placeholders here + const config = { + codec: 'avc1.42401f', + codedWidth: 1920, + codedHeight: 1080, + optimizeForLatency: true, + }; + + const result = await VideoDecoder.isConfigSupported(config); + supportsWebCodecsH264Decode = result.supported; +} +_checkWebCodecsH264DecodeSupport(); + /* * The functions for detection of platforms and browsers below are exported * but the use of these should be minimized as much as possible. From bbb6a5b938d4dd85a99ca78718b09dc08bc9a81d Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 19 Aug 2024 14:01:00 +0200 Subject: [PATCH 052/166] Fix host and port via query string We need to call initSetting() even if we don't have any interesting default to set, as that is what checks if values have been provided as a query string. Fixes 96c76f7. --- app/ui.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/ui.js b/app/ui.js index 90dc71893..71933af63 100644 --- a/app/ui.js +++ b/app/ui.js @@ -159,6 +159,8 @@ const UI = { UI.updateLogging(); /* Populate the controls if defaults are provided in the URL */ + UI.initSetting('host', ''); + UI.initSetting('port', 0); UI.initSetting('encrypt', (window.location.protocol === "https:")); UI.initSetting('view_clip', false); UI.initSetting('resize', 'off'); From c1bba972f401b1e87e2266a05a647f44b3356020 Mon Sep 17 00:00:00 2001 From: Andri Yngvason Date: Sun, 11 Aug 2024 23:58:43 +0000 Subject: [PATCH 053/166] Add unit tests for H.264 decoder --- core/decoders/h264.js | 4 +- tests/test.h264.js | 264 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 tests/test.h264.js diff --git a/core/decoders/h264.js b/core/decoders/h264.js index db144fcd3..2587d61be 100644 --- a/core/decoders/h264.js +++ b/core/decoders/h264.js @@ -9,7 +9,7 @@ import * as Log from '../util/logging.js'; -class H264Parser { +export class H264Parser { constructor(data) { this._data = data; this._index = 0; @@ -109,7 +109,7 @@ class H264Parser { } } -class H264Context { +export class H264Context { constructor(width, height) { this.lastUsed = 0; this._width = width; diff --git a/tests/test.h264.js b/tests/test.h264.js new file mode 100644 index 000000000..42273e7ce --- /dev/null +++ b/tests/test.h264.js @@ -0,0 +1,264 @@ +import Websock from '../core/websock.js'; +import Display from '../core/display.js'; + +import { H264Parser } from '../core/decoders/h264.js'; +import H264Decoder from '../core/decoders/h264.js'; +import Base64 from '../core/base64.js'; + +import FakeWebSocket from './fake.websocket.js'; + +/* This is a 3 frame 16x16 video where the first frame is solid red, the second + * is solid green and the third is solid blue. + * + * The colour space is BT.709. It is encoded into the stream. + */ +const redGreenBlue16x16Video = new Uint8Array(Base64.decode( + 'AAAAAWdCwBTZnpuAgICgAAADACAAAAZB4oVNAAAAAWjJYyyAAAABBgX//4HcRem95tlIt5Ys' + + '2CDZI+7veDI2NCAtIGNvcmUgMTY0IHIzMTA4IDMxZTE5ZjkgLSBILjI2NC9NUEVHLTQgQVZD' + + 'IGNvZGVjIC0gQ29weWxlZnQgMjAwMy0yMDIzIC0gaHR0cDovL3d3dy52aWRlb2xhbi5vcmcv' + + 'eDI2NC5odG1sIC0gb3B0aW9uczogY2FiYWM9MCByZWY9NSBkZWJsb2NrPTE6MDowIGFuYWx5' + + 'c2U9MHgxOjB4MTExIG1lPWhleCBzdWJtZT04IHBzeT0xIHBzeV9yZD0xLjAwOjAuMDAgbWl4' + + 'ZWRfcmVmPTEgbWVfcmFuZ2U9MTYgY2hyb21hX21lPTEgdHJlbGxpcz0yIDh4OGRjdD0wIGNx' + + 'bT0wIGRlYWR6b25lPTIxLDExIGZhc3RfcHNraXA9MSBjaHJvbWFfcXBfb2Zmc2V0PS0yIHRo' + + 'cmVhZHM9MSBsb29rYWhlYWRfdGhyZWFkcz0xIHNsaWNlZF90aHJlYWRzPTAgbnI9MCBkZWNp' + + 'bWF0ZT0xIGludGVybGFjZWQ9MCBibHVyYXlfY29tcGF0PTAgY29uc3RyYWluZWRfaW50cmE9' + + 'MCBiZnJhbWVzPTAgd2VpZ2h0cD0wIGtleWludD1pbmZpbml0ZSBrZXlpbnRfbWluPTI1IHNj' + + 'ZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NTAgcmM9YWJyIG1idHJl' + + 'ZT0xIGJpdHJhdGU9NDAwIHJhdGV0b2w9MS4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02' + + 'OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFxPTE6MS4wMACAAAABZYiEBrxmKAAPVccAAS04' + + '4AA5DRJMnkycJk4TPwAAAAFBiIga8RigADVVHAAGaGOAANtuAAAAAUGIkBr///wRRQABVf8c' + + 'AAcho4AAiD4=')); + +let _haveH264Decode = null; + +async function haveH264Decode() { + if (_haveH264Decode !== null) { + return _haveH264Decode; + } + + if (!('VideoDecoder' in window)) { + _haveH264Decode = false; + return false; + } + + // We'll need to make do with some placeholders here + const config = { + codec: 'avc1.42401f', + codedWidth: 1920, + codedHeight: 1080, + optimizeForLatency: true, + }; + + _haveH264Decode = await VideoDecoder.isConfigSupported(config); + return _haveH264Decode; +} + +function createSolidColorFrameBuffer(color, width, height) { + const r = (color >> 24) & 0xff; + const g = (color >> 16) & 0xff; + const b = (color >> 8) & 0xff; + const a = (color >> 0) & 0xff; + + const size = width * height * 4; + let array = new Uint8ClampedArray(size); + + for (let i = 0; i < size / 4; ++i) { + array[i * 4 + 0] = r; + array[i * 4 + 1] = g; + array[i * 4 + 2] = b; + array[i * 4 + 3] = a; + } + + return array; +} + +function makeMessageHeader(length, resetContext, resetAllContexts) { + let flags = 0; + if (resetContext) { + flags |= 1; + } + if (resetAllContexts) { + flags |= 2; + } + + let header = new Uint8Array(8); + let i = 0; + + let appendU32 = (v) => { + header[i++] = (v >> 24) & 0xff; + header[i++] = (v >> 16) & 0xff; + header[i++] = (v >> 8) & 0xff; + header[i++] = v & 0xff; + }; + + appendU32(length); + appendU32(flags); + + return header; +} + +function wrapRectData(data, resetContext, resetAllContexts) { + let header = makeMessageHeader(data.length, resetContext, resetAllContexts); + return Array.from(header).concat(Array.from(data)); +} + +function testDecodeRect(decoder, x, y, width, height, data, display, depth) { + let sock; + let done = false; + + sock = new Websock; + sock.open("ws://example.com"); + + sock.on('message', () => { + done = decoder.decodeRect(x, y, width, height, sock, display, depth); + }); + + // Empty messages are filtered at multiple layers, so we need to + // do a direct call + if (data.length === 0) { + done = decoder.decodeRect(x, y, width, height, sock, display, depth); + } else { + sock._websocket._receiveData(new Uint8Array(data)); + } + + display.flip(); + + return done; +} + +function almost(a, b) { + let diff = Math.abs(a - b); + return diff < 5; +} + +describe('H.264 Parser', function () { + it('should parse constrained baseline video', function () { + let parser = new H264Parser(redGreenBlue16x16Video); + + let frame = parser.parse(); + expect(frame).to.have.property('key', true); + + expect(parser).to.have.property('profileIdc', 66); + expect(parser).to.have.property('constraintSet', 192); + expect(parser).to.have.property('levelIdc', 20); + + frame = parser.parse(); + expect(frame).to.have.property('key', false); + + frame = parser.parse(); + expect(frame).to.have.property('key', false); + + frame = parser.parse(); + expect(frame).to.be.null; + }); +}); + +describe('H.264 Decoder Unit Test', function () { + let decoder; + + beforeEach(async function () { + if (!await haveH264Decode()) { + this.skip(); + return; + } + decoder = new H264Decoder(); + }); + + it('creates and resets context', function () { + let context = decoder._getContext(1, 2, 3, 4); + expect(context._width).to.equal(3); + expect(context._height).to.equal(4); + expect(decoder._contexts).to.not.be.empty; + decoder._resetContext(1, 2, 3, 4); + expect(decoder._contexts).to.be.empty; + }); + + it('resets all contexts', function () { + decoder._getContext(0, 0, 1, 1); + decoder._getContext(2, 2, 1, 1); + expect(decoder._contexts).to.not.be.empty; + decoder._resetAllContexts(); + expect(decoder._contexts).to.be.empty; + }); + + it('caches contexts', function () { + let c1 = decoder._getContext(1, 2, 3, 4); + c1.lastUsed = 1; + let c2 = decoder._getContext(1, 2, 3, 4); + c2.lastUsed = 2; + expect(Object.keys(decoder._contexts).length).to.equal(1); + expect(c1.lastUsed).to.equal(c2.lastUsed); + }); + + it('deletes oldest context', function () { + for (let i = 0; i < 65; ++i) { + let context = decoder._getContext(i, 0, 1, 1); + context.lastUsed = i; + } + + expect(decoder._findOldestContextId()).to.equal('1,0,1,1'); + expect(decoder._contexts[decoder._contextId(0, 0, 1, 1)]).to.be.undefined; + expect(decoder._contexts[decoder._contextId(1, 0, 1, 1)]).to.not.be.null; + expect(decoder._contexts[decoder._contextId(63, 0, 1, 1)]).to.not.be.null; + expect(decoder._contexts[decoder._contextId(64, 0, 1, 1)]).to.not.be.null; + }); +}); + +describe('H.264 Decoder Functional Test', function () { + let decoder; + let display; + + before(FakeWebSocket.replace); + after(FakeWebSocket.restore); + + beforeEach(async function () { + if (!await haveH264Decode()) { + this.skip(); + return; + } + decoder = new H264Decoder(); + display = new Display(document.createElement('canvas')); + display.resize(16, 16); + }); + + it('should handle H.264 rect', async function () { + let data = wrapRectData(redGreenBlue16x16Video, false, false); + let done = testDecodeRect(decoder, 0, 0, 16, 16, data, display, 24); + expect(done).to.be.true; + await display.flush(); + let targetData = createSolidColorFrameBuffer(0x0000ffff, 16, 16); + expect(display).to.have.displayed(targetData, almost); + }); + + it('should handle specific context reset', async function () { + let data = wrapRectData(redGreenBlue16x16Video, false, false); + let done = testDecodeRect(decoder, 0, 0, 16, 16, data, display, 24); + expect(done).to.be.true; + await display.flush(); + let targetData = createSolidColorFrameBuffer(0x0000ffff, 16, 16); + expect(display).to.have.displayed(targetData, almost); + + data = wrapRectData([], true, false); + done = testDecodeRect(decoder, 0, 0, 16, 16, data, display, 24); + expect(done).to.be.true; + await display.flush(); + + expect(decoder._contexts[decoder._contextId(0, 0, 16, 16)]._decoder).to.be.null; + }); + + it('should handle global context reset', async function () { + let data = wrapRectData(redGreenBlue16x16Video, false, false); + let done = testDecodeRect(decoder, 0, 0, 16, 16, data, display, 24); + expect(done).to.be.true; + await display.flush(); + let targetData = createSolidColorFrameBuffer(0x0000ffff, 16, 16); + expect(display).to.have.displayed(targetData, almost); + + data = wrapRectData([], false, true); + done = testDecodeRect(decoder, 0, 0, 16, 16, data, display, 24); + expect(done).to.be.true; + await display.flush(); + + expect(decoder._contexts[decoder._contextId(0, 0, 16, 16)]._decoder).to.be.null; + }); +}); From a4465516df31f71136a25a91df98530489a4be81 Mon Sep 17 00:00:00 2001 From: Tomasz Kalisiak Date: Fri, 23 Aug 2024 13:14:36 +0200 Subject: [PATCH 054/166] Fix sQpushBytes sending the beginning of the array multiple times --- core/websock.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/websock.js b/core/websock.js index 21327c31a..61a3091a3 100644 --- a/core/websock.js +++ b/core/websock.js @@ -208,7 +208,7 @@ export default class Websock { chunkSize = bytes.length - offset; } - this._sQ.set(bytes.subarray(offset, chunkSize), this._sQlen); + this._sQ.set(bytes.subarray(offset, offset + chunkSize), this._sQlen); this._sQlen += chunkSize; offset += chunkSize; } From ffb4c0bf563a09903b0bca5ccc5483bed1522aba Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 29 Aug 2024 16:51:16 +0200 Subject: [PATCH 055/166] Let fake WebSocket handle large sends Dynamically grow the recorded send buffer if the test needs to send a lot of data. --- tests/fake.websocket.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/fake.websocket.js b/tests/fake.websocket.js index d273fe07a..f1b307136 100644 --- a/tests/fake.websocket.js +++ b/tests/fake.websocket.js @@ -37,6 +37,15 @@ export default class FakeWebSocket { } else { data = new Uint8Array(data); } + if (this.bufferedAmount + data.length > this._sendQueue.length) { + let newlen = this._sendQueue.length; + while (this.bufferedAmount + data.length > newlen) { + newlen *= 2; + } + let newbuf = new Uint8Array(newlen); + newbuf.set(this._sendQueue); + this._sendQueue = newbuf; + } this._sendQueue.set(data, this.bufferedAmount); this.bufferedAmount += data.length; } From 50e4685bfff9c52a9de878bc095d4bfe7e4e39c2 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 29 Aug 2024 16:51:51 +0200 Subject: [PATCH 056/166] Fix tests for large WebSocket sends These failed to test that the data was correctly split as they only checked the first chunk transmitted. Use random values to avoid the risk of aligning our test data with the split boundaries and hence allowing false positives. --- tests/test.websock.js | 38 ++++++++++++-------------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/tests/test.websock.js b/tests/test.websock.js index 53145b360..ab09f4508 100644 --- a/tests/test.websock.js +++ b/tests/test.websock.js @@ -261,20 +261,15 @@ describe('Websock', function () { }); it('should implicitly split a large buffer', function () { let str = ''; - for (let i = 0;i <= bufferSize/5;i++) { - str += '\x12\x34\x56\x78\x90'; + let expected = []; + for (let i = 0;i < bufferSize * 3;i++) { + let byte = Math.random() * 0xff; + str += String.fromCharCode(byte); + expected.push(byte); } sock.sQpushString(str); - - let expected = []; - for (let i = 0;i < bufferSize/5;i++) { - expected.push(0x12); - expected.push(0x34); - expected.push(0x56); - expected.push(0x78); - expected.push(0x90); - } + sock.flush(); expect(sock).to.have.sent(new Uint8Array(expected)); }); @@ -308,24 +303,15 @@ describe('Websock', function () { }); it('should implicitly split a large buffer', function () { let buffer = []; - for (let i = 0;i <= bufferSize/5;i++) { - buffer.push(0x12); - buffer.push(0x34); - buffer.push(0x56); - buffer.push(0x78); - buffer.push(0x90); + let expected = []; + for (let i = 0;i < bufferSize * 3;i++) { + let byte = Math.random() * 0xff; + buffer.push(byte); + expected.push(byte); } sock.sQpushBytes(new Uint8Array(buffer)); - - let expected = []; - for (let i = 0;i < bufferSize/5;i++) { - expected.push(0x12); - expected.push(0x34); - expected.push(0x56); - expected.push(0x78); - expected.push(0x90); - } + sock.flush(); expect(sock).to.have.sent(new Uint8Array(expected)); }); From 438e5b360826b435036ee981f2588526ac08fcaa Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 26 Jun 2020 16:00:32 +0200 Subject: [PATCH 057/166] Make it easier for downstream to modify settings Expose a simple and stable API to override default settings, and force settings that users shouldn't be able to change. --- app/ui.js | 43 ++++++++++++++++++++++++++++++++----------- vnc.html | 22 +++++++++++++++++++++- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/app/ui.js b/app/ui.js index 316042911..49c6c2579 100644 --- a/app/ui.js +++ b/app/ui.js @@ -24,6 +24,8 @@ const LINGUAS = ["cs", "de", "el", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt const UI = { + customSettings: {}, + connected: false, desktopName: "", @@ -44,7 +46,15 @@ const UI = { reconnectCallback: null, reconnectPassword: null, - async start() { + async start(options={}) { + UI.customSettings = options.settings || {}; + if (UI.customSettings.defaults === undefined) { + UI.customSettings.defaults = {}; + } + if (UI.customSettings.mandatory === undefined) { + UI.customSettings.mandatory = {}; + } + // Set up translations try { await l10n.setup(LINGUAS, "app/locale/"); @@ -159,6 +169,8 @@ const UI = { UI.initSetting('logging', 'warn'); UI.updateLogging(); + UI.setupSettingLabels(); + /* Populate the controls if defaults are provided in the URL */ UI.initSetting('encrypt', (window.location.protocol === "https:")); UI.initSetting('password'); @@ -175,8 +187,6 @@ const UI = { UI.initSetting('repeaterID', ''); UI.initSetting('reconnect', false); UI.initSetting('reconnect_delay', 5000); - - UI.setupSettingLabels(); }, // Adds a link to the label elements on the corresponding input elements setupSettingLabels() { @@ -738,6 +748,10 @@ const UI = { // Initial page load read/initialization of settings initSetting(name, defVal) { + // Has the user overridden the default value? + if (name in UI.customSettings.defaults) { + defVal = UI.customSettings.defaults[name]; + } // Check Query string followed by cookie let val = WebUtil.getConfigVar(name); if (val === null) { @@ -745,6 +759,11 @@ const UI = { } WebUtil.setSetting(name, val); UI.updateSetting(name); + // Has the user forced a value? + if (name in UI.customSettings.mandatory) { + val = UI.customSettings.mandatory[name]; + UI.forceSetting(name, val); + } return val; }, @@ -817,17 +836,21 @@ const UI = { // disable the labels that belong to disabled input elements. disableSetting(name) { const ctrl = document.getElementById('noVNC_setting_' + name); - ctrl.disabled = true; - if (ctrl.label !== undefined) { - ctrl.label.classList.add('noVNC_disabled'); + if (ctrl !== null) { + ctrl.disabled = true; + if (ctrl.label !== undefined) { + ctrl.label.classList.add('noVNC_disabled'); + } } }, enableSetting(name) { const ctrl = document.getElementById('noVNC_setting_' + name); - ctrl.disabled = false; - if (ctrl.label !== undefined) { - ctrl.label.classList.remove('noVNC_disabled'); + if (ctrl !== null) { + ctrl.disabled = false; + if (ctrl.label !== undefined) { + ctrl.label.classList.remove('noVNC_disabled'); + } } }, @@ -1771,6 +1794,4 @@ const UI = { */ }; -UI.start(); - export default UI; diff --git a/vnc.html b/vnc.html index 89ee11e36..d6a7e56aa 100644 --- a/vnc.html +++ b/vnc.html @@ -46,7 +46,27 @@ - + + From 28d4020302e155fb693ee297d6aa51366ba71518 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 8 Aug 2024 16:20:51 +0200 Subject: [PATCH 058/166] Load settings from web server Make it even easier to customize things by loading the settings from separate configuration files. --- defaults.json | 1 + mandatory.json | 1 + vnc.html | 31 ++++++++++++++++++++++++++++++- 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 defaults.json create mode 100644 mandatory.json diff --git a/defaults.json b/defaults.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/defaults.json @@ -0,0 +1 @@ +{} diff --git a/mandatory.json b/mandatory.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/mandatory.json @@ -0,0 +1 @@ +{} diff --git a/vnc.html b/vnc.html index d6a7e56aa..fd8877304 100644 --- a/vnc.html +++ b/vnc.html @@ -49,11 +49,40 @@ diff --git a/utils/genkeysymdef.js b/utils/genkeysymdef.js index f539a0b28..b10240ec4 100755 --- a/utils/genkeysymdef.js +++ b/utils/genkeysymdef.js @@ -1,7 +1,7 @@ #!/usr/bin/env node /* * genkeysymdef: X11 keysymdef.h to JavaScript converter - * Copyright (C) 2018 The noVNC Authors + * Copyright (C) 2018 The noVNC authors * Licensed under MPL 2.0 (see LICENSE.txt) */ diff --git a/utils/novnc_proxy b/utils/novnc_proxy index 4b2e3032d..6b55504a5 100755 --- a/utils/novnc_proxy +++ b/utils/novnc_proxy @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Copyright (C) 2018 The noVNC Authors +# Copyright (C) 2018 The noVNC authors # Licensed under MPL 2.0 or any later version (see LICENSE.txt) usage() { @@ -82,7 +82,7 @@ cleanup() { fi } -# Process Arguments +# Process arguments # Arguments that only apply to chrooter itself while [ "$*" ]; do diff --git a/vnc.html b/vnc.html index fd8877304..c2cc4e555 100644 --- a/vnc.html +++ b/vnc.html @@ -4,7 +4,7 @@ +
@@ -123,18 +123,18 @@

no
VNC

+ title="Move/Drag viewport"> - +
+ id="noVNC_keyboard_button" class="noVNC_button" title="Show keyboard">
+ title="Show extra keys">
no
VNC
- + title="Full screen"> no
VNC
  • - +
  • - +

  • - +
  • - +

  • @@ -261,15 +261,15 @@

    no
    VNC


  • - +
  • - +

  • - +

  • @@ -290,7 +290,7 @@

    no
    VNC

- + @@ -305,7 +305,7 @@

no
VNC

- +
@@ -320,7 +320,7 @@

no
VNC

- +
@@ -344,7 +344,7 @@

no
VNC

- +
@@ -359,12 +359,12 @@

no
VNC

- +
- +
diff --git a/vnc_lite.html b/vnc_lite.html index eaf75f869..79d481460 100644 --- a/vnc_lite.html +++ b/vnc_lite.html @@ -7,7 +7,7 @@ This is a self-contained file which doesn't import WebUtil or external CSS. - Copyright (C) 2019 The noVNC Authors + Copyright (C) 2019 The noVNC authors noVNC is licensed under the MPL 2.0 (see LICENSE.txt) This file is licensed under the 2-Clause BSD license (see LICENSE.txt). @@ -80,7 +80,7 @@ // When this function is called, the server requires // credentials to authenticate function credentialsAreRequired(e) { - const password = prompt("Password Required:"); + const password = prompt("Password required:"); rfb.sendCredentials({ password: password }); } From 7335bb440d1fc3eb527cb5e6851aa21c3a3293e3 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Wed, 27 Nov 2024 14:57:55 +0100 Subject: [PATCH 068/166] Also adjust to "sentence case" in translations This would have resolved itself automatically on the next translation update, but this commit will reduce unnecessary noise in that change. --- app/locale/cs.json | 24 ++++++++++++------------ app/locale/de.json | 22 +++++++++++----------- app/locale/es.json | 16 ++++++++-------- app/locale/fr.json | 24 ++++++++++++------------ app/locale/it.json | 6 +++--- app/locale/ja.json | 28 ++++++++++++++-------------- app/locale/ko.json | 22 +++++++++++----------- app/locale/nl.json | 6 +++--- app/locale/pl.json | 12 ++++++------ app/locale/pt_BR.json | 26 +++++++++++++------------- app/locale/ru.json | 20 ++++++++++---------- app/locale/sv.json | 8 ++++---- app/locale/tr.json | 2 +- app/locale/zh_CN.json | 22 +++++++++++----------- app/locale/zh_TW.json | 22 +++++++++++----------- po/cs.po | 26 +++++++++++++------------- po/de.po | 24 ++++++++++++------------ po/el.po | 2 +- po/es.po | 18 +++++++++--------- po/fr.po | 26 +++++++++++++------------- po/it.po | 12 ++++++------ po/ja.po | 30 +++++++++++++++--------------- po/ko.po | 24 ++++++++++++------------ po/nl.po | 8 ++++---- po/noVNC.pot | 30 +++++++++++++++--------------- po/pl.po | 14 +++++++------- po/pt_BR.po | 28 ++++++++++++++-------------- po/ru.po | 20 ++++++++++---------- po/sv.po | 10 +++++----- po/tr.po | 4 ++-- po/zh_CN.po | 24 ++++++++++++------------ po/zh_TW.po | 24 ++++++++++++------------ 32 files changed, 292 insertions(+), 292 deletions(-) diff --git a/app/locale/cs.json b/app/locale/cs.json index 589145ef3..dd31e6c18 100644 --- a/app/locale/cs.json +++ b/app/locale/cs.json @@ -14,7 +14,7 @@ "Password is required": "Je vyžadováno heslo", "noVNC encountered an error:": "noVNC narazilo na chybu:", "Hide/Show the control bar": "Skrýt/zobrazit ovládací panel", - "Move/Drag Viewport": "Přesunout/přetáhnout výřez", + "Move/Drag viewport": "Přesunout/přetáhnout výřez", "viewport drag": "přesun výřezu", "Active Mouse Button": "Aktivní tlačítka myši", "No mousebutton": "Žádné", @@ -22,9 +22,9 @@ "Middle mousebutton": "Prostřední tlačítko myši", "Right mousebutton": "Pravé tlačítko myši", "Keyboard": "Klávesnice", - "Show Keyboard": "Zobrazit klávesnici", + "Show keyboard": "Zobrazit klávesnici", "Extra keys": "Extra klávesy", - "Show Extra Keys": "Zobrazit extra klávesy", + "Show extra keys": "Zobrazit extra klávesy", "Ctrl": "Ctrl", "Toggle Ctrl": "Přepnout Ctrl", "Alt": "Alt", @@ -45,13 +45,13 @@ "Clear": "Vymazat", "Fullscreen": "Celá obrazovka", "Settings": "Nastavení", - "Shared Mode": "Sdílený režim", - "View Only": "Pouze prohlížení", - "Clip to Window": "Přizpůsobit oknu", - "Scaling Mode:": "Přizpůsobení velikosti", + "Shared mode": "Sdílený režim", + "View only": "Pouze prohlížení", + "Clip to window": "Přizpůsobit oknu", + "Scaling mode:": "Přizpůsobení velikosti", "None": "Žádné", - "Local Scaling": "Místní", - "Remote Resizing": "Vzdálené", + "Local scaling": "Místní", + "Remote resizing": "Vzdálené", "Advanced": "Pokročilé", "Repeater ID:": "ID opakovače", "WebSocket": "WebSocket", @@ -59,9 +59,9 @@ "Host:": "Hostitel:", "Port:": "Port:", "Path:": "Cesta", - "Automatic Reconnect": "Automatická obnova připojení", - "Reconnect Delay (ms):": "Zpoždění připojení (ms)", - "Show Dot when No Cursor": "Tečka místo chybějícího kurzoru myši", + "Automatic reconnect": "Automatická obnova připojení", + "Reconnect delay (ms):": "Zpoždění připojení (ms)", + "Show dot when no cursor": "Tečka místo chybějícího kurzoru myši", "Logging:": "Logování:", "Disconnect": "Odpojit", "Connect": "Připojit", diff --git a/app/locale/de.json b/app/locale/de.json index 62e73360f..e92825a8b 100644 --- a/app/locale/de.json +++ b/app/locale/de.json @@ -13,7 +13,7 @@ "Password is required": "Passwort ist erforderlich", "noVNC encountered an error:": "Ein Fehler ist aufgetreten:", "Hide/Show the control bar": "Kontrollleiste verstecken/anzeigen", - "Move/Drag Viewport": "Ansichtsfenster verschieben/ziehen", + "Move/Drag viewport": "Ansichtsfenster verschieben/ziehen", "viewport drag": "Ansichtsfenster ziehen", "Active Mouse Button": "Aktive Maustaste", "No mousebutton": "Keine Maustaste", @@ -21,9 +21,9 @@ "Middle mousebutton": "Mittlere Maustaste", "Right mousebutton": "Rechte Maustaste", "Keyboard": "Tastatur", - "Show Keyboard": "Tastatur anzeigen", + "Show keyboard": "Tastatur anzeigen", "Extra keys": "Zusatztasten", - "Show Extra Keys": "Zusatztasten anzeigen", + "Show extra keys": "Zusatztasten anzeigen", "Ctrl": "Strg", "Toggle Ctrl": "Strg umschalten", "Alt": "Alt", @@ -44,13 +44,13 @@ "Clear": "Löschen", "Fullscreen": "Vollbild", "Settings": "Einstellungen", - "Shared Mode": "Geteilter Modus", - "View Only": "Nur betrachten", - "Clip to Window": "Auf Fenster begrenzen", - "Scaling Mode:": "Skalierungsmodus:", + "Shared mode": "Geteilter Modus", + "View only": "Nur betrachten", + "Clip to window": "Auf Fenster begrenzen", + "Scaling mode:": "Skalierungsmodus:", "None": "Keiner", - "Local Scaling": "Lokales skalieren", - "Remote Resizing": "Serverseitiges skalieren", + "Local scaling": "Lokales skalieren", + "Remote resizing": "Serverseitiges skalieren", "Advanced": "Erweitert", "Repeater ID:": "Repeater ID:", "WebSocket": "WebSocket", @@ -58,8 +58,8 @@ "Host:": "Server:", "Port:": "Port:", "Path:": "Pfad:", - "Automatic Reconnect": "Automatisch wiederverbinden", - "Reconnect Delay (ms):": "Wiederverbindungsverzögerung (ms):", + "Automatic reconnect": "Automatisch wiederverbinden", + "Reconnect delay (ms):": "Wiederverbindungsverzögerung (ms):", "Logging:": "Protokollierung:", "Disconnect": "Verbindung trennen", "Connect": "Verbinden", diff --git a/app/locale/es.json b/app/locale/es.json index b9e663a3d..bb088243c 100644 --- a/app/locale/es.json +++ b/app/locale/es.json @@ -10,7 +10,7 @@ "Disconnect timeout": "Tiempo de desconexión agotado", "noVNC encountered an error:": "noVNC ha encontrado un error:", "Hide/Show the control bar": "Ocultar/Mostrar la barra de control", - "Move/Drag Viewport": "Mover/Arrastrar la ventana", + "Move/Drag viewport": "Mover/Arrastrar la ventana", "viewport drag": "Arrastrar la ventana", "Active Mouse Button": "Botón activo del ratón", "No mousebutton": "Ningún botón del ratón", @@ -18,7 +18,7 @@ "Middle mousebutton": "Botón central del ratón", "Right mousebutton": "Botón derecho del ratón", "Keyboard": "Teclado", - "Show Keyboard": "Mostrar teclado", + "Show keyboard": "Mostrar teclado", "Extra keys": "Teclas adicionales", "Show Extra Keys": "Mostrar Teclas Adicionales", "Ctrl": "Ctrl", @@ -43,13 +43,13 @@ "Settings": "Configuraciones", "Encrypt": "Encriptar", "Shared Mode": "Modo Compartido", - "View Only": "Solo visualización", - "Clip to Window": "Recortar al tamaño de la ventana", - "Scaling Mode:": "Modo de escalado:", + "View only": "Solo visualización", + "Clip to window": "Recortar al tamaño de la ventana", + "Scaling mode:": "Modo de escalado:", "None": "Ninguno", "Local Scaling": "Escalado Local", "Local Downscaling": "Reducción de escala local", - "Remote Resizing": "Cambio de tamaño remoto", + "Remote resizing": "Cambio de tamaño remoto", "Advanced": "Avanzado", "Local Cursor": "Cursor Local", "Repeater ID:": "ID del Repetidor:", @@ -57,8 +57,8 @@ "Host:": "Host:", "Port:": "Puerto:", "Path:": "Ruta:", - "Automatic Reconnect": "Reconexión automática", - "Reconnect Delay (ms):": "Retraso en la reconexión (ms):", + "Automatic reconnect": "Reconexión automática", + "Reconnect delay (ms):": "Retraso en la reconexión (ms):", "Logging:": "Registrando:", "Disconnect": "Desconectar", "Connect": "Conectar", diff --git a/app/locale/fr.json b/app/locale/fr.json index c0eeec7d3..be9a4f422 100644 --- a/app/locale/fr.json +++ b/app/locale/fr.json @@ -17,9 +17,9 @@ "Drag": "Faire glisser", "Move/Drag Viewport": "Déplacer/faire glisser le Viewport", "Keyboard": "Clavier", - "Show Keyboard": "Afficher le clavier", + "Show keyboard": "Afficher le clavier", "Extra keys": "Touches supplémentaires", - "Show Extra Keys": "Afficher les touches supplémentaires", + "Show extra keys": "Afficher les touches supplémentaires", "Ctrl": "Ctrl", "Toggle Ctrl": "Basculer Ctrl", "Alt": "Alt", @@ -42,13 +42,13 @@ "Clear": "Effacer", "Fullscreen": "Plein écran", "Settings": "Paramètres", - "Shared Mode": "Mode partagé", - "View Only": "Afficher uniquement", - "Clip to Window": "Clip à fenêtre", - "Scaling Mode:": "Mode mise à l'échelle :", + "Shared mode": "Mode partagé", + "View only": "Afficher uniquement", + "Clip to window": "Clip à fenêtre", + "Scaling mode:": "Mode mise à l'échelle :", "None": "Aucun", - "Local Scaling": "Mise à l'échelle locale", - "Remote Resizing": "Redimensionnement à distance", + "Local scaling": "Mise à l'échelle locale", + "Remote resizing": "Redimensionnement à distance", "Advanced": "Avancé", "Quality:": "Qualité :", "Compression level:": "Niveau de compression :", @@ -58,15 +58,15 @@ "Host:": "Hôte :", "Port:": "Port :", "Path:": "Chemin :", - "Automatic Reconnect": "Reconnecter automatiquemen", - "Reconnect Delay (ms):": "Délai de reconnexion (ms) :", - "Show Dot when No Cursor": "Afficher le point lorsqu'il n'y a pas de curseur", + "Automatic reconnect": "Reconnecter automatiquemen", + "Reconnect delay (ms):": "Délai de reconnexion (ms) :", + "Show dot when no cursor": "Afficher le point lorsqu'il n'y a pas de curseur", "Logging:": "Se connecter :", "Version:": "Version :", "Disconnect": "Déconnecter", "Connect": "Connecter", "Username:": "Nom d'utilisateur :", "Password:": "Mot de passe :", - "Send Credentials": "Envoyer les identifiants", + "Send credentials": "Envoyer les identifiants", "Cancel": "Annuler" } \ No newline at end of file diff --git a/app/locale/it.json b/app/locale/it.json index 18a7f7447..28e6f721b 100644 --- a/app/locale/it.json +++ b/app/locale/it.json @@ -15,7 +15,7 @@ "noVNC encountered an error:": "noVNC ha riscontrato un errore:", "Hide/Show the control bar": "Nascondi/Mostra la barra di controllo", "Keyboard": "Tastiera", - "Show Keyboard": "Mostra tastiera", + "Show keyboard": "Mostra tastiera", "Extra keys": "Tasti Aggiuntivi", "Show Extra Keys": "Mostra Tasti Aggiuntivi", "Ctrl": "Ctrl", @@ -40,9 +40,9 @@ "Clear": "Pulisci", "Fullscreen": "Schermo intero", "Settings": "Impostazioni", - "Shared Mode": "Modalità condivisa", + "Shared mode": "Modalità condivisa", "View Only": "Sola Visualizzazione", - "Scaling Mode:": "Modalità di ridimensionamento:", + "Scaling mode:": "Modalità di ridimensionamento:", "None": "Nessuna", "Local Scaling": "Ridimensionamento Locale", "Remote Resizing": "Ridimensionamento Remoto", diff --git a/app/locale/ja.json b/app/locale/ja.json index 70fd7a5d1..078adcc85 100644 --- a/app/locale/ja.json +++ b/app/locale/ja.json @@ -16,11 +16,11 @@ "noVNC encountered an error:": "noVNC でエラーが発生しました:", "Hide/Show the control bar": "コントロールバーを隠す/表示する", "Drag": "ドラッグ", - "Move/Drag Viewport": "ビューポートを移動/ドラッグ", + "Move/Drag viewport": "ビューポートを移動/ドラッグ", "Keyboard": "キーボード", - "Show Keyboard": "キーボードを表示", + "Show keyboard": "キーボードを表示", "Extra keys": "追加キー", - "Show Extra Keys": "追加キーを表示", + "Show extra keys": "追加キーを表示", "Ctrl": "Ctrl", "Toggle Ctrl": "Ctrl キーをトグル", "Alt": "Alt", @@ -41,15 +41,15 @@ "Reset": "リセット", "Clipboard": "クリップボード", "Edit clipboard content in the textarea below.": "以下の入力欄からクリップボードの内容を編集できます。", - "Full Screen": "全画面表示", + "Full screen": "全画面表示", "Settings": "設定", - "Shared Mode": "共有モード", - "View Only": "表示専用", - "Clip to Window": "ウィンドウにクリップ", - "Scaling Mode:": "スケーリングモード:", + "Shared mode": "共有モード", + "View only": "表示専用", + "Clip to window": "ウィンドウにクリップ", + "Scaling mode:": "スケーリングモード:", "None": "なし", - "Local Scaling": "ローカルスケーリング", - "Remote Resizing": "リモートでリサイズ", + "Local scaling": "ローカルスケーリング", + "Remote resizing": "リモートでリサイズ", "Advanced": "高度", "Quality:": "品質:", "Compression level:": "圧縮レベル:", @@ -59,9 +59,9 @@ "Host:": "ホスト:", "Port:": "ポート:", "Path:": "パス:", - "Automatic Reconnect": "自動再接続", - "Reconnect Delay (ms):": "再接続する遅延 (ミリ秒):", - "Show Dot when No Cursor": "カーソルがないときにドットを表示する", + "Automatic reconnect": "自動再接続", + "Reconnect delay (ms):": "再接続する遅延 (ミリ秒):", + "Show dot when no cursor": "カーソルがないときにドットを表示する", "Logging:": "ロギング:", "Version:": "バージョン:", "Disconnect": "切断", @@ -75,6 +75,6 @@ "Credentials": "資格情報", "Username:": "ユーザー名:", "Password:": "パスワード:", - "Send Credentials": "資格情報を送信", + "Send credentials": "資格情報を送信", "Cancel": "キャンセル" } \ No newline at end of file diff --git a/app/locale/ko.json b/app/locale/ko.json index e4ecddcfd..47b0805c9 100644 --- a/app/locale/ko.json +++ b/app/locale/ko.json @@ -14,7 +14,7 @@ "Password is required": "비밀번호가 필요합니다.", "noVNC encountered an error:": "noVNC에 오류가 발생했습니다:", "Hide/Show the control bar": "컨트롤 바 숨기기/보이기", - "Move/Drag Viewport": "움직이기/드래그 뷰포트", + "Move/Drag viewport": "움직이기/드래그 뷰포트", "viewport drag": "뷰포트 드래그", "Active Mouse Button": "마우스 버튼 활성화", "No mousebutton": "마우스 버튼 없음", @@ -22,9 +22,9 @@ "Middle mousebutton": "중간 마우스 버튼", "Right mousebutton": "오른쪽 마우스 버튼", "Keyboard": "키보드", - "Show Keyboard": "키보드 보이기", + "Show keyboard": "키보드 보이기", "Extra keys": "기타 키들", - "Show Extra Keys": "기타 키들 보이기", + "Show extra keys": "기타 키들 보이기", "Ctrl": "Ctrl", "Toggle Ctrl": "Ctrl 켜기/끄기", "Alt": "Alt", @@ -45,13 +45,13 @@ "Clear": "지우기", "Fullscreen": "전체화면", "Settings": "설정", - "Shared Mode": "공유 모드", - "View Only": "보기 전용", - "Clip to Window": "창에 클립", - "Scaling Mode:": "스케일링 모드:", + "Shared mode": "공유 모드", + "View only": "보기 전용", + "Clip to window": "창에 클립", + "Scaling mode:": "스케일링 모드:", "None": "없음", - "Local Scaling": "로컬 스케일링", - "Remote Resizing": "원격 크기 조절", + "Local scaling": "로컬 스케일링", + "Remote resizing": "원격 크기 조절", "Advanced": "고급", "Repeater ID:": "중계 ID", "WebSocket": "웹소켓", @@ -59,8 +59,8 @@ "Host:": "호스트:", "Port:": "포트:", "Path:": "위치:", - "Automatic Reconnect": "자동 재연결", - "Reconnect Delay (ms):": "재연결 지연 시간 (ms)", + "Automatic reconnect": "자동 재연결", + "Reconnect delay (ms):": "재연결 지연 시간 (ms)", "Logging:": "로깅", "Disconnect": "연결 해제", "Connect": "연결", diff --git a/app/locale/nl.json b/app/locale/nl.json index 0cdcc92a9..ea6335bd8 100644 --- a/app/locale/nl.json +++ b/app/locale/nl.json @@ -49,8 +49,8 @@ "Settings": "Instellingen", "Shared Mode": "Gedeelde Modus", "View Only": "Alleen Kijken", - "Clip to Window": "Randen buiten venster afsnijden", - "Scaling Mode:": "Schaalmodus:", + "Clip to window": "Randen buiten venster afsnijden", + "Scaling mode:": "Schaalmodus:", "None": "Geen", "Local Scaling": "Lokaal Schalen", "Remote Resizing": "Op Afstand Formaat Wijzigen", @@ -63,7 +63,7 @@ "Path:": "Pad:", "Automatic Reconnect": "Automatisch Opnieuw Verbinden", "Reconnect Delay (ms):": "Vertraging voor Opnieuw Verbinden (ms):", - "Show Dot when No Cursor": "Geef stip weer indien geen cursor", + "Show dot when no cursor": "Geef stip weer indien geen cursor", "Logging:": "Logmeldingen:", "Disconnect": "Verbinding verbreken", "Connect": "Verbinden", diff --git a/app/locale/pl.json b/app/locale/pl.json index 006ac7a55..865f90ca1 100644 --- a/app/locale/pl.json +++ b/app/locale/pl.json @@ -21,9 +21,9 @@ "Middle mousebutton": "Środkowy przycisk myszy", "Right mousebutton": "Prawy przycisk myszy", "Keyboard": "Klawiatura", - "Show Keyboard": "Pokaż klawiaturę", + "Show keyboard": "Pokaż klawiaturę", "Extra keys": "Przyciski dodatkowe", - "Show Extra Keys": "Pokaż przyciski dodatkowe", + "Show extra keys": "Pokaż przyciski dodatkowe", "Ctrl": "Ctrl", "Toggle Ctrl": "Przełącz Ctrl", "Alt": "Alt", @@ -49,8 +49,8 @@ "Clip to Window": "Przytnij do Okna", "Scaling Mode:": "Tryb Skalowania:", "None": "Brak", - "Local Scaling": "Skalowanie lokalne", - "Remote Resizing": "Skalowanie zdalne", + "Local scaling": "Skalowanie lokalne", + "Remote resizing": "Skalowanie zdalne", "Advanced": "Zaawansowane", "Repeater ID:": "ID Repeatera:", "WebSocket": "WebSocket", @@ -58,8 +58,8 @@ "Host:": "Host:", "Port:": "Port:", "Path:": "Ścieżka:", - "Automatic Reconnect": "Automatycznie wznawiaj połączenie", - "Reconnect Delay (ms):": "Opóźnienie wznawiania (ms):", + "Automatic reconnect": "Automatycznie wznawiaj połączenie", + "Reconnect delay (ms):": "Opóźnienie wznawiania (ms):", "Logging:": "Poziom logowania:", "Disconnect": "Rozłącz", "Connect": "Połącz", diff --git a/app/locale/pt_BR.json b/app/locale/pt_BR.json index aa130f764..224b86efd 100644 --- a/app/locale/pt_BR.json +++ b/app/locale/pt_BR.json @@ -15,11 +15,11 @@ "noVNC encountered an error:": "O noVNC encontrou um erro:", "Hide/Show the control bar": "Esconder/mostrar a barra de controles", "Drag": "Arrastar", - "Move/Drag Viewport": "Mover/arrastar a janela", + "Move/Drag viewport": "Mover/arrastar a janela", "Keyboard": "Teclado", - "Show Keyboard": "Mostrar teclado", + "Show keyboard": "Mostrar teclado", "Extra keys": "Teclas adicionais", - "Show Extra Keys": "Mostar teclas adicionais", + "Show extra keys": "Mostar teclas adicionais", "Ctrl": "Ctrl", "Toggle Ctrl": "Pressionar/soltar Ctrl", "Alt": "Alt", @@ -42,13 +42,13 @@ "Clear": "Limpar", "Fullscreen": "Tela cheia", "Settings": "Configurações", - "Shared Mode": "Modo compartilhado", - "View Only": "Apenas visualizar", - "Clip to Window": "Recortar à janela", - "Scaling Mode:": "Modo de dimensionamento:", + "Shared mode": "Modo compartilhado", + "View only": "Apenas visualizar", + "Clip to window": "Recortar à janela", + "Scaling mode:": "Modo de dimensionamento:", "None": "Nenhum", - "Local Scaling": "Local", - "Remote Resizing": "Remoto", + "Local scaling": "Local", + "Remote resizing": "Remoto", "Advanced": "Avançado", "Quality:": "Qualidade:", "Compression level:": "Nível de compressão:", @@ -58,15 +58,15 @@ "Host:": "Host:", "Port:": "Porta:", "Path:": "Caminho:", - "Automatic Reconnect": "Reconexão automática", - "Reconnect Delay (ms):": "Atraso da reconexão (ms)", - "Show Dot when No Cursor": "Mostrar ponto quando não há cursor", + "Automatic reconnect": "Reconexão automática", + "Reconnect delay (ms):": "Atraso da reconexão (ms)", + "Show dot when no cursor": "Mostrar ponto quando não há cursor", "Logging:": "Registros:", "Version:": "Versão:", "Disconnect": "Desconectar", "Connect": "Conectar", "Username:": "Nome de usuário:", "Password:": "Senha:", - "Send Credentials": "Enviar credenciais", + "Send credentials": "Enviar credenciais", "Cancel": "Cancelar" } \ No newline at end of file diff --git a/app/locale/ru.json b/app/locale/ru.json index 5a1a1b9ba..bd1bb534a 100644 --- a/app/locale/ru.json +++ b/app/locale/ru.json @@ -15,9 +15,9 @@ "noVNC encountered an error:": "Ошибка noVNC: ", "Hide/Show the control bar": "Скрыть/Показать контрольную панель", "Drag": "Переместить", - "Move/Drag Viewport": "Переместить окно", + "Move/Drag viewport": "Переместить окно", "Keyboard": "Клавиатура", - "Show Keyboard": "Показать клавиатуру", + "Show keyboard": "Показать клавиатуру", "Extra keys": "Дополнительные Кнопки", "Show Extra Keys": "Показать Дополнительные Кнопки", "Ctrl": "Ctrl", @@ -42,13 +42,13 @@ "Clear": "Очистить", "Fullscreen": "Во весь экран", "Settings": "Настройки", - "Shared Mode": "Общий режим", + "Shared mode": "Общий режим", "View Only": "Только Просмотр", - "Clip to Window": "В окно", - "Scaling Mode:": "Масштаб:", + "Clip to window": "В окно", + "Scaling mode:": "Масштаб:", "None": "Нет", - "Local Scaling": "Локальный масштаб", - "Remote Resizing": "Удаленная перенастройка размера", + "Local scaling": "Локальный масштаб", + "Remote resizing": "Удаленная перенастройка размера", "Advanced": "Дополнительно", "Quality:": "Качество", "Compression level:": "Уровень Сжатия", @@ -58,9 +58,9 @@ "Host:": "Сервер:", "Port:": "Порт:", "Path:": "Путь:", - "Automatic Reconnect": "Автоматическое переподключение", - "Reconnect Delay (ms):": "Задержка переподключения (мс):", - "Show Dot when No Cursor": "Показать точку вместо курсора", + "Automatic reconnect": "Автоматическое переподключение", + "Reconnect delay (ms):": "Задержка переподключения (мс):", + "Show dot when no cursor": "Показать точку вместо курсора", "Logging:": "Лог:", "Version:": "Версия", "Disconnect": "Отключение", diff --git a/app/locale/sv.json b/app/locale/sv.json index 80a400bfa..83385cd24 100644 --- a/app/locale/sv.json +++ b/app/locale/sv.json @@ -42,12 +42,12 @@ "Reset": "Återställ", "Clipboard": "Urklipp", "Edit clipboard content in the textarea below.": "Redigera urklippets innehåll i fältet nedan.", - "Full Screen": "Fullskärm", + "Full screen": "Fullskärm", "Settings": "Inställningar", "Shared Mode": "Delat Läge", "View Only": "Endast Visning", "Clip to Window": "Begränsa till Fönster", - "Scaling Mode:": "Skalningsläge:", + "Scaling mode:": "Skalningsläge:", "None": "Ingen", "Local Scaling": "Lokal Skalning", "Remote Resizing": "Ändra Storlek", @@ -61,8 +61,8 @@ "Port:": "Port:", "Path:": "Sökväg:", "Automatic Reconnect": "Automatisk Återanslutning", - "Reconnect Delay (ms):": "Fördröjning (ms):", - "Show Dot when No Cursor": "Visa prick när ingen muspekare finns", + "Reconnect delay (ms):": "Fördröjning (ms):", + "Show dot when no cursor": "Visa prick när ingen muspekare finns", "Logging:": "Loggning:", "Version:": "Version:", "Disconnect": "Koppla från", diff --git a/app/locale/tr.json b/app/locale/tr.json index 451c1b8a6..90f816244 100644 --- a/app/locale/tr.json +++ b/app/locale/tr.json @@ -23,7 +23,7 @@ "Keyboard": "Klavye", "Show Keyboard": "Klavye Düzenini Göster", "Extra keys": "Ekstra tuşlar", - "Show Extra Keys": "Ekstra tuşları göster", + "Show extra keys": "Ekstra tuşları göster", "Ctrl": "Ctrl", "Toggle Ctrl": "Ctrl Değiştir ", "Alt": "Alt", diff --git a/app/locale/zh_CN.json b/app/locale/zh_CN.json index 3679eaddd..63014c017 100644 --- a/app/locale/zh_CN.json +++ b/app/locale/zh_CN.json @@ -10,7 +10,7 @@ "Disconnect timeout": "超时断开", "noVNC encountered an error:": "noVNC 遇到一个错误:", "Hide/Show the control bar": "显示/隐藏控制栏", - "Move/Drag Viewport": "移动/拖动窗口", + "Move/Drag viewport": "移动/拖动窗口", "viewport drag": "窗口拖动", "Active Mouse Button": "启动鼠标按键", "No mousebutton": "禁用鼠标按键", @@ -18,9 +18,9 @@ "Middle mousebutton": "鼠标中键", "Right mousebutton": "鼠标右键", "Keyboard": "键盘", - "Show Keyboard": "显示键盘", + "Show keyboard": "显示键盘", "Extra keys": "额外按键", - "Show Extra Keys": "显示额外按键", + "Show extra keys": "显示额外按键", "Ctrl": "Ctrl", "Toggle Ctrl": "切换 Ctrl", "Edit clipboard content in the textarea below.": "在下面的文本区域中编辑剪贴板内容。", @@ -43,14 +43,14 @@ "Fullscreen": "全屏", "Settings": "设置", "Encrypt": "加密", - "Shared Mode": "分享模式", - "View Only": "仅查看", - "Clip to Window": "限制/裁切窗口大小", - "Scaling Mode:": "缩放模式:", + "Shared mode": "分享模式", + "View only": "仅查看", + "Clip to window": "限制/裁切窗口大小", + "Scaling mode:": "缩放模式:", "None": "无", - "Local Scaling": "本地缩放", + "Local scaling": "本地缩放", "Local Downscaling": "降低本地尺寸", - "Remote Resizing": "远程调整大小", + "Remote resizing": "远程调整大小", "Advanced": "高级", "Local Cursor": "本地光标", "Repeater ID:": "中继站 ID", @@ -58,8 +58,8 @@ "Host:": "主机:", "Port:": "端口:", "Path:": "路径:", - "Automatic Reconnect": "自动重新连接", - "Reconnect Delay (ms):": "重新连接间隔 (ms):", + "Automatic reconnect": "自动重新连接", + "Reconnect delay (ms):": "重新连接间隔 (ms):", "Logging:": "日志级别:", "Disconnect": "断开连接", "Connect": "连接", diff --git a/app/locale/zh_TW.json b/app/locale/zh_TW.json index 8ddf813f0..9d292a315 100644 --- a/app/locale/zh_TW.json +++ b/app/locale/zh_TW.json @@ -14,7 +14,7 @@ "Password is required": "請提供密碼", "noVNC encountered an error:": "noVNC 遇到一個錯誤:", "Hide/Show the control bar": "顯示/隱藏控制列", - "Move/Drag Viewport": "拖放顯示範圍", + "Move/Drag viewport": "拖放顯示範圍", "viewport drag": "顯示範圍拖放", "Active Mouse Button": "啟用滑鼠按鍵", "No mousebutton": "無滑鼠按鍵", @@ -22,9 +22,9 @@ "Middle mousebutton": "滑鼠中鍵", "Right mousebutton": "滑鼠右鍵", "Keyboard": "鍵盤", - "Show Keyboard": "顯示鍵盤", + "Show keyboard": "顯示鍵盤", "Extra keys": "額外按鍵", - "Show Extra Keys": "顯示額外按鍵", + "Show extra keys": "顯示額外按鍵", "Ctrl": "Ctrl", "Toggle Ctrl": "切換 Ctrl", "Alt": "Alt", @@ -45,13 +45,13 @@ "Clear": "清除", "Fullscreen": "全螢幕", "Settings": "設定", - "Shared Mode": "分享模式", - "View Only": "僅檢視", - "Clip to Window": "限制/裁切視窗大小", - "Scaling Mode:": "縮放模式:", + "Shared mode": "分享模式", + "View only": "僅檢視", + "Clip to window": "限制/裁切視窗大小", + "Scaling mode:": "縮放模式:", "None": "無", - "Local Scaling": "本機縮放", - "Remote Resizing": "遠端調整大小", + "Local scaling": "本機縮放", + "Remote resizing": "遠端調整大小", "Advanced": "進階", "Repeater ID:": "中繼站 ID", "WebSocket": "WebSocket", @@ -59,8 +59,8 @@ "Host:": "主機:", "Port:": "連接埠:", "Path:": "路徑:", - "Automatic Reconnect": "自動重新連線", - "Reconnect Delay (ms):": "重新連線間隔 (ms):", + "Automatic reconnect": "自動重新連線", + "Reconnect delay (ms):": "重新連線間隔 (ms):", "Logging:": "日誌級別:", "Disconnect": "中斷連線", "Connect": "連線", diff --git a/po/cs.po b/po/cs.po index 2b1efd8d9..cb3773c4e 100644 --- a/po/cs.po +++ b/po/cs.po @@ -1,5 +1,5 @@ # Czech translations for noVNC package. -# Copyright (C) 2018 The noVNC Authors +# Copyright (C) 2018 The noVNC authors # This file is distributed under the same license as the noVNC package. # Petr , 2018. # @@ -78,7 +78,7 @@ msgid "Hide/Show the control bar" msgstr "Skrýt/zobrazit ovládací panel" #: ../vnc.html:101 -msgid "Move/Drag Viewport" +msgid "Move/Drag viewport" msgstr "Přesunout/přetáhnout výřez" #: ../vnc.html:101 @@ -110,7 +110,7 @@ msgid "Keyboard" msgstr "Klávesnice" #: ../vnc.html:119 -msgid "Show Keyboard" +msgid "Show keyboard" msgstr "Zobrazit klávesnici" #: ../vnc.html:126 @@ -118,7 +118,7 @@ msgid "Extra keys" msgstr "Extra klávesy" #: ../vnc.html:126 -msgid "Show Extra Keys" +msgid "Show extra keys" msgstr "Zobrazit extra klávesy" #: ../vnc.html:131 @@ -202,19 +202,19 @@ msgid "Settings" msgstr "Nastavení" #: ../vnc.html:197 -msgid "Shared Mode" +msgid "Shared mode" msgstr "Sdílený režim" #: ../vnc.html:200 -msgid "View Only" +msgid "View only" msgstr "Pouze prohlížení" #: ../vnc.html:204 -msgid "Clip to Window" +msgid "Clip to window" msgstr "Přizpůsobit oknu" #: ../vnc.html:207 -msgid "Scaling Mode:" +msgid "Scaling mode:" msgstr "Přizpůsobení velikosti" #: ../vnc.html:209 @@ -222,11 +222,11 @@ msgid "None" msgstr "Žádné" #: ../vnc.html:210 -msgid "Local Scaling" +msgid "Local scaling" msgstr "Místní" #: ../vnc.html:211 -msgid "Remote Resizing" +msgid "Remote resizing" msgstr "Vzdálené" #: ../vnc.html:216 @@ -258,15 +258,15 @@ msgid "Path:" msgstr "Cesta" #: ../vnc.html:244 -msgid "Automatic Reconnect" +msgid "Automatic reconnect" msgstr "Automatická obnova připojení" #: ../vnc.html:247 -msgid "Reconnect Delay (ms):" +msgid "Reconnect delay (ms):" msgstr "Zpoždění připojení (ms)" #: ../vnc.html:252 -msgid "Show Dot when No Cursor" +msgid "Show dot when no cursor" msgstr "Tečka místo chybějícího kurzoru myši" #: ../vnc.html:257 diff --git a/po/de.po b/po/de.po index 0c3fa0d48..d06ce4285 100644 --- a/po/de.po +++ b/po/de.po @@ -1,6 +1,6 @@ # German translations for noVNC package # German translation for noVNC. -# Copyright (C) 2018 The noVNC Authors +# Copyright (C) 2018 The noVNC authors # This file is distributed under the same license as the noVNC package. # Loek Janssen , 2016. # @@ -76,7 +76,7 @@ msgid "Hide/Show the control bar" msgstr "Kontrollleiste verstecken/anzeigen" #: ../vnc.html:106 -msgid "Move/Drag Viewport" +msgid "Move/Drag viewport" msgstr "Ansichtsfenster verschieben/ziehen" #: ../vnc.html:106 @@ -108,7 +108,7 @@ msgid "Keyboard" msgstr "Tastatur" #: ../vnc.html:124 -msgid "Show Keyboard" +msgid "Show keyboard" msgstr "Tastatur anzeigen" #: ../vnc.html:131 @@ -116,7 +116,7 @@ msgid "Extra keys" msgstr "Zusatztasten" #: ../vnc.html:131 -msgid "Show Extra Keys" +msgid "Show extra keys" msgstr "Zusatztasten anzeigen" #: ../vnc.html:136 @@ -200,19 +200,19 @@ msgid "Settings" msgstr "Einstellungen" #: ../vnc.html:202 -msgid "Shared Mode" +msgid "Shared mode" msgstr "Geteilter Modus" #: ../vnc.html:205 -msgid "View Only" +msgid "View only" msgstr "Nur betrachten" #: ../vnc.html:209 -msgid "Clip to Window" +msgid "Clip to window" msgstr "Auf Fenster begrenzen" #: ../vnc.html:212 -msgid "Scaling Mode:" +msgid "Scaling mode:" msgstr "Skalierungsmodus:" #: ../vnc.html:214 @@ -220,11 +220,11 @@ msgid "None" msgstr "Keiner" #: ../vnc.html:215 -msgid "Local Scaling" +msgid "Local scaling" msgstr "Lokales skalieren" #: ../vnc.html:216 -msgid "Remote Resizing" +msgid "Remote resizing" msgstr "Serverseitiges skalieren" #: ../vnc.html:221 @@ -256,11 +256,11 @@ msgid "Path:" msgstr "Pfad:" #: ../vnc.html:249 -msgid "Automatic Reconnect" +msgid "Automatic reconnect" msgstr "Automatisch wiederverbinden" #: ../vnc.html:252 -msgid "Reconnect Delay (ms):" +msgid "Reconnect delay (ms):" msgstr "Wiederverbindungsverzögerung (ms):" #: ../vnc.html:258 diff --git a/po/el.po b/po/el.po index de690fe93..2939270ab 100644 --- a/po/el.po +++ b/po/el.po @@ -1,5 +1,5 @@ # Greek translations for noVNC package. -# Copyright (C) 2018 The noVNC Authors +# Copyright (C) 2018 The noVNC authors # This file is distributed under the same license as the noVNC package. # Giannis Kosmas , 2016. # diff --git a/po/es.po b/po/es.po index 1230402f6..27aaaefea 100644 --- a/po/es.po +++ b/po/es.po @@ -1,6 +1,6 @@ # Spanish translations for noVNC package # Traducciones al español para el paquete noVNC. -# Copyright (C) 2018 The noVNC Authors +# Copyright (C) 2018 The noVNC authors # This file is distributed under the same license as the noVNC package. # Juanjo Diaz , 2018. # Adrian Scillato , 2021. @@ -64,7 +64,7 @@ msgid "Hide/Show the control bar" msgstr "Ocultar/Mostrar la barra de control" #: ../vnc.html:106 -msgid "Move/Drag Viewport" +msgid "Move/Drag viewport" msgstr "Mover/Arrastrar la ventana" #: ../vnc.html:106 @@ -96,7 +96,7 @@ msgid "Keyboard" msgstr "Teclado" #: ../vnc.html:124 -msgid "Show Keyboard" +msgid "Show keyboard" msgstr "Mostrar teclado" #: ../vnc.html:131 @@ -196,15 +196,15 @@ msgid "Shared Mode" msgstr "Modo Compartido" #: ../vnc.html:205 -msgid "View Only" +msgid "View only" msgstr "Solo visualización" #: ../vnc.html:209 -msgid "Clip to Window" +msgid "Clip to window" msgstr "Recortar al tamaño de la ventana" #: ../vnc.html:212 -msgid "Scaling Mode:" +msgid "Scaling mode:" msgstr "Modo de escalado:" #: ../vnc.html:214 @@ -220,7 +220,7 @@ msgid "Local Downscaling" msgstr "Reducción de escala local" #: ../vnc.html:217 -msgid "Remote Resizing" +msgid "Remote resizing" msgstr "Cambio de tamaño remoto" #: ../vnc.html:222 @@ -252,11 +252,11 @@ msgid "Path:" msgstr "Ruta:" #: ../vnc.html:254 -msgid "Automatic Reconnect" +msgid "Automatic reconnect" msgstr "Reconexión automática" #: ../vnc.html:257 -msgid "Reconnect Delay (ms):" +msgid "Reconnect delay (ms):" msgstr "Retraso en la reconexión (ms):" #: ../vnc.html:263 diff --git a/po/fr.po b/po/fr.po index 748a4dbed..fa3619cc3 100644 --- a/po/fr.po +++ b/po/fr.po @@ -1,6 +1,6 @@ # French translations for noVNC package # Traductions françaises du paquet noVNC. -# Copyright (C) 2021 The noVNC Authors +# Copyright (C) 2021 The noVNC authors # This file is distributed under the same license as the noVNC package. # Jose , 2021. # Lowxorx , 2022. @@ -92,7 +92,7 @@ msgid "Keyboard" msgstr "Clavier" #: ../vnc.html:97 -msgid "Show Keyboard" +msgid "Show keyboard" msgstr "Afficher le clavier" #: ../vnc.html:102 @@ -100,7 +100,7 @@ msgid "Extra keys" msgstr "Touches supplémentaires" #: ../vnc.html:102 -msgid "Show Extra Keys" +msgid "Show extra keys" msgstr "Afficher les touches supplémentaires" #: ../vnc.html:107 @@ -192,19 +192,19 @@ msgid "Settings" msgstr "Paramètres" #: ../vnc.html:175 -msgid "Shared Mode" +msgid "Shared mode" msgstr "Mode partagé" #: ../vnc.html:178 -msgid "View Only" +msgid "View only" msgstr "Afficher uniquement" #: ../vnc.html:182 -msgid "Clip to Window" +msgid "Clip to window" msgstr "Clip à fenêtre" #: ../vnc.html:185 -msgid "Scaling Mode:" +msgid "Scaling mode:" msgstr "Mode mise à l'échelle :" #: ../vnc.html:187 @@ -212,11 +212,11 @@ msgid "None" msgstr "Aucun" #: ../vnc.html:188 -msgid "Local Scaling" +msgid "Local scaling" msgstr "Mise à l'échelle locale" #: ../vnc.html:189 -msgid "Remote Resizing" +msgid "Remote resizing" msgstr "Redimensionnement à distance" #: ../vnc.html:194 @@ -256,15 +256,15 @@ msgid "Path:" msgstr "Chemin :" #: ../vnc.html:231 -msgid "Automatic Reconnect" +msgid "Automatic reconnect" msgstr "Reconnecter automatiquemen" #: ../vnc.html:234 -msgid "Reconnect Delay (ms):" +msgid "Reconnect delay (ms):" msgstr "Délai de reconnexion (ms) :" #: ../vnc.html:239 -msgid "Show Dot when No Cursor" +msgid "Show dot when no cursor" msgstr "Afficher le point lorsqu'il n'y a pas de curseur" #: ../vnc.html:244 @@ -292,7 +292,7 @@ msgid "Password:" msgstr "Mot de passe :" #: ../vnc.html:298 -msgid "Send Credentials" +msgid "Send credentials" msgstr "Envoyer les identifiants" #: ../vnc.html:308 diff --git a/po/it.po b/po/it.po index d08ec5384..a066bb3a4 100644 --- a/po/it.po +++ b/po/it.po @@ -1,6 +1,6 @@ # Italian translations for noVNC # Traduzione italiana di noVNC -# Copyright (C) 2022 The noVNC Authors +# Copyright (C) 2022 The noVNC authors # This file is distributed under the same license as the noVNC package. # Fabio Fantoni , 2022. # @@ -84,7 +84,7 @@ msgid "Drag" msgstr "" #: ../vnc.html:78 -msgid "Move/Drag Viewport" +msgid "Move/Drag viewport" msgstr "" #: ../vnc.html:84 @@ -92,7 +92,7 @@ msgid "Keyboard" msgstr "Tastiera" #: ../vnc.html:84 -msgid "Show Keyboard" +msgid "Show keyboard" msgstr "Mostra tastiera" #: ../vnc.html:89 @@ -192,7 +192,7 @@ msgid "Settings" msgstr "Impostazioni" #: ../vnc.html:162 -msgid "Shared Mode" +msgid "Shared mode" msgstr "Modalità condivisa" #: ../vnc.html:165 @@ -200,11 +200,11 @@ msgid "View Only" msgstr "Sola Visualizzazione" #: ../vnc.html:169 -msgid "Clip to Window" +msgid "Clip to window" msgstr "" #: ../vnc.html:172 -msgid "Scaling Mode:" +msgid "Scaling mode:" msgstr "Modalità di ridimensionamento:" #: ../vnc.html:174 diff --git a/po/ja.po b/po/ja.po index 64da73237..0f95bc6fe 100644 --- a/po/ja.po +++ b/po/ja.po @@ -1,6 +1,6 @@ # Japanese translations for noVNC package # noVNC パッケージに対する日訳 -# Copyright (C) 2019 The noVNC Authors +# Copyright (C) 2019 The noVNC authors # This file is distributed under the same license as the noVNC package. # nnn1590 , 2019-2020. # @@ -88,7 +88,7 @@ msgid "Drag" msgstr "ドラッグ" #: ../vnc.html:76 -msgid "Move/Drag Viewport" +msgid "Move/Drag viewport" msgstr "ビューポートを移動/ドラッグ" #: ../vnc.html:82 @@ -96,7 +96,7 @@ msgid "Keyboard" msgstr "キーボード" #: ../vnc.html:82 -msgid "Show Keyboard" +msgid "Show keyboard" msgstr "キーボードを表示" #: ../vnc.html:87 @@ -104,7 +104,7 @@ msgid "Extra keys" msgstr "追加キー" #: ../vnc.html:87 -msgid "Show Extra Keys" +msgid "Show extra keys" msgstr "追加キーを表示" #: ../vnc.html:92 @@ -188,7 +188,7 @@ msgid "Edit clipboard content in the textarea below." msgstr "以下の入力欄からクリップボードの内容を編集できます。" #: ../vnc.html:145 -msgid "Full Screen" +msgid "Full screen" msgstr "全画面表示" #: ../vnc.html:150 ../vnc.html:156 @@ -196,19 +196,19 @@ msgid "Settings" msgstr "設定" #: ../vnc.html:160 -msgid "Shared Mode" +msgid "Shared mode" msgstr "共有モード" #: ../vnc.html:163 -msgid "View Only" +msgid "View only" msgstr "表示専用" #: ../vnc.html:167 -msgid "Clip to Window" +msgid "Clip to window" msgstr "ウィンドウにクリップ" #: ../vnc.html:170 -msgid "Scaling Mode:" +msgid "Scaling mode:" msgstr "スケーリングモード:" #: ../vnc.html:172 @@ -216,11 +216,11 @@ msgid "None" msgstr "なし" #: ../vnc.html:173 -msgid "Local Scaling" +msgid "Local scaling" msgstr "ローカルスケーリング" #: ../vnc.html:174 -msgid "Remote Resizing" +msgid "Remote resizing" msgstr "リモートでリサイズ" #: ../vnc.html:179 @@ -260,15 +260,15 @@ msgid "Path:" msgstr "パス:" #: ../vnc.html:216 -msgid "Automatic Reconnect" +msgid "Automatic reconnect" msgstr "自動再接続" #: ../vnc.html:219 -msgid "Reconnect Delay (ms):" +msgid "Reconnect delay (ms):" msgstr "再接続する遅延 (ミリ秒):" #: ../vnc.html:224 -msgid "Show Dot when No Cursor" +msgid "Show dot when no cursor" msgstr "カーソルがないときにドットを表示する" #: ../vnc.html:229 @@ -328,7 +328,7 @@ msgid "Password:" msgstr "パスワード:" #: ../vnc.html:314 -msgid "Send Credentials" +msgid "Send credentials" msgstr "資格情報を送信" #: ../vnc.html:323 diff --git a/po/ko.po b/po/ko.po index 87ae10697..4673927da 100644 --- a/po/ko.po +++ b/po/ko.po @@ -1,5 +1,5 @@ # SOME DESCRIPTIVE TITLE. -# Copyright (C) 2018 The noVNC Authors +# Copyright (C) 2018 The noVNC authors # This file is distributed under the same license as the noVNC package. # Baw Appie , 2018. # @@ -78,7 +78,7 @@ msgid "Hide/Show the control bar" msgstr "컨트롤 바 숨기기/보이기" #: ../vnc.html:108 -msgid "Move/Drag Viewport" +msgid "Move/Drag viewport" msgstr "움직이기/드래그 뷰포트" #: ../vnc.html:108 @@ -110,7 +110,7 @@ msgid "Keyboard" msgstr "키보드" #: ../vnc.html:126 -msgid "Show Keyboard" +msgid "Show keyboard" msgstr "키보드 보이기" #: ../vnc.html:133 @@ -118,7 +118,7 @@ msgid "Extra keys" msgstr "기타 키들" #: ../vnc.html:133 -msgid "Show Extra Keys" +msgid "Show extra keys" msgstr "기타 키들 보이기" #: ../vnc.html:138 @@ -202,19 +202,19 @@ msgid "Settings" msgstr "설정" #: ../vnc.html:204 -msgid "Shared Mode" +msgid "Shared mode" msgstr "공유 모드" #: ../vnc.html:207 -msgid "View Only" +msgid "View only" msgstr "보기 전용" #: ../vnc.html:211 -msgid "Clip to Window" +msgid "Clip to window" msgstr "창에 클립" #: ../vnc.html:214 -msgid "Scaling Mode:" +msgid "Scaling mode:" msgstr "스케일링 모드:" #: ../vnc.html:216 @@ -222,11 +222,11 @@ msgid "None" msgstr "없음" #: ../vnc.html:217 -msgid "Local Scaling" +msgid "Local scaling" msgstr "로컬 스케일링" #: ../vnc.html:218 -msgid "Remote Resizing" +msgid "Remote resizing" msgstr "원격 크기 조절" #: ../vnc.html:223 @@ -258,11 +258,11 @@ msgid "Path:" msgstr "위치:" #: ../vnc.html:251 -msgid "Automatic Reconnect" +msgid "Automatic reconnect" msgstr "자동 재연결" #: ../vnc.html:254 -msgid "Reconnect Delay (ms):" +msgid "Reconnect delay (ms):" msgstr "재연결 지연 시간 (ms)" #: ../vnc.html:260 diff --git a/po/nl.po b/po/nl.po index 343204a9f..80f9d6f4d 100644 --- a/po/nl.po +++ b/po/nl.po @@ -1,6 +1,6 @@ # Dutch translations for noVNC package # Nederlandse vertalingen voor het pakket noVNC. -# Copyright (C) 2018 The noVNC Authors +# Copyright (C) 2018 The noVNC authors # This file is distributed under the same license as the noVNC package. # Loek Janssen , 2016. # @@ -219,11 +219,11 @@ msgid "View Only" msgstr "Alleen Kijken" #: ../vnc.html:202 -msgid "Clip to Window" +msgid "Clip to window" msgstr "Randen buiten venster afsnijden" #: ../vnc.html:205 -msgid "Scaling Mode:" +msgid "Scaling mode:" msgstr "Schaalmodus:" #: ../vnc.html:207 @@ -275,7 +275,7 @@ msgid "Reconnect Delay (ms):" msgstr "Vertraging voor Opnieuw Verbinden (ms):" #: ../vnc.html:250 -msgid "Show Dot when No Cursor" +msgid "Show dot when no cursor" msgstr "Geef stip weer indien geen cursor" #: ../vnc.html:255 diff --git a/po/noVNC.pot b/po/noVNC.pot index 0f85a82e5..a939d5366 100644 --- a/po/noVNC.pot +++ b/po/noVNC.pot @@ -1,5 +1,5 @@ # SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR The noVNC Authors +# Copyright (C) YEAR The noVNC authors # This file is distributed under the same license as the noVNC package. # FIRST AUTHOR , YEAR. # @@ -91,7 +91,7 @@ msgid "Drag" msgstr "" #: ../vnc.html:74 -msgid "Move/Drag Viewport" +msgid "Move/Drag viewport" msgstr "" #: ../vnc.html:80 @@ -99,7 +99,7 @@ msgid "Keyboard" msgstr "" #: ../vnc.html:80 -msgid "Show Keyboard" +msgid "Show keyboard" msgstr "" #: ../vnc.html:85 @@ -107,7 +107,7 @@ msgid "Extra keys" msgstr "" #: ../vnc.html:85 -msgid "Show Extra Keys" +msgid "Show extra keys" msgstr "" #: ../vnc.html:90 @@ -191,7 +191,7 @@ msgid "Edit clipboard content in the textarea below." msgstr "" #: ../vnc.html:143 -msgid "Full Screen" +msgid "Full screen" msgstr "" #: ../vnc.html:148 ../vnc.html:154 @@ -199,19 +199,19 @@ msgid "Settings" msgstr "" #: ../vnc.html:158 -msgid "Shared Mode" +msgid "Shared mode" msgstr "" #: ../vnc.html:161 -msgid "View Only" +msgid "View only" msgstr "" #: ../vnc.html:165 -msgid "Clip to Window" +msgid "Clip to window" msgstr "" #: ../vnc.html:168 -msgid "Scaling Mode:" +msgid "Scaling mode:" msgstr "" #: ../vnc.html:170 @@ -219,11 +219,11 @@ msgid "None" msgstr "" #: ../vnc.html:171 -msgid "Local Scaling" +msgid "Local scaling" msgstr "" #: ../vnc.html:172 -msgid "Remote Resizing" +msgid "Remote resizing" msgstr "" #: ../vnc.html:177 @@ -263,15 +263,15 @@ msgid "Path:" msgstr "" #: ../vnc.html:214 -msgid "Automatic Reconnect" +msgid "Automatic reconnect" msgstr "" #: ../vnc.html:217 -msgid "Reconnect Delay (ms):" +msgid "Reconnect delay (ms):" msgstr "" #: ../vnc.html:222 -msgid "Show Dot when No Cursor" +msgid "Show dot when no cursor" msgstr "" #: ../vnc.html:227 @@ -329,7 +329,7 @@ msgid "Password:" msgstr "" #: ../vnc.html:312 -msgid "Send Credentials" +msgid "Send credentials" msgstr "" #: ../vnc.html:321 diff --git a/po/pl.po b/po/pl.po index 5acfdc4f4..8e696f44d 100644 --- a/po/pl.po +++ b/po/pl.po @@ -1,5 +1,5 @@ # Polish translations for noVNC package. -# Copyright (C) 2018 The noVNC Authors +# Copyright (C) 2018 The noVNC authors # This file is distributed under the same license as the noVNC package. # Mariusz Jamro , 2017. # @@ -108,7 +108,7 @@ msgid "Keyboard" msgstr "Klawiatura" #: ../vnc.html:124 -msgid "Show Keyboard" +msgid "Show keyboard" msgstr "Pokaż klawiaturę" #: ../vnc.html:131 @@ -116,7 +116,7 @@ msgid "Extra keys" msgstr "Przyciski dodatkowe" #: ../vnc.html:131 -msgid "Show Extra Keys" +msgid "Show extra keys" msgstr "Pokaż przyciski dodatkowe" #: ../vnc.html:136 @@ -220,11 +220,11 @@ msgid "None" msgstr "Brak" #: ../vnc.html:215 -msgid "Local Scaling" +msgid "Local scaling" msgstr "Skalowanie lokalne" #: ../vnc.html:216 -msgid "Remote Resizing" +msgid "Remote resizing" msgstr "Skalowanie zdalne" #: ../vnc.html:221 @@ -256,11 +256,11 @@ msgid "Path:" msgstr "Ścieżka:" #: ../vnc.html:249 -msgid "Automatic Reconnect" +msgid "Automatic reconnect" msgstr "Automatycznie wznawiaj połączenie" #: ../vnc.html:252 -msgid "Reconnect Delay (ms):" +msgid "Reconnect delay (ms):" msgstr "Opóźnienie wznawiania (ms):" #: ../vnc.html:258 diff --git a/po/pt_BR.po b/po/pt_BR.po index 77951aef0..422b94266 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -1,5 +1,5 @@ # Portuguese translations for noVNC package. -# Copyright (C) 2021 The noVNC Authors +# Copyright (C) 2021 The noVNC authors # This file is distributed under the same license as the noVNC package. # , 2021. # @@ -83,7 +83,7 @@ msgid "Drag" msgstr "Arrastar" #: ../vnc.html:78 -msgid "Move/Drag Viewport" +msgid "Move/Drag viewport" msgstr "Mover/arrastar a janela" #: ../vnc.html:84 @@ -91,7 +91,7 @@ msgid "Keyboard" msgstr "Teclado" #: ../vnc.html:84 -msgid "Show Keyboard" +msgid "Show keyboard" msgstr "Mostrar teclado" #: ../vnc.html:89 @@ -99,7 +99,7 @@ msgid "Extra keys" msgstr "Teclas adicionais" #: ../vnc.html:89 -msgid "Show Extra Keys" +msgid "Show extra keys" msgstr "Mostar teclas adicionais" #: ../vnc.html:94 @@ -191,19 +191,19 @@ msgid "Settings" msgstr "Configurações" #: ../vnc.html:162 -msgid "Shared Mode" +msgid "Shared mode" msgstr "Modo compartilhado" #: ../vnc.html:165 -msgid "View Only" +msgid "View only" msgstr "Apenas visualizar" #: ../vnc.html:169 -msgid "Clip to Window" +msgid "Clip to window" msgstr "Recortar à janela" #: ../vnc.html:172 -msgid "Scaling Mode:" +msgid "Scaling mode:" msgstr "Modo de dimensionamento:" #: ../vnc.html:174 @@ -211,11 +211,11 @@ msgid "None" msgstr "Nenhum" #: ../vnc.html:175 -msgid "Local Scaling" +msgid "Local scaling" msgstr "Local" #: ../vnc.html:176 -msgid "Remote Resizing" +msgid "Remote resizing" msgstr "Remoto" #: ../vnc.html:181 @@ -255,15 +255,15 @@ msgid "Path:" msgstr "Caminho:" #: ../vnc.html:218 -msgid "Automatic Reconnect" +msgid "Automatic reconnect" msgstr "Reconexão automática" #: ../vnc.html:221 -msgid "Reconnect Delay (ms):" +msgid "Reconnect delay (ms):" msgstr "Atraso da reconexão (ms)" #: ../vnc.html:226 -msgid "Show Dot when No Cursor" +msgid "Show dot when no cursor" msgstr "Mostrar ponto quando não há cursor" #: ../vnc.html:231 @@ -291,7 +291,7 @@ msgid "Password:" msgstr "Senha:" #: ../vnc.html:285 -msgid "Send Credentials" +msgid "Send credentials" msgstr "Enviar credenciais" #: ../vnc.html:295 diff --git a/po/ru.po b/po/ru.po index dbc1d840f..1a19e4ebd 100644 --- a/po/ru.po +++ b/po/ru.po @@ -86,7 +86,7 @@ msgid "Drag" msgstr "Переместить" #: ../vnc.html:78 -msgid "Move/Drag Viewport" +msgid "Move/Drag viewport" msgstr "Переместить окно" #: ../vnc.html:84 @@ -94,7 +94,7 @@ msgid "Keyboard" msgstr "Клавиатура" #: ../vnc.html:84 -msgid "Show Keyboard" +msgid "Show keyboard" msgstr "Показать клавиатуру" #: ../vnc.html:89 @@ -194,7 +194,7 @@ msgid "Settings" msgstr "Настройки" #: ../vnc.html:162 -msgid "Shared Mode" +msgid "Shared mode" msgstr "Общий режим" #: ../vnc.html:165 @@ -202,11 +202,11 @@ msgid "View Only" msgstr "Только Просмотр" #: ../vnc.html:169 -msgid "Clip to Window" +msgid "Clip to window" msgstr "В окно" #: ../vnc.html:172 -msgid "Scaling Mode:" +msgid "Scaling mode:" msgstr "Масштаб:" #: ../vnc.html:174 @@ -214,11 +214,11 @@ msgid "None" msgstr "Нет" #: ../vnc.html:175 -msgid "Local Scaling" +msgid "Local scaling" msgstr "Локальный масштаб" #: ../vnc.html:176 -msgid "Remote Resizing" +msgid "Remote resizing" msgstr "Удаленная перенастройка размера" #: ../vnc.html:181 @@ -258,15 +258,15 @@ msgid "Path:" msgstr "Путь:" #: ../vnc.html:218 -msgid "Automatic Reconnect" +msgid "Automatic reconnect" msgstr "Автоматическое переподключение" #: ../vnc.html:221 -msgid "Reconnect Delay (ms):" +msgid "Reconnect delay (ms):" msgstr "Задержка переподключения (мс):" #: ../vnc.html:226 -msgid "Show Dot when No Cursor" +msgid "Show dot when no cursor" msgstr "Показать точку вместо курсора" #: ../vnc.html:231 diff --git a/po/sv.po b/po/sv.po index 85c4e3054..f86c1ef37 100644 --- a/po/sv.po +++ b/po/sv.po @@ -1,6 +1,6 @@ # Swedish translations for noVNC package # Svenska översättningar för paketet noVNC. -# Copyright (C) 2020 The noVNC Authors +# Copyright (C) 2020 The noVNC authors # This file is distributed under the same license as the noVNC package. # Samuel Mannehed , 2020. # @@ -195,7 +195,7 @@ msgid "Edit clipboard content in the textarea below." msgstr "Redigera urklippets innehåll i fältet nedan." #: ../vnc.html:143 -msgid "Full Screen" +msgid "Full screen" msgstr "Fullskärm" #: ../vnc.html:148 ../vnc.html:154 @@ -215,7 +215,7 @@ msgid "Clip to Window" msgstr "Begränsa till Fönster" #: ../vnc.html:168 -msgid "Scaling Mode:" +msgid "Scaling mode:" msgstr "Skalningsläge:" #: ../vnc.html:170 @@ -271,11 +271,11 @@ msgid "Automatic Reconnect" msgstr "Automatisk Återanslutning" #: ../vnc.html:217 -msgid "Reconnect Delay (ms):" +msgid "Reconnect delay (ms):" msgstr "Fördröjning (ms):" #: ../vnc.html:222 -msgid "Show Dot when No Cursor" +msgid "Show dot when no cursor" msgstr "Visa prick när ingen muspekare finns" #: ../vnc.html:227 diff --git a/po/tr.po b/po/tr.po index 8b5c18134..010a1a876 100644 --- a/po/tr.po +++ b/po/tr.po @@ -1,6 +1,6 @@ # Turkish translations for noVNC package # Turkish translation for noVNC. -# Copyright (C) 2018 The noVNC Authors +# Copyright (C) 2018 The noVNC authors # This file is distributed under the same license as the noVNC package. # Ömer ÇAKMAK , 2018. # @@ -116,7 +116,7 @@ msgid "Extra keys" msgstr "Ekstra tuşlar" #: ../vnc.html:131 -msgid "Show Extra Keys" +msgid "Show extra keys" msgstr "Ekstra tuşları göster" #: ../vnc.html:136 diff --git a/po/zh_CN.po b/po/zh_CN.po index caae28504..ef0a48497 100644 --- a/po/zh_CN.po +++ b/po/zh_CN.po @@ -1,5 +1,5 @@ # Simplified Chinese translations for noVNC package. -# Copyright (C) 2020 The noVNC Authors +# Copyright (C) 2020 The noVNC authors # This file is distributed under the same license as the noVNC package. # Peter Dave Hello , 2018. # @@ -60,7 +60,7 @@ msgid "Hide/Show the control bar" msgstr "显示/隐藏控制栏" #: ../vnc.html:106 -msgid "Move/Drag Viewport" +msgid "Move/Drag viewport" msgstr "移动/拖动窗口" #: ../vnc.html:106 @@ -92,7 +92,7 @@ msgid "Keyboard" msgstr "键盘" #: ../vnc.html:124 -msgid "Show Keyboard" +msgid "Show keyboard" msgstr "显示键盘" #: ../vnc.html:131 @@ -100,7 +100,7 @@ msgid "Extra keys" msgstr "额外按键" #: ../vnc.html:131 -msgid "Show Extra Keys" +msgid "Show extra keys" msgstr "显示额外按键" #: ../vnc.html:136 @@ -192,19 +192,19 @@ msgid "Encrypt" msgstr "加密" #: ../vnc.html:202 -msgid "Shared Mode" +msgid "Shared mode" msgstr "分享模式" #: ../vnc.html:205 -msgid "View Only" +msgid "View only" msgstr "仅查看" #: ../vnc.html:209 -msgid "Clip to Window" +msgid "Clip to window" msgstr "限制/裁切窗口大小" #: ../vnc.html:212 -msgid "Scaling Mode:" +msgid "Scaling mode:" msgstr "缩放模式:" #: ../vnc.html:214 @@ -212,7 +212,7 @@ msgid "None" msgstr "无" #: ../vnc.html:215 -msgid "Local Scaling" +msgid "Local scaling" msgstr "本地缩放" #: ../vnc.html:216 @@ -220,7 +220,7 @@ msgid "Local Downscaling" msgstr "降低本地尺寸" #: ../vnc.html:217 -msgid "Remote Resizing" +msgid "Remote resizing" msgstr "远程调整大小" #: ../vnc.html:222 @@ -252,11 +252,11 @@ msgid "Path:" msgstr "路径:" #: ../vnc.html:254 -msgid "Automatic Reconnect" +msgid "Automatic reconnect" msgstr "自动重新连接" #: ../vnc.html:257 -msgid "Reconnect Delay (ms):" +msgid "Reconnect delay (ms):" msgstr "重新连接间隔 (ms):" #: ../vnc.html:263 diff --git a/po/zh_TW.po b/po/zh_TW.po index 9ddf550c1..61a4523d3 100644 --- a/po/zh_TW.po +++ b/po/zh_TW.po @@ -1,5 +1,5 @@ # Traditional Chinese translations for noVNC package. -# Copyright (C) 2018 The noVNC Authors +# Copyright (C) 2018 The noVNC authors # This file is distributed under the same license as the noVNC package. # Peter Dave Hello , 2018. # @@ -77,7 +77,7 @@ msgid "Hide/Show the control bar" msgstr "顯示/隱藏控制列" #: ../vnc.html:106 -msgid "Move/Drag Viewport" +msgid "Move/Drag viewport" msgstr "拖放顯示範圍" #: ../vnc.html:106 @@ -109,7 +109,7 @@ msgid "Keyboard" msgstr "鍵盤" #: ../vnc.html:124 -msgid "Show Keyboard" +msgid "Show keyboard" msgstr "顯示鍵盤" #: ../vnc.html:131 @@ -117,7 +117,7 @@ msgid "Extra keys" msgstr "額外按鍵" #: ../vnc.html:131 -msgid "Show Extra Keys" +msgid "Show extra keys" msgstr "顯示額外按鍵" #: ../vnc.html:136 @@ -201,19 +201,19 @@ msgid "Settings" msgstr "設定" #: ../vnc.html:202 -msgid "Shared Mode" +msgid "Shared mode" msgstr "分享模式" #: ../vnc.html:205 -msgid "View Only" +msgid "View only" msgstr "僅檢視" #: ../vnc.html:209 -msgid "Clip to Window" +msgid "Clip to window" msgstr "限制/裁切視窗大小" #: ../vnc.html:212 -msgid "Scaling Mode:" +msgid "Scaling mode:" msgstr "縮放模式:" #: ../vnc.html:214 @@ -221,11 +221,11 @@ msgid "None" msgstr "無" #: ../vnc.html:215 -msgid "Local Scaling" +msgid "Local scaling" msgstr "本機縮放" #: ../vnc.html:216 -msgid "Remote Resizing" +msgid "Remote resizing" msgstr "遠端調整大小" #: ../vnc.html:221 @@ -257,11 +257,11 @@ msgid "Path:" msgstr "路徑:" #: ../vnc.html:249 -msgid "Automatic Reconnect" +msgid "Automatic reconnect" msgstr "自動重新連線" #: ../vnc.html:252 -msgid "Reconnect Delay (ms):" +msgid "Reconnect delay (ms):" msgstr "重新連線間隔 (ms):" #: ../vnc.html:258 From 52392ec150fd79d023718c424bdf6cbc0b758fd9 Mon Sep 17 00:00:00 2001 From: NNN1590 Date: Sat, 14 Dec 2024 15:59:01 +0900 Subject: [PATCH 069/166] Update Japanese translation --- app/locale/ja.json | 7 +- po/ja.po | 199 ++++++++++++++++++++------------------------- 2 files changed, 93 insertions(+), 113 deletions(-) diff --git a/app/locale/ja.json b/app/locale/ja.json index 078adcc85..4fc9b8a9e 100644 --- a/app/locale/ja.json +++ b/app/locale/ja.json @@ -1,13 +1,14 @@ { - "HTTPS is required for full functionality": "すべての機能を使用するにはHTTPS接続が必要です", + "Running without HTTPS is not recommended, crashes or other issues are likely.": "HTTPS接続なしで実行することは推奨されません。クラッシュしたりその他の問題が発生したりする可能性があります。", "Connecting...": "接続しています...", "Disconnecting...": "切断しています...", "Reconnecting...": "再接続しています...", "Internal error": "内部エラー", "Must set host": "ホストを設定する必要があります", + "Failed to connect to server: ": "サーバーへの接続に失敗しました: ", "Connected (encrypted) to ": "接続しました (暗号化済み): ", "Connected (unencrypted) to ": "接続しました (暗号化されていません): ", - "Something went wrong, connection is closed": "何らかの問題で、接続が閉じられました", + "Something went wrong, connection is closed": "問題が発生したため、接続が閉じられました", "Failed to connect to server": "サーバーへの接続に失敗しました", "Disconnected": "切断しました", "New connection has been rejected with reason: ": "新規接続は次の理由で拒否されました: ", @@ -48,7 +49,7 @@ "Clip to window": "ウィンドウにクリップ", "Scaling mode:": "スケーリングモード:", "None": "なし", - "Local scaling": "ローカルスケーリング", + "Local scaling": "ローカルでスケーリング", "Remote resizing": "リモートでリサイズ", "Advanced": "高度", "Quality:": "品質:", diff --git a/po/ja.po b/po/ja.po index 0f95bc6fe..af0a1ba1a 100644 --- a/po/ja.po +++ b/po/ja.po @@ -1,15 +1,15 @@ # Japanese translations for noVNC package # noVNC パッケージに対する日訳 -# Copyright (C) 2019 The noVNC authors +# Copyright (C) 2019-2024 The noVNC authors # This file is distributed under the same license as the noVNC package. -# nnn1590 , 2019-2020. +# nnn1590 , 2019-2024. # msgid "" msgstr "" -"Project-Id-Version: noVNC 1.1.0\n" +"Project-Id-Version: noVNC 1.5.0\n" "Report-Msgid-Bugs-To: novnc@googlegroups.com\n" -"POT-Creation-Date: 2022-12-27 15:24+0100\n" -"PO-Revision-Date: 2023-03-21 12:42+0900\n" +"POT-Creation-Date: 2024-06-03 14:10+0200\n" +"PO-Revision-Date: 2024-12-14 15:22+0900\n" "Last-Translator: nnn1590 \n" "Language-Team: Japanese\n" "Language: ja\n" @@ -20,8 +20,11 @@ msgstr "" "X-Generator: Poedit 2.3\n" #: ../app/ui.js:69 -msgid "HTTPS is required for full functionality" -msgstr "すべての機能を使用するにはHTTPS接続が必要です" +msgid "" +"Running without HTTPS is not recommended, crashes or other issues are likely." +msgstr "" +"HTTPS接続なしで実行することは推奨されません。クラッシュしたりその他の問題が発" +"生したりする可能性があります。" #: ../app/ui.js:410 msgid "Connecting..." @@ -43,321 +46,297 @@ msgstr "内部エラー" msgid "Must set host" msgstr "ホストを設定する必要があります" -#: ../app/ui.js:1110 +#: ../app/ui.js:1052 +msgid "Failed to connect to server: " +msgstr "サーバーへの接続に失敗しました: " + +#: ../app/ui.js:1118 msgid "Connected (encrypted) to " msgstr "接続しました (暗号化済み): " -#: ../app/ui.js:1112 +#: ../app/ui.js:1120 msgid "Connected (unencrypted) to " msgstr "接続しました (暗号化されていません): " -#: ../app/ui.js:1135 +#: ../app/ui.js:1143 msgid "Something went wrong, connection is closed" -msgstr "何らかの問題で、接続が閉じられました" +msgstr "問題が発生したため、接続が閉じられました" -#: ../app/ui.js:1138 +#: ../app/ui.js:1146 msgid "Failed to connect to server" msgstr "サーバーへの接続に失敗しました" -#: ../app/ui.js:1150 +#: ../app/ui.js:1158 msgid "Disconnected" msgstr "切断しました" -#: ../app/ui.js:1165 +#: ../app/ui.js:1173 msgid "New connection has been rejected with reason: " msgstr "新規接続は次の理由で拒否されました: " -#: ../app/ui.js:1168 +#: ../app/ui.js:1176 msgid "New connection has been rejected" msgstr "新規接続は拒否されました" -#: ../app/ui.js:1234 +#: ../app/ui.js:1242 msgid "Credentials are required" msgstr "資格情報が必要です" -#: ../vnc.html:57 +#: ../vnc.html:55 msgid "noVNC encountered an error:" msgstr "noVNC でエラーが発生しました:" -#: ../vnc.html:67 +#: ../vnc.html:65 msgid "Hide/Show the control bar" msgstr "コントロールバーを隠す/表示する" -#: ../vnc.html:76 +#: ../vnc.html:74 msgid "Drag" msgstr "ドラッグ" -#: ../vnc.html:76 +#: ../vnc.html:74 msgid "Move/Drag viewport" msgstr "ビューポートを移動/ドラッグ" -#: ../vnc.html:82 +#: ../vnc.html:80 msgid "Keyboard" msgstr "キーボード" -#: ../vnc.html:82 +#: ../vnc.html:80 msgid "Show keyboard" msgstr "キーボードを表示" -#: ../vnc.html:87 +#: ../vnc.html:85 msgid "Extra keys" msgstr "追加キー" -#: ../vnc.html:87 +#: ../vnc.html:85 msgid "Show extra keys" msgstr "追加キーを表示" -#: ../vnc.html:92 +#: ../vnc.html:90 msgid "Ctrl" msgstr "Ctrl" -#: ../vnc.html:92 +#: ../vnc.html:90 msgid "Toggle Ctrl" msgstr "Ctrl キーをトグル" -#: ../vnc.html:95 +#: ../vnc.html:93 msgid "Alt" msgstr "Alt" -#: ../vnc.html:95 +#: ../vnc.html:93 msgid "Toggle Alt" msgstr "Alt キーをトグル" -#: ../vnc.html:98 +#: ../vnc.html:96 msgid "Toggle Windows" msgstr "Windows キーをトグル" -#: ../vnc.html:98 +#: ../vnc.html:96 msgid "Windows" msgstr "Windows" -#: ../vnc.html:101 +#: ../vnc.html:99 msgid "Send Tab" msgstr "Tab キーを送信" -#: ../vnc.html:101 +#: ../vnc.html:99 msgid "Tab" msgstr "Tab" -#: ../vnc.html:104 +#: ../vnc.html:102 msgid "Esc" msgstr "Esc" -#: ../vnc.html:104 +#: ../vnc.html:102 msgid "Send Escape" msgstr "Escape キーを送信" -#: ../vnc.html:107 +#: ../vnc.html:105 msgid "Ctrl+Alt+Del" msgstr "Ctrl+Alt+Del" -#: ../vnc.html:107 +#: ../vnc.html:105 msgid "Send Ctrl-Alt-Del" msgstr "Ctrl-Alt-Del を送信" -#: ../vnc.html:114 +#: ../vnc.html:112 msgid "Shutdown/Reboot" msgstr "シャットダウン/再起動" -#: ../vnc.html:114 +#: ../vnc.html:112 msgid "Shutdown/Reboot..." msgstr "シャットダウン/再起動..." -#: ../vnc.html:120 +#: ../vnc.html:118 msgid "Power" msgstr "電源" -#: ../vnc.html:122 +#: ../vnc.html:120 msgid "Shutdown" msgstr "シャットダウン" -#: ../vnc.html:123 +#: ../vnc.html:121 msgid "Reboot" msgstr "再起動" -#: ../vnc.html:124 +#: ../vnc.html:122 msgid "Reset" msgstr "リセット" -#: ../vnc.html:129 ../vnc.html:135 +#: ../vnc.html:127 ../vnc.html:133 msgid "Clipboard" msgstr "クリップボード" -#: ../vnc.html:137 +#: ../vnc.html:135 msgid "Edit clipboard content in the textarea below." msgstr "以下の入力欄からクリップボードの内容を編集できます。" -#: ../vnc.html:145 +#: ../vnc.html:143 msgid "Full screen" msgstr "全画面表示" -#: ../vnc.html:150 ../vnc.html:156 +#: ../vnc.html:148 ../vnc.html:154 msgid "Settings" msgstr "設定" -#: ../vnc.html:160 +#: ../vnc.html:158 msgid "Shared mode" msgstr "共有モード" -#: ../vnc.html:163 +#: ../vnc.html:161 msgid "View only" msgstr "表示専用" -#: ../vnc.html:167 +#: ../vnc.html:165 msgid "Clip to window" msgstr "ウィンドウにクリップ" -#: ../vnc.html:170 +#: ../vnc.html:168 msgid "Scaling mode:" msgstr "スケーリングモード:" -#: ../vnc.html:172 +#: ../vnc.html:170 msgid "None" msgstr "なし" -#: ../vnc.html:173 +#: ../vnc.html:171 msgid "Local scaling" -msgstr "ローカルスケーリング" +msgstr "ローカルでスケーリング" -#: ../vnc.html:174 +#: ../vnc.html:172 msgid "Remote resizing" msgstr "リモートでリサイズ" -#: ../vnc.html:179 +#: ../vnc.html:177 msgid "Advanced" msgstr "高度" -#: ../vnc.html:182 +#: ../vnc.html:180 msgid "Quality:" msgstr "品質:" -#: ../vnc.html:186 +#: ../vnc.html:184 msgid "Compression level:" msgstr "圧縮レベル:" -#: ../vnc.html:191 +#: ../vnc.html:189 msgid "Repeater ID:" msgstr "リピーター ID:" -#: ../vnc.html:195 +#: ../vnc.html:193 msgid "WebSocket" msgstr "WebSocket" -#: ../vnc.html:198 +#: ../vnc.html:196 msgid "Encrypt" msgstr "暗号化" -#: ../vnc.html:201 +#: ../vnc.html:199 msgid "Host:" msgstr "ホスト:" -#: ../vnc.html:205 +#: ../vnc.html:203 msgid "Port:" msgstr "ポート:" -#: ../vnc.html:209 +#: ../vnc.html:207 msgid "Path:" msgstr "パス:" -#: ../vnc.html:216 +#: ../vnc.html:214 msgid "Automatic reconnect" msgstr "自動再接続" -#: ../vnc.html:219 +#: ../vnc.html:217 msgid "Reconnect delay (ms):" msgstr "再接続する遅延 (ミリ秒):" -#: ../vnc.html:224 +#: ../vnc.html:222 msgid "Show dot when no cursor" msgstr "カーソルがないときにドットを表示する" -#: ../vnc.html:229 +#: ../vnc.html:227 msgid "Logging:" msgstr "ロギング:" -#: ../vnc.html:238 +#: ../vnc.html:236 msgid "Version:" msgstr "バージョン:" -#: ../vnc.html:246 +#: ../vnc.html:244 msgid "Disconnect" msgstr "切断" -#: ../vnc.html:269 +#: ../vnc.html:267 msgid "Connect" msgstr "接続" -#: ../vnc.html:278 +#: ../vnc.html:276 msgid "Server identity" msgstr "サーバーの識別情報" -#: ../vnc.html:281 +#: ../vnc.html:279 msgid "The server has provided the following identifying information:" msgstr "サーバーは以下の識別情報を提供しています:" -#: ../vnc.html:285 +#: ../vnc.html:283 msgid "Fingerprint:" msgstr "フィンガープリント:" -#: ../vnc.html:288 +#: ../vnc.html:286 msgid "" "Please verify that the information is correct and press \"Approve\". " "Otherwise press \"Reject\"." msgstr "" -"この情報が正しい場合は「承認」を、そうでない場合は「拒否」を押してく" -"ださい。" +"この情報が正しい場合は「承認」を、そうでない場合は「拒否」を押してください。" -#: ../vnc.html:293 +#: ../vnc.html:291 msgid "Approve" msgstr "承認" -#: ../vnc.html:294 +#: ../vnc.html:292 msgid "Reject" msgstr "拒否" -#: ../vnc.html:302 +#: ../vnc.html:300 msgid "Credentials" msgstr "資格情報" -#: ../vnc.html:306 +#: ../vnc.html:304 msgid "Username:" msgstr "ユーザー名:" -#: ../vnc.html:310 +#: ../vnc.html:308 msgid "Password:" msgstr "パスワード:" -#: ../vnc.html:314 +#: ../vnc.html:312 msgid "Send credentials" msgstr "資格情報を送信" -#: ../vnc.html:323 +#: ../vnc.html:321 msgid "Cancel" msgstr "キャンセル" - -#~ msgid "Clear" -#~ msgstr "クリア" - -#~ msgid "Password is required" -#~ msgstr "パスワードが必要です" - -#~ msgid "viewport drag" -#~ msgstr "ビューポートをドラッグ" - -#~ msgid "Active Mouse Button" -#~ msgstr "アクティブなマウスボタン" - -#~ msgid "No mousebutton" -#~ msgstr "マウスボタンなし" - -#~ msgid "Left mousebutton" -#~ msgstr "左マウスボタン" - -#~ msgid "Middle mousebutton" -#~ msgstr "中マウスボタン" - -#~ msgid "Right mousebutton" -#~ msgstr "右マウスボタン" - -#~ msgid "Send Password" -#~ msgstr "パスワードを送信" From 3e2e04bea132de6c5485fce41cc947aa86f33d5f Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 17 Dec 2024 16:43:44 +0100 Subject: [PATCH 070/166] Replace node-getopt with commander for args node-getopt isn't maintained and nodejs has started complaining about deprecated features in it. --- package.json | 1 - po/po2js | 18 +++++++----------- po/xgettext-html | 16 ++++++++-------- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index e28850a8d..e45fde83d 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,6 @@ "karma-safari-launcher": "latest", "karma-script-launcher": "latest", "mocha": "latest", - "node-getopt": "latest", "po2json": "latest", "sinon": "latest", "sinon-chai": "latest" diff --git a/po/po2js b/po/po2js index 23b8bb24a..8d353a1d4 100755 --- a/po/po2js +++ b/po/po2js @@ -17,20 +17,16 @@ * along with this program. If not, see . */ -const getopt = require('node-getopt'); +const { program } = require('commander'); const fs = require('fs'); const po2json = require("po2json"); -const opt = getopt.create([ - ['h', 'help', 'display this help'], -]).bindHelp().parseSystem(); +program + .argument('') + .argument('') + .parse(process.argv); -if (opt.argv.length != 2) { - console.error("Incorrect number of arguments given"); - process.exit(1); -} - -const data = po2json.parseFileSync(opt.argv[0]); +const data = po2json.parseFileSync(program.args[0]); const bodyPart = Object.keys(data) .filter(msgid => msgid !== "") @@ -42,4 +38,4 @@ const bodyPart = Object.keys(data) const output = "{\n" + bodyPart + "\n}"; -fs.writeFileSync(opt.argv[1], output); +fs.writeFileSync(program.args[1], output); diff --git a/po/xgettext-html b/po/xgettext-html index 72d492354..f5ba57cc5 100755 --- a/po/xgettext-html +++ b/po/xgettext-html @@ -5,14 +5,14 @@ * Licensed under MPL 2.0 (see LICENSE.txt) */ -const getopt = require('node-getopt'); +const { program } = require('commander'); const jsdom = require("jsdom"); const fs = require("fs"); -const opt = getopt.create([ - ['o', 'output=FILE', 'write output to specified file'], - ['h', 'help', 'display this help'], -]).bindHelp().parseSystem(); +program + .argument('') + .requiredOption('-o, --output ', 'write output to specified file') + .parse(process.argv); const strings = {}; @@ -87,8 +87,8 @@ function process(elem, locator, enabled) { } } -for (let i = 0; i < opt.argv.length; i++) { - const fn = opt.argv[i]; +for (let i = 0; i < program.args.length; i++) { + const fn = program.args[i]; const file = fs.readFileSync(fn, "utf8"); const dom = new jsdom.JSDOM(file, { includeNodeLocations: true }); const body = dom.window.document.body; @@ -116,4 +116,4 @@ for (let str in strings) { output += "\n"; } -fs.writeFileSync(opt.options.output, output); +fs.writeFileSync(program.opts().output, output); From 673cb349fd9ec31dd928590e3ac4efe452a30b60 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 17 Dec 2024 17:13:30 +0100 Subject: [PATCH 071/166] Replace po2json with pofile The former doesn't seem to be properly maintained and nodejs gives deprecation warnings. --- package.json | 2 +- po/po2js | 17 ++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index e45fde83d..907cf6309 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "karma-safari-launcher": "latest", "karma-script-launcher": "latest", "mocha": "latest", - "po2json": "latest", + "pofile": "latest", "sinon": "latest", "sinon-chai": "latest" }, diff --git a/po/po2js b/po/po2js index 8d353a1d4..e74b1d4f4 100755 --- a/po/po2js +++ b/po/po2js @@ -19,22 +19,21 @@ const { program } = require('commander'); const fs = require('fs'); -const po2json = require("po2json"); +const pofile = require("pofile"); program .argument('') .argument('') .parse(process.argv); -const data = po2json.parseFileSync(program.args[0]); +let data = fs.readFileSync(program.args[0], "utf8"); +let po = pofile.parse(data); -const bodyPart = Object.keys(data) - .filter(msgid => msgid !== "") - .filter(msgid => data[msgid][1] !== "") - .map((msgid) => { - const msgstr = data[msgid][1]; - return " " + JSON.stringify(msgid) + ": " + JSON.stringify(msgstr); - }).join(",\n"); +const bodyPart = po.items + .filter(item => item.msgid !== "") + .filter(item => item.msgstr[0] !== "") + .map(item => " " + JSON.stringify(item.msgid) + ": " + JSON.stringify(item.msgstr[0])) + .join(",\n"); const output = "{\n" + bodyPart + "\n}"; From e6e03a226f8574a9b2743cac1d757360be7eb1b4 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Fri, 27 Dec 2024 12:47:38 +0100 Subject: [PATCH 072/166] Fix resizes back to initial remote session size Since the expected client size wasn't updated when the browser window resized, noVNC didn't resize the canvas properly when going back to the exact same dimensions. Fixes issue #1903 --- core/rfb.js | 1 + tests/test.rfb.js | 65 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/core/rfb.js b/core/rfb.js index 9559e487d..e1482c3f4 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -730,6 +730,7 @@ export default class RFB extends EventTargetMixin { window.requestAnimationFrame(() => { this._updateClip(); this._updateScale(); + this._saveExpectedClientSize(); }); if (this._resizeSession) { diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 19894ba11..7e4063217 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -730,6 +730,28 @@ describe('Remote Frame Buffer protocol client', function () { container.style.width = '70px'; container.style.height = '80px'; client.scaleViewport = true; + + const incoming = [ 0x00, // msg-type=FBU + 0x00, // padding + 0x00, 0x01, // number of rects = 1 + 0x00, 0x00, // reason = server initialized + 0x00, 0x00, // status = no error + 0x00, 0x04, // new width = 4 + 0x00, 0x04, // new height = 4 + 0xff, 0xff, + 0xfe, 0xcc, // enc = (-308) ExtendedDesktopSize + 0x01, // number of screens = 1 + 0x00, 0x00, + 0x00, // padding + 0x78, 0x90, + 0xab, 0xcd, // screen id = 0 + 0x00, 0x00, // screen x = 0 + 0x00, 0x00, // screen y = 0 + 0x00, 0x04, // screen width = 4 + 0x00, 0x04, // screen height = 4 + 0x12, 0x34, + 0x56, 0x78]; // screen flags + client._sock._websocket._receiveData(new Uint8Array(incoming)); }); it('should update display scale factor when changing the property', function () { @@ -774,6 +796,28 @@ describe('Remote Frame Buffer protocol client', function () { expect(client._display.autoscale).to.have.been.calledWith(40, 50); }); + it('should update the scaling resized back to initial size', function () { + sinon.spy(client._display, "autoscale"); + + container.style.width = '40px'; + container.style.height = '50px'; + fakeResizeObserver.fire(); + clock.tick(1000); + + expect(client._display.autoscale).to.have.been.calledOnce; + expect(client._display.autoscale).to.have.been.calledWith(40, 50); + client._display.autoscale.resetHistory(); + + container.style.width = '70px'; + container.style.height = '80px'; + fakeResizeObserver.fire(); + clock.tick(1000); + + expect(client._display.autoscale).to.have.been.calledOnce; + expect(client._display.autoscale).to.have.been.calledWith(70, 80); + client._display.autoscale.resetHistory(); + }); + it('should update the scaling when the remote session resizes', function () { // Simple ExtendedDesktopSize FBU message const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, @@ -941,6 +985,27 @@ describe('Remote Frame Buffer protocol client', function () { expect(RFB.messages.setDesktopSize).to.not.have.been.called; }); + it('should request a resize when resized back to initial size', function () { + container.style.width = '40px'; + container.style.height = '50px'; + fakeResizeObserver.fire(); + clock.tick(1000); + + expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; + expect(RFB.messages.setDesktopSize).to.have.been.calledWith( + sinon.match.object, 40, 50, 0x7890abcd, 0x12345678); + RFB.messages.setDesktopSize.resetHistory(); + + container.style.width = '70px'; + container.style.height = '80px'; + fakeResizeObserver.fire(); + clock.tick(1000); + + expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; + expect(RFB.messages.setDesktopSize).to.have.been.calledWith( + sinon.match.object, 70, 80, 0x7890abcd, 0x12345678); + }); + it('should not resize until the container size is stable', function () { container.style.width = '20px'; container.style.height = '30px'; From 3193f808b5366392de94c1a9b144fc6845daeabb Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Fri, 27 Dec 2024 14:48:20 +0100 Subject: [PATCH 073/166] Comment different resize functions in rfb.js --- core/rfb.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/rfb.js b/core/rfb.js index e1482c3f4..87fac3c23 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -720,6 +720,7 @@ export default class RFB extends EventTargetMixin { currentHeight == this._expectedClientHeight; } + // Handle browser window resizes _handleResize() { // Don't change anything if the client size is already as expected if (this._clientHasExpectedSize()) { @@ -2872,6 +2873,7 @@ export default class RFB extends EventTargetMixin { this._fbWidth, this._fbHeight); } + // Handle resize-messages from the server _resize(width, height) { this._fbWidth = width; this._fbHeight = height; From 4bbed1dc12e6cd8b7a7fff23b5b7b3e9f7c8eecc Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sat, 11 Jan 2025 17:51:02 +0100 Subject: [PATCH 074/166] Standardize on 4 space-indentation in CSS files This is what we use in every other file in noVNC. It also much more common for a CSS file in general. By standardizing on 4 spaces we can avoid indentation mistakes. --- app/styles/base.css | 890 +++++++++++++++++++++---------------------- app/styles/input.css | 262 ++++++------- 2 files changed, 576 insertions(+), 576 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index 395c18e62..06e3d5ac0 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -30,30 +30,30 @@ */ :root { - font-family: sans-serif; + font-family: sans-serif; } body { - margin:0; - padding:0; - /*Background image with light grey curve.*/ - background-color:#494949; - background-repeat:no-repeat; - background-position:right bottom; - height:100%; - touch-action: none; + margin:0; + padding:0; + /*Background image with light grey curve.*/ + background-color:#494949; + background-repeat:no-repeat; + background-position:right bottom; + height:100%; + touch-action: none; } html { - height:100%; + height:100%; } .noVNC_only_touch.noVNC_hidden { - display: none; + display: none; } .noVNC_disabled { - color: rgb(128, 128, 128); + color: rgb(128, 128, 128); } /* ---------------------------------------- @@ -62,33 +62,33 @@ html { */ .noVNC_spinner { - position: relative; + position: relative; } .noVNC_spinner, .noVNC_spinner::before, .noVNC_spinner::after { - width: 10px; - height: 10px; - border-radius: 2px; - box-shadow: -60px 10px 0 rgba(255, 255, 255, 0); - animation: noVNC_spinner 1.0s linear infinite; + width: 10px; + height: 10px; + border-radius: 2px; + box-shadow: -60px 10px 0 rgba(255, 255, 255, 0); + animation: noVNC_spinner 1.0s linear infinite; } .noVNC_spinner::before { - content: ""; - position: absolute; - left: 0px; - top: 0px; - animation-delay: -0.1s; + content: ""; + position: absolute; + left: 0px; + top: 0px; + animation-delay: -0.1s; } .noVNC_spinner::after { - content: ""; - position: absolute; - top: 0px; - left: 0px; - animation-delay: 0.1s; + content: ""; + position: absolute; + top: 0px; + left: 0px; + animation-delay: 0.1s; } @keyframes noVNC_spinner { - 0% { box-shadow: -60px 10px 0 rgba(255, 255, 255, 0); width: 20px; } - 25% { box-shadow: 20px 10px 0 rgba(255, 255, 255, 1); width: 10px; } - 50% { box-shadow: 60px 10px 0 rgba(255, 255, 255, 0); width: 10px; } + 0% { box-shadow: -60px 10px 0 rgba(255, 255, 255, 0); width: 20px; } + 25% { box-shadow: 20px 10px 0 rgba(255, 255, 255, 1); width: 10px; } + 50% { box-shadow: 60px 10px 0 rgba(255, 255, 255, 0); width: 10px; } } /* ---------------------------------------- @@ -97,39 +97,39 @@ html { */ .noVNC_center { - /* - * This is a workaround because webkit misrenders transforms and - * uses non-integer coordinates, resulting in blurry content. - * Ideally we'd use "top: 50%; transform: translateY(-50%);" on - * the objects instead. - */ - display: flex; - align-items: center; - justify-content: center; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - pointer-events: none; + /* + * This is a workaround because webkit misrenders transforms and + * uses non-integer coordinates, resulting in blurry content. + * Ideally we'd use "top: 50%; transform: translateY(-50%);" on + * the objects instead. + */ + display: flex; + align-items: center; + justify-content: center; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; } .noVNC_center > * { - pointer-events: auto; + pointer-events: auto; } .noVNC_vcenter { - display: flex !important; - flex-direction: column; - justify-content: center; - position: fixed; - top: 0; - left: 0; - height: 100%; - margin: 0 !important; - padding: 0 !important; - pointer-events: none; + display: flex !important; + flex-direction: column; + justify-content: center; + position: fixed; + top: 0; + left: 0; + height: 100%; + margin: 0 !important; + padding: 0 !important; + pointer-events: none; } .noVNC_vcenter > * { - pointer-events: auto; + pointer-events: auto; } /* ---------------------------------------- @@ -138,7 +138,7 @@ html { */ .noVNC_connect_layer { - z-index: 60; + z-index: 60; } /* ---------------------------------------- @@ -147,69 +147,69 @@ html { */ #noVNC_fallback_error { - z-index: 1000; - visibility: hidden; - /* Put a dark background in front of everything but the error, - and don't let mouse events pass through */ - background: rgba(0, 0, 0, 0.8); - pointer-events: all; + z-index: 1000; + visibility: hidden; + /* Put a dark background in front of everything but the error, + and don't let mouse events pass through */ + background: rgba(0, 0, 0, 0.8); + pointer-events: all; } #noVNC_fallback_error.noVNC_open { - visibility: visible; + visibility: visible; } #noVNC_fallback_error > div { - max-width: calc(100vw - 30px - 30px); - max-height: calc(100vh - 30px - 30px); - overflow: auto; + max-width: calc(100vw - 30px - 30px); + max-height: calc(100vh - 30px - 30px); + overflow: auto; - padding: 15px; + padding: 15px; - transition: 0.5s ease-in-out; + transition: 0.5s ease-in-out; - transform: translateY(-50px); - opacity: 0; + transform: translateY(-50px); + opacity: 0; - text-align: center; - font-weight: bold; - color: #fff; + text-align: center; + font-weight: bold; + color: #fff; - border-radius: 10px; - box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5); - background: rgba(200,55,55,0.8); + border-radius: 10px; + box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5); + background: rgba(200,55,55,0.8); } #noVNC_fallback_error.noVNC_open > div { - transform: translateY(0); - opacity: 1; + transform: translateY(0); + opacity: 1; } #noVNC_fallback_errormsg { - font-weight: normal; + font-weight: normal; } #noVNC_fallback_errormsg .noVNC_message { - display: inline-block; - text-align: left; - font-family: monospace; - white-space: pre-wrap; + display: inline-block; + text-align: left; + font-family: monospace; + white-space: pre-wrap; } #noVNC_fallback_error .noVNC_location { - font-style: italic; - font-size: 0.8em; - color: rgba(255, 255, 255, 0.8); + font-style: italic; + font-size: 0.8em; + color: rgba(255, 255, 255, 0.8); } #noVNC_fallback_error .noVNC_stack { - padding: 10px; - margin: 10px; - font-size: 0.8em; - text-align: left; - font-family: monospace; - white-space: pre; - border: 1px solid rgba(0, 0, 0, 0.5); - background: rgba(0, 0, 0, 0.2); - overflow: auto; + padding: 10px; + margin: 10px; + font-size: 0.8em; + text-align: left; + font-family: monospace; + white-space: pre; + border: 1px solid rgba(0, 0, 0, 0.5); + background: rgba(0, 0, 0, 0.2); + overflow: auto; } /* ---------------------------------------- @@ -218,325 +218,325 @@ html { */ #noVNC_control_bar_anchor { - /* The anchor is needed to get z-stacking to work */ - position: fixed; - z-index: 10; + /* The anchor is needed to get z-stacking to work */ + position: fixed; + z-index: 10; - transition: 0.5s ease-in-out; + transition: 0.5s ease-in-out; - /* Edge misrenders animations wihthout this */ - transform: translateX(0); + /* Edge misrenders animations wihthout this */ + transform: translateX(0); } :root.noVNC_connected #noVNC_control_bar_anchor.noVNC_idle { - opacity: 0.8; + opacity: 0.8; } #noVNC_control_bar_anchor.noVNC_right { - left: auto; - right: 0; + left: auto; + right: 0; } #noVNC_control_bar { - position: relative; - left: -100%; + position: relative; + left: -100%; - transition: 0.5s ease-in-out; + transition: 0.5s ease-in-out; - background-color: rgb(110, 132, 163); - border-radius: 0 10px 10px 0; + background-color: rgb(110, 132, 163); + border-radius: 0 10px 10px 0; - user-select: none; - -webkit-user-select: none; - -webkit-touch-callout: none; /* Disable iOS image long-press popup */ + user-select: none; + -webkit-user-select: none; + -webkit-touch-callout: none; /* Disable iOS image long-press popup */ } #noVNC_control_bar.noVNC_open { - box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5); - left: 0; + box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5); + left: 0; } #noVNC_control_bar::before { - /* This extra element is to get a proper shadow */ - content: ""; - position: absolute; - z-index: -1; - height: 100%; - width: 30px; - left: -30px; - transition: box-shadow 0.5s ease-in-out; + /* This extra element is to get a proper shadow */ + content: ""; + position: absolute; + z-index: -1; + height: 100%; + width: 30px; + left: -30px; + transition: box-shadow 0.5s ease-in-out; } #noVNC_control_bar.noVNC_open::before { - box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5); + box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5); } .noVNC_right #noVNC_control_bar { - left: 100%; - border-radius: 10px 0 0 10px; + left: 100%; + border-radius: 10px 0 0 10px; } .noVNC_right #noVNC_control_bar.noVNC_open { - left: 0; + left: 0; } .noVNC_right #noVNC_control_bar::before { - visibility: hidden; + visibility: hidden; } #noVNC_control_bar_handle { - position: absolute; - left: -15px; - top: 0; - transform: translateY(35px); - width: calc(100% + 30px); - height: 50px; - z-index: -1; - cursor: pointer; - border-radius: 5px; - background-color: rgb(83, 99, 122); - background-image: url("../images/handle_bg.svg"); - background-repeat: no-repeat; - background-position: right; - box-shadow: 3px 3px 0px rgba(0, 0, 0, 0.5); + position: absolute; + left: -15px; + top: 0; + transform: translateY(35px); + width: calc(100% + 30px); + height: 50px; + z-index: -1; + cursor: pointer; + border-radius: 5px; + background-color: rgb(83, 99, 122); + background-image: url("../images/handle_bg.svg"); + background-repeat: no-repeat; + background-position: right; + box-shadow: 3px 3px 0px rgba(0, 0, 0, 0.5); } #noVNC_control_bar_handle:after { - content: ""; - transition: transform 0.5s ease-in-out; - background: url("../images/handle.svg"); - position: absolute; - top: 22px; /* (50px-6px)/2 */ - right: 5px; - width: 5px; - height: 6px; + content: ""; + transition: transform 0.5s ease-in-out; + background: url("../images/handle.svg"); + position: absolute; + top: 22px; /* (50px-6px)/2 */ + right: 5px; + width: 5px; + height: 6px; } #noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after { - transform: translateX(1px) rotate(180deg); + transform: translateX(1px) rotate(180deg); } :root:not(.noVNC_connected) #noVNC_control_bar_handle { - display: none; + display: none; } .noVNC_right #noVNC_control_bar_handle { - background-position: left; + background-position: left; } .noVNC_right #noVNC_control_bar_handle:after { - left: 5px; - right: 0; - transform: translateX(1px) rotate(180deg); + left: 5px; + right: 0; + transform: translateX(1px) rotate(180deg); } .noVNC_right #noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after { - transform: none; + transform: none; } /* Larger touch area for the handle, used when a touch screen is available */ #noVNC_control_bar_handle div { - position: absolute; - right: -35px; - top: 0; - width: 50px; - height: 100%; - display: none; + position: absolute; + right: -35px; + top: 0; + width: 50px; + height: 100%; + display: none; } @media (any-pointer: coarse) { - #noVNC_control_bar_handle div { - display: initial; - } + #noVNC_control_bar_handle div { + display: initial; + } } .noVNC_right #noVNC_control_bar_handle div { - left: -35px; - right: auto; + left: -35px; + right: auto; } #noVNC_control_bar > .noVNC_scroll { - max-height: 100vh; /* Chrome is buggy with 100% */ - overflow-x: hidden; - overflow-y: auto; - padding: 0 10px; + max-height: 100vh; /* Chrome is buggy with 100% */ + overflow-x: hidden; + overflow-y: auto; + padding: 0 10px; } #noVNC_control_bar > .noVNC_scroll > * { - display: block; - margin: 10px auto; + display: block; + margin: 10px auto; } /* Control bar hint */ #noVNC_hint_anchor { - position: fixed; - right: -50px; - left: auto; + position: fixed; + right: -50px; + left: auto; } #noVNC_control_bar_anchor.noVNC_right + #noVNC_hint_anchor { - left: -50px; - right: auto; + left: -50px; + right: auto; } #noVNC_control_bar_hint { - position: relative; - transform: scale(0); - width: 100px; - height: 50%; - max-height: 600px; - - visibility: hidden; - opacity: 0; - transition: 0.2s ease-in-out; - background: transparent; - box-shadow: 0 0 10px black, inset 0 0 10px 10px rgba(110, 132, 163, 0.8); - border-radius: 10px; - transition-delay: 0s; + position: relative; + transform: scale(0); + width: 100px; + height: 50%; + max-height: 600px; + + visibility: hidden; + opacity: 0; + transition: 0.2s ease-in-out; + background: transparent; + box-shadow: 0 0 10px black, inset 0 0 10px 10px rgba(110, 132, 163, 0.8); + border-radius: 10px; + transition-delay: 0s; } #noVNC_control_bar_hint.noVNC_active { - visibility: visible; - opacity: 1; - transition-delay: 0.2s; - transform: scale(1); + visibility: visible; + opacity: 1; + transition-delay: 0.2s; + transform: scale(1); } #noVNC_control_bar_hint.noVNC_notransition { - transition: none !important; + transition: none !important; } /* Control bar buttons */ #noVNC_control_bar .noVNC_button { - padding: 4px 4px; - vertical-align: middle; - border:1px solid rgba(255, 255, 255, 0.2); - border-radius: 6px; - background-color: transparent; - background-image: unset; /* we don't want the gradiant from input.css */ + padding: 4px 4px; + vertical-align: middle; + border:1px solid rgba(255, 255, 255, 0.2); + border-radius: 6px; + background-color: transparent; + background-image: unset; /* we don't want the gradiant from input.css */ } #noVNC_control_bar .noVNC_button.noVNC_selected { - border-color: rgba(0, 0, 0, 0.8); - background-color: rgba(0, 0, 0, 0.5); + border-color: rgba(0, 0, 0, 0.8); + background-color: rgba(0, 0, 0, 0.5); } #noVNC_control_bar .noVNC_button.noVNC_selected:not(:disabled):hover { - border-color: rgba(0, 0, 0, 0.4); - background-color: rgba(0, 0, 0, 0.2); + border-color: rgba(0, 0, 0, 0.4); + background-color: rgba(0, 0, 0, 0.2); } #noVNC_control_bar .noVNC_button:not(:disabled):hover { - background-color: rgba(255, 255, 255, 0.2); + background-color: rgba(255, 255, 255, 0.2); } #noVNC_control_bar .noVNC_button:not(:disabled):active { - padding-top: 5px; - padding-bottom: 3px; + padding-top: 5px; + padding-bottom: 3px; } #noVNC_control_bar .noVNC_button.noVNC_hidden { - display: none !important; + display: none !important; } /* Android browsers don't properly update hover state if touch events are * intercepted, like they are when clicking on the remote screen. */ @media (any-pointer: coarse) { - #noVNC_control_bar .noVNC_button:not(:disabled):hover { - background-color: transparent; - } - #noVNC_control_bar .noVNC_button.noVNC_selected:not(:disabled):hover { - border-color: rgba(0, 0, 0, 0.8); - background-color: rgba(0, 0, 0, 0.5); - } + #noVNC_control_bar .noVNC_button:not(:disabled):hover { + background-color: transparent; + } + #noVNC_control_bar .noVNC_button.noVNC_selected:not(:disabled):hover { + border-color: rgba(0, 0, 0, 0.8); + background-color: rgba(0, 0, 0, 0.5); + } } /* Panels */ .noVNC_panel { - transform: translateX(25px); + transform: translateX(25px); - transition: 0.5s ease-in-out; + transition: 0.5s ease-in-out; - box-sizing: border-box; /* so max-width don't have to care about padding */ - max-width: calc(100vw - 75px - 25px); /* minus left and right margins */ - max-height: 100vh; /* Chrome is buggy with 100% */ - overflow-x: hidden; - overflow-y: auto; + box-sizing: border-box; /* so max-width don't have to care about padding */ + max-width: calc(100vw - 75px - 25px); /* minus left and right margins */ + max-height: 100vh; /* Chrome is buggy with 100% */ + overflow-x: hidden; + overflow-y: auto; - visibility: hidden; - opacity: 0; + visibility: hidden; + opacity: 0; - padding: 15px; + padding: 15px; - background: #fff; - border-radius: 10px; - color: #000; - border: 2px solid #E0E0E0; - box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5); + background: #fff; + border-radius: 10px; + color: #000; + border: 2px solid #E0E0E0; + box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5); } .noVNC_panel.noVNC_open { - visibility: visible; - opacity: 1; - transform: translateX(75px); + visibility: visible; + opacity: 1; + transform: translateX(75px); } .noVNC_right .noVNC_vcenter { - left: auto; - right: 0; + left: auto; + right: 0; } .noVNC_right .noVNC_panel { - transform: translateX(-25px); + transform: translateX(-25px); } .noVNC_right .noVNC_panel.noVNC_open { - transform: translateX(-75px); + transform: translateX(-75px); } .noVNC_panel > * { - display: block; - margin: 10px auto; + display: block; + margin: 10px auto; } .noVNC_panel > *:first-child { - margin-top: 0 !important; + margin-top: 0 !important; } .noVNC_panel > *:last-child { - margin-bottom: 0 !important; + margin-bottom: 0 !important; } .noVNC_panel hr { - border: none; - border-top: 1px solid rgb(192, 192, 192); + border: none; + border-top: 1px solid rgb(192, 192, 192); } .noVNC_panel label { - display: block; - white-space: nowrap; - margin: 5px; + display: block; + white-space: nowrap; + margin: 5px; } .noVNC_panel li { - margin: 5px; + margin: 5px; } .noVNC_panel .noVNC_heading { - background-color: rgb(110, 132, 163); - border-radius: 5px; - padding: 5px; - /* Compensate for padding in image */ - padding-right: 8px; - color: white; - font-size: 20px; - white-space: nowrap; + background-color: rgb(110, 132, 163); + border-radius: 5px; + padding: 5px; + /* Compensate for padding in image */ + padding-right: 8px; + color: white; + font-size: 20px; + white-space: nowrap; } .noVNC_panel .noVNC_heading img { - vertical-align: bottom; + vertical-align: bottom; } .noVNC_submit { - float: right; + float: right; } /* Expanders */ .noVNC_expander { - cursor: pointer; + cursor: pointer; } .noVNC_expander::before { - content: url("../images/expander.svg"); - display: inline-block; - margin-right: 5px; - transition: 0.2s ease-in-out; + content: url("../images/expander.svg"); + display: inline-block; + margin-right: 5px; + transition: 0.2s ease-in-out; } .noVNC_expander.noVNC_open::before { - transform: rotateZ(90deg); + transform: rotateZ(90deg); } .noVNC_expander ~ * { - margin: 5px; - margin-left: 10px; - padding: 5px; - background: rgba(0, 0, 0, 0.05); - border-radius: 5px; + margin: 5px; + margin-left: 10px; + padding: 5px; + background: rgba(0, 0, 0, 0.05); + border-radius: 5px; } .noVNC_expander:not(.noVNC_open) ~ * { - display: none; + display: none; } /* Control bar content */ #noVNC_control_bar .noVNC_logo { - font-size: 13px; + font-size: 13px; } .noVNC_logo + hr { @@ -546,92 +546,92 @@ html { } :root:not(.noVNC_connected) #noVNC_view_drag_button { - display: none; + display: none; } /* noVNC Touch Device only buttons */ :root:not(.noVNC_connected) #noVNC_mobile_buttons { - display: none; + display: none; } @media not all and (any-pointer: coarse) { - /* FIXME: The button for the virtual keyboard is the only button in this - group of "mobile buttons". It is bad to assume that no touch - devices have physical keyboards available. Hopefully we can get - a media query for this: - https://github.com/w3c/csswg-drafts/issues/3871 */ - :root.noVNC_connected #noVNC_mobile_buttons { - display: none; - } + /* FIXME: The button for the virtual keyboard is the only button in this + group of "mobile buttons". It is bad to assume that no touch + devices have physical keyboards available. Hopefully we can get + a media query for this: + https://github.com/w3c/csswg-drafts/issues/3871 */ + :root.noVNC_connected #noVNC_mobile_buttons { + display: none; + } } /* Extra manual keys */ :root:not(.noVNC_connected) #noVNC_toggle_extra_keys_button { - display: none; + display: none; } #noVNC_modifiers { - background-color: rgb(92, 92, 92); - border: none; - padding: 10px; + background-color: rgb(92, 92, 92); + border: none; + padding: 10px; } /* Shutdown/Reboot */ :root:not(.noVNC_connected) #noVNC_power_button { - display: none; + display: none; } #noVNC_power { } #noVNC_power_buttons { - display: none; + display: none; } #noVNC_power input[type=button] { - width: 100%; + width: 100%; } /* Clipboard */ :root:not(.noVNC_connected) #noVNC_clipboard_button { - display: none; + display: none; } #noVNC_clipboard_text { - width: 360px; - min-width: 150px; - height: 160px; - min-height: 70px; + width: 360px; + min-width: 150px; + height: 160px; + min-height: 70px; - box-sizing: border-box; - max-width: 100%; - /* minus approximate height of title, height of subtitle, and margin */ - max-height: calc(100vh - 10em - 25px); + box-sizing: border-box; + max-width: 100%; + /* minus approximate height of title, height of subtitle, and margin */ + max-height: calc(100vh - 10em - 25px); } /* Settings */ #noVNC_settings { } #noVNC_settings ul { - list-style: none; - padding: 0px; + list-style: none; + padding: 0px; } #noVNC_setting_port { - width: 80px; + width: 80px; } #noVNC_setting_path { - width: 100px; + width: 100px; } /* Version */ .noVNC_version_wrapper { - font-size: small; + font-size: small; } .noVNC_version { - margin-left: 1rem; + margin-left: 1rem; } /* Connection controls */ :root:not(.noVNC_connected) #noVNC_disconnect_button { - display: none; + display: none; } /* ---------------------------------------- @@ -640,64 +640,64 @@ html { */ #noVNC_status { - position: fixed; - top: 0; - left: 0; - width: 100%; - z-index: 100; - transform: translateY(-100%); + position: fixed; + top: 0; + left: 0; + width: 100%; + z-index: 100; + transform: translateY(-100%); - cursor: pointer; + cursor: pointer; - transition: 0.5s ease-in-out; + transition: 0.5s ease-in-out; - visibility: hidden; - opacity: 0; + visibility: hidden; + opacity: 0; - padding: 5px; + padding: 5px; - display: flex; - flex-direction: row; - justify-content: center; - align-content: center; + display: flex; + flex-direction: row; + justify-content: center; + align-content: center; - line-height: 1.6; - word-wrap: break-word; - color: #fff; + line-height: 1.6; + word-wrap: break-word; + color: #fff; - border-bottom: 1px solid rgba(0, 0, 0, 0.9); + border-bottom: 1px solid rgba(0, 0, 0, 0.9); } #noVNC_status.noVNC_open { - transform: translateY(0); - visibility: visible; - opacity: 1; + transform: translateY(0); + visibility: visible; + opacity: 1; } #noVNC_status::before { - content: ""; - display: inline-block; - width: 25px; - height: 25px; - margin-right: 5px; + content: ""; + display: inline-block; + width: 25px; + height: 25px; + margin-right: 5px; } #noVNC_status.noVNC_status_normal { - background: rgba(128,128,128,0.9); + background: rgba(128,128,128,0.9); } #noVNC_status.noVNC_status_normal::before { - content: url("../images/info.svg") " "; + content: url("../images/info.svg") " "; } #noVNC_status.noVNC_status_error { - background: rgba(200,55,55,0.9); + background: rgba(200,55,55,0.9); } #noVNC_status.noVNC_status_error::before { - content: url("../images/error.svg") " "; + content: url("../images/error.svg") " "; } #noVNC_status.noVNC_status_warn { - background: rgba(180,180,30,0.9); + background: rgba(180,180,30,0.9); } #noVNC_status.noVNC_status_warn::before { - content: url("../images/warning.svg") " "; + content: url("../images/warning.svg") " "; } /* ---------------------------------------- @@ -706,67 +706,67 @@ html { */ #noVNC_connect_dlg { - transition: 0.5s ease-in-out; + transition: 0.5s ease-in-out; - transform: scale(0, 0); - visibility: hidden; - opacity: 0; + transform: scale(0, 0); + visibility: hidden; + opacity: 0; } #noVNC_connect_dlg.noVNC_open { - transform: scale(1, 1); - visibility: visible; - opacity: 1; + transform: scale(1, 1); + visibility: visible; + opacity: 1; } #noVNC_connect_dlg .noVNC_logo { - transition: 0.5s ease-in-out; - padding: 10px; - margin-bottom: 10px; + transition: 0.5s ease-in-out; + padding: 10px; + margin-bottom: 10px; - font-size: 80px; - text-align: center; + font-size: 80px; + text-align: center; - border-radius: 5px; + border-radius: 5px; } @media (max-width: 440px) { - #noVNC_connect_dlg { - max-width: calc(100vw - 100px); - } - #noVNC_connect_dlg .noVNC_logo { - font-size: calc(25vw - 30px); - } + #noVNC_connect_dlg { + max-width: calc(100vw - 100px); + } + #noVNC_connect_dlg .noVNC_logo { + font-size: calc(25vw - 30px); + } } #noVNC_connect_dlg div { - padding: 12px; + padding: 12px; - background-color: rgb(110, 132, 163); - border-radius: 12px; - text-align: center; - font-size: 20px; + background-color: rgb(110, 132, 163); + border-radius: 12px; + text-align: center; + font-size: 20px; - box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5); + box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5); } #noVNC_connect_button { - width: 100%; - padding: 5px 30px; + width: 100%; + padding: 5px 30px; - cursor: pointer; + cursor: pointer; - border-color: rgb(83, 99, 122); - border-radius: 5px; + border-color: rgb(83, 99, 122); + border-radius: 5px; - background: linear-gradient(to top, rgb(110, 132, 163), rgb(99, 119, 147)); - color: white; + background: linear-gradient(to top, rgb(110, 132, 163), rgb(99, 119, 147)); + color: white; - /* This avoids it jumping around when :active */ - vertical-align: middle; + /* This avoids it jumping around when :active */ + vertical-align: middle; } #noVNC_connect_button:hover { - background: linear-gradient(to top, rgb(110, 132, 163), rgb(105, 125, 155)); + background: linear-gradient(to top, rgb(110, 132, 163), rgb(105, 125, 155)); } #noVNC_connect_button img { - vertical-align: bottom; - height: 1.3em; + vertical-align: bottom; + height: 1.3em; } /* ---------------------------------------- @@ -775,15 +775,15 @@ html { */ #noVNC_verify_server_dlg { - position: relative; + position: relative; - transform: translateY(-50px); + transform: translateY(-50px); } #noVNC_verify_server_dlg.noVNC_open { - transform: translateY(0); + transform: translateY(0); } #noVNC_fingerprint_block { - margin: 10px; + margin: 10px; } /* ---------------------------------------- @@ -792,16 +792,16 @@ html { */ #noVNC_credentials_dlg { - position: relative; + position: relative; - transform: translateY(-50px); + transform: translateY(-50px); } #noVNC_credentials_dlg.noVNC_open { - transform: translateY(0); + transform: translateY(0); } #noVNC_username_block.noVNC_hidden, #noVNC_password_block.noVNC_hidden { - display: none; + display: none; } @@ -812,90 +812,90 @@ html { /* Transition screen */ #noVNC_transition { - transition: 0.5s ease-in-out; + transition: 0.5s ease-in-out; - display: flex; - opacity: 0; - visibility: hidden; + display: flex; + opacity: 0; + visibility: hidden; - position: fixed; - top: 0; - left: 0; - bottom: 0; - right: 0; + position: fixed; + top: 0; + left: 0; + bottom: 0; + right: 0; - color: white; - background: rgba(0, 0, 0, 0.5); - z-index: 50; + color: white; + background: rgba(0, 0, 0, 0.5); + z-index: 50; - /*display: flex;*/ - align-items: center; - justify-content: center; - flex-direction: column; + /*display: flex;*/ + align-items: center; + justify-content: center; + flex-direction: column; } :root.noVNC_loading #noVNC_transition, :root.noVNC_connecting #noVNC_transition, :root.noVNC_disconnecting #noVNC_transition, :root.noVNC_reconnecting #noVNC_transition { - opacity: 1; - visibility: visible; + opacity: 1; + visibility: visible; } :root:not(.noVNC_reconnecting) #noVNC_cancel_reconnect_button { - display: none; + display: none; } #noVNC_transition_text { - font-size: 1.5em; + font-size: 1.5em; } /* Main container */ #noVNC_container { - width: 100%; - height: 100%; - background-color: #313131; - border-bottom-right-radius: 800px 600px; - /*border-top-left-radius: 800px 600px;*/ + width: 100%; + height: 100%; + background-color: #313131; + border-bottom-right-radius: 800px 600px; + /*border-top-left-radius: 800px 600px;*/ - /* If selection isn't disabled, long-pressing stuff in the sidebar - can accidentally select the container or the canvas. This can - happen when attempting to move the handle. */ - user-select: none; - -webkit-user-select: none; + /* If selection isn't disabled, long-pressing stuff in the sidebar + can accidentally select the container or the canvas. This can + happen when attempting to move the handle. */ + user-select: none; + -webkit-user-select: none; } #noVNC_keyboardinput { - width: 1px; - height: 1px; - background-color: #fff; - color: #fff; - border: 0; - position: absolute; - left: -40px; - z-index: -1; - ime-mode: disabled; + width: 1px; + height: 1px; + background-color: #fff; + color: #fff; + border: 0; + position: absolute; + left: -40px; + z-index: -1; + ime-mode: disabled; } /*Default noVNC logo.*/ /* From: http://fonts.googleapis.com/css?family=Orbitron:700 */ @font-face { - font-family: 'Orbitron'; - font-style: normal; - font-weight: 700; - src: local('?'), url('Orbitron700.woff') format('woff'), - url('Orbitron700.ttf') format('truetype'); + font-family: 'Orbitron'; + font-style: normal; + font-weight: 700; + src: local('?'), url('Orbitron700.woff') format('woff'), + url('Orbitron700.ttf') format('truetype'); } .noVNC_logo { - color:yellow; - font-family: 'Orbitron', 'OrbitronTTF', sans-serif; - line-height: 0.9; - text-shadow: 0.1em 0.1em 0 black; + color:yellow; + font-family: 'Orbitron', 'OrbitronTTF', sans-serif; + line-height: 0.9; + text-shadow: 0.1em 0.1em 0 black; } .noVNC_logo span{ - color:green; + color:green; } #noVNC_bell { - display: none; + display: none; } /* ---------------------------------------- @@ -904,19 +904,19 @@ html { */ @media screen and (max-width: 640px){ - #noVNC_logo { - font-size: 150px; - } + #noVNC_logo { + font-size: 150px; + } } @media screen and (min-width: 321px) and (max-width: 480px) { - #noVNC_logo { - font-size: 110px; - } + #noVNC_logo { + font-size: 110px; + } } @media screen and (max-width: 320px) { - #noVNC_logo { - font-size: 90px; - } + #noVNC_logo { + font-size: 90px; + } } diff --git a/app/styles/input.css b/app/styles/input.css index 911cf20cf..a0cba473f 100644 --- a/app/styles/input.css +++ b/app/styles/input.css @@ -9,19 +9,19 @@ * Common for all inputs */ input, input::file-selector-button, button, select, textarea { - /* Respect standard font settings */ - font: inherit; + /* Respect standard font settings */ + font: inherit; - /* Disable default rendering */ - appearance: none; - background: none; + /* Disable default rendering */ + appearance: none; + background: none; - padding: 5px; - border: 1px solid rgb(192, 192, 192); - border-radius: 5px; - color: black; - --bg-gradient: linear-gradient(to top, rgb(255, 255, 255) 80%, rgb(240, 240, 240)); - background-image: var(--bg-gradient); + padding: 5px; + border: 1px solid rgb(192, 192, 192); + border-radius: 5px; + color: black; + --bg-gradient: linear-gradient(to top, rgb(255, 255, 255) 80%, rgb(240, 240, 240)); + background-image: var(--bg-gradient); } /* @@ -35,151 +35,151 @@ input[type=submit], input::file-selector-button, button, select { - border-bottom-width: 2px; + border-bottom-width: 2px; - /* This avoids it jumping around when :active */ - vertical-align: middle; - margin-top: 0; + /* This avoids it jumping around when :active */ + vertical-align: middle; + margin-top: 0; - padding-left: 20px; - padding-right: 20px; + padding-left: 20px; + padding-right: 20px; - /* Disable Chrome's touch tap highlight */ - -webkit-tap-highlight-color: transparent; + /* Disable Chrome's touch tap highlight */ + -webkit-tap-highlight-color: transparent; } /* * Select dropdowns */ select { - --select-arrow: url('data:image/svg+xml;utf8, \ - \ - \ - '); - background-image: var(--select-arrow), var(--bg-gradient); - background-position: calc(100% - 7px), left top; - background-repeat: no-repeat; - padding-right: calc(2*7px + 8px); - padding-left: 7px; + --select-arrow: url('data:image/svg+xml;utf8, \ + \ + \ + '); + background-image: var(--select-arrow), var(--bg-gradient); + background-position: calc(100% - 7px), left top; + background-repeat: no-repeat; + padding-right: calc(2*7px + 8px); + padding-left: 7px; } /* FIXME: :active isn't set when the is opened in Firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=1805406 */ From 72cac2ef6a6f5f4536fcb2ec2f430a78bd0a9194 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sat, 11 Jan 2025 23:20:33 +0100 Subject: [PATCH 076/166] Add margin between label and input in noVNC_panel To make stuff feel less cramped, lets add some margin here. As of comitting this, it only affects the logging-level select dropdown in the settings, but this is a general rule of thumb. It doesn't apply to checkboxes or radios since they have a margin by default, and their label to the left. --- app/styles/base.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/styles/base.css b/app/styles/base.css index 06e3d5ac0..f2f49869f 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -491,6 +491,13 @@ html { margin: 5px; } +.noVNC_panel label > button, +.noVNC_panel label > select, +.noVNC_panel label > textarea, +.noVNC_panel label > input:not([type=checkbox]):not([type=radio]) { + margin-left: 6px; +} + .noVNC_panel .noVNC_heading { background-color: rgb(110, 132, 163); border-radius: 5px; From 7603ced54e9e73fb969b705da50716699fbee608 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sun, 12 Jan 2025 00:09:25 +0100 Subject: [PATCH 077/166] Create CSS variables for common noVNC-colors --- app/styles/base.css | 26 +++++++++++++------------- app/styles/constants.css | 21 +++++++++++++++++++++ app/styles/input.css | 14 +++++++------- vnc.html | 1 + 4 files changed, 42 insertions(+), 20 deletions(-) create mode 100644 app/styles/constants.css diff --git a/app/styles/base.css b/app/styles/base.css index f2f49869f..ecef3771a 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -53,7 +53,7 @@ html { } .noVNC_disabled { - color: rgb(128, 128, 128); + color: var(--novnc-grey); } /* ---------------------------------------- @@ -241,7 +241,7 @@ html { transition: 0.5s ease-in-out; - background-color: rgb(110, 132, 163); + background-color: var(--novnc-blue); border-radius: 0 10px 10px 0; user-select: none; @@ -286,7 +286,7 @@ html { z-index: -1; cursor: pointer; border-radius: 5px; - background-color: rgb(83, 99, 122); + background-color: var(--novnc-darkblue); background-image: url("../images/handle_bg.svg"); background-repeat: no-repeat; background-position: right; @@ -371,7 +371,7 @@ html { opacity: 0; transition: 0.2s ease-in-out; background: transparent; - box-shadow: 0 0 10px black, inset 0 0 10px 10px rgba(110, 132, 163, 0.8); + box-shadow: 0 0 10px black, inset 0 0 10px 10px var(--novnc-darkblue); border-radius: 10px; transition-delay: 0s; } @@ -478,7 +478,7 @@ html { .noVNC_panel hr { border: none; - border-top: 1px solid rgb(192, 192, 192); + border-top: 1px solid var(--novnc-lightgrey); } .noVNC_panel label { @@ -499,7 +499,7 @@ html { } .noVNC_panel .noVNC_heading { - background-color: rgb(110, 132, 163); + background-color: var(--novnc-blue); border-radius: 5px; padding: 5px; /* Compensate for padding in image */ @@ -577,7 +577,7 @@ html { } #noVNC_modifiers { - background-color: rgb(92, 92, 92); + background-color: var(--novnc-darkgrey); border: none; padding: 10px; } @@ -745,7 +745,7 @@ html { #noVNC_connect_dlg div { padding: 12px; - background-color: rgb(110, 132, 163); + background-color: var(--novnc-blue); border-radius: 12px; text-align: center; font-size: 20px; @@ -758,17 +758,17 @@ html { cursor: pointer; - border-color: rgb(83, 99, 122); + border-color: var(--novnc-darkblue); border-radius: 5px; - background: linear-gradient(to top, rgb(110, 132, 163), rgb(99, 119, 147)); + background: linear-gradient(to top, var(--novnc-blue), rgb(99, 119, 147)); color: white; /* This avoids it jumping around when :active */ vertical-align: middle; } #noVNC_connect_button:hover { - background: linear-gradient(to top, rgb(110, 132, 163), rgb(105, 125, 155)); + background: linear-gradient(to top, var(--novnc-blue), rgb(105, 125, 155)); } #noVNC_connect_button img { @@ -892,13 +892,13 @@ html { } .noVNC_logo { - color:yellow; + color: var(--novnc-yellow); font-family: 'Orbitron', 'OrbitronTTF', sans-serif; line-height: 0.9; text-shadow: 0.1em 0.1em 0 black; } .noVNC_logo span{ - color:green; + color: var(--novnc-green); } #noVNC_bell { diff --git a/app/styles/constants.css b/app/styles/constants.css new file mode 100644 index 000000000..fb1f5509a --- /dev/null +++ b/app/styles/constants.css @@ -0,0 +1,21 @@ +/* + * noVNC general CSS constant variables + * Copyright (C) 2025 The noVNC authors + * noVNC is licensed under the MPL 2.0 (see LICENSE.txt) + * This file is licensed under the 2-Clause BSD license (see LICENSE.txt). + */ + +/* ---------- COLORS ----------- */ + +:root { + --novnc-grey: rgb(128, 128, 128); + --novnc-lightgrey: rgb(192, 192, 192); + --novnc-darkgrey: rgb(92, 92, 92); + + --novnc-blue: rgb(110, 132, 163); + --novnc-lightblue: rgb(74, 144, 217); + --novnc-darkblue: rgb(83, 99, 122); + + --novnc-green: rgb(0, 128, 0); + --novnc-yellow: rgb(255, 255, 0); +} diff --git a/app/styles/input.css b/app/styles/input.css index fc36e71e2..a2b289bf5 100644 --- a/app/styles/input.css +++ b/app/styles/input.css @@ -21,7 +21,7 @@ input, input::file-selector-button, button, select, textarea { background: none; padding: 0.5em var(--input-xpadding); - border: 1px solid rgb(192, 192, 192); + border: 1px solid var(--novnc-lightgrey); border-radius: 5px; color: black; --bg-gradient: linear-gradient(to top, rgb(255, 255, 255) 80%, rgb(240, 240, 240)); @@ -101,8 +101,8 @@ input[type=checkbox] { transition: 0.2s background-color linear; } input[type=checkbox]:checked { - background-color: rgb(110, 132, 163); - border-color: rgb(110, 132, 163); + background-color: var(--novnc-blue); + border-color: var(--novnc-blue); } input[type=checkbox]:checked::after { content: ""; @@ -127,7 +127,7 @@ input[type=radio] { transition: 0.2s border linear; } input[type=radio]:checked { - border: 6px solid rgb(110, 132, 163); + border: 6px solid var(--novnc-blue); } /* @@ -143,12 +143,12 @@ input[type=range] { /* -webkit-slider.. & -moz-range.. cant be in selector lists: https://bugs.chromium.org/p/chromium/issues/detail?id=1154623 */ input[type=range]::-webkit-slider-runnable-track { - background-color: rgb(110, 132, 163); + background-color: var(--novnc-blue); height: 6px; border-radius: 3px; } input[type=range]::-moz-range-track { - background-color: rgb(110, 132, 163); + background-color: var(--novnc-blue); height: 6px; border-radius: 3px; } @@ -239,7 +239,7 @@ input:focus-visible::file-selector-button, button:focus-visible, select:focus-visible, textarea:focus-visible { - outline: 2px solid rgb(74, 144, 217); + outline: 2px solid var(--novnc-lightblue); outline-offset: 1px; } input[type=file]:focus-visible { diff --git a/vnc.html b/vnc.html index c2cc4e555..ed82b603f 100644 --- a/vnc.html +++ b/vnc.html @@ -37,6 +37,7 @@ + From 7a4d1a82749060ba5f3167e387e1c39af5c8c9ff Mon Sep 17 00:00:00 2001 From: Adam Halim Date: Thu, 9 Jan 2025 14:51:44 +0100 Subject: [PATCH 078/166] Move mouse event help functions to broader scope These functions can be used elsewhere in the tests. We want to use these in the dragging tests in the future instead of directly calling private methods. --- tests/test.rfb.js | 163 +++++++++++++++++++++++----------------------- 1 file changed, 82 insertions(+), 81 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 7e4063217..c8d2693f4 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -158,6 +158,50 @@ describe('Remote Frame Buffer protocol client', function () { return rfb; } + function elementToClient(x, y, client) { + let res = { x: 0, y: 0 }; + + let bounds = client._canvas.getBoundingClientRect(); + + /* + * If the canvas is on a fractional position we will calculate + * a fractional mouse position. But that gets truncated when we + * send the event, AND the same thing happens in RFB when it + * generates the PointerEvent message. To compensate for that + * fact we round the value upwards here. + */ + res.x = Math.ceil(bounds.left + x); + res.y = Math.ceil(bounds.top + y); + + return res; + } + + function sendMouseMoveEvent(x, y, client) { + let pos = elementToClient(x, y, client); + let ev; + + ev = new MouseEvent('mousemove', + { 'screenX': pos.x + window.screenX, + 'screenY': pos.y + window.screenY, + 'clientX': pos.x, + 'clientY': pos.y }); + client._canvas.dispatchEvent(ev); + } + + function sendMouseButtonEvent(x, y, down, button, client) { + let pos = elementToClient(x, y, client); + let ev; + + ev = new MouseEvent(down ? 'mousedown' : 'mouseup', + { 'screenX': pos.x + window.screenX, + 'screenY': pos.y + window.screenY, + 'clientX': pos.x, + 'clientY': pos.y, + 'button': button, + 'buttons': 1 << button }); + client._canvas.dispatchEvent(ev); + } + describe('Connecting/Disconnecting', function () { describe('#RFB (constructor)', function () { let open, attach; @@ -3584,107 +3628,64 @@ describe('Remote Frame Buffer protocol client', function () { qemuKeyEvent.restore(); }); - function elementToClient(x, y) { - let res = { x: 0, y: 0 }; - - let bounds = client._canvas.getBoundingClientRect(); - - /* - * If the canvas is on a fractional position we will calculate - * a fractional mouse position. But that gets truncated when we - * send the event, AND the same thing happens in RFB when it - * generates the PointerEvent message. To compensate for that - * fact we round the value upwards here. - */ - res.x = Math.ceil(bounds.left + x); - res.y = Math.ceil(bounds.top + y); - - return res; - } - describe('Mouse events', function () { - function sendMouseMoveEvent(x, y) { - let pos = elementToClient(x, y); - let ev; - - ev = new MouseEvent('mousemove', - { 'screenX': pos.x + window.screenX, - 'screenY': pos.y + window.screenY, - 'clientX': pos.x, - 'clientY': pos.y }); - client._canvas.dispatchEvent(ev); - } - - function sendMouseButtonEvent(x, y, down, button) { - let pos = elementToClient(x, y); - let ev; - - ev = new MouseEvent(down ? 'mousedown' : 'mouseup', - { 'screenX': pos.x + window.screenX, - 'screenY': pos.y + window.screenY, - 'clientX': pos.x, - 'clientY': pos.y, - 'button': button, - 'buttons': 1 << button }); - client._canvas.dispatchEvent(ev); - } it('should not send button messages in view-only mode', function () { client._viewOnly = true; - sendMouseButtonEvent(10, 10, true, 0); + sendMouseButtonEvent(10, 10, true, 0, client); clock.tick(50); expect(pointerEvent).to.not.have.been.called; }); it('should not send movement messages in view-only mode', function () { client._viewOnly = true; - sendMouseMoveEvent(10, 10); + sendMouseMoveEvent(10, 10, client); clock.tick(50); expect(pointerEvent).to.not.have.been.called; }); it('should handle left mouse button', function () { - sendMouseButtonEvent(10, 10, true, 0); + sendMouseButtonEvent(10, 10, true, 0, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 10, 10, 0x1); pointerEvent.resetHistory(); - sendMouseButtonEvent(10, 10, false, 0); + sendMouseButtonEvent(10, 10, false, 0, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 10, 10, 0x0); }); it('should handle middle mouse button', function () { - sendMouseButtonEvent(10, 10, true, 1); + sendMouseButtonEvent(10, 10, true, 1, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 10, 10, 0x2); pointerEvent.resetHistory(); - sendMouseButtonEvent(10, 10, false, 1); + sendMouseButtonEvent(10, 10, false, 1, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 10, 10, 0x0); }); it('should handle right mouse button', function () { - sendMouseButtonEvent(10, 10, true, 2); + sendMouseButtonEvent(10, 10, true, 2, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 10, 10, 0x4); pointerEvent.resetHistory(); - sendMouseButtonEvent(10, 10, false, 2); + sendMouseButtonEvent(10, 10, false, 2, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 10, 10, 0x0); }); it('should handle multiple mouse buttons', function () { - sendMouseButtonEvent(10, 10, true, 0); - sendMouseButtonEvent(10, 10, true, 2); + sendMouseButtonEvent(10, 10, true, 0, client); + sendMouseButtonEvent(10, 10, true, 2, client); expect(pointerEvent).to.have.been.calledTwice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -3694,8 +3695,8 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); - sendMouseButtonEvent(10, 10, false, 0); - sendMouseButtonEvent(10, 10, false, 2); + sendMouseButtonEvent(10, 10, false, 0, client); + sendMouseButtonEvent(10, 10, false, 2, client); expect(pointerEvent).to.have.been.calledTwice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -3705,14 +3706,14 @@ describe('Remote Frame Buffer protocol client', function () { }); it('should handle mouse movement', function () { - sendMouseMoveEvent(50, 70); + sendMouseMoveEvent(50, 70, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 50, 70, 0x0); }); it('should handle click and drag', function () { - sendMouseButtonEvent(10, 10, true, 0); - sendMouseMoveEvent(50, 70); + sendMouseButtonEvent(10, 10, true, 0, client); + sendMouseMoveEvent(50, 70, client); expect(pointerEvent).to.have.been.calledTwice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -3722,7 +3723,7 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); - sendMouseButtonEvent(50, 70, false, 0); + sendMouseButtonEvent(50, 70, false, 0, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 50, 70, 0x0); @@ -3730,15 +3731,15 @@ describe('Remote Frame Buffer protocol client', function () { describe('Event aggregation', function () { it('should send a single pointer event on mouse movement', function () { - sendMouseMoveEvent(50, 70); + sendMouseMoveEvent(50, 70, client); clock.tick(100); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 50, 70, 0x0); }); it('should delay one move if two events are too close', function () { - sendMouseMoveEvent(18, 30); - sendMouseMoveEvent(20, 50); + sendMouseMoveEvent(18, 30, client); + sendMouseMoveEvent(20, 50, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 18, 30, 0x0); @@ -3751,9 +3752,9 @@ describe('Remote Frame Buffer protocol client', function () { }); it('should only send first and last move of many close events', function () { - sendMouseMoveEvent(18, 30); - sendMouseMoveEvent(20, 50); - sendMouseMoveEvent(21, 55); + sendMouseMoveEvent(18, 30, client); + sendMouseMoveEvent(20, 50, client); + sendMouseMoveEvent(21, 55, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 18, 30, 0x0); @@ -3767,46 +3768,46 @@ describe('Remote Frame Buffer protocol client', function () { // We selected the 17ms since that is ~60 FPS it('should send move events every 17 ms', function () { - sendMouseMoveEvent(1, 10); // instant send + sendMouseMoveEvent(1, 10, client); // instant send clock.tick(10); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 1, 10, 0x0); pointerEvent.resetHistory(); - sendMouseMoveEvent(2, 20); // delayed + sendMouseMoveEvent(2, 20, client); // delayed clock.tick(10); // timeout send expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 2, 20, 0x0); pointerEvent.resetHistory(); - sendMouseMoveEvent(3, 30); // delayed + sendMouseMoveEvent(3, 30, client); // delayed clock.tick(10); - sendMouseMoveEvent(4, 40); // delayed + sendMouseMoveEvent(4, 40, client); // delayed clock.tick(10); // timeout send expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 4, 40, 0x0); pointerEvent.resetHistory(); - sendMouseMoveEvent(5, 50); // delayed + sendMouseMoveEvent(5, 50, client); // delayed expect(pointerEvent).to.not.have.been.called; }); it('should send waiting move events before a button press', function () { - sendMouseMoveEvent(13, 9); + sendMouseMoveEvent(13, 9, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 13, 9, 0x0); pointerEvent.resetHistory(); - sendMouseMoveEvent(20, 70); + sendMouseMoveEvent(20, 70, client); expect(pointerEvent).to.not.have.been.called; - sendMouseButtonEvent(20, 70, true, 0); + sendMouseButtonEvent(20, 70, true, 0, client); expect(pointerEvent).to.have.been.calledTwice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -3816,7 +3817,7 @@ describe('Remote Frame Buffer protocol client', function () { }); it('should send move events with enough time apart normally', function () { - sendMouseMoveEvent(58, 60); + sendMouseMoveEvent(58, 60, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 58, 60, 0x0); @@ -3824,7 +3825,7 @@ describe('Remote Frame Buffer protocol client', function () { clock.tick(20); - sendMouseMoveEvent(25, 60); + sendMouseMoveEvent(25, 60, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 25, 60, 0x0); @@ -3832,13 +3833,13 @@ describe('Remote Frame Buffer protocol client', function () { }); it('should not send waiting move events if disconnected', function () { - sendMouseMoveEvent(88, 99); + sendMouseMoveEvent(88, 99, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 88, 99, 0x0); pointerEvent.resetHistory(); - sendMouseMoveEvent(66, 77); + sendMouseMoveEvent(66, 77, client); client.disconnect(); clock.tick(20); @@ -3857,7 +3858,7 @@ describe('Remote Frame Buffer protocol client', function () { describe('Wheel events', function () { function sendWheelEvent(x, y, dx, dy, mode=0) { - let pos = elementToClient(x, y); + let pos = elementToClient(x, y, client); let ev; ev = new WheelEvent('wheel', @@ -3990,7 +3991,7 @@ describe('Remote Frame Buffer protocol client', function () { describe('Gesture event handlers', function () { function gestureStart(gestureType, x, y, magnitudeX = 0, magnitudeY = 0) { - let pos = elementToClient(x, y); + let pos = elementToClient(x, y, client); let detail = {type: gestureType, clientX: pos.x, clientY: pos.y}; detail.magnitudeX = magnitudeX; @@ -4002,7 +4003,7 @@ describe('Remote Frame Buffer protocol client', function () { function gestureMove(gestureType, x, y, magnitudeX = 0, magnitudeY = 0) { - let pos = elementToClient(x, y); + let pos = elementToClient(x, y, client); let detail = {type: gestureType, clientX: pos.x, clientY: pos.y}; detail.magnitudeX = magnitudeX; @@ -4013,7 +4014,7 @@ describe('Remote Frame Buffer protocol client', function () { } function gestureEnd(gestureType, x, y) { - let pos = elementToClient(x, y); + let pos = elementToClient(x, y, client); let detail = {type: gestureType, clientX: pos.x, clientY: pos.y}; let ev = new CustomEvent('gestureend', { detail: detail }); client._canvas.dispatchEvent(ev); From de9d6888db04a6b43bc8a5d39f26c0dd51f71247 Mon Sep 17 00:00:00 2001 From: Adam Halim Date: Fri, 10 Jan 2025 14:22:14 +0100 Subject: [PATCH 079/166] Add unit test for wheel + buttons pressed --- tests/test.rfb.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index c8d2693f4..66050bb0d 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -3964,6 +3964,21 @@ describe('Remote Frame Buffer protocol client', function () { expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock, 10, 10, 0); }); + + it('should handle wheel event with buttons pressed', function () { + sendMouseButtonEvent(10, 10, true, 0, client); + sendWheelEvent(10, 10, 0, 50); + + expect(pointerEvent).to.have.been.called.calledThrice; + + expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, + 10, 10, 0x1); + expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock, + 10, 10, 0x11); + expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock, + 10, 10, 0x1); + }); + }); describe('Keyboard events', function () { From db22ec6ee6ecc88e8d45f60a9022facd64784143 Mon Sep 17 00:00:00 2001 From: Adam Halim Date: Thu, 9 Jan 2025 14:59:19 +0100 Subject: [PATCH 080/166] Split button click with dragging test --- tests/test.rfb.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 66050bb0d..861b808ad 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -686,9 +686,9 @@ describe('Remote Frame Buffer protocol client', function () { client._handleMouseButton(13, 9, 0x001); client._handleMouseButton(13, 9, 0x000); expect(RFB.messages.pointerEvent).to.have.been.calledTwice; + }); - RFB.messages.pointerEvent.resetHistory(); - + it('should send button messages when release with small movement', function () { // Small movement client._handleMouseButton(13, 9, 0x001); client._handleMouseMove(15, 14); From c3934e0938ebe10e2749b8a51d70aa208dc73346 Mon Sep 17 00:00:00 2001 From: Adam Halim Date: Tue, 14 Jan 2025 12:14:34 +0100 Subject: [PATCH 081/166] Move mouse move flushing to separate function --- core/rfb.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index 87fac3c23..3f946142d 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -1104,11 +1104,7 @@ export default class RFB extends EventTargetMixin { } // Flush waiting move event first - if (this._mouseMoveTimer !== null) { - clearTimeout(this._mouseMoveTimer); - this._mouseMoveTimer = null; - this._sendMouse(x, y, this._mouseButtonMask); - } + this._flushMouseMoveTimer(x, y); if (down) { this._mouseButtonMask |= bmask; @@ -1380,6 +1376,14 @@ export default class RFB extends EventTargetMixin { } } + _flushMouseMoveTimer(x, y) { + if (this._mouseMoveTimer !== null) { + clearTimeout(this._mouseMoveTimer); + this._mouseMoveTimer = null; + this._sendMouse(x, y, this._mouseButtonMask); + } + } + // Message handlers _negotiateProtocolVersion() { From dce8ab395b3efdc79ee5db12edfe390528bda6e5 Mon Sep 17 00:00:00 2001 From: Adam Halim Date: Thu, 9 Jan 2025 15:58:35 +0100 Subject: [PATCH 082/166] Dispatch mouse events in dragging unit tests This makes our tests reflect the real world better, as we now send real mouse events instead of calling private methods directly. --- tests/test.rfb.js | 47 +++++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 861b808ad..061aff307 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -668,7 +668,10 @@ describe('Remote Frame Buffer protocol client', function () { describe('Dragging', function () { beforeEach(function () { + client = makeRFB(); client.dragViewport = true; + client._display.resize(100, 100); + sinon.spy(RFB.messages, "pointerEvent"); }); @@ -677,35 +680,39 @@ describe('Remote Frame Buffer protocol client', function () { }); it('should not send button messages when initiating viewport dragging', function () { - client._handleMouseButton(13, 9, 0x001); + sendMouseButtonEvent(13, 9, true, 0, client); expect(RFB.messages.pointerEvent).to.not.have.been.called; }); it('should send button messages when release without movement', function () { // Just up and down - client._handleMouseButton(13, 9, 0x001); - client._handleMouseButton(13, 9, 0x000); + sendMouseButtonEvent(13, 9, true, 0, client); + sendMouseButtonEvent(13, 9, false, 0, client); + expect(RFB.messages.pointerEvent).to.have.been.calledTwice; }); it('should send button messages when release with small movement', function () { // Small movement - client._handleMouseButton(13, 9, 0x001); - client._handleMouseMove(15, 14); - client._handleMouseButton(15, 14, 0x000); + sendMouseButtonEvent(13, 9, true, 0, client); + sendMouseMoveEvent(15, 14, client); + sendMouseButtonEvent(15, 14, false, 0, client); + expect(RFB.messages.pointerEvent).to.have.been.calledTwice; }); it('should not send button messages when in view only', function () { client._viewOnly = true; - client._handleMouseButton(13, 9, 0x001); - client._handleMouseButton(13, 9, 0x000); + + sendMouseButtonEvent(13, 9, true, 0, client); + sendMouseButtonEvent(13, 9, false, 0, client); + expect(RFB.messages.pointerEvent).to.not.have.been.called; }); it('should send button message directly when drag is disabled', function () { client.dragViewport = false; - client._handleMouseButton(13, 9, 0x001); + sendMouseButtonEvent(13, 9, true, 0, client); expect(RFB.messages.pointerEvent).to.have.been.calledOnce; }); @@ -713,16 +720,16 @@ describe('Remote Frame Buffer protocol client', function () { sinon.spy(client._display, "viewportChangePos"); // Too small movement + sendMouseButtonEvent(13, 9, true, 0, client); + sendMouseMoveEvent(18, 9, client); - client._handleMouseButton(13, 9, 0x001); - client._handleMouseMove(18, 9); expect(RFB.messages.pointerEvent).to.not.have.been.called; expect(client._display.viewportChangePos).to.not.have.been.called; // Sufficient movement - client._handleMouseMove(43, 9); + sendMouseMoveEvent(43, 9, client); expect(RFB.messages.pointerEvent).to.not.have.been.called; expect(client._display.viewportChangePos).to.have.been.calledOnce; @@ -732,7 +739,7 @@ describe('Remote Frame Buffer protocol client', function () { // Now a small movement should move right away - client._handleMouseMove(43, 14); + sendMouseMoveEvent(43, 14, client); expect(RFB.messages.pointerEvent).to.not.have.been.called; expect(client._display.viewportChangePos).to.have.been.calledOnce; @@ -742,9 +749,9 @@ describe('Remote Frame Buffer protocol client', function () { it('should not send button messages when dragging ends', function () { // First the movement - client._handleMouseButton(13, 9, 0x001); - client._handleMouseMove(43, 9); - client._handleMouseButton(43, 9, 0x000); + sendMouseButtonEvent(13, 9, true, 0, client); + sendMouseMoveEvent(43, 9, client); + sendMouseButtonEvent(43, 9, false, 0, client); expect(RFB.messages.pointerEvent).to.not.have.been.called; }); @@ -752,15 +759,15 @@ describe('Remote Frame Buffer protocol client', function () { it('should terminate viewport dragging on a button up event', function () { // First the dragging movement - client._handleMouseButton(13, 9, 0x001); - client._handleMouseMove(43, 9); - client._handleMouseButton(43, 9, 0x000); + sendMouseButtonEvent(13, 9, true, 0, client); + sendMouseMoveEvent(43, 9, client); + sendMouseButtonEvent(43, 9, false, 0, client); // Another movement now should not move the viewport sinon.spy(client._display, "viewportChangePos"); - client._handleMouseMove(43, 59); + sendMouseMoveEvent(43, 59, client); expect(client._display.viewportChangePos).to.not.have.been.called; }); From 31d6a77af63feb211bcf5fc103b28eb755173fc4 Mon Sep 17 00:00:00 2001 From: Adam Halim Date: Fri, 10 Jan 2025 12:21:52 +0100 Subject: [PATCH 083/166] Check for correct button events in dragging tests Previously, these unit tests did not check which events were sent to the server, only how many events were sent. This commit adds checks to see that the expected button events are sent. --- tests/test.rfb.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 061aff307..7bd25e4ef 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -690,6 +690,10 @@ describe('Remote Frame Buffer protocol client', function () { sendMouseButtonEvent(13, 9, false, 0, client); expect(RFB.messages.pointerEvent).to.have.been.calledTwice; + expect(RFB.messages.pointerEvent.firstCall).to.have.been.calledWith(client._sock, + 13, 9, 0x1); + expect(RFB.messages.pointerEvent.secondCall).to.have.been.calledWith(client._sock, + 13, 9, 0x0); }); it('should send button messages when release with small movement', function () { @@ -699,6 +703,10 @@ describe('Remote Frame Buffer protocol client', function () { sendMouseButtonEvent(15, 14, false, 0, client); expect(RFB.messages.pointerEvent).to.have.been.calledTwice; + expect(RFB.messages.pointerEvent.firstCall).to.have.been.calledWith(client._sock, + 15, 14, 0x1); + expect(RFB.messages.pointerEvent.secondCall).to.have.been.calledWith(client._sock, + 15, 14, 0x0); }); it('should not send button messages when in view only', function () { @@ -714,6 +722,8 @@ describe('Remote Frame Buffer protocol client', function () { client.dragViewport = false; sendMouseButtonEvent(13, 9, true, 0, client); expect(RFB.messages.pointerEvent).to.have.been.calledOnce; + expect(RFB.messages.pointerEvent.firstCall).to.have.been.calledWith(client._sock, + 13, 9, 0x1); }); it('should be initiate viewport dragging on sufficient movement', function () { From ea057d079329c9e22a3e940cfdb13eef132c9ad2 Mon Sep 17 00:00:00 2001 From: Adam Halim Date: Fri, 10 Jan 2025 13:14:49 +0100 Subject: [PATCH 084/166] Move gesture event help functions to broader scope This is needed if we want to test gestures with dragging. --- tests/test.rfb.js | 184 +++++++++++++++++++++++----------------------- 1 file changed, 92 insertions(+), 92 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 7bd25e4ef..bdf86e606 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -202,6 +202,37 @@ describe('Remote Frame Buffer protocol client', function () { client._canvas.dispatchEvent(ev); } + function gestureStart(gestureType, x, y, client, + magnitudeX = 0, magnitudeY = 0) { + let pos = elementToClient(x, y, client); + let detail = { type: gestureType, clientX: pos.x, clientY: pos.y }; + + detail.magnitudeX = magnitudeX; + detail.magnitudeY = magnitudeY; + + let ev = new CustomEvent('gesturestart', { detail: detail }); + client._canvas.dispatchEvent(ev); + } + + function gestureMove(gestureType, x, y, client, + magnitudeX = 0, magnitudeY = 0) { + let pos = elementToClient(x, y, client); + let detail = { type: gestureType, clientX: pos.x, clientY: pos.y }; + + detail.magnitudeX = magnitudeX; + detail.magnitudeY = magnitudeY; + + let ev = new CustomEvent('gesturemove', { detail: detail }, client); + client._canvas.dispatchEvent(ev); + } + + function gestureEnd(gestureType, x, y, client) { + let pos = elementToClient(x, y, client); + let detail = { type: gestureType, clientX: pos.x, clientY: pos.y }; + let ev = new CustomEvent('gestureend', { detail: detail }); + client._canvas.dispatchEvent(ev); + } + describe('Connecting/Disconnecting', function () { describe('#RFB (constructor)', function () { let open, attach; @@ -4021,43 +4052,12 @@ describe('Remote Frame Buffer protocol client', function () { }); describe('Gesture event handlers', function () { - function gestureStart(gestureType, x, y, - magnitudeX = 0, magnitudeY = 0) { - let pos = elementToClient(x, y, client); - let detail = {type: gestureType, clientX: pos.x, clientY: pos.y}; - - detail.magnitudeX = magnitudeX; - detail.magnitudeY = magnitudeY; - - let ev = new CustomEvent('gesturestart', { detail: detail }); - client._canvas.dispatchEvent(ev); - } - - function gestureMove(gestureType, x, y, - magnitudeX = 0, magnitudeY = 0) { - let pos = elementToClient(x, y, client); - let detail = {type: gestureType, clientX: pos.x, clientY: pos.y}; - - detail.magnitudeX = magnitudeX; - detail.magnitudeY = magnitudeY; - - let ev = new CustomEvent('gesturemove', { detail: detail }); - client._canvas.dispatchEvent(ev); - } - - function gestureEnd(gestureType, x, y) { - let pos = elementToClient(x, y, client); - let detail = {type: gestureType, clientX: pos.x, clientY: pos.y}; - let ev = new CustomEvent('gestureend', { detail: detail }); - client._canvas.dispatchEvent(ev); - } - describe('Gesture onetap', function () { it('should handle onetap events', function () { let bmask = 0x1; - gestureStart('onetap', 20, 40); - gestureEnd('onetap', 20, 40); + gestureStart('onetap', 20, 40, client); + gestureEnd('onetap', 20, 40, client); expect(pointerEvent).to.have.been.calledThrice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4071,8 +4071,8 @@ describe('Remote Frame Buffer protocol client', function () { it('should keep same position for multiple onetap events', function () { let bmask = 0x1; - gestureStart('onetap', 20, 40); - gestureEnd('onetap', 20, 40); + gestureStart('onetap', 20, 40, client); + gestureEnd('onetap', 20, 40, client); expect(pointerEvent).to.have.been.calledThrice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4084,8 +4084,8 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); - gestureStart('onetap', 20, 50); - gestureEnd('onetap', 20, 50); + gestureStart('onetap', 20, 50, client); + gestureEnd('onetap', 20, 50, client); expect(pointerEvent).to.have.been.calledThrice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4097,8 +4097,8 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); - gestureStart('onetap', 30, 50); - gestureEnd('onetap', 30, 50); + gestureStart('onetap', 30, 50, client); + gestureEnd('onetap', 30, 50, client); expect(pointerEvent).to.have.been.calledThrice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4112,8 +4112,8 @@ describe('Remote Frame Buffer protocol client', function () { it('should not keep same position for onetap events when too far apart', function () { let bmask = 0x1; - gestureStart('onetap', 20, 40); - gestureEnd('onetap', 20, 40); + gestureStart('onetap', 20, 40, client); + gestureEnd('onetap', 20, 40, client); expect(pointerEvent).to.have.been.calledThrice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4125,8 +4125,8 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); - gestureStart('onetap', 80, 95); - gestureEnd('onetap', 80, 95); + gestureStart('onetap', 80, 95, client); + gestureEnd('onetap', 80, 95, client); expect(pointerEvent).to.have.been.calledThrice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4140,8 +4140,8 @@ describe('Remote Frame Buffer protocol client', function () { it('should not keep same position for onetap events when enough time inbetween', function () { let bmask = 0x1; - gestureStart('onetap', 10, 20); - gestureEnd('onetap', 10, 20); + gestureStart('onetap', 10, 20, client); + gestureEnd('onetap', 10, 20, client); expect(pointerEvent).to.have.been.calledThrice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4154,8 +4154,8 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); this.clock.tick(1500); - gestureStart('onetap', 15, 20); - gestureEnd('onetap', 15, 20); + gestureStart('onetap', 15, 20, client); + gestureEnd('onetap', 15, 20, client); expect(pointerEvent).to.have.been.calledThrice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4173,7 +4173,7 @@ describe('Remote Frame Buffer protocol client', function () { it('should handle gesture twotap events', function () { let bmask = 0x4; - gestureStart("twotap", 20, 40); + gestureStart("twotap", 20, 40, client); expect(pointerEvent).to.have.been.calledThrice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4190,8 +4190,8 @@ describe('Remote Frame Buffer protocol client', function () { for (let offset = 0;offset < 30;offset += 10) { pointerEvent.resetHistory(); - gestureStart('twotap', 20, 40 + offset); - gestureEnd('twotap', 20, 40 + offset); + gestureStart('twotap', 20, 40 + offset, client); + gestureEnd('twotap', 20, 40 + offset, client); expect(pointerEvent).to.have.been.calledThrice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4208,7 +4208,7 @@ describe('Remote Frame Buffer protocol client', function () { it('should handle gesture start for threetap events', function () { let bmask = 0x2; - gestureStart("threetap", 20, 40); + gestureStart("threetap", 20, 40, client); expect(pointerEvent).to.have.been.calledThrice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4225,8 +4225,8 @@ describe('Remote Frame Buffer protocol client', function () { for (let offset = 0;offset < 30;offset += 10) { pointerEvent.resetHistory(); - gestureStart('threetap', 20, 40 + offset); - gestureEnd('threetap', 20, 40 + offset); + gestureStart('threetap', 20, 40 + offset, client); + gestureEnd('threetap', 20, 40 + offset, client); expect(pointerEvent).to.have.been.calledThrice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4243,7 +4243,7 @@ describe('Remote Frame Buffer protocol client', function () { it('should handle gesture drag events', function () { let bmask = 0x1; - gestureStart('drag', 20, 40); + gestureStart('drag', 20, 40, client); expect(pointerEvent).to.have.been.calledTwice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4253,7 +4253,7 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); - gestureMove('drag', 30, 50); + gestureMove('drag', 30, 50, client); clock.tick(50); expect(pointerEvent).to.have.been.calledOnce; @@ -4262,7 +4262,7 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); - gestureEnd('drag', 30, 50); + gestureEnd('drag', 30, 50, client); expect(pointerEvent).to.have.been.calledTwice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4276,7 +4276,7 @@ describe('Remote Frame Buffer protocol client', function () { it('should handle long press events', function () { let bmask = 0x4; - gestureStart('longpress', 20, 40); + gestureStart('longpress', 20, 40, client); expect(pointerEvent).to.have.been.calledTwice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4285,7 +4285,7 @@ describe('Remote Frame Buffer protocol client', function () { 20, 40, bmask); pointerEvent.resetHistory(); - gestureMove('longpress', 40, 60); + gestureMove('longpress', 40, 60, client); clock.tick(50); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, @@ -4293,7 +4293,7 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); - gestureEnd('longpress', 40, 60); + gestureEnd('longpress', 40, 60, client); expect(pointerEvent).to.have.been.calledTwice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4307,14 +4307,14 @@ describe('Remote Frame Buffer protocol client', function () { it('should handle gesture twodrag up events', function () { let bmask = 0x10; // Button mask for scroll down - gestureStart('twodrag', 20, 40, 0, 0); + gestureStart('twodrag', 20, 40, client, 0, 0); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 20, 40, 0x0); pointerEvent.resetHistory(); - gestureMove('twodrag', 20, 40, 0, -60); + gestureMove('twodrag', 20, 40, client, 0, -60); expect(pointerEvent).to.have.been.calledThrice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4328,14 +4328,14 @@ describe('Remote Frame Buffer protocol client', function () { it('should handle gesture twodrag down events', function () { let bmask = 0x8; // Button mask for scroll up - gestureStart('twodrag', 20, 40, 0, 0); + gestureStart('twodrag', 20, 40, client, 0, 0); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 20, 40, 0x0); pointerEvent.resetHistory(); - gestureMove('twodrag', 20, 40, 0, 60); + gestureMove('twodrag', 20, 40, client, 0, 60); expect(pointerEvent).to.have.been.calledThrice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4349,14 +4349,14 @@ describe('Remote Frame Buffer protocol client', function () { it('should handle gesture twodrag right events', function () { let bmask = 0x20; // Button mask for scroll right - gestureStart('twodrag', 20, 40, 0, 0); + gestureStart('twodrag', 20, 40, client, 0, 0); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 20, 40, 0x0); pointerEvent.resetHistory(); - gestureMove('twodrag', 20, 40, 60, 0); + gestureMove('twodrag', 20, 40, client, 60, 0); expect(pointerEvent).to.have.been.calledThrice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4370,14 +4370,14 @@ describe('Remote Frame Buffer protocol client', function () { it('should handle gesture twodrag left events', function () { let bmask = 0x40; // Button mask for scroll left - gestureStart('twodrag', 20, 40, 0, 0); + gestureStart('twodrag', 20, 40, client, 0, 0); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 20, 40, 0x0); pointerEvent.resetHistory(); - gestureMove('twodrag', 20, 40, -60, 0); + gestureMove('twodrag', 20, 40, client, -60, 0); expect(pointerEvent).to.have.been.calledThrice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4392,14 +4392,14 @@ describe('Remote Frame Buffer protocol client', function () { let scrlUp = 0x8; // Button mask for scroll up let scrlRight = 0x20; // Button mask for scroll right - gestureStart('twodrag', 20, 40, 0, 0); + gestureStart('twodrag', 20, 40, client, 0, 0); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 20, 40, 0x0); pointerEvent.resetHistory(); - gestureMove('twodrag', 20, 40, 60, 60); + gestureMove('twodrag', 20, 40, client, 60, 60); expect(pointerEvent).to.have.been.callCount(5); expect(pointerEvent.getCall(0)).to.have.been.calledWith(client._sock, @@ -4417,14 +4417,14 @@ describe('Remote Frame Buffer protocol client', function () { it('should handle multiple small gesture twodrag events', function () { let bmask = 0x8; // Button mask for scroll up - gestureStart('twodrag', 20, 40, 0, 0); + gestureStart('twodrag', 20, 40, client, 0, 0); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 20, 40, 0x0); pointerEvent.resetHistory(); - gestureMove('twodrag', 20, 40, 0, 10); + gestureMove('twodrag', 20, 40, client, 0, 10); clock.tick(50); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, @@ -4432,7 +4432,7 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); - gestureMove('twodrag', 20, 40, 0, 20); + gestureMove('twodrag', 20, 40, client, 0, 20); clock.tick(50); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, @@ -4440,7 +4440,7 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); - gestureMove('twodrag', 20, 40, 0, 60); + gestureMove('twodrag', 20, 40, client, 0, 60); expect(pointerEvent).to.have.been.calledThrice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4454,14 +4454,14 @@ describe('Remote Frame Buffer protocol client', function () { it('should handle large gesture twodrag events', function () { let bmask = 0x8; // Button mask for scroll up - gestureStart('twodrag', 30, 50, 0, 0); + gestureStart('twodrag', 30, 50, client, 0, 0); expect(pointerEvent). to.have.been.calledOnceWith(client._sock, 30, 50, 0x0); pointerEvent.resetHistory(); - gestureMove('twodrag', 30, 50, 0, 200); + gestureMove('twodrag', 30, 50, client, 0, 200); expect(pointerEvent).to.have.callCount(7); expect(pointerEvent.getCall(0)).to.have.been.calledWith(client._sock, @@ -4486,7 +4486,7 @@ describe('Remote Frame Buffer protocol client', function () { let keysym = KeyTable.XK_Control_L; let bmask = 0x10; // Button mask for scroll down - gestureStart('pinch', 20, 40, 90, 90); + gestureStart('pinch', 20, 40, client, 90, 90); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 20, 40, 0x0); @@ -4494,7 +4494,7 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); - gestureMove('pinch', 20, 40, 30, 30); + gestureMove('pinch', 20, 40, client, 30, 30); expect(pointerEvent).to.have.been.calledThrice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4516,7 +4516,7 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); keyEvent.resetHistory(); - gestureEnd('pinch', 20, 40); + gestureEnd('pinch', 20, 40, client); expect(pointerEvent).to.not.have.been.called; expect(keyEvent).to.not.have.been.called; @@ -4526,7 +4526,7 @@ describe('Remote Frame Buffer protocol client', function () { let keysym = KeyTable.XK_Control_L; let bmask = 0x8; // Button mask for scroll up - gestureStart('pinch', 10, 20, 10, 20); + gestureStart('pinch', 10, 20, client, 10, 20); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 10, 20, 0x0); @@ -4534,7 +4534,7 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); - gestureMove('pinch', 10, 20, 70, 80); + gestureMove('pinch', 10, 20, client, 70, 80); expect(pointerEvent).to.have.been.calledThrice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4556,7 +4556,7 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); keyEvent.resetHistory(); - gestureEnd('pinch', 10, 20); + gestureEnd('pinch', 10, 20, client); expect(pointerEvent).to.not.have.been.called; expect(keyEvent).to.not.have.been.called; @@ -4566,7 +4566,7 @@ describe('Remote Frame Buffer protocol client', function () { let keysym = KeyTable.XK_Control_L; let bmask = 0x10; // Button mask for scroll down - gestureStart('pinch', 20, 40, 150, 150); + gestureStart('pinch', 20, 40, client, 150, 150); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 20, 40, 0x0); @@ -4574,7 +4574,7 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); - gestureMove('pinch', 20, 40, 30, 30); + gestureMove('pinch', 20, 40, client, 30, 30); expect(pointerEvent).to.have.been.callCount(5); expect(pointerEvent.getCall(0)).to.have.been.calledWith(client._sock, @@ -4600,7 +4600,7 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); keyEvent.resetHistory(); - gestureEnd('pinch', 20, 40); + gestureEnd('pinch', 20, 40, client); expect(pointerEvent).to.not.have.been.called; expect(keyEvent).to.not.have.been.called; @@ -4610,7 +4610,7 @@ describe('Remote Frame Buffer protocol client', function () { let keysym = KeyTable.XK_Control_L; let bmask = 0x8; // Button mask for scroll down - gestureStart('pinch', 20, 40, 0, 10); + gestureStart('pinch', 20, 40, client, 0, 10); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 20, 40, 0x0); @@ -4618,7 +4618,7 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); - gestureMove('pinch', 20, 40, 0, 30); + gestureMove('pinch', 20, 40, client, 0, 30); clock.tick(50); expect(pointerEvent).to.have.been.calledWith(client._sock, @@ -4626,7 +4626,7 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); - gestureMove('pinch', 20, 40, 0, 60); + gestureMove('pinch', 20, 40, client, 0, 60); clock.tick(50); expect(pointerEvent).to.have.been.calledWith(client._sock, @@ -4635,7 +4635,7 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); keyEvent.resetHistory(); - gestureMove('pinch', 20, 40, 0, 90); + gestureMove('pinch', 20, 40, client, 0, 90); expect(pointerEvent).to.have.been.calledThrice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4657,7 +4657,7 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); keyEvent.resetHistory(); - gestureEnd('pinch', 20, 40); + gestureEnd('pinch', 20, 40, client); expect(keyEvent).to.not.have.been.called; }); @@ -4669,7 +4669,7 @@ describe('Remote Frame Buffer protocol client', function () { client._qemuExtKeyEventSupported = true; - gestureStart('pinch', 20, 40, 90, 90); + gestureStart('pinch', 20, 40, client, 90, 90); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 20, 40, 0x0); @@ -4677,7 +4677,7 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); - gestureMove('pinch', 20, 40, 30, 30); + gestureMove('pinch', 20, 40, client, 30, 30); expect(pointerEvent).to.have.been.calledThrice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -4703,7 +4703,7 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); qemuKeyEvent.resetHistory(); - gestureEnd('pinch', 20, 40); + gestureEnd('pinch', 20, 40, client); expect(pointerEvent).to.not.have.been.called; expect(qemuKeyEvent).to.not.have.been.called; From f9eb476f6df2dc1bf05411d2efbc4ec49658939f Mon Sep 17 00:00:00 2001 From: Adam Halim Date: Tue, 14 Jan 2025 09:39:03 +0100 Subject: [PATCH 085/166] Add tests for dragging with gestures There were no test for viewport dragging using gesture previously, so let's add some. Note that there currently are some viewport dragging behaviours that we don't want to have, so some tests have commented out what our desired behaviour should be. --- tests/test.rfb.js | 93 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index bdf86e606..b643e169b 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -727,6 +727,20 @@ describe('Remote Frame Buffer protocol client', function () { 13, 9, 0x0); }); + it('should send button messages when tapping', function () { + // Just up and down + gestureStart('onetap', 13, 9, client); + gestureEnd('onetap', 13, 9, client); + + expect(RFB.messages.pointerEvent).to.have.been.calledThrice; + expect(RFB.messages.pointerEvent.firstCall).to.have.been.calledWith(client._sock, + 13, 9, 0x0); + expect(RFB.messages.pointerEvent.secondCall).to.have.been.calledWith(client._sock, + 13, 9, 0x1); + expect(RFB.messages.pointerEvent.thirdCall).to.have.been.calledWith(client._sock, + 13, 9, 0x0); + }); + it('should send button messages when release with small movement', function () { // Small movement sendMouseButtonEvent(13, 9, true, 0, client); @@ -787,6 +801,85 @@ describe('Remote Frame Buffer protocol client', function () { expect(client._display.viewportChangePos).to.have.been.calledWith(0, -5); }); + it('should initiate viewport dragging on sufficient drag gesture movement', function () { + sinon.spy(client._display, "viewportChangePos"); + + // Sufficient movement + gestureStart('drag', 13, 9, client); + gestureMove('drag', 43, 9, client); + + // FIXME: We don't want to send a pointer event here + // expect(RFB.messages.pointerEvent).to.not.have.been.called; + expect(client._display.viewportChangePos).to.have.been.calledOnce; + expect(client._display.viewportChangePos).to.have.been.calledWith(-30, 0); + + client._display.viewportChangePos.resetHistory(); + RFB.messages.pointerEvent.resetHistory(); + + // Now a small movement should move right away + + gestureMove('drag', 43, 14, client); + gestureEnd('drag', 43, 14, client); + + expect(RFB.messages.pointerEvent).to.not.have.been.called; + + // FIXME: We only want to move the viewport once + // expect(client._display.viewportChangePos).to.have.been.calledOnce; + expect(client._display.viewportChangePos).to.have.been.calledWith(0, -5); + }); + + it('should initiate viewport dragging on sufficient longpress gesture movement', function () { + sinon.spy(client._display, "viewportChangePos"); + + // A small movement below the threshold should not move. + gestureStart('longpress', 13, 9, client); + gestureMove('longpress', 14, 9, client); + + // FIXME: We don't want to send a pointer event here + // expect(RFB.messages.pointerEvent).to.not.have.been.called; + expect(client._display.viewportChangePos).to.not.have.been.called; + + client._display.viewportChangePos.resetHistory(); + RFB.messages.pointerEvent.resetHistory(); + + gestureMove('longpress', 43, 9, client); + gestureEnd('longpress', 43, 9, client); + + expect(RFB.messages.pointerEvent).to.not.have.been.called; + // FIXME: We only want to move the viewport once + // expect(client._display.viewportChangePos).to.have.been.calledOnce; + expect(client._display.viewportChangePos).to.have.been.calledWith(-30, 0); + }); + + it('should send button messages on small longpress gesture movement', function () { + sinon.spy(client._display, "viewportChangePos"); + + // A small movement below the threshold should not move. + gestureStart('longpress', 13, 9, client); + gestureMove('longpress', 14, 10, client); + + // FIXME: We don't want to send a pointer event here + // expect(RFB.messages.pointerEvent).to.not.have.been.called; + expect(client._display.viewportChangePos).to.not.have.been.called; + + client._display.viewportChangePos.resetHistory(); + RFB.messages.pointerEvent.resetHistory(); + + gestureEnd('longpress', 14, 9, client); + + // FIXME: We want the pointer event to come after the + // 'gestureEnd' call instead. + // expect(RFB.messages.pointerEvent).to.have.been.calledThrice; + // expect(RFB.messages.pointerEvent.firstCall).to.have.been.calledWith(client._sock, + // 14, 9, 0x0); + expect(RFB.messages.pointerEvent.firstCall).to.have.been.calledWith(client._sock, + 14, 9, 0x4); + expect(RFB.messages.pointerEvent.secondCall).to.have.been.calledWith(client._sock, + 14, 9, 0x0); + + expect(client._display.viewportChangePos).to.not.have.been.called; + }); + it('should not send button messages when dragging ends', function () { // First the movement From b9230cf23eb0f57dec14b5d7f7fb6e753638b0f5 Mon Sep 17 00:00:00 2001 From: Adam Halim Date: Fri, 29 Nov 2024 14:47:19 +0100 Subject: [PATCH 086/166] Use MouseEvent.buttons for button state tracking Instead of keeping track of button states ourselves by looking at MouseEvent.button, we can use the MouseEvent.buttons which already contains the state of all buttons. --- core/rfb.js | 244 ++++++++++++++++++++++++++++++---------------- tests/test.rfb.js | 143 +++++++++++++-------------- 2 files changed, 233 insertions(+), 154 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index 3f946142d..4b105cb57 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -1033,6 +1033,35 @@ export default class RFB extends EventTargetMixin { this.sendKey(keysym, code, down); } + static _convertButtonMask(buttons) { + /* The bits in MouseEvent.buttons property correspond + * to the following mouse buttons: + * 0: Left + * 1: Right + * 2: Middle + * 3: Back + * 4: Forward + * + * These bits needs to be converted to what they are defined as + * in the RFB protocol. + */ + + const buttonMaskMap = { + 0: 1 << 0, // Left + 1: 1 << 2, // Right + 2: 1 << 1, // Middle + 3: 1 << 7, // Back + }; + + let bmask = 0; + for (let i = 0; i < 4; i++) { + if (buttons & (1 << i)) { + bmask |= buttonMaskMap[i]; + } + } + return bmask; + } + _handleMouse(ev) { /* * We don't check connection status or viewOnly here as the @@ -1062,76 +1091,73 @@ export default class RFB extends EventTargetMixin { let pos = clientToElement(ev.clientX, ev.clientY, this._canvas); + let bmask = RFB._convertButtonMask(ev.buttons); + + let down = ev.type == 'mousedown'; switch (ev.type) { case 'mousedown': - setCapture(this._canvas); - this._handleMouseButton(pos.x, pos.y, - true, 1 << ev.button); - break; case 'mouseup': - this._handleMouseButton(pos.x, pos.y, - false, 1 << ev.button); + if (this.dragViewport) { + if (down && !this._viewportDragging) { + this._viewportDragging = true; + this._viewportDragPos = {'x': pos.x, 'y': pos.y}; + this._viewportHasMoved = false; + + // Skip sending mouse events, instead save the current + // mouse mask so we can send it later. + this._mouseButtonMask = bmask; + break; + } else { + this._viewportDragging = false; + + // If we actually performed a drag then we are done + // here and should not send any mouse events + if (this._viewportHasMoved) { + this._mouseButtonMask = bmask; + break; + } + // Otherwise we treat this as a mouse click event. + // Send the previously saved button mask, followed + // by the current button mask at the end of this + // function. + this._sendMouse(pos.x, pos.y, this._mouseButtonMask); + } + } + if (down) { + setCapture(this._canvas); + } + this._handleMouseButton(pos.x, pos.y, bmask); break; case 'mousemove': - this._handleMouseMove(pos.x, pos.y); - break; - } - } + if (this._viewportDragging) { + const deltaX = this._viewportDragPos.x - pos.x; + const deltaY = this._viewportDragPos.y - pos.y; - _handleMouseButton(x, y, down, bmask) { - if (this.dragViewport) { - if (down && !this._viewportDragging) { - this._viewportDragging = true; - this._viewportDragPos = {'x': x, 'y': y}; - this._viewportHasMoved = false; + if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold || + Math.abs(deltaY) > dragThreshold)) { + this._viewportHasMoved = true; - // Skip sending mouse events - return; - } else { - this._viewportDragging = false; + this._viewportDragPos = {'x': pos.x, 'y': pos.y}; + this._display.viewportChangePos(deltaX, deltaY); + } - // If we actually performed a drag then we are done - // here and should not send any mouse events - if (this._viewportHasMoved) { - return; + // Skip sending mouse events + break; } - - // Otherwise we treat this as a mouse click event. - // Send the button down event here, as the button up - // event is sent at the end of this function. - this._sendMouse(x, y, bmask); - } + this._handleMouseMove(pos.x, pos.y); + break; } + } + _handleMouseButton(x, y, bmask) { // Flush waiting move event first this._flushMouseMoveTimer(x, y); - if (down) { - this._mouseButtonMask |= bmask; - } else { - this._mouseButtonMask &= ~bmask; - } - + this._mouseButtonMask = bmask; this._sendMouse(x, y, this._mouseButtonMask); } _handleMouseMove(x, y) { - if (this._viewportDragging) { - const deltaX = this._viewportDragPos.x - x; - const deltaY = this._viewportDragPos.y - y; - - if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold || - Math.abs(deltaY) > dragThreshold)) { - this._viewportHasMoved = true; - - this._viewportDragPos = {'x': x, 'y': y}; - this._display.viewportChangePos(deltaX, deltaY); - } - - // Skip sending mouse events - return; - } - this._mousePos = { 'x': x, 'y': y }; // Limit many mouse move events to one every MOUSE_MOVE_DELAY ms @@ -1175,6 +1201,7 @@ export default class RFB extends EventTargetMixin { let pos = clientToElement(ev.clientX, ev.clientY, this._canvas); + let bmask = RFB._convertButtonMask(ev.buttons); let dX = ev.deltaX; let dY = ev.deltaY; @@ -1194,26 +1221,27 @@ export default class RFB extends EventTargetMixin { this._accumulatedWheelDeltaX += dX; this._accumulatedWheelDeltaY += dY; + // Generate a mouse wheel step event when the accumulated delta // for one of the axes is large enough. if (Math.abs(this._accumulatedWheelDeltaX) >= WHEEL_STEP) { if (this._accumulatedWheelDeltaX < 0) { - this._handleMouseButton(pos.x, pos.y, true, 1 << 5); - this._handleMouseButton(pos.x, pos.y, false, 1 << 5); + this._handleMouseButton(pos.x, pos.y, bmask | 1 << 5); + this._handleMouseButton(pos.x, pos.y, bmask); } else if (this._accumulatedWheelDeltaX > 0) { - this._handleMouseButton(pos.x, pos.y, true, 1 << 6); - this._handleMouseButton(pos.x, pos.y, false, 1 << 6); + this._handleMouseButton(pos.x, pos.y, bmask | 1 << 6); + this._handleMouseButton(pos.x, pos.y, bmask); } this._accumulatedWheelDeltaX = 0; } if (Math.abs(this._accumulatedWheelDeltaY) >= WHEEL_STEP) { if (this._accumulatedWheelDeltaY < 0) { - this._handleMouseButton(pos.x, pos.y, true, 1 << 3); - this._handleMouseButton(pos.x, pos.y, false, 1 << 3); + this._handleMouseButton(pos.x, pos.y, bmask | 1 << 3); + this._handleMouseButton(pos.x, pos.y, bmask); } else if (this._accumulatedWheelDeltaY > 0) { - this._handleMouseButton(pos.x, pos.y, true, 1 << 4); - this._handleMouseButton(pos.x, pos.y, false, 1 << 4); + this._handleMouseButton(pos.x, pos.y, bmask | 1 << 4); + this._handleMouseButton(pos.x, pos.y, bmask); } this._accumulatedWheelDeltaY = 0; @@ -1252,8 +1280,8 @@ export default class RFB extends EventTargetMixin { this._gestureLastTapTime = Date.now(); this._fakeMouseMove(this._gestureFirstDoubleTapEv, pos.x, pos.y); - this._handleMouseButton(pos.x, pos.y, true, bmask); - this._handleMouseButton(pos.x, pos.y, false, bmask); + this._handleMouseButton(pos.x, pos.y, bmask); + this._handleMouseButton(pos.x, pos.y, 0x0); } _handleGesture(ev) { @@ -1274,14 +1302,31 @@ export default class RFB extends EventTargetMixin { this._handleTapEvent(ev, 0x2); break; case 'drag': - this._fakeMouseMove(ev, pos.x, pos.y); - this._handleMouseButton(pos.x, pos.y, true, 0x1); + if (this.dragViewport) { + this._viewportHasMoved = false; + this._viewportDragging = true; + this._viewportDragPos = {'x': pos.x, 'y': pos.y}; + + this._fakeMouseMove(ev, pos.x, pos.y); + } else { + this._fakeMouseMove(ev, pos.x, pos.y); + this._handleMouseButton(pos.x, pos.y, 0x1); + } break; case 'longpress': - this._fakeMouseMove(ev, pos.x, pos.y); - this._handleMouseButton(pos.x, pos.y, true, 0x4); + if (this.dragViewport) { + // If dragViewport is true, we need to wait to see + // if we have dragged outside the threshold before + // sending any events to the server. + this._viewportHasMoved = false; + this._viewportDragPos = {'x': pos.x, 'y': pos.y}; + + this._fakeMouseMove(ev, pos.x, pos.y); + } else { + this._fakeMouseMove(ev, pos.x, pos.y); + this._handleMouseButton(pos.x, pos.y, 0x4); + } break; - case 'twodrag': this._gestureLastMagnitudeX = ev.detail.magnitudeX; this._gestureLastMagnitudeY = ev.detail.magnitudeY; @@ -1303,6 +1348,19 @@ export default class RFB extends EventTargetMixin { break; case 'drag': case 'longpress': + if (this.dragViewport) { + this._viewportDragging = true; + const deltaX = this._viewportDragPos.x - pos.x; + const deltaY = this._viewportDragPos.y - pos.y; + + if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold || + Math.abs(deltaY) > dragThreshold)) { + this._viewportHasMoved = true; + + this._viewportDragPos = {'x': pos.x, 'y': pos.y}; + this._display.viewportChangePos(deltaX, deltaY); + } + } this._fakeMouseMove(ev, pos.x, pos.y); break; case 'twodrag': @@ -1311,23 +1369,23 @@ export default class RFB extends EventTargetMixin { // every update. this._fakeMouseMove(ev, pos.x, pos.y); while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) > GESTURE_SCRLSENS) { - this._handleMouseButton(pos.x, pos.y, true, 0x8); - this._handleMouseButton(pos.x, pos.y, false, 0x8); + this._handleMouseButton(pos.x, pos.y, 0x8); + this._handleMouseButton(pos.x, pos.y, 0x0); this._gestureLastMagnitudeY += GESTURE_SCRLSENS; } while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) < -GESTURE_SCRLSENS) { - this._handleMouseButton(pos.x, pos.y, true, 0x10); - this._handleMouseButton(pos.x, pos.y, false, 0x10); + this._handleMouseButton(pos.x, pos.y, 0x10); + this._handleMouseButton(pos.x, pos.y, 0x0); this._gestureLastMagnitudeY -= GESTURE_SCRLSENS; } while ((ev.detail.magnitudeX - this._gestureLastMagnitudeX) > GESTURE_SCRLSENS) { - this._handleMouseButton(pos.x, pos.y, true, 0x20); - this._handleMouseButton(pos.x, pos.y, false, 0x20); + this._handleMouseButton(pos.x, pos.y, 0x20); + this._handleMouseButton(pos.x, pos.y, 0x0); this._gestureLastMagnitudeX += GESTURE_SCRLSENS; } while ((ev.detail.magnitudeX - this._gestureLastMagnitudeX) < -GESTURE_SCRLSENS) { - this._handleMouseButton(pos.x, pos.y, true, 0x40); - this._handleMouseButton(pos.x, pos.y, false, 0x40); + this._handleMouseButton(pos.x, pos.y, 0x40); + this._handleMouseButton(pos.x, pos.y, 0x0); this._gestureLastMagnitudeX -= GESTURE_SCRLSENS; } break; @@ -1340,13 +1398,13 @@ export default class RFB extends EventTargetMixin { if (Math.abs(magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) { this._handleKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); while ((magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) { - this._handleMouseButton(pos.x, pos.y, true, 0x8); - this._handleMouseButton(pos.x, pos.y, false, 0x8); + this._handleMouseButton(pos.x, pos.y, 0x8); + this._handleMouseButton(pos.x, pos.y, 0x0); this._gestureLastMagnitudeX += GESTURE_ZOOMSENS; } while ((magnitude - this._gestureLastMagnitudeX) < -GESTURE_ZOOMSENS) { - this._handleMouseButton(pos.x, pos.y, true, 0x10); - this._handleMouseButton(pos.x, pos.y, false, 0x10); + this._handleMouseButton(pos.x, pos.y, 0x10); + this._handleMouseButton(pos.x, pos.y, 0x0); this._gestureLastMagnitudeX -= GESTURE_ZOOMSENS; } } @@ -1364,12 +1422,32 @@ export default class RFB extends EventTargetMixin { case 'twodrag': break; case 'drag': - this._fakeMouseMove(ev, pos.x, pos.y); - this._handleMouseButton(pos.x, pos.y, false, 0x1); + if (this.dragViewport) { + this._viewportDragging = false; + } else { + this._fakeMouseMove(ev, pos.x, pos.y); + this._handleMouseButton(pos.x, pos.y, 0x0); + } break; case 'longpress': - this._fakeMouseMove(ev, pos.x, pos.y); - this._handleMouseButton(pos.x, pos.y, false, 0x4); + if (this._viewportHasMoved) { + // We don't want to send any events if we have moved + // our viewport + break; + } + + if (this.dragViewport && !this._viewportHasMoved) { + this._fakeMouseMove(ev, pos.x, pos.y); + // If dragViewport is true, we need to wait to see + // if we have dragged outside the threshold before + // sending any events to the server. + this._handleMouseButton(pos.x, pos.y, 0x4); + this._handleMouseButton(pos.x, pos.y, 0x0); + this._viewportDragging = false; + } else { + this._fakeMouseMove(ev, pos.x, pos.y); + this._handleMouseButton(pos.x, pos.y, 0x0); + } break; } break; diff --git a/tests/test.rfb.js b/tests/test.rfb.js index b643e169b..842981882 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -176,7 +176,7 @@ describe('Remote Frame Buffer protocol client', function () { return res; } - function sendMouseMoveEvent(x, y, client) { + function sendMouseMoveEvent(x, y, buttons, client) { let pos = elementToClient(x, y, client); let ev; @@ -184,11 +184,12 @@ describe('Remote Frame Buffer protocol client', function () { { 'screenX': pos.x + window.screenX, 'screenY': pos.y + window.screenY, 'clientX': pos.x, - 'clientY': pos.y }); + 'clientY': pos.y, + 'buttons': buttons }); client._canvas.dispatchEvent(ev); } - function sendMouseButtonEvent(x, y, down, button, client) { + function sendMouseButtonEvent(x, y, down, buttons, client) { let pos = elementToClient(x, y, client); let ev; @@ -197,8 +198,7 @@ describe('Remote Frame Buffer protocol client', function () { 'screenY': pos.y + window.screenY, 'clientX': pos.x, 'clientY': pos.y, - 'button': button, - 'buttons': 1 << button }); + 'buttons': buttons}); client._canvas.dispatchEvent(ev); } @@ -711,14 +711,14 @@ describe('Remote Frame Buffer protocol client', function () { }); it('should not send button messages when initiating viewport dragging', function () { - sendMouseButtonEvent(13, 9, true, 0, client); + sendMouseButtonEvent(13, 9, true, 0x1, client); expect(RFB.messages.pointerEvent).to.not.have.been.called; }); it('should send button messages when release without movement', function () { // Just up and down - sendMouseButtonEvent(13, 9, true, 0, client); - sendMouseButtonEvent(13, 9, false, 0, client); + sendMouseButtonEvent(13, 9, true, 0x1, client); + sendMouseButtonEvent(13, 9, false, 0x0, client); expect(RFB.messages.pointerEvent).to.have.been.calledTwice; expect(RFB.messages.pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -743,9 +743,9 @@ describe('Remote Frame Buffer protocol client', function () { it('should send button messages when release with small movement', function () { // Small movement - sendMouseButtonEvent(13, 9, true, 0, client); - sendMouseMoveEvent(15, 14, client); - sendMouseButtonEvent(15, 14, false, 0, client); + sendMouseButtonEvent(13, 9, true, 0x1, client); + sendMouseMoveEvent(15, 14, 0x1, client); + sendMouseButtonEvent(15, 14, false, 0x0, client); expect(RFB.messages.pointerEvent).to.have.been.calledTwice; expect(RFB.messages.pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -757,15 +757,15 @@ describe('Remote Frame Buffer protocol client', function () { it('should not send button messages when in view only', function () { client._viewOnly = true; - sendMouseButtonEvent(13, 9, true, 0, client); - sendMouseButtonEvent(13, 9, false, 0, client); + sendMouseButtonEvent(13, 9, true, 0x1, client); + sendMouseButtonEvent(13, 9, false, 0x0, client); expect(RFB.messages.pointerEvent).to.not.have.been.called; }); it('should send button message directly when drag is disabled', function () { client.dragViewport = false; - sendMouseButtonEvent(13, 9, true, 0, client); + sendMouseButtonEvent(13, 9, true, 0x1, client); expect(RFB.messages.pointerEvent).to.have.been.calledOnce; expect(RFB.messages.pointerEvent.firstCall).to.have.been.calledWith(client._sock, 13, 9, 0x1); @@ -775,8 +775,8 @@ describe('Remote Frame Buffer protocol client', function () { sinon.spy(client._display, "viewportChangePos"); // Too small movement - sendMouseButtonEvent(13, 9, true, 0, client); - sendMouseMoveEvent(18, 9, client); + sendMouseButtonEvent(13, 9, true, 0x1, client); + sendMouseMoveEvent(18, 9, 0x1, client); expect(RFB.messages.pointerEvent).to.not.have.been.called; @@ -784,7 +784,7 @@ describe('Remote Frame Buffer protocol client', function () { // Sufficient movement - sendMouseMoveEvent(43, 9, client); + sendMouseMoveEvent(43, 9, 0x1, client); expect(RFB.messages.pointerEvent).to.not.have.been.called; expect(client._display.viewportChangePos).to.have.been.calledOnce; @@ -794,7 +794,7 @@ describe('Remote Frame Buffer protocol client', function () { // Now a small movement should move right away - sendMouseMoveEvent(43, 14, client); + sendMouseMoveEvent(43, 14, 0x1, client); expect(RFB.messages.pointerEvent).to.not.have.been.called; expect(client._display.viewportChangePos).to.have.been.calledOnce; @@ -823,8 +823,7 @@ describe('Remote Frame Buffer protocol client', function () { expect(RFB.messages.pointerEvent).to.not.have.been.called; - // FIXME: We only want to move the viewport once - // expect(client._display.viewportChangePos).to.have.been.calledOnce; + expect(client._display.viewportChangePos).to.have.been.calledOnce; expect(client._display.viewportChangePos).to.have.been.calledWith(0, -5); }); @@ -867,14 +866,12 @@ describe('Remote Frame Buffer protocol client', function () { gestureEnd('longpress', 14, 9, client); - // FIXME: We want the pointer event to come after the - // 'gestureEnd' call instead. - // expect(RFB.messages.pointerEvent).to.have.been.calledThrice; - // expect(RFB.messages.pointerEvent.firstCall).to.have.been.calledWith(client._sock, - // 14, 9, 0x0); + expect(RFB.messages.pointerEvent).to.have.been.calledThrice; expect(RFB.messages.pointerEvent.firstCall).to.have.been.calledWith(client._sock, - 14, 9, 0x4); + 14, 9, 0x0); expect(RFB.messages.pointerEvent.secondCall).to.have.been.calledWith(client._sock, + 14, 9, 0x4); + expect(RFB.messages.pointerEvent.thirdCall).to.have.been.calledWith(client._sock, 14, 9, 0x0); expect(client._display.viewportChangePos).to.not.have.been.called; @@ -883,9 +880,9 @@ describe('Remote Frame Buffer protocol client', function () { it('should not send button messages when dragging ends', function () { // First the movement - sendMouseButtonEvent(13, 9, true, 0, client); - sendMouseMoveEvent(43, 9, client); - sendMouseButtonEvent(43, 9, false, 0, client); + sendMouseButtonEvent(13, 9, true, 0x1, client); + sendMouseMoveEvent(43, 9, 0x1, client); + sendMouseButtonEvent(43, 9, false, 0x0, client); expect(RFB.messages.pointerEvent).to.not.have.been.called; }); @@ -893,15 +890,15 @@ describe('Remote Frame Buffer protocol client', function () { it('should terminate viewport dragging on a button up event', function () { // First the dragging movement - sendMouseButtonEvent(13, 9, true, 0, client); - sendMouseMoveEvent(43, 9, client); - sendMouseButtonEvent(43, 9, false, 0, client); + sendMouseButtonEvent(13, 9, true, 0x1, client); + sendMouseMoveEvent(43, 9, 0x1, client); + sendMouseButtonEvent(43, 9, false, 0x0, client); // Another movement now should not move the viewport sinon.spy(client._display, "viewportChangePos"); - sendMouseMoveEvent(43, 59, client); + sendMouseMoveEvent(43, 59, 0x0, client); expect(client._display.viewportChangePos).to.not.have.been.called; }); @@ -3773,60 +3770,62 @@ describe('Remote Frame Buffer protocol client', function () { it('should not send button messages in view-only mode', function () { client._viewOnly = true; - sendMouseButtonEvent(10, 10, true, 0, client); + sendMouseButtonEvent(10, 10, true, 0x1, client); + clock.tick(50); expect(pointerEvent).to.not.have.been.called; }); it('should not send movement messages in view-only mode', function () { client._viewOnly = true; - sendMouseMoveEvent(10, 10, client); + sendMouseMoveEvent(10, 10, 0x0, client); + clock.tick(50); expect(pointerEvent).to.not.have.been.called; }); it('should handle left mouse button', function () { - sendMouseButtonEvent(10, 10, true, 0, client); + sendMouseButtonEvent(10, 10, true, 0x1, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 10, 10, 0x1); pointerEvent.resetHistory(); - sendMouseButtonEvent(10, 10, false, 0, client); + sendMouseButtonEvent(10, 10, false, 0x0, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 10, 10, 0x0); }); it('should handle middle mouse button', function () { - sendMouseButtonEvent(10, 10, true, 1, client); + sendMouseButtonEvent(10, 10, true, 0x4, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 10, 10, 0x2); pointerEvent.resetHistory(); - sendMouseButtonEvent(10, 10, false, 1, client); + sendMouseButtonEvent(10, 10, false, 0x0, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 10, 10, 0x0); }); it('should handle right mouse button', function () { - sendMouseButtonEvent(10, 10, true, 2, client); + sendMouseButtonEvent(10, 10, true, 0x2, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 10, 10, 0x4); pointerEvent.resetHistory(); - sendMouseButtonEvent(10, 10, false, 2, client); + sendMouseButtonEvent(10, 10, false, 0x0, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 10, 10, 0x0); }); it('should handle multiple mouse buttons', function () { - sendMouseButtonEvent(10, 10, true, 0, client); - sendMouseButtonEvent(10, 10, true, 2, client); + sendMouseButtonEvent(10, 10, true, 0x1, client); + sendMouseButtonEvent(10, 10, true, 0x3, client); expect(pointerEvent).to.have.been.calledTwice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -3836,8 +3835,9 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); - sendMouseButtonEvent(10, 10, false, 0, client); - sendMouseButtonEvent(10, 10, false, 2, client); + + sendMouseButtonEvent(10, 10, false, 0x2, client); + sendMouseButtonEvent(10, 10, false, 0x0, client); expect(pointerEvent).to.have.been.calledTwice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -3847,14 +3847,14 @@ describe('Remote Frame Buffer protocol client', function () { }); it('should handle mouse movement', function () { - sendMouseMoveEvent(50, 70, client); + sendMouseMoveEvent(50, 70, 0x0, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 50, 70, 0x0); }); it('should handle click and drag', function () { - sendMouseButtonEvent(10, 10, true, 0, client); - sendMouseMoveEvent(50, 70, client); + sendMouseButtonEvent(10, 10, true, 0x1, client); + sendMouseMoveEvent(50, 70, 0x1, client); expect(pointerEvent).to.have.been.calledTwice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -3864,7 +3864,7 @@ describe('Remote Frame Buffer protocol client', function () { pointerEvent.resetHistory(); - sendMouseButtonEvent(50, 70, false, 0, client); + sendMouseButtonEvent(50, 70, false, 0x0, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 50, 70, 0x0); @@ -3872,15 +3872,15 @@ describe('Remote Frame Buffer protocol client', function () { describe('Event aggregation', function () { it('should send a single pointer event on mouse movement', function () { - sendMouseMoveEvent(50, 70, client); + sendMouseMoveEvent(50, 70, 0x0, client); clock.tick(100); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 50, 70, 0x0); }); it('should delay one move if two events are too close', function () { - sendMouseMoveEvent(18, 30, client); - sendMouseMoveEvent(20, 50, client); + sendMouseMoveEvent(18, 30, 0x0, client); + sendMouseMoveEvent(20, 50, 0x0, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 18, 30, 0x0); @@ -3893,9 +3893,9 @@ describe('Remote Frame Buffer protocol client', function () { }); it('should only send first and last move of many close events', function () { - sendMouseMoveEvent(18, 30, client); - sendMouseMoveEvent(20, 50, client); - sendMouseMoveEvent(21, 55, client); + sendMouseMoveEvent(18, 30, 0x0, client); + sendMouseMoveEvent(20, 50, 0x0, client); + sendMouseMoveEvent(21, 55, 0x0, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 18, 30, 0x0); @@ -3909,46 +3909,46 @@ describe('Remote Frame Buffer protocol client', function () { // We selected the 17ms since that is ~60 FPS it('should send move events every 17 ms', function () { - sendMouseMoveEvent(1, 10, client); // instant send + sendMouseMoveEvent(1, 10, 0x0, client); // instant send clock.tick(10); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 1, 10, 0x0); pointerEvent.resetHistory(); - sendMouseMoveEvent(2, 20, client); // delayed + sendMouseMoveEvent(2, 20, 0x0, client); // delayed clock.tick(10); // timeout send expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 2, 20, 0x0); pointerEvent.resetHistory(); - sendMouseMoveEvent(3, 30, client); // delayed + sendMouseMoveEvent(3, 30, 0x0, client); // delayed clock.tick(10); - sendMouseMoveEvent(4, 40, client); // delayed + sendMouseMoveEvent(4, 40, 0x0, client); // delayed clock.tick(10); // timeout send expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 4, 40, 0x0); pointerEvent.resetHistory(); - sendMouseMoveEvent(5, 50, client); // delayed + sendMouseMoveEvent(5, 50, 0x0, client); // delayed expect(pointerEvent).to.not.have.been.called; }); it('should send waiting move events before a button press', function () { - sendMouseMoveEvent(13, 9, client); + sendMouseMoveEvent(13, 9, 0x0, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 13, 9, 0x0); pointerEvent.resetHistory(); - sendMouseMoveEvent(20, 70, client); + sendMouseMoveEvent(20, 70, 0x0, client); expect(pointerEvent).to.not.have.been.called; - sendMouseButtonEvent(20, 70, true, 0, client); + sendMouseButtonEvent(20, 70, true, 0x1, client); expect(pointerEvent).to.have.been.calledTwice; expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock, @@ -3958,7 +3958,7 @@ describe('Remote Frame Buffer protocol client', function () { }); it('should send move events with enough time apart normally', function () { - sendMouseMoveEvent(58, 60, client); + sendMouseMoveEvent(58, 60, 0x0, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 58, 60, 0x0); @@ -3966,7 +3966,7 @@ describe('Remote Frame Buffer protocol client', function () { clock.tick(20); - sendMouseMoveEvent(25, 60, client); + sendMouseMoveEvent(25, 60, 0x0, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 25, 60, 0x0); @@ -3974,13 +3974,13 @@ describe('Remote Frame Buffer protocol client', function () { }); it('should not send waiting move events if disconnected', function () { - sendMouseMoveEvent(88, 99, client); + sendMouseMoveEvent(88, 99, 0x0, client); expect(pointerEvent).to.have.been.calledOnceWith(client._sock, 88, 99, 0x0); pointerEvent.resetHistory(); - sendMouseMoveEvent(66, 77, client); + sendMouseMoveEvent(66, 77, 0x0, client); client.disconnect(); clock.tick(20); @@ -3998,7 +3998,7 @@ describe('Remote Frame Buffer protocol client', function () { }); describe('Wheel events', function () { - function sendWheelEvent(x, y, dx, dy, mode=0) { + function sendWheelEvent(x, y, dx, dy, mode=0, buttons=0) { let pos = elementToClient(x, y, client); let ev; @@ -4009,7 +4009,8 @@ describe('Remote Frame Buffer protocol client', function () { 'clientY': pos.y, 'deltaX': dx, 'deltaY': dy, - 'deltaMode': mode }); + 'deltaMode': mode, + 'buttons': buttons }); client._canvas.dispatchEvent(ev); } @@ -4107,8 +4108,8 @@ describe('Remote Frame Buffer protocol client', function () { }); it('should handle wheel event with buttons pressed', function () { - sendMouseButtonEvent(10, 10, true, 0, client); - sendWheelEvent(10, 10, 0, 50); + sendMouseButtonEvent(10, 10, true, 0x1, client); + sendWheelEvent(10, 10, 0, 50, 0, 0x1); expect(pointerEvent).to.have.been.called.calledThrice; From d1548c12ecb08a2aa5a871d8a621563f9688c1d1 Mon Sep 17 00:00:00 2001 From: Adam Halim Date: Mon, 13 Jan 2025 15:43:47 +0100 Subject: [PATCH 087/166] Don't send mouse events when dragging viewport We don't want to send any mouse events to the server when dragging the viewport. Instead, we treat them as a client-only operation. --- core/rfb.js | 7 ++----- tests/test.rfb.js | 16 ++++++---------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index 4b105cb57..5e08e32f5 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -1306,8 +1306,6 @@ export default class RFB extends EventTargetMixin { this._viewportHasMoved = false; this._viewportDragging = true; this._viewportDragPos = {'x': pos.x, 'y': pos.y}; - - this._fakeMouseMove(ev, pos.x, pos.y); } else { this._fakeMouseMove(ev, pos.x, pos.y); this._handleMouseButton(pos.x, pos.y, 0x1); @@ -1320,8 +1318,6 @@ export default class RFB extends EventTargetMixin { // sending any events to the server. this._viewportHasMoved = false; this._viewportDragPos = {'x': pos.x, 'y': pos.y}; - - this._fakeMouseMove(ev, pos.x, pos.y); } else { this._fakeMouseMove(ev, pos.x, pos.y); this._handleMouseButton(pos.x, pos.y, 0x4); @@ -1360,8 +1356,9 @@ export default class RFB extends EventTargetMixin { this._viewportDragPos = {'x': pos.x, 'y': pos.y}; this._display.viewportChangePos(deltaX, deltaY); } + } else { + this._fakeMouseMove(ev, pos.x, pos.y); } - this._fakeMouseMove(ev, pos.x, pos.y); break; case 'twodrag': // Always scroll in the same position. diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 842981882..85c9eb66b 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -808,8 +808,7 @@ describe('Remote Frame Buffer protocol client', function () { gestureStart('drag', 13, 9, client); gestureMove('drag', 43, 9, client); - // FIXME: We don't want to send a pointer event here - // expect(RFB.messages.pointerEvent).to.not.have.been.called; + expect(RFB.messages.pointerEvent).to.not.have.been.called; expect(client._display.viewportChangePos).to.have.been.calledOnce; expect(client._display.viewportChangePos).to.have.been.calledWith(-30, 0); @@ -834,8 +833,7 @@ describe('Remote Frame Buffer protocol client', function () { gestureStart('longpress', 13, 9, client); gestureMove('longpress', 14, 9, client); - // FIXME: We don't want to send a pointer event here - // expect(RFB.messages.pointerEvent).to.not.have.been.called; + expect(RFB.messages.pointerEvent).to.not.have.been.called; expect(client._display.viewportChangePos).to.not.have.been.called; client._display.viewportChangePos.resetHistory(); @@ -845,8 +843,7 @@ describe('Remote Frame Buffer protocol client', function () { gestureEnd('longpress', 43, 9, client); expect(RFB.messages.pointerEvent).to.not.have.been.called; - // FIXME: We only want to move the viewport once - // expect(client._display.viewportChangePos).to.have.been.calledOnce; + expect(client._display.viewportChangePos).to.have.been.calledOnce; expect(client._display.viewportChangePos).to.have.been.calledWith(-30, 0); }); @@ -857,8 +854,7 @@ describe('Remote Frame Buffer protocol client', function () { gestureStart('longpress', 13, 9, client); gestureMove('longpress', 14, 10, client); - // FIXME: We don't want to send a pointer event here - // expect(RFB.messages.pointerEvent).to.not.have.been.called; + expect(RFB.messages.pointerEvent).to.not.have.been.called; expect(client._display.viewportChangePos).to.not.have.been.called; client._display.viewportChangePos.resetHistory(); @@ -870,9 +866,9 @@ describe('Remote Frame Buffer protocol client', function () { expect(RFB.messages.pointerEvent.firstCall).to.have.been.calledWith(client._sock, 14, 9, 0x0); expect(RFB.messages.pointerEvent.secondCall).to.have.been.calledWith(client._sock, - 14, 9, 0x4); + 14, 9, 0x4); expect(RFB.messages.pointerEvent.thirdCall).to.have.been.calledWith(client._sock, - 14, 9, 0x0); + 14, 9, 0x0); expect(client._display.viewportChangePos).to.not.have.been.called; }); From 6383fa6384ed672c83f63693cdc3609851889a0a Mon Sep 17 00:00:00 2001 From: Adam Halim Date: Tue, 14 Jan 2025 12:32:51 +0100 Subject: [PATCH 088/166] Flush mouseMove when initiating viewport dragging We want to flush pending mouse moves before we initiate viewport dragging. Before this commit, there were scenarios where the _mouseButtonMask would track a released button as being down. --- core/rfb.js | 2 ++ tests/test.rfb.js | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/core/rfb.js b/core/rfb.js index 5e08e32f5..89e9197d2 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -1103,6 +1103,8 @@ export default class RFB extends EventTargetMixin { this._viewportDragPos = {'x': pos.x, 'y': pos.y}; this._viewportHasMoved = false; + this._flushMouseMoveTimer(pos.x, pos.y); + // Skip sending mouse events, instead save the current // mouse mask so we can send it later. this._mouseButtonMask = bmask; diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 85c9eb66b..62f2a6498 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -898,6 +898,24 @@ describe('Remote Frame Buffer protocol client', function () { expect(client._display.viewportChangePos).to.not.have.been.called; }); + + it('should flush move events when initiating viewport drag', function () { + sendMouseMoveEvent(13, 9, 0x0, client); + sendMouseMoveEvent(14, 9, 0x0, client); + sendMouseButtonEvent(14, 9, true, 0x1, client); + + expect(RFB.messages.pointerEvent).to.have.been.calledTwice; + expect(RFB.messages.pointerEvent.firstCall).to.have.been.calledWith(client._sock, + 13, 9, 0x0); + expect(RFB.messages.pointerEvent.secondCall).to.have.been.calledWith(client._sock, + 14, 9, 0x0); + + RFB.messages.pointerEvent.resetHistory(); + + clock.tick(100); + + expect(RFB.messages.pointerEvent).to.not.have.been.called;; + }); }); }); From e8602f23abede3fb981ff605b2c8e511a024f3f8 Mon Sep 17 00:00:00 2001 From: Adam Halim Date: Tue, 14 Jan 2025 16:26:11 +0100 Subject: [PATCH 089/166] Move sendFbuMsg() to broader scope This is needed if we want to use this function elsewhere in our tests. --- tests/test.rfb.js | 48 +++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 62f2a6498..cb2945a54 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -233,6 +233,30 @@ describe('Remote Frame Buffer protocol client', function () { client._canvas.dispatchEvent(ev); } + function sendFbuMsg(rectInfo, rectData, client, rectCnt) { + let data = []; + + if (!rectCnt || rectCnt > -1) { + // header + data.push(0); // msg type + data.push(0); // padding + push16(data, rectCnt || rectData.length); + } + + for (let i = 0; i < rectData.length; i++) { + if (rectInfo[i]) { + push16(data, rectInfo[i].x); + push16(data, rectInfo[i].y); + push16(data, rectInfo[i].width); + push16(data, rectInfo[i].height); + push32(data, rectInfo[i].encoding); + } + data = data.concat(rectData[i]); + } + + client._sock._websocket._receiveData(new Uint8Array(data)); + } + describe('Connecting/Disconnecting', function () { describe('#RFB (constructor)', function () { let open, attach; @@ -2757,30 +2781,6 @@ describe('Remote Frame Buffer protocol client', function () { }); describe('Framebuffer update handling', function () { - function sendFbuMsg(rectInfo, rectData, client, rectCnt) { - let data = []; - - if (!rectCnt || rectCnt > -1) { - // header - data.push(0); // msg type - data.push(0); // padding - push16(data, rectCnt || rectData.length); - } - - for (let i = 0; i < rectData.length; i++) { - if (rectInfo[i]) { - push16(data, rectInfo[i].x); - push16(data, rectInfo[i].y); - push16(data, rectInfo[i].width); - push16(data, rectInfo[i].height); - push32(data, rectInfo[i].encoding); - } - data = data.concat(rectData[i]); - } - - client._sock._websocket._receiveData(new Uint8Array(data)); - } - it('should send an update request if there is sufficient data', function () { let esock = new Websock(); let ews = new FakeWebSocket(); From e081d1415ac0f3620d5ff3f81080711ef20002de Mon Sep 17 00:00:00 2001 From: Adam Halim Date: Thu, 9 Jan 2025 09:19:21 +0100 Subject: [PATCH 090/166] Add support for forward and back mouse buttons This commit implements the extendedMouseButtons pseudo-encoding, which makes it possible to use the forward and back mouse buttons. --- core/encodings.js | 1 + core/rfb.js | 51 ++++++++++++++++++++++++++++++++++++++++++++--- tests/test.rfb.js | 48 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 96 insertions(+), 4 deletions(-) diff --git a/core/encodings.js b/core/encodings.js index bf25ac917..7afcb17fc 100644 --- a/core/encodings.js +++ b/core/encodings.js @@ -30,6 +30,7 @@ export const encodings = { pseudoEncodingXvp: -309, pseudoEncodingFence: -312, pseudoEncodingContinuousUpdates: -313, + pseudoEncodingExtendedMouseButtons: -316, pseudoEncodingCompressLevel9: -247, pseudoEncodingCompressLevel0: -256, pseudoEncodingVMwareCursor: 0x574d5664, diff --git a/core/rfb.js b/core/rfb.js index 89e9197d2..57f025814 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -152,6 +152,8 @@ export default class RFB extends EventTargetMixin { this._qemuExtKeyEventSupported = false; + this._extendedPointerEventSupported = false; + this._clipboardText = null; this._clipboardServerCapabilitiesActions = {}; this._clipboardServerCapabilitiesFormats = {}; @@ -1051,10 +1053,11 @@ export default class RFB extends EventTargetMixin { 1: 1 << 2, // Right 2: 1 << 1, // Middle 3: 1 << 7, // Back + 4: 1 << 8, // Forward }; let bmask = 0; - for (let i = 0; i < 4; i++) { + for (let i = 0; i < 5; i++) { if (buttons & (1 << i)) { bmask |= buttonMaskMap[i]; } @@ -1189,8 +1192,20 @@ export default class RFB extends EventTargetMixin { if (this._rfbConnectionState !== 'connected') { return; } if (this._viewOnly) { return; } // View only, skip mouse events - RFB.messages.pointerEvent(this._sock, this._display.absX(x), - this._display.absY(y), mask); + // Highest bit in mask is never sent to the server + if (mask & 0x8000) { + throw new Error("Illegal mouse button mask (mask: " + mask + ")"); + } + + let extendedMouseButtons = mask & 0x7f80; + + if (this._extendedPointerEventSupported && extendedMouseButtons) { + RFB.messages.extendedPointerEvent(this._sock, this._display.absX(x), + this._display.absY(y), mask); + } else { + RFB.messages.pointerEvent(this._sock, this._display.absX(x), + this._display.absY(y), mask); + } } _handleWheel(ev) { @@ -2229,6 +2244,7 @@ export default class RFB extends EventTargetMixin { encs.push(encodings.pseudoEncodingContinuousUpdates); encs.push(encodings.pseudoEncodingDesktopName); encs.push(encodings.pseudoEncodingExtendedClipboard); + encs.push(encodings.pseudoEncodingExtendedMouseButtons); if (this._fbDepth == 24) { encs.push(encodings.pseudoEncodingVMwareCursor); @@ -2658,6 +2674,10 @@ export default class RFB extends EventTargetMixin { case encodings.pseudoEncodingExtendedDesktopSize: return this._handleExtendedDesktopSize(); + case encodings.pseudoEncodingExtendedMouseButtons: + this._extendedPointerEventSupported = true; + return true; + case encodings.pseudoEncodingQEMULedEvent: return this._handleLedEvent(); @@ -3067,6 +3087,10 @@ RFB.messages = { pointerEvent(sock, x, y, mask) { sock.sQpush8(5); // msg-type + // Marker bit must be set to 0, otherwise the server might + // confuse the marker bit with the highest bit in a normal + // PointerEvent message. + mask = mask & 0x7f; sock.sQpush8(mask); sock.sQpush16(x); @@ -3075,6 +3099,27 @@ RFB.messages = { sock.flush(); }, + extendedPointerEvent(sock, x, y, mask) { + sock.sQpush8(5); // msg-type + + let higherBits = (mask >> 7) & 0xff; + + // Bits 2-7 are reserved + if (higherBits & 0xfc) { + throw new Error("Invalid mouse button mask: " + mask); + } + + let lowerBits = mask & 0x7f; + lowerBits |= 0x80; // Set marker bit to 1 + + sock.sQpush8(lowerBits); + sock.sQpush16(x); + sock.sQpush16(y); + sock.sQpush8(higherBits); + + sock.flush(); + }, + // Used to build Notify and Request data. _buildExtendedClipboardFlags(actions, formats) { let data = new Uint8Array(4); diff --git a/tests/test.rfb.js b/tests/test.rfb.js index cb2945a54..8cdd2e36b 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -3265,6 +3265,7 @@ describe('Remote Frame Buffer protocol client', function () { expect(spy).to.have.been.calledOnce; expect(spy.args[0][0].detail.name).to.equal('som€ nam€'); }); + }); describe('Caps Lock and Num Lock remote fixup', function () { @@ -3757,6 +3758,7 @@ describe('Remote Frame Buffer protocol client', function () { describe('Asynchronous events', function () { let client; let pointerEvent; + let extendedPointerEvent; let keyEvent; let qemuKeyEvent; @@ -3770,12 +3772,14 @@ describe('Remote Frame Buffer protocol client', function () { client.focusOnClick = false; pointerEvent = sinon.spy(RFB.messages, 'pointerEvent'); + extendedPointerEvent = sinon.spy(RFB.messages, 'extendedPointerEvent'); keyEvent = sinon.spy(RFB.messages, 'keyEvent'); qemuKeyEvent = sinon.spy(RFB.messages, 'QEMUExtendedKeyEvent'); }); afterEach(function () { pointerEvent.restore(); + extendedPointerEvent.restore(); keyEvent.restore(); qemuKeyEvent.restore(); }); @@ -3884,6 +3888,23 @@ describe('Remote Frame Buffer protocol client', function () { 50, 70, 0x0); }); + it('should send extended pointer event when server supports extended pointer events', function () { + // Enable extended pointer events + sendFbuMsg([{ x: 0, y: 0, width: 0, height: 0, encoding: -316 }], [[]], client); + + sendMouseButtonEvent(50, 70, true, 0x10, client); + + expect(extendedPointerEvent).to.have.been.calledOnceWith(client._sock, + 50, 70, 0x100); + }); + + it('should send normal pointer event when server does not support extended pointer events', function () { + sendMouseButtonEvent(50, 70, true, 0x10, client); + + expect(pointerEvent).to.have.been.calledOnceWith(client._sock, + 50, 70, 0x100); + }); + describe('Event aggregation', function () { it('should send a single pointer event on mouse movement', function () { sendMouseMoveEvent(50, 70, 0x0, client); @@ -5135,11 +5156,36 @@ describe('RFB messages', function () { }); it('should send correct data for pointer events', function () { + RFB.messages.pointerEvent(sock, 12345, 54321, 0x2b); + let expected = + [ 5, 0x2b, 0x30, 0x39, 0xd4, 0x31]; + expect(sock).to.have.sent(new Uint8Array(expected)); + }); + + it('should send correct data for pointer events with marker bit set', function () { RFB.messages.pointerEvent(sock, 12345, 54321, 0xab); let expected = - [ 5, 0xab, 0x30, 0x39, 0xd4, 0x31]; + [ 5, 0x2b, 0x30, 0x39, 0xd4, 0x31]; + expect(sock).to.have.sent(new Uint8Array(expected)); + }); + + it('should send correct data for pointer events with extended button bits set', function () { + RFB.messages.pointerEvent(sock, 12345, 54321, 0x3ab); + let expected = + [ 5, 0x2b, 0x30, 0x39, 0xd4, 0x31]; expect(sock).to.have.sent(new Uint8Array(expected)); }); + + it('should send correct data for extended pointer events', function () { + RFB.messages.extendedPointerEvent(sock, 12345, 54321, 0xab); + let expected = + [ 5, 0xab, 0x30, 0x39, 0xd4, 0x31, 0x1]; + expect(sock).to.have.sent(new Uint8Array(expected)); + }); + + it('should not send invalid data for extended pointer events', function () { + expect(() => RFB.messages.extendedPointerEvent(sock, 12345, 54321, 0x3ab)).to.throw(Error); + }); }); describe('Clipboard events', function () { From 83a5e9e9dbf420b606439626642ecdbdf18721f8 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Wed, 15 Jan 2025 12:43:44 +0100 Subject: [PATCH 091/166] Also test Ctrl+AltGr, as that is what browsers use Modern browsers now send the odd sequence of Ctrl+AltGr, rather than the raw Ctrl+Alt, or the fully adjusted just AltGr. Make sure we have a test for this scenario and don't break it. --- tests/test.keyboard.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test.keyboard.js b/tests/test.keyboard.js index 47be623d8..ccc01247d 100644 --- a/tests/test.keyboard.js +++ b/tests/test.keyboard.js @@ -525,6 +525,37 @@ describe('Key event handling', function () { expect(kbd.onkeyevent).to.not.have.been.called; }); + it('should generate AltGraph for quick Ctrl+AltGraph sequence', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1, timeStamp: Date.now()})); + this.clock.tick(20); + kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'AltGraph', location: 2, timeStamp: Date.now()})); + expect(kbd.onkeyevent).to.have.been.calledOnce; + expect(kbd.onkeyevent).to.have.been.calledWith(0xfe03, 'AltRight', true); + + // Check that the timer is properly dead + kbd.onkeyevent.resetHistory(); + this.clock.tick(100); + expect(kbd.onkeyevent).to.not.have.been.called; + }); + + it('should generate Ctrl, AltGraph for slow Ctrl+AltGraph sequence', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1, timeStamp: Date.now()})); + this.clock.tick(60); + kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'AltGraph', location: 2, timeStamp: Date.now()})); + expect(kbd.onkeyevent).to.have.been.calledTwice; + expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xffe3, "ControlLeft", true); + expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xfe03, "AltRight", true); + + // Check that the timer is properly dead + kbd.onkeyevent.resetHistory(); + this.clock.tick(100); + expect(kbd.onkeyevent).to.not.have.been.called; + }); + it('should pass through single Alt', function () { const kbd = new Keyboard(document); kbd.onkeyevent = sinon.spy(); From 4f284c2f153ae7d2597d8d7d91724462d736362a Mon Sep 17 00:00:00 2001 From: Alexander Zeijlon Date: Fri, 17 Jan 2025 11:29:07 +0100 Subject: [PATCH 092/166] Remove bold styling tags in vnc.html We aren't emphasizing important information with bold tags anywhere else, so we shouldn't do it here either. --- vnc.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vnc.html b/vnc.html index c2cc4e555..c4033c2d0 100644 --- a/vnc.html +++ b/vnc.html @@ -330,7 +330,7 @@

no
VNC

The server has provided the following identifying information:
- Fingerprint: + Fingerprint:
From ca270efcc36a8f6df97d44977d14bb64693a6217 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sun, 12 Jan 2025 00:36:44 +0100 Subject: [PATCH 093/166] Standardize on 6px or 12px border-radius This results in a few things becoming slighly more rounded, for example the controlbar, the settings panel and buttons/inputs. Increased rounding gives a more friendly feel. --- app/styles/base.css | 20 ++++++++++---------- app/styles/input.css | 6 +++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index ecef3771a..de2624f56 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -174,7 +174,7 @@ html { font-weight: bold; color: #fff; - border-radius: 10px; + border-radius: 12px; box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5); background: rgba(200,55,55,0.8); } @@ -242,7 +242,7 @@ html { transition: 0.5s ease-in-out; background-color: var(--novnc-blue); - border-radius: 0 10px 10px 0; + border-radius: 0 12px 12px 0; user-select: none; -webkit-user-select: none; @@ -267,7 +267,7 @@ html { } .noVNC_right #noVNC_control_bar { left: 100%; - border-radius: 10px 0 0 10px; + border-radius: 12px 0 0 12px; } .noVNC_right #noVNC_control_bar.noVNC_open { left: 0; @@ -285,7 +285,7 @@ html { height: 50px; z-index: -1; cursor: pointer; - border-radius: 5px; + border-radius: 6px; background-color: var(--novnc-darkblue); background-image: url("../images/handle_bg.svg"); background-repeat: no-repeat; @@ -372,7 +372,7 @@ html { transition: 0.2s ease-in-out; background: transparent; box-shadow: 0 0 10px black, inset 0 0 10px 10px var(--novnc-darkblue); - border-radius: 10px; + border-radius: 12px; transition-delay: 0s; } #noVNC_control_bar_hint.noVNC_active { @@ -444,7 +444,7 @@ html { padding: 15px; background: #fff; - border-radius: 10px; + border-radius: 12px; color: #000; border: 2px solid #E0E0E0; box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5); @@ -500,7 +500,7 @@ html { .noVNC_panel .noVNC_heading { background-color: var(--novnc-blue); - border-radius: 5px; + border-radius: 6px; padding: 5px; /* Compensate for padding in image */ padding-right: 8px; @@ -534,7 +534,7 @@ html { margin-left: 10px; padding: 5px; background: rgba(0, 0, 0, 0.05); - border-radius: 5px; + border-radius: 6px; } .noVNC_expander:not(.noVNC_open) ~ * { display: none; @@ -732,7 +732,7 @@ html { font-size: 80px; text-align: center; - border-radius: 5px; + border-radius: 6px; } @media (max-width: 440px) { #noVNC_connect_dlg { @@ -759,7 +759,7 @@ html { cursor: pointer; border-color: var(--novnc-darkblue); - border-radius: 5px; + border-radius: 6px; background: linear-gradient(to top, var(--novnc-blue), rgb(99, 119, 147)); color: white; diff --git a/app/styles/input.css b/app/styles/input.css index a2b289bf5..2be7fe442 100644 --- a/app/styles/input.css +++ b/app/styles/input.css @@ -22,7 +22,7 @@ input, input::file-selector-button, button, select, textarea { padding: 0.5em var(--input-xpadding); border: 1px solid var(--novnc-lightgrey); - border-radius: 5px; + border-radius: 6px; color: black; --bg-gradient: linear-gradient(to top, rgb(255, 255, 255) 80%, rgb(240, 240, 240)); background-image: var(--bg-gradient); @@ -156,7 +156,7 @@ input[type=range]::-webkit-slider-thumb { appearance: none; width: 18px; height: 20px; - border-radius: 5px; + border-radius: 6px; background-color: white; border: 1px solid dimgray; margin-top: -7px; @@ -165,7 +165,7 @@ input[type=range]::-moz-range-thumb { appearance: none; width: 18px; height: 20px; - border-radius: 5px; + border-radius: 6px; background-color: white; border: 1px solid dimgray; margin-top: -7px; From e9b48ae40938865fa83ebd2d39b25d64f92582b2 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sun, 12 Jan 2025 01:29:06 +0100 Subject: [PATCH 094/166] Get rid of gradients on buttons and inputs Lets make things more flat and modern. --- app/styles/base.css | 4 ++-- app/styles/input.css | 29 ++++++++++++++--------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index de2624f56..3c96933fe 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -761,14 +761,14 @@ html { border-color: var(--novnc-darkblue); border-radius: 6px; - background: linear-gradient(to top, var(--novnc-blue), rgb(99, 119, 147)); + background-color: var(--novnc-blue); color: white; /* This avoids it jumping around when :active */ vertical-align: middle; } #noVNC_connect_button:hover { - background: linear-gradient(to top, var(--novnc-blue), rgb(105, 125, 155)); + background-color: var(--novnc-darkblue); } #noVNC_connect_button img { diff --git a/app/styles/input.css b/app/styles/input.css index 2be7fe442..1a51e54e7 100644 --- a/app/styles/input.css +++ b/app/styles/input.css @@ -18,14 +18,10 @@ input, input::file-selector-button, button, select, textarea { /* Disable default rendering */ appearance: none; - background: none; padding: 0.5em var(--input-xpadding); border: 1px solid var(--novnc-lightgrey); border-radius: 6px; - color: black; - --bg-gradient: linear-gradient(to top, rgb(255, 255, 255) 80%, rgb(240, 240, 240)); - background-image: var(--bg-gradient); } /* @@ -44,6 +40,8 @@ select { /* This avoids it jumping around when :active */ vertical-align: middle; margin-top: 0; + color: black; + background-color: white; /* Disable Chrome's touch tap highlight */ -webkit-tap-highlight-color: transparent; @@ -60,7 +58,8 @@ select { stroke="rgb(31,31,31)" fill="none" \ stroke-linecap="round" stroke-linejoin="round" /> \ '); - background-image: var(--select-arrow), var(--bg-gradient); + background-color: white; + background-image: var(--select-arrow); background-position: calc(100% - var(--input-xpadding)), left top, left top; background-repeat: no-repeat; padding-right: calc(2*var(--input-xpadding) + 8px); @@ -75,7 +74,7 @@ select:active { \ - '), var(--bg-gradient); + '); } option { color: black; @@ -192,13 +191,13 @@ input[type=reset]:hover, input[type=submit]:hover, input::file-selector-button:hover, button:hover { - background-image: linear-gradient(to top, rgb(255, 255, 255), rgb(250, 250, 250)); + background-color: var(--novnc-lightgrey); } select:hover { - background-image: var(--select-arrow), - linear-gradient(to top, rgb(255, 255, 255), rgb(250, 250, 250)); - background-position: calc(100% - 7px), left top; - background-repeat: no-repeat; + background-image: + var(--select-arrow), + linear-gradient(var(--novnc-lightgrey) 100%, + transparent); } @media (any-pointer: coarse) { /* We don't want a hover style after touch input */ @@ -209,10 +208,10 @@ select:hover { input[type=submit]:hover, input::file-selector-button:hover, button:hover { - background-image: var(--bg-gradient); + background-color: white; } select:hover { - background-image: var(--select-arrow), var(--bg-gradient); + background-image: var(--select-arrow); } } @@ -264,7 +263,7 @@ input[type=submit]:disabled, input:disabled::file-selector-button, button:disabled, select:disabled { - background-image: var(--bg-gradient); + background-color: white; border-bottom-width: 2px; margin-top: 0; } @@ -272,7 +271,7 @@ input[type=file]:disabled { background-image: none; } select:disabled { - background-image: var(--select-arrow), var(--bg-gradient); + background-image: var(--select-arrow), } input[type=image]:disabled { /* See Firefox bug: From 3f29c9d9930cf5f6b67cb6692477948bb441ea2c Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sun, 12 Jan 2025 01:37:19 +0100 Subject: [PATCH 095/166] Differentiate buttons from text inputs By making buttons grey with bold text, they are easy to distinguish from text inputs. --- app/styles/constants.css | 3 +++ app/styles/input.css | 34 ++++++++++++++++++++++++++-------- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/app/styles/constants.css b/app/styles/constants.css index fb1f5509a..daf396160 100644 --- a/app/styles/constants.css +++ b/app/styles/constants.css @@ -12,6 +12,9 @@ --novnc-lightgrey: rgb(192, 192, 192); --novnc-darkgrey: rgb(92, 92, 92); + /* Transparent to make button colors adapt to the background */ + --novnc-buttongrey: rgba(192, 192, 192, 0.5); + --novnc-blue: rgb(110, 132, 163); --novnc-lightblue: rgb(74, 144, 217); --novnc-darkblue: rgb(83, 99, 122); diff --git a/app/styles/input.css b/app/styles/input.css index 1a51e54e7..e2468ae41 100644 --- a/app/styles/input.css +++ b/app/styles/input.css @@ -41,7 +41,8 @@ select { vertical-align: middle; margin-top: 0; color: black; - background-color: white; + font-weight: bold; + background-color: var(--novnc-buttongrey); /* Disable Chrome's touch tap highlight */ -webkit-tap-highlight-color: transparent; @@ -58,8 +59,18 @@ select { stroke="rgb(31,31,31)" fill="none" \ stroke-linecap="round" stroke-linejoin="round" /> \ '); + /* FIXME: A bug in Firefox, requires a workaround for the background: + https://bugzilla.mozilla.org/show_bug.cgi?id=1810958 */ + /* The dropdown list will show the select element's background above and + below the options in Firefox. We want the entire dropdown to be white. */ background-color: white; - background-image: var(--select-arrow); + /* However, we don't want the select element to actually show a white + background, so let's place a gradient above it with the color we want. */ + --grey-background: linear-gradient(var(--novnc-buttongrey) 100%, + transparent); + background-image: + var(--select-arrow), + var(--grey-background); background-position: calc(100% - var(--input-xpadding)), left top, left top; background-repeat: no-repeat; padding-right: calc(2*var(--input-xpadding) + 8px); @@ -74,11 +85,14 @@ select:active { \ - '); + '), + var(--grey-background); } option { + /* Prevent Chrome from inheriting background-color from the is opened in Firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=1805406 */ select:active { /* Rotated arrow */ background-image: url('data:image/svg+xml;utf8, \ - \ - \ + \ + \ '), var(--grey-background); } From 633b4c266d8167e565e6f81d9ece8c022b1bfae7 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sun, 12 Jan 2025 02:06:51 +0100 Subject: [PATCH 097/166] Redesign checkboxes and radiobuttons Makes them bigger and gets rid of their borders. The change also allowed for some shared styling between them. --- app/styles/input.css | 100 ++++++++++++++++++++++++++++++++----------- 1 file changed, 75 insertions(+), 25 deletions(-) diff --git a/app/styles/input.css b/app/styles/input.css index f206a6570..d9b9067c6 100644 --- a/app/styles/input.css +++ b/app/styles/input.css @@ -97,35 +97,80 @@ option { } /* - * Checkboxes + * Shared between checkboxes and radiobuttons */ + +input[type=radio], input[type=checkbox] { display: inline-flex; justify-content: center; align-items: center; - background-color: white; - background-image: unset; - border: 1px solid dimgrey; - border-radius: 3px; - width: 13px; - height: 13px; + border-color: transparent; + background-color: var(--novnc-buttongrey); + /* Disable Chrome's touch tap highlight to avoid conflicts with overlay */ + -webkit-tap-highlight-color: transparent; + width: 16px; + --checkradio-height: 16px; + height: var(--checkradio-height); padding: 0; - margin-right: 6px; - vertical-align: bottom; - transition: 0.2s background-color linear; + margin: 0 6px 0 0; + /* Don't have transitions for outline in order to be consistent + with other elements */ + transition: all 0.2s, outline-color 0s, outline-offset 0s; + + /* A transparent outline in order to work around a graphical clipping issue + in WebKit. See bug: https://bugs.webkit.org/show_bug.cgi?id=256003 */ + outline: 1px solid transparent; + position: relative; /* Since ::before & ::after are absolute positioned */ + + /* We want to align with the middle of capital letters, this requires + a workaround. The default behavior is to align the bottom of the element + on top of the text baseline, this is too far up. + We want to push the element down half the difference in height between + it and a capital X. In our font, the height of a capital "X" is 0.698em. + */ + vertical-align: calc(0px - (var(--checkradio-height) - 0.698em) / 2); + /* FIXME: Could write 1cap instead of 0.698em, but it's only supported in + Firefox as of 2023 */ + /* FIXME: We probably want to use round() here, see bug 8148 */ } -input[type=checkbox]:checked { - background-color: var(--novnc-blue); - border-color: var(--novnc-blue); +input[type=radio]:focus-visible, +input[type=checkbox]:focus-visible { + outline-color: var(--novnc-lightblue); } -input[type=checkbox]:checked::after { +input[type=checkbox]::before, +input[type=radio]::before { content: ""; display: block; /* width & height doesn't work on inline elements */ - width: 3px; - height: 7px; - border: 1px solid white; + transition: inherit; + /* Let's prevent the pseudo-elements from taking up layout space so that + the ::before and ::after pseudo-elements can be in the same place. This + is also required for vertical-align: baseline to work like we want it to + on radio/checkboxes. If the pseudo-elements take up layout space, the + baseline of text inside them will be used instead. */ + position: absolute; +} + +/* + * Checkboxes + */ +input[type=checkbox] { + border-radius: 4px; +} +input[type=checkbox]:checked { + background-color: var(--novnc-blue); +} +input[type=checkbox]::before { + width: 25%; + height: 55%; + border-style: solid; + border-color: transparent; border-width: 0 2px 2px 0; - transform: rotate(40deg) translateY(-1px); + border-radius: 1px; + transform: translateY(-1px) rotate(35deg); +} +input[type=checkbox]:checked::before { + border-color: white; } /* @@ -133,15 +178,20 @@ input[type=checkbox]:checked::after { */ input[type=radio] { border-radius: 50%; - border: 1px solid dimgrey; - width: 12px; - height: 12px; - padding: 0; - margin-right: 6px; - transition: 0.2s border linear; + border: 1px solid transparent; /* To ensure a smooth transition */ } input[type=radio]:checked { - border: 6px solid var(--novnc-blue); + border: 4px solid var(--novnc-blue); + background-color: white; +} +input[type=radio]::before { + width: inherit; + height: inherit; + border-radius: inherit; + opacity: 0; +} +input[type=radio]:checked::before { + opacity: 1; } /* From 7fdcc66d2c924baa984e83f04f759387ba132747 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sun, 12 Jan 2025 02:10:12 +0100 Subject: [PATCH 098/166] Add indeterminate styling to checkboxes and radios This is used when the control is neither checked or unchecked. --- app/styles/input.css | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/app/styles/input.css b/app/styles/input.css index d9b9067c6..7aa3dc7f0 100644 --- a/app/styles/input.css +++ b/app/styles/input.css @@ -139,7 +139,9 @@ input[type=checkbox]:focus-visible { outline-color: var(--novnc-lightblue); } input[type=checkbox]::before, -input[type=radio]::before { +input[type=checkbox]::after, +input[type=radio]::before, +input[type=radio]::after { content: ""; display: block; /* width & height doesn't work on inline elements */ transition: inherit; @@ -150,6 +152,13 @@ input[type=radio]::before { baseline of text inside them will be used instead. */ position: absolute; } +input[type=checkbox]::after, +input[type=radio]::after { + width: 10px; + height: 2px; + background-color: transparent; + border-radius: 2px; +} /* * Checkboxes @@ -157,7 +166,8 @@ input[type=radio]::before { input[type=checkbox] { border-radius: 4px; } -input[type=checkbox]:checked { +input[type=checkbox]:checked, +input[type=checkbox]:indeterminate { background-color: var(--novnc-blue); } input[type=checkbox]::before { @@ -172,6 +182,9 @@ input[type=checkbox]::before { input[type=checkbox]:checked::before { border-color: white; } +input[type=checkbox]:indeterminate::after { + background-color: white; +} /* * Radiobuttons @@ -193,6 +206,9 @@ input[type=radio]::before { input[type=radio]:checked::before { opacity: 1; } +input[type=radio]:indeterminate::after { + background-color: black; +} /* * Range sliders From 017888c9a8fd07f62403103eec42340233faac44 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sun, 12 Jan 2025 02:29:26 +0100 Subject: [PATCH 099/166] Rework how buttons react to :hover and :active Instead of having two different types of effects (hover had a different color, and active had a 3d-effect simulating a pressed button), we now have an increasing activation-level. That means the button goes a bit dark for hover, and then even darker when pressed. There is also a variant that goes lighter for each activation level, that can be used when the initial color is dark. With this change, we can get rid of special :hover and :active styling for the connect button and the control bar buttons. We can use the same activation level principle for all buttons. --- app/styles/base.css | 31 --------- app/styles/input.css | 155 ++++++++++++++++++++++++------------------- 2 files changed, 85 insertions(+), 101 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index 3c96933fe..fba8981ea 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -392,40 +392,15 @@ html { border:1px solid rgba(255, 255, 255, 0.2); border-radius: 6px; background-color: transparent; - background-image: unset; /* we don't want the gradiant from input.css */ } #noVNC_control_bar .noVNC_button.noVNC_selected { border-color: rgba(0, 0, 0, 0.8); background-color: rgba(0, 0, 0, 0.5); } -#noVNC_control_bar .noVNC_button.noVNC_selected:not(:disabled):hover { - border-color: rgba(0, 0, 0, 0.4); - background-color: rgba(0, 0, 0, 0.2); -} -#noVNC_control_bar .noVNC_button:not(:disabled):hover { - background-color: rgba(255, 255, 255, 0.2); -} -#noVNC_control_bar .noVNC_button:not(:disabled):active { - padding-top: 5px; - padding-bottom: 3px; -} #noVNC_control_bar .noVNC_button.noVNC_hidden { display: none !important; } -/* Android browsers don't properly update hover state if touch events are - * intercepted, like they are when clicking on the remote screen. */ -@media (any-pointer: coarse) { - #noVNC_control_bar .noVNC_button:not(:disabled):hover { - background-color: transparent; - } - #noVNC_control_bar .noVNC_button.noVNC_selected:not(:disabled):hover { - border-color: rgba(0, 0, 0, 0.8); - background-color: rgba(0, 0, 0, 0.5); - } -} - - /* Panels */ .noVNC_panel { transform: translateX(25px); @@ -763,12 +738,6 @@ html { background-color: var(--novnc-blue); color: white; - - /* This avoids it jumping around when :active */ - vertical-align: middle; -} -#noVNC_connect_button:hover { - background-color: var(--novnc-darkblue); } #noVNC_connect_button img { diff --git a/app/styles/input.css b/app/styles/input.css index 7aa3dc7f0..e4c5b1a1a 100644 --- a/app/styles/input.css +++ b/app/styles/input.css @@ -24,6 +24,71 @@ input, input::file-selector-button, button, select, textarea { border-radius: 6px; } +/* + * Button activations + */ + +/* A color overlay that depends on the activation level. The level can then be + set for different states on an element, for example hover and click on a + '), + var(--button-activation-overlay), var(--grey-background); } option { @@ -94,6 +158,7 @@ option { background-color: white; color: black; font-weight: normal; + background-image: var(--button-activation-overlay); } /* @@ -107,6 +172,7 @@ input[type=checkbox] { align-items: center; border-color: transparent; background-color: var(--novnc-buttongrey); + background-image: var(--button-activation-overlay); /* Disable Chrome's touch tap highlight to avoid conflicts with overlay */ -webkit-tap-highlight-color: transparent; width: 16px; @@ -169,6 +235,8 @@ input[type=checkbox] { input[type=checkbox]:checked, input[type=checkbox]:indeterminate { background-color: var(--novnc-blue); + background-image: var(--button-activation-overlay-light); + background-blend-mode: overlay; } input[type=checkbox]::before { width: 25%; @@ -196,11 +264,20 @@ input[type=radio] { input[type=radio]:checked { border: 4px solid var(--novnc-blue); background-color: white; + /* button-activation-overlay should be removed from the radio + element to not interfere with button-activation-overlay-light + that is set on the ::before element. */ + background-image: none; } input[type=radio]::before { width: inherit; height: inherit; border-radius: inherit; + /* We can achieve the highlight overlay effect on border colors by + setting button-activation-overlay-light on an element that stays + on top (z-axis) of the element with a border. */ + background-image: var(--button-activation-overlay-light); + mix-blend-mode: overlay; opacity: 0; } input[type=radio]:checked::before { @@ -238,6 +315,9 @@ input[type=range]::-webkit-slider-thumb { height: 20px; border-radius: 6px; background-color: white; + background-image: var(--button-activation-overlay); + /* Disable Chrome's touch tap highlight to avoid conflicts with overlay */ + -webkit-tap-highlight-color: transparent; border: 1px solid dimgray; margin-top: -7px; } @@ -247,6 +327,7 @@ input[type=range]::-moz-range-thumb { height: 20px; border-radius: 6px; background-color: white; + background-image: var(--button-activation-overlay); border: 1px solid dimgray; margin-top: -7px; } @@ -262,57 +343,6 @@ input::file-selector-button { margin-right: 6px; } -/* - * Hover - */ -input[type=button]:hover, -input[type=color]:hover, -input[type=image]:hover, -input[type=reset]:hover, -input[type=submit]:hover, -input::file-selector-button:hover, -button:hover { - background-color: var(--novnc-lightgrey); -} -select:hover { - background-image: - var(--select-arrow), - linear-gradient(var(--novnc-lightgrey) 100%, - transparent); -} -@media (any-pointer: coarse) { - /* We don't want a hover style after touch input */ - input[type=button]:hover, - input[type=color]:hover, - input[type=image]:hover, - input[type=reset]:hover, - input[type=submit]:hover, - input::file-selector-button:hover, - button:hover { - background-color: var(--novnc-buttongrey); - } - select:hover { - background-image: - var(--select-arrow), - var(--grey-background); - } -} - -/* - * Active (clicked) - */ -input[type=button]:active, -input[type=color]:active, -input[type=image]:active, -input[type=reset]:active, -input[type=submit]:active, -input::file-selector-button:active, -button:active, -select:active { - border-bottom-width: 1px; - margin-top: 1px; -} - /* * Focus (tab) */ @@ -338,21 +368,6 @@ select:disabled, textarea:disabled { opacity: 0.4; } -input[type=button]:disabled, -input[type=color]:disabled, -input[type=image]:disabled, -input[type=reset]:disabled, -input[type=submit]:disabled, -input:disabled::file-selector-button, -button:disabled, -select:disabled { - background-color: var(--novnc-buttongrey); - border-bottom-width: 2px; - margin-top: 0; -} -input[type=file]:disabled { - background-image: none; -} select:disabled { background-image: var(--select-arrow), From 33a2548fcb4d9c437b90b08c6b951755995bd577 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sun, 12 Jan 2025 02:49:35 +0100 Subject: [PATCH 100/166] Make buttons flat by removing borders Gives a more clean look that fits well with the new checkboxes and radios. The old border was mostly used to contribute to a 3d-effect, that was used for :active. That :active-styling has been replaced by activation levels. --- app/styles/base.css | 7 ++----- app/styles/input.css | 31 +++++++++++++++++++++++++------ 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index fba8981ea..c9e60bd1a 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -729,13 +729,10 @@ html { } #noVNC_connect_button { width: 100%; - padding: 5px 30px; - + padding: 6px 30px; cursor: pointer; - - border-color: var(--novnc-darkblue); + border-color: transparent; border-radius: 6px; - background-color: var(--novnc-blue); color: white; } diff --git a/app/styles/input.css b/app/styles/input.css index e4c5b1a1a..7608f918a 100644 --- a/app/styles/input.css +++ b/app/styles/input.css @@ -13,15 +13,35 @@ } input, input::file-selector-button, button, select, textarea { + padding: 0.5em var(--input-xpadding); + border-radius: 6px; + appearance: none; + /* Respect standard font settings */ font: inherit; +} - /* Disable default rendering */ - appearance: none; +/* + * Text input + */ - padding: 0.5em var(--input-xpadding); +input:not([type]), +input[type=date], +input[type=datetime-local], +input[type=email], +input[type=month], +input[type=number], +input[type=password], +input[type=search], +input[type=tel], +input[type=text], +input[type=time], +input[type=url], +input[type=week], +textarea { border: 1px solid var(--novnc-lightgrey); - border-radius: 6px; + /* Account for borders on text inputs, buttons dont have borders */ + padding: calc(0.5em - 1px) var(--input-xpadding); } /* @@ -100,7 +120,7 @@ input[type=submit], input::file-selector-button, button, select { - border-bottom-width: 2px; + border: none; color: black; font-weight: bold; background-color: var(--novnc-buttongrey); @@ -170,7 +190,6 @@ input[type=checkbox] { display: inline-flex; justify-content: center; align-items: center; - border-color: transparent; background-color: var(--novnc-buttongrey); background-image: var(--button-activation-overlay); /* Disable Chrome's touch tap highlight to avoid conflicts with overlay */ From 30d46a00fa3e99610f5acc54b2bdc7576bf10e9e Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sun, 12 Jan 2025 19:50:10 +0100 Subject: [PATCH 101/166] Fix :disabled styling of file-selector-button By applying the rule to the button within the input, we effectively applied the opacity twice - making the button almost disappear. Applying the opacity to the input element is enough. --- app/styles/input.css | 1 - 1 file changed, 1 deletion(-) diff --git a/app/styles/input.css b/app/styles/input.css index 7608f918a..dda08b683 100644 --- a/app/styles/input.css +++ b/app/styles/input.css @@ -381,7 +381,6 @@ input[type=file]:focus-visible { * Disabled */ input:disabled, -input:disabled::file-selector-button, button:disabled, select:disabled, textarea:disabled { From ee08032fe74828962f07422eab02ac3115e2a261 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sun, 12 Jan 2025 19:52:26 +0100 Subject: [PATCH 102/166] Put specific :disabled rules with its element It makes more sense to group rules per element type. --- app/styles/input.css | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/styles/input.css b/app/styles/input.css index dda08b683..d9c350415 100644 --- a/app/styles/input.css +++ b/app/styles/input.css @@ -129,6 +129,11 @@ select { /* Disable Chrome's touch tap highlight */ -webkit-tap-highlight-color: transparent; } +input[type=image]:disabled { + /* See Firefox bug: + https://bugzilla.mozilla.org/show_bug.cgi?id=1798304 */ + cursor: default; +} /* * Select dropdowns @@ -173,6 +178,11 @@ select:active { var(--button-activation-overlay), var(--grey-background); } +select:disabled { + background-image: + var(--select-arrow), + var(--grey-background); +} option { /* Prevent Chrome from inheriting background-color from the is opened in Firefox: - https://bugzilla.mozilla.org/show_bug.cgi?id=1805406 */ -select:active { - /* Rotated arrow */ - background-image: url('data:image/svg+xml;utf8, \ - \ - \ - '), - var(--button-activation-overlay), - var(--grey-background); -} -select:disabled { - background-image: - var(--select-arrow), - var(--grey-background); -} -option { - /* Prevent Chrome from inheriting background-color from the is opened in Firefox: + https://bugzilla.mozilla.org/show_bug.cgi?id=1805406 */ +select:active { + /* Rotated arrow */ + background-image: url('data:image/svg+xml;utf8, \ + \ + \ + '), + var(--button-activation-overlay), + var(--grey-background); +} +select:disabled { + background-image: + var(--select-arrow), + var(--grey-background); +} +option { + /* Prevent Chrome from inheriting background-color from the is opened in Firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=1805406 */ From 7b58cb96bcb9eb9b3620054dcbbce9217a83d9d6 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Mon, 13 Jan 2025 00:19:56 +0100 Subject: [PATCH 111/166] Add minimum width to buttons This ensures they aren't too small, even if the text label is short. --- app/styles/base.css | 1 + app/styles/input.css | 1 + 2 files changed, 2 insertions(+) diff --git a/app/styles/base.css b/app/styles/base.css index c9e60bd1a..aa21ea5c5 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -387,6 +387,7 @@ html { /* Control bar buttons */ #noVNC_control_bar .noVNC_button { + min-width: unset; padding: 4px 4px; vertical-align: middle; border:1px solid rgba(255, 255, 255, 0.2); diff --git a/app/styles/input.css b/app/styles/input.css index efc1804ff..bd543b0ee 100644 --- a/app/styles/input.css +++ b/app/styles/input.css @@ -176,6 +176,7 @@ input[type=submit], input::file-selector-button, button, select { + min-width: 8em; border: none; color: black; font-weight: bold; From 54e76817df2614a21469258c6a271fd5b5bb3a90 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Mon, 13 Jan 2025 00:22:20 +0100 Subject: [PATCH 112/166] Pointer cursor on buttons & grab on sliders This makes buttons and slider stand out more. --- app/styles/input.css | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/app/styles/input.css b/app/styles/input.css index bd543b0ee..95ae34747 100644 --- a/app/styles/input.css +++ b/app/styles/input.css @@ -182,11 +182,18 @@ select { font-weight: bold; background-color: var(--novnc-buttongrey); background-image: var(--button-activation-overlay); - + cursor: pointer; /* Disable Chrome's touch tap highlight */ -webkit-tap-highlight-color: transparent; } -input[type=image]:disabled { +input[type=button]:disabled, +input[type=color]:disabled, +input[type=image]:disabled, +input[type=reset]:disabled, +input[type=submit]:disabled, +input:disabled::file-selector-button, +button:disabled, +select:disabled { /* See Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1798304 */ cursor: default; @@ -332,6 +339,15 @@ input[type=range] { padding: 0; background: transparent; } +input[type=range]:hover { + cursor: grab; +} +input[type=range]:active { + cursor: grabbing; +} +input[type=range]:disabled { + cursor: default; +} /* -webkit-slider.. & -moz-range.. cant be in selector lists: https://bugs.chromium.org/p/chromium/issues/detail?id=1154623 */ input[type=range]::-webkit-slider-runnable-track { From 2bc505741fc5c508db2d9896e96538c8a64e9e47 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Mon, 13 Jan 2025 00:28:10 +0100 Subject: [PATCH 113/166] Add styling for color pickers Note that no color picker elements are currently in use, this is for completeness. --- app/styles/input.css | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/app/styles/input.css b/app/styles/input.css index 95ae34747..374b93e4b 100644 --- a/app/styles/input.css +++ b/app/styles/input.css @@ -209,6 +209,28 @@ input[type=submit] { overflow: clip; } +/* ------- COLOR PICKERS ------- */ + +input[type=color] { + min-width: unset; + box-sizing: content-box; + width: 1.4em; + height: 1.4em; +} +input[type=color]::-webkit-color-swatch-wrapper { + padding: 0; +} +/* -webkit-color-swatch & -moz-color-swatch cant be in a selector list: + https://bugs.chromium.org/p/chromium/issues/detail?id=1154623 */ +input[type=color]::-webkit-color-swatch { + border: none; + border-radius: 6px; +} +input[type=color]::-moz-color-swatch { + border: none; + border-radius: 6px; +} + /* -- SHARED BETWEEN CHECKBOXES AND RADIOBUTTONS -- */ input[type=radio], From 331ad34d90e0992193d1b7a533fb3edbc91d9bd4 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Mon, 13 Jan 2025 00:57:25 +0100 Subject: [PATCH 114/166] Make interface airier by increasing line-height Modern interfaces are less cramped, this makes noVNC feel more up to date. Note that this required some adjustments on noVNC_headings and noVNC_connect_button since the text now takes up more height than the images. --- app/styles/base.css | 13 +++++++++++-- app/styles/input.css | 3 ++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index aa21ea5c5..60c092a59 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -31,6 +31,7 @@ :root { font-family: sans-serif; + line-height: 1.6; } body { @@ -477,9 +478,12 @@ html { .noVNC_panel .noVNC_heading { background-color: var(--novnc-blue); border-radius: 6px; - padding: 5px; + padding: 5px 8px; /* Compensate for padding in image */ - padding-right: 8px; + padding-right: 11px; + display: flex; + align-items: center; + gap: 6px; color: white; font-size: 20px; white-space: nowrap; @@ -736,6 +740,11 @@ html { border-radius: 6px; background-color: var(--novnc-blue); color: white; + + display: flex; + justify-content: center; + place-items: center; + gap: 4px; } #noVNC_connect_button img { diff --git a/app/styles/input.css b/app/styles/input.css index 374b93e4b..731f36a99 100644 --- a/app/styles/input.css +++ b/app/styles/input.css @@ -23,6 +23,7 @@ input::file-selector-button { /* Respect standard font settings */ font: inherit; + line-height: 1.6; } input:disabled, textarea:disabled, @@ -85,7 +86,7 @@ textarea { /* Make textareas show at minimum one line. This does not work when using box-sizing border-box, in which case, vertical padding and border width needs to be taken into account. */ - min-height: 1em; + min-height: 1lh; vertical-align: baseline; /* Firefox gives "text-bottom" by default */ } From 24f99e548d8135980d49124553321b995489c5bf Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Mon, 13 Jan 2025 01:18:32 +0100 Subject: [PATCH 115/166] Add styling for toggle switches These are a type of checkbox that is suitable for ON/OFF-type switches. --- app/styles/input.css | 66 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/app/styles/input.css b/app/styles/input.css index 731f36a99..5eeac9402 100644 --- a/app/styles/input.css +++ b/app/styles/input.css @@ -232,7 +232,7 @@ input[type=color]::-moz-color-swatch { border-radius: 6px; } -/* -- SHARED BETWEEN CHECKBOXES AND RADIOBUTTONS -- */ +/* -- SHARED BETWEEN CHECKBOXES, RADIOBUTTONS AND THE TOGGLE CLASS -- */ input[type=radio], input[type=checkbox] { @@ -273,7 +273,7 @@ input[type=checkbox]:focus-visible { outline-color: var(--novnc-lightblue); } input[type=checkbox]::before, -input[type=checkbox]::after, +input[type=checkbox]:not(.toggle)::after, input[type=radio]::before, input[type=radio]::after { content: ""; @@ -286,7 +286,7 @@ input[type=radio]::after { baseline of text inside them will be used instead. */ position: absolute; } -input[type=checkbox]::after, +input[type=checkbox]:not(.toggle)::after, input[type=radio]::after { width: 10px; height: 2px; @@ -296,16 +296,16 @@ input[type=radio]::after { /* ------- CHECKBOXES ------- */ -input[type=checkbox] { +input[type=checkbox]:not(.toggle) { border-radius: 4px; } -input[type=checkbox]:checked, -input[type=checkbox]:indeterminate { +input[type=checkbox]:not(.toggle):checked, +input[type=checkbox]:not(.toggle):indeterminate { background-color: var(--novnc-blue); background-image: var(--button-activation-overlay-light); background-blend-mode: overlay; } -input[type=checkbox]::before { +input[type=checkbox]:not(.toggle)::before { width: 25%; height: 55%; border-style: solid; @@ -314,10 +314,10 @@ input[type=checkbox]::before { border-radius: 1px; transform: translateY(-1px) rotate(35deg); } -input[type=checkbox]:checked::before { +input[type=checkbox]:not(.toggle):checked::before { border-color: white; } -input[type=checkbox]:indeterminate::after { +input[type=checkbox]:not(.toggle):indeterminate::after { background-color: white; } @@ -353,6 +353,54 @@ input[type=radio]:indeterminate::after { background-color: black; } +/* ------- TOGGLE SWITCHES ------- */ + +/* These are meant to be used instead of checkboxes in some cases. If all of + the following critera are true you should use a toggle switch: + + * The choice is a simple ON/OFF or ENABLE/DISABLE + * The choice doesn't give the feeling of "I agree" or "I confirm" + * There are not multiple related & grouped options + */ + +input[type=checkbox].toggle { + display: inline-block; + --checkradio-height: 18px; /* Height value used in calc, see above */ + width: 31px; + cursor: pointer; + user-select: none; + -webkit-user-select: none; + border-radius: 9px; +} +input[type=checkbox].toggle:disabled { + cursor: default; +} +input[type=checkbox].toggle:indeterminate { + background-color: var(--novnc-buttongrey); + background-image: var(--button-activation-overlay); +} +input[type=checkbox].toggle:checked { + background-color: var(--novnc-blue); + background-image: var(--button-activation-overlay-light); + background-blend-mode: overlay; +} +input[type=checkbox].toggle::before { + --circle-diameter: 10px; + --circle-offset: 4px; + width: var(--circle-diameter); + height: var(--circle-diameter); + top: var(--circle-offset); + left: var(--circle-offset); + background: white; + border-radius: 6px; +} +input[type=checkbox].toggle:checked::before { + left: calc(100% - var(--circle-offset) - var(--circle-diameter)); +} +input[type=checkbox].toggle:indeterminate::before { + left: calc(50% - var(--circle-diameter) / 2); +} + /* ------- RANGE SLIDERS ------- */ input[type=range] { From 6c1e7bc50768b07c7cf81628a45bace90f5aa23b Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Mon, 13 Jan 2025 01:19:40 +0100 Subject: [PATCH 116/166] Utilize toggle switch in settings These settings are well suited to use toggle switches. This makes these settings more approachable and user-friendly. --- vnc.html | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/vnc.html b/vnc.html index ed82b603f..304b1a593 100644 --- a/vnc.html +++ b/vnc.html @@ -206,14 +206,26 @@

no
VNC

  • - +
  • - +

  • - +
  • @@ -244,7 +256,11 @@

    no
    VNC

    WebSocket
    • - +
    • @@ -262,7 +278,11 @@

      no
      VNC


    • - +
    • @@ -270,7 +290,11 @@

      no
      VNC


    • - +

    • From 3a5dd22603efddbed6be721a87bca2c2cff106e0 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Mon, 13 Jan 2025 01:32:57 +0100 Subject: [PATCH 117/166] Add styling for checked options in select boxes --- app/styles/input.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/styles/input.css b/app/styles/input.css index 5eeac9402..8b5f3ca2a 100644 --- a/app/styles/input.css +++ b/app/styles/input.css @@ -515,6 +515,9 @@ select:disabled { var(--select-arrow), var(--grey-background); } +/* Note that styling for
    -
    - - +
    + +
@@ -383,8 +383,8 @@

no
VNC

-
- +
+
@@ -393,7 +393,7 @@

no
VNC

- +
From 6db9dbcf905792570670f255d52b9159fb61a56b Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Tue, 14 Jan 2025 10:29:25 +0100 Subject: [PATCH 123/166] Tweak design of noVNC connect button Make the color contrast with the background and the button more rounded. The goal is to make the button stand out. --- app/styles/base.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index 640893929..1d61bc469 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -737,9 +737,9 @@ html { } } #noVNC_connect_dlg div { - padding: 12px; + padding: 18px; - background-color: var(--novnc-blue); + background-color: var(--novnc-darkgrey); border-radius: 12px; text-align: center; font-size: 20px; @@ -751,7 +751,7 @@ html { padding: 6px 30px; cursor: pointer; border-color: transparent; - border-radius: 6px; + border-radius: 12px; background-color: var(--novnc-blue); color: white; From 14f9ea5880f32f2a4867006d46c8e871942c698e Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Thu, 16 Jan 2025 11:20:47 +0100 Subject: [PATCH 124/166] Fix settings panel layout on small screens Both labels and inputs protruded outside the panel on for example a phone in portrait mode. This commit fixes that by allowing wrapping and setting a max-width. Since the --input-xpadding variable is now used in two different CSS files, it was moved to constants.css. --- app/styles/base.css | 16 ++++++++++++---- app/styles/constants.css | 6 ++++++ app/styles/input.css | 4 ---- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index 1d61bc469..96315703a 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -464,16 +464,24 @@ html { white-space: nowrap; margin: 5px; } +@media (max-width: 540px) { + /* Allow wrapping on small screens */ + .noVNC_panel label { + white-space: unset; + } +} .noVNC_panel li { margin: 5px; } -.noVNC_panel label > button, -.noVNC_panel label > select, -.noVNC_panel label > textarea, -.noVNC_panel label > input:not([type=checkbox]):not([type=radio]) { +.noVNC_panel button, +.noVNC_panel select, +.noVNC_panel textarea, +.noVNC_panel input:not([type=checkbox]):not([type=radio]) { margin-left: 6px; + /* Prevent inputs in panels from being too wide */ + max-width: calc(100% - 6px - var(--input-xpadding) * 2); } .noVNC_panel .noVNC_heading { diff --git a/app/styles/constants.css b/app/styles/constants.css index daf396160..1123a3efc 100644 --- a/app/styles/constants.css +++ b/app/styles/constants.css @@ -22,3 +22,9 @@ --novnc-green: rgb(0, 128, 0); --novnc-yellow: rgb(255, 255, 0); } + +/* ------ MISC PROPERTIES ------ */ + +:root { + --input-xpadding: 1em; +} diff --git a/app/styles/input.css b/app/styles/input.css index 1be95972c..8273d70ad 100644 --- a/app/styles/input.css +++ b/app/styles/input.css @@ -7,10 +7,6 @@ /* ------- SHARED BETWEEN INPUT ELEMENTS -------- */ -:root { - --input-xpadding: 1em; -} - input, textarea, button, From 237a34dfb39d8937e4c502618f3749e81f64f72a Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Thu, 23 Jan 2025 13:16:14 +0100 Subject: [PATCH 125/166] Add exceptions for CSS validator false positives Some new CSS incorrectly give errors from validator.w3.org. Issues were opened in that repo, so hopefully we can remove these exceptions soon. I searched for alternative validators, but couldn't find a different one that had a simple API like this one. In order to reliably detect & handle these exceptions we unfortunately need to make the validator output parsing quite a bit more complicated. --- utils/validate | 57 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/utils/validate b/utils/validate index a6b5507d2..3f5cb871d 100755 --- a/utils/validate +++ b/utils/validate @@ -28,16 +28,63 @@ for fn in "$@"; do curl --silent \ --header "Content-Type: ${type}; charset=utf-8" \ --data-binary @${fn} \ - https://validator.w3.org/nu/?out=text > $OUT - cat $OUT - echo + "https://validator.w3.org/nu/?out=gnu&level=error&asciiquotes=yes" \ + > $OUT # We don't fail the check for warnings as some warnings are # not relevant for us, and we don't currently have a way to # ignore just those - if grep -q -s -E "^Error:" $OUT; then + while read -r line; do + echo + + line_info=$(echo $line | cut -d ":" -f 2) + start_info=$(echo $line_info | cut -d "-" -f 1) + end_info=$(echo $line_info | cut -d "-" -f 2) + + line_start=$(echo $start_info | cut -d "." -f 1) + col_start=$(echo $start_info | cut -d "." -f 2) + + line_end=$(echo $end_info | cut -d "." -f 1) + col_end=$(echo $end_info | cut -d "." -f 2) + + error=$(echo $line | cut -d ":" -f 4-) + + case $error in + *"\"scrollbar-gutter\": Property \"scrollbar-gutter\" doesn't exist.") + # FIXME: https://github.com/validator/validator/issues/1788 + echo "Ignoring below error on line ${line_start}," \ + "the scrollbar-gutter property actually exist and is widely" \ + "supported:" + echo $error + continue + ;; + *"\"clip-path\": \"path("*) + # FIXME: https://github.com/validator/validator/issues/1786 + echo "Ignoring below error on line ${line_start}," \ + "the path() function is valid for clip-path and is" \ + "widely supported:" + echo $error + continue + ;; + *"Parse Error.") + # FIXME: https://github.com/validator/validator/issues/1786 + lineofselector=$(grep -n "@supports selector(" $fn | cut -d ":" -f 1) + linediff=$((lineofselector-line_start)) + # Only ignore if parse error is within 50 lines of "selector()" + if [ ${linediff#-} -lt 50 ]; then + echo "Ignoring below error on line ${line_start}," \ + "the @supports selector() function should not give a parse" \ + "error:" + echo $error + continue + fi + ;; + esac + echo "ERROR between line ${line_start} (col ${col_start})" \ + "and line ${line_end} (col ${col_end}):" + echo $error RET=1 - fi + done < "$OUT" done rm $OUT From 24835bdda4ff705c38f64616501635a1b6ba81ec Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Thu, 23 Jan 2025 14:22:32 +0100 Subject: [PATCH 126/166] Make the background of expanded settings lighter A very slight change to the background color, to make the contrast better with the light-grey input elements. --- app/styles/base.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/styles/base.css b/app/styles/base.css index 96315703a..87bfb45c5 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -535,7 +535,7 @@ html { margin: 5px; margin-left: 10px; padding: 5px; - background: rgba(0, 0, 0, 0.05); + background: rgba(0, 0, 0, 0.04); border-radius: 6px; } .noVNC_expander:not(.noVNC_open) ~ * { From b9f172dcdbb8e082de63b36f2ec30a39b7cd37cd Mon Sep 17 00:00:00 2001 From: Alexander Zeijlon Date: Tue, 28 Jan 2025 11:01:53 +0100 Subject: [PATCH 127/166] Update README.md with ExtendedMouseButtons feature --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c497ad220..4b0b0eac8 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ profits such as: * Supported VNC encodings: raw, copyrect, rre, hextile, tight, tightPNG, ZRLE, JPEG, Zlib * Supports scaling, clipping and resizing the desktop +* Supports ExtendedMouseButtons pseudo-encoding * Local cursor rendering * Clipboard copy/paste with full Unicode support * Translations From 4e410a0619916970d5027b3ec8c6107d09684bce Mon Sep 17 00:00:00 2001 From: "Samuel Mannehed (ThinLinc team)" Date: Tue, 4 Feb 2025 16:14:31 +0100 Subject: [PATCH 128/166] Use less technical phrasing in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b0b0eac8..0a6d87eb8 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ profits such as: * Supported VNC encodings: raw, copyrect, rre, hextile, tight, tightPNG, ZRLE, JPEG, Zlib * Supports scaling, clipping and resizing the desktop -* Supports ExtendedMouseButtons pseudo-encoding +* Supports back & forward mouse buttons * Local cursor rendering * Clipboard copy/paste with full Unicode support * Translations From bbbef2d9fa0f3d8ca301dd233e0f1625be7b19fe Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Wed, 5 Feb 2025 16:11:19 +0100 Subject: [PATCH 129/166] Add helper for ExtendedDesktopSize in tests --- tests/test.rfb.js | 129 ++++++++++++---------------------------------- 1 file changed, 32 insertions(+), 97 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 8cdd2e36b..96f8f6965 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -257,6 +257,29 @@ describe('Remote Frame Buffer protocol client', function () { client._sock._websocket._receiveData(new Uint8Array(data)); } + function sendExtendedDesktopSize(client, reason, result, width, height, screenId, screenFlags) { + let rectInfo = { x: reason, y: result, width: width, height: height, encoding: -308 }; + let rectData = [ + 0x01, // number of screens = 1 + 0x00, 0x00, + 0x00, // padding + (screenId >> 24) & 0xff, + (screenId >> 16) & 0xff, + (screenId >> 8) & 0xff, + screenId & 0xff, + 0x00, 0x00, // screen x + 0x00, 0x00, // screen y + (width >> 8) & 0xff, + width & 0xff, + (height >> 8) & 0xff, + height & 0xff, + (screenFlags >> 24) & 0xff, + (screenFlags >> 16) & 0xff, + (screenFlags >> 8) & 0xff, + screenFlags & 0xff]; + sendFbuMsg([rectInfo], [rectData], client); + } + describe('Connecting/Disconnecting', function () { describe('#RFB (constructor)', function () { let open, attach; @@ -650,16 +673,10 @@ describe('Remote Frame Buffer protocol client', function () { }); it('should update the viewport when the remote session resizes', function () { - // Simple ExtendedDesktopSize FBU message - const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xff, 0x00, 0xff, 0xff, 0xff, 0xfe, 0xcc, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, - 0x00, 0x00, 0x00, 0x00 ]; - sinon.spy(client._display, "viewportChangeSize"); - client._sock._websocket._receiveData(new Uint8Array(incoming)); + // Simple ExtendedDesktopSize FBU message + sendExtendedDesktopSize(client, 0, 0, 4, 4, 0x7890abcd, 0x12345678); // The resize will cause scrollbars on the container, this causes a // resize observation in the browsers fakeResizeObserver.fire(); @@ -951,27 +968,7 @@ describe('Remote Frame Buffer protocol client', function () { container.style.height = '80px'; client.scaleViewport = true; - const incoming = [ 0x00, // msg-type=FBU - 0x00, // padding - 0x00, 0x01, // number of rects = 1 - 0x00, 0x00, // reason = server initialized - 0x00, 0x00, // status = no error - 0x00, 0x04, // new width = 4 - 0x00, 0x04, // new height = 4 - 0xff, 0xff, - 0xfe, 0xcc, // enc = (-308) ExtendedDesktopSize - 0x01, // number of screens = 1 - 0x00, 0x00, - 0x00, // padding - 0x78, 0x90, - 0xab, 0xcd, // screen id = 0 - 0x00, 0x00, // screen x = 0 - 0x00, 0x00, // screen y = 0 - 0x00, 0x04, // screen width = 4 - 0x00, 0x04, // screen height = 4 - 0x12, 0x34, - 0x56, 0x78]; // screen flags - client._sock._websocket._receiveData(new Uint8Array(incoming)); + sendExtendedDesktopSize(client, 0, 0, 4, 4, 0x7890abcd, 0x12345678); }); it('should update display scale factor when changing the property', function () { @@ -1039,16 +1036,9 @@ describe('Remote Frame Buffer protocol client', function () { }); it('should update the scaling when the remote session resizes', function () { - // Simple ExtendedDesktopSize FBU message - const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xff, 0x00, 0xff, 0xff, 0xff, 0xfe, 0xcc, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, - 0x00, 0x00, 0x00, 0x00 ]; - sinon.spy(client._display, "autoscale"); - client._sock._websocket._receiveData(new Uint8Array(incoming)); + sendExtendedDesktopSize(client, 0, 0, 4, 4, 0x7890abcd, 0x12345678); // The resize will cause scrollbars on the container, this causes a // resize observation in the browsers fakeResizeObserver.fire(); @@ -1080,27 +1070,7 @@ describe('Remote Frame Buffer protocol client', function () { container.style.width = '70px'; container.style.height = '80px'; - const incoming = [ 0x00, // msg-type=FBU - 0x00, // padding - 0x00, 0x01, // number of rects = 1 - 0x00, 0x00, // reason = server initialized - 0x00, 0x00, // status = no error - 0x00, 0x04, // new width = 4 - 0x00, 0x04, // new height = 4 - 0xff, 0xff, - 0xfe, 0xcc, // enc = (-308) ExtendedDesktopSize - 0x01, // number of screens = 1 - 0x00, 0x00, - 0x00, // padding - 0x78, 0x90, - 0xab, 0xcd, // screen id = 0 - 0x00, 0x00, // screen x = 0 - 0x00, 0x00, // screen y = 0 - 0x00, 0x04, // screen width = 4 - 0x00, 0x04, // screen height = 4 - 0x12, 0x34, - 0x56, 0x78]; // screen flags - client._sock._websocket._receiveData(new Uint8Array(incoming)); + sendExtendedDesktopSize(client, 0, 0, 4, 4, 0x7890abcd, 0x12345678); sinon.spy(RFB.messages, "setDesktopSize"); }); @@ -1124,31 +1094,9 @@ describe('Remote Frame Buffer protocol client', function () { container.style.width = '70px'; container.style.height = '80px'; - // Simple ExtendedDesktopSize FBU message - const incoming = [ 0x00, // msg-type=FBU - 0x00, // padding - 0x00, 0x01, // number of rects = 1 - 0x00, 0x00, // reason = server initialized - 0x00, 0x00, // status = no error - 0x00, 0x04, // new width = 4 - 0x00, 0x04, // new height = 4 - 0xff, 0xff, - 0xfe, 0xcc, // enc = (-308) ExtendedDesktopSize - 0x01, // number of screens = 1 - 0x00, 0x00, - 0x00, // padding - 0x78, 0x90, - 0xab, 0xcd, // screen id = 0 - 0x00, 0x00, // screen x = 0 - 0x00, 0x00, // screen y = 0 - 0x00, 0x04, // screen width = 4 - 0x00, 0x04, // screen height = 4 - 0x12, 0x34, - 0x56, 0x78]; // screen flags - // First message should trigger a resize - client._sock._websocket._receiveData(new Uint8Array(incoming)); + sendExtendedDesktopSize(client, 0, 0, 4, 4, 0x7890abcd, 0x12345678); // It should match the current size of the container, // not the reported size from the server @@ -1160,7 +1108,7 @@ describe('Remote Frame Buffer protocol client', function () { // Second message should not trigger a resize - client._sock._websocket._receiveData(new Uint8Array(incoming)); + sendExtendedDesktopSize(client, 0, 0, 4, 4, 0x7890abcd, 0x12345678); expect(RFB.messages.setDesktopSize).to.not.have.been.called; }); @@ -1187,13 +1135,7 @@ describe('Remote Frame Buffer protocol client', function () { sinon.match.object, 40, 50, 0x7890abcd, 0x12345678); // Server responds with the requested size 40x50 - const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x28, 0x00, 0x32, 0xff, 0xff, 0xfe, 0xcc, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x32, - 0x00, 0x00, 0x00, 0x00]; - - client._sock._websocket._receiveData(new Uint8Array(incoming)); + sendExtendedDesktopSize(client, 1, 0, 40, 50, 0x7890abcd, 0x12345678); clock.tick(1000); RFB.messages.setDesktopSize.resetHistory(); @@ -1282,18 +1224,11 @@ describe('Remote Frame Buffer protocol client', function () { }); it('should not try to override a server resize', function () { - // Simple ExtendedDesktopSize FBU message, new size: 100x100 - const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x64, 0x00, 0x64, 0xff, 0xff, 0xfe, 0xcc, - 0x01, 0x00, 0x00, 0x00, 0xab, 0xab, 0xab, 0xab, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, - 0x11, 0x22, 0x33, 0x44 ]; - // Note that this will cause the browser to display scrollbars // since the framebuffer is 100x100 and the container is 70x80. // The usable space (clientWidth/clientHeight) will be even smaller // due to the scrollbars taking up space. - client._sock._websocket._receiveData(new Uint8Array(incoming)); + sendExtendedDesktopSize(client, 0, 0, 100, 100, 0xabababab, 0x11223344); // The scrollbars cause the ResizeObserver to fire fakeResizeObserver.fire(); clock.tick(1000); From 70446bf74250d85d4b315b1cf3f93a363a0291f6 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Wed, 5 Feb 2025 16:17:56 +0100 Subject: [PATCH 130/166] Make resizeSession setting test more realistic We shouldn't expect a resize request to be sent if the container didn't change size first. --- tests/test.rfb.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 96f8f6965..5185ae6e3 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1082,6 +1082,13 @@ describe('Remote Frame Buffer protocol client', function () { it('should only request a resize when turned on', function () { client.resizeSession = false; expect(RFB.messages.setDesktopSize).to.not.have.been.called; + + container.style.width = '40px'; + container.style.height = '50px'; + fakeResizeObserver.fire(); + clock.tick(1000); + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + client.resizeSession = true; expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; }); From c82178348ab6c67fd3655d1de677c1a0075a2ddb Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Wed, 5 Feb 2025 16:19:00 +0100 Subject: [PATCH 131/166] Include SetDesktopSize responses in tests There might be subtle changes in behaviour, so we should mimic what a real server does. --- tests/test.rfb.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 5185ae6e3..9e792704d 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1070,9 +1070,16 @@ describe('Remote Frame Buffer protocol client', function () { container.style.width = '70px'; container.style.height = '80px'; + sinon.spy(RFB.messages, "setDesktopSize"); + sendExtendedDesktopSize(client, 0, 0, 4, 4, 0x7890abcd, 0x12345678); - sinon.spy(RFB.messages, "setDesktopSize"); + if (RFB.messages.setDesktopSize.calledOnce) { + let width = RFB.messages.setDesktopSize.args[0][1]; + let height = RFB.messages.setDesktopSize.args[0][2]; + sendExtendedDesktopSize(client, 1, 0, width, height, 0x7890abcd, 0x12345678); + RFB.messages.setDesktopSize.resetHistory(); + } }); afterEach(function () { @@ -1111,6 +1118,7 @@ describe('Remote Frame Buffer protocol client', function () { expect(RFB.messages.setDesktopSize).to.have.been.calledWith( sinon.match.object, 70, 80, 0x7890abcd, 0x12345678); + sendExtendedDesktopSize(client, 1, 0, 70, 80, 0x7890abcd, 0x12345678); RFB.messages.setDesktopSize.resetHistory(); // Second message should not trigger a resize @@ -1163,6 +1171,8 @@ describe('Remote Frame Buffer protocol client', function () { expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; expect(RFB.messages.setDesktopSize).to.have.been.calledWith( sinon.match.object, 40, 50, 0x7890abcd, 0x12345678); + + sendExtendedDesktopSize(client, 1, 0, 40, 50, 0x7890abcd, 0x12345678); RFB.messages.setDesktopSize.resetHistory(); container.style.width = '70px'; From 0b5e968e14075519c8ef6ff7201f3355b4536d04 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Wed, 5 Feb 2025 16:33:13 +0100 Subject: [PATCH 132/166] Better resize rate limiting Be more aggressive with resizing, limiting it to once ever 100 ms instead of after a 500 ms idle period. This gives a more responsive user experience. --- core/rfb.js | 55 ++++++++++++++++------ tests/test.rfb.js | 116 +++++++++++++++++++++++++++++++++++++++------- 2 files changed, 140 insertions(+), 31 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index 57f025814..e3266cc8d 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -149,6 +149,8 @@ export default class RFB extends EventTargetMixin { this._supportsSetDesktopSize = false; this._screenID = 0; this._screenFlags = 0; + this._pendingRemoteResize = false; + this._lastResize = 0; this._qemuExtKeyEventSupported = false; @@ -736,15 +738,9 @@ export default class RFB extends EventTargetMixin { this._saveExpectedClientSize(); }); - if (this._resizeSession) { - // Request changing the resolution of the remote display to - // the size of the local browser viewport. - - // In order to not send multiple requests before the browser-resize - // is finished we wait 0.5 seconds before sending the request. - clearTimeout(this._resizeTimeout); - this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this), 500); - } + // Request changing the resolution of the remote display to + // the size of the local browser viewport. + this._requestRemoteResize(); } // Update state of clipping in Display object, and make sure the @@ -794,16 +790,39 @@ export default class RFB extends EventTargetMixin { // Requests a change of remote desktop size. This message is an extension // and may only be sent if we have received an ExtendedDesktopSize message _requestRemoteResize() { - clearTimeout(this._resizeTimeout); - this._resizeTimeout = null; + if (!this._resizeSession) { + return; + } + if (this._viewOnly) { + return; + } + if (!this._supportsSetDesktopSize) { + return; + } + + // Rate limit to one pending resize at a time + if (this._pendingRemoteResize) { + return; + } - if (!this._resizeSession || this._viewOnly || - !this._supportsSetDesktopSize) { + // And no more than once every 100ms + if ((Date.now() - this._lastResize) < 100) { + clearTimeout(this._resizeTimeout); + this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this), + 100 - (Date.now() - this._lastResize)); return; } + this._resizeTimeout = null; const size = this._screenSize(); + // Do we actually change anything? + if (size.w === this._fbWidth && size.h === this._fbHeight) { + return; + } + + this._pendingRemoteResize = true; + this._lastResize = Date.now(); RFB.messages.setDesktopSize(this._sock, Math.floor(size.w), Math.floor(size.h), this._screenID, this._screenFlags); @@ -2913,6 +2932,10 @@ export default class RFB extends EventTargetMixin { * 2 - another client requested the resize */ + if (this._FBU.x === 1) { + this._pendingRemoteResize = false; + } + // We need to handle errors when we requested the resize. if (this._FBU.x === 1 && this._FBU.y !== 0) { let msg = ""; @@ -2945,6 +2968,12 @@ export default class RFB extends EventTargetMixin { this._requestRemoteResize(); } + if (this._FBU.x === 1 && this._FBU.y === 0) { + // We might have resized again whilst waiting for the + // previous request, so check if we are in sync + this._requestRemoteResize(); + } + return true; } diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 9e792704d..2a7bbeaab 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -680,7 +680,6 @@ describe('Remote Frame Buffer protocol client', function () { // The resize will cause scrollbars on the container, this causes a // resize observation in the browsers fakeResizeObserver.fire(); - clock.tick(1000); // FIXME: Display implicitly calls viewportChangeSize() when // resizing the framebuffer, hence calledTwice. @@ -1042,7 +1041,6 @@ describe('Remote Frame Buffer protocol client', function () { // The resize will cause scrollbars on the container, this causes a // resize observation in the browsers fakeResizeObserver.fire(); - clock.tick(1000); expect(client._display.autoscale).to.have.been.calledOnce; expect(client._display.autoscale).to.have.been.calledWith(70, 80); @@ -1079,6 +1077,7 @@ describe('Remote Frame Buffer protocol client', function () { let height = RFB.messages.setDesktopSize.args[0][2]; sendExtendedDesktopSize(client, 1, 0, width, height, 0x7890abcd, 0x12345678); RFB.messages.setDesktopSize.resetHistory(); + clock.tick(10000); } }); @@ -1093,7 +1092,6 @@ describe('Remote Frame Buffer protocol client', function () { container.style.width = '40px'; container.style.height = '50px'; fakeResizeObserver.fire(); - clock.tick(1000); expect(RFB.messages.setDesktopSize).to.not.have.been.called; client.resizeSession = true; @@ -1132,7 +1130,6 @@ describe('Remote Frame Buffer protocol client', function () { container.style.width = '40px'; container.style.height = '50px'; fakeResizeObserver.fire(); - clock.tick(1000); expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; expect(RFB.messages.setDesktopSize).to.have.been.calledWith( @@ -1143,7 +1140,6 @@ describe('Remote Frame Buffer protocol client', function () { container.style.width = '40px'; container.style.height = '50px'; fakeResizeObserver.fire(); - clock.tick(1000); expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; expect(RFB.messages.setDesktopSize).to.have.been.calledWith( @@ -1151,13 +1147,12 @@ describe('Remote Frame Buffer protocol client', function () { // Server responds with the requested size 40x50 sendExtendedDesktopSize(client, 1, 0, 40, 50, 0x7890abcd, 0x12345678); - clock.tick(1000); RFB.messages.setDesktopSize.resetHistory(); // size is still 40x50 - fakeResizeObserver.fire(); clock.tick(1000); + fakeResizeObserver.fire(); expect(RFB.messages.setDesktopSize).to.not.have.been.called; }); @@ -1166,7 +1161,6 @@ describe('Remote Frame Buffer protocol client', function () { container.style.width = '40px'; container.style.height = '50px'; fakeResizeObserver.fire(); - clock.tick(1000); expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; expect(RFB.messages.setDesktopSize).to.have.been.calledWith( @@ -1175,45 +1169,135 @@ describe('Remote Frame Buffer protocol client', function () { sendExtendedDesktopSize(client, 1, 0, 40, 50, 0x7890abcd, 0x12345678); RFB.messages.setDesktopSize.resetHistory(); + clock.tick(1000); container.style.width = '70px'; container.style.height = '80px'; fakeResizeObserver.fire(); - clock.tick(1000); expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; expect(RFB.messages.setDesktopSize).to.have.been.calledWith( sinon.match.object, 70, 80, 0x7890abcd, 0x12345678); }); - it('should not resize until the container size is stable', function () { + it('should rate limit resizes', function () { container.style.width = '20px'; container.style.height = '30px'; fakeResizeObserver.fire(); - clock.tick(400); + + expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; + expect(RFB.messages.setDesktopSize).to.have.been.calledWith( + sinon.match.object, 20, 30, 0x7890abcd, 0x12345678); + + sendExtendedDesktopSize(client, 1, 0, 20, 30, 0x7890abcd, 0x12345678); + RFB.messages.setDesktopSize.resetHistory(); + + clock.tick(20); + + container.style.width = '30px'; + container.style.height = '40px'; + fakeResizeObserver.fire(); expect(RFB.messages.setDesktopSize).to.not.have.been.called; + clock.tick(20); + container.style.width = '40px'; container.style.height = '50px'; fakeResizeObserver.fire(); - clock.tick(400); expect(RFB.messages.setDesktopSize).to.not.have.been.called; - clock.tick(200); + clock.tick(80); expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; expect(RFB.messages.setDesktopSize).to.have.been.calledWith( sinon.match.object, 40, 50, 0x7890abcd, 0x12345678); }); + it('should not have overlapping resize requests', function () { + container.style.width = '40px'; + container.style.height = '50px'; + fakeResizeObserver.fire(); + + expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; + + RFB.messages.setDesktopSize.resetHistory(); + + clock.tick(1000); + container.style.width = '20px'; + container.style.height = '30px'; + fakeResizeObserver.fire(); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + }); + + it('should finalize any pending resizes', function () { + container.style.width = '40px'; + container.style.height = '50px'; + fakeResizeObserver.fire(); + + expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; + + RFB.messages.setDesktopSize.resetHistory(); + + clock.tick(1000); + container.style.width = '20px'; + container.style.height = '30px'; + fakeResizeObserver.fire(); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + + // Server responds with the requested size 40x50 + sendExtendedDesktopSize(client, 1, 0, 40, 50, 0x7890abcd, 0x12345678); + + expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; + expect(RFB.messages.setDesktopSize).to.have.been.calledWith( + sinon.match.object, 20, 30, 0x7890abcd, 0x12345678); + }); + + it('should not finalize any pending resize if not needed', function () { + container.style.width = '40px'; + container.style.height = '50px'; + fakeResizeObserver.fire(); + + expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; + + RFB.messages.setDesktopSize.resetHistory(); + + // Server responds with the requested size 40x50 + sendExtendedDesktopSize(client, 1, 0, 40, 50, 0x7890abcd, 0x12345678); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + }); + + it('should not finalize any pending resizes on errors', function () { + container.style.width = '40px'; + container.style.height = '50px'; + fakeResizeObserver.fire(); + + expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; + + RFB.messages.setDesktopSize.resetHistory(); + + clock.tick(1000); + container.style.width = '20px'; + container.style.height = '30px'; + fakeResizeObserver.fire(); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + + // Server failed the requested size 40x50 + sendExtendedDesktopSize(client, 1, 1, 40, 50, 0x7890abcd, 0x12345678); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + }); + it('should not resize when resize is disabled', function () { client._resizeSession = false; container.style.width = '40px'; container.style.height = '50px'; fakeResizeObserver.fire(); - clock.tick(1000); expect(RFB.messages.setDesktopSize).to.not.have.been.called; }); @@ -1224,7 +1308,6 @@ describe('Remote Frame Buffer protocol client', function () { container.style.width = '40px'; container.style.height = '50px'; fakeResizeObserver.fire(); - clock.tick(1000); expect(RFB.messages.setDesktopSize).to.not.have.been.called; }); @@ -1235,7 +1318,6 @@ describe('Remote Frame Buffer protocol client', function () { container.style.width = '40px'; container.style.height = '50px'; fakeResizeObserver.fire(); - clock.tick(1000); expect(RFB.messages.setDesktopSize).to.not.have.been.called; }); @@ -1248,7 +1330,6 @@ describe('Remote Frame Buffer protocol client', function () { sendExtendedDesktopSize(client, 0, 0, 100, 100, 0xabababab, 0x11223344); // The scrollbars cause the ResizeObserver to fire fakeResizeObserver.fire(); - clock.tick(1000); expect(RFB.messages.setDesktopSize).to.not.have.been.called; @@ -1256,7 +1337,6 @@ describe('Remote Frame Buffer protocol client', function () { container.style.width = '120px'; container.style.height = '130px'; fakeResizeObserver.fire(); - clock.tick(1000); expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; expect(RFB.messages.setDesktopSize).to.have.been.calledWith( From 7ee792276696ee9cc8f80847506678f9c264e357 Mon Sep 17 00:00:00 2001 From: tianzedavid Date: Thu, 13 Feb 2025 01:00:46 +0800 Subject: [PATCH 133/166] chore: fix some typos Signed-off-by: tianzedavid --- README.md | 2 +- core/util/browser.js | 2 +- snap/local/svc_wrapper.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0a6d87eb8..6976a7469 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ If you want to use certificate files, due to standard snap confinement restricti #### Running noVNC from snap as a service (daemon) The snap package also has the capability to run a 'novnc' service which can be configured to listen on multiple ports connecting to multiple VNC servers -(effectively a service runing multiple instances of novnc). +(effectively a service running multiple instances of novnc). Instructions (with example values): List current services (out-of-box this will be blank): diff --git a/core/util/browser.js b/core/util/browser.js index 2c3667650..fc1b77f9e 100644 --- a/core/util/browser.js +++ b/core/util/browser.js @@ -13,7 +13,7 @@ import Base64 from '../base64.js'; // Touch detection export let isTouchDevice = ('ontouchstart' in document.documentElement) || - // requried for Chrome debugger + // required for Chrome debugger (document.ontouchstart !== undefined) || // required for MS Surface (navigator.maxTouchPoints > 0) || diff --git a/snap/local/svc_wrapper.sh b/snap/local/svc_wrapper.sh index 77db53942..5c6650a1f 100755 --- a/snap/local/svc_wrapper.sh +++ b/snap/local/svc_wrapper.sh @@ -11,7 +11,7 @@ # "vnc": "localhost:5902" #} #} -snapctl get services | jq -c '.[]' | while read service; do # for each service the user sepcified.. +snapctl get services | jq -c '.[]' | while read service; do # for each service the user specified.. # get the important data for the service (listen port, VNC host:port) listen_port="$(echo $service | jq --raw-output '.listen')" vnc_host_port="$(echo $service | jq --raw-output '.vnc')" # --raw-output removes any quotation marks from the output From e8030a9fb124d5c3d5342d966b5cc86280cd4923 Mon Sep 17 00:00:00 2001 From: Alexander Zeijlon Date: Fri, 14 Feb 2025 10:16:08 +0100 Subject: [PATCH 134/166] Update translation template file --- po/noVNC.pot | 164 +++++++++++++++++++++++++-------------------------- 1 file changed, 80 insertions(+), 84 deletions(-) diff --git a/po/noVNC.pot b/po/noVNC.pot index a939d5366..7c32a3edc 100644 --- a/po/noVNC.pot +++ b/po/noVNC.pot @@ -6,9 +6,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: noVNC 1.5.0\n" +"Project-Id-Version: noVNC 1.6.0\n" "Report-Msgid-Bugs-To: novnc@googlegroups.com\n" -"POT-Creation-Date: 2024-06-03 14:10+0200\n" +"POT-Creation-Date: 2025-02-14 10:14+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,321 +17,317 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: ../app/ui.js:69 +#: ../app/ui.js:84 msgid "" "Running without HTTPS is not recommended, crashes or other issues are likely." msgstr "" -#: ../app/ui.js:410 +#: ../app/ui.js:413 msgid "Connecting..." msgstr "" -#: ../app/ui.js:417 +#: ../app/ui.js:420 msgid "Disconnecting..." msgstr "" -#: ../app/ui.js:423 +#: ../app/ui.js:426 msgid "Reconnecting..." msgstr "" -#: ../app/ui.js:428 +#: ../app/ui.js:431 msgid "Internal error" msgstr "" -#: ../app/ui.js:1026 -msgid "Must set host" -msgstr "" - -#: ../app/ui.js:1052 +#: ../app/ui.js:1079 msgid "Failed to connect to server: " msgstr "" -#: ../app/ui.js:1118 +#: ../app/ui.js:1145 msgid "Connected (encrypted) to " msgstr "" -#: ../app/ui.js:1120 +#: ../app/ui.js:1147 msgid "Connected (unencrypted) to " msgstr "" -#: ../app/ui.js:1143 +#: ../app/ui.js:1170 msgid "Something went wrong, connection is closed" msgstr "" -#: ../app/ui.js:1146 +#: ../app/ui.js:1173 msgid "Failed to connect to server" msgstr "" -#: ../app/ui.js:1158 +#: ../app/ui.js:1185 msgid "Disconnected" msgstr "" -#: ../app/ui.js:1173 +#: ../app/ui.js:1200 msgid "New connection has been rejected with reason: " msgstr "" -#: ../app/ui.js:1176 +#: ../app/ui.js:1203 msgid "New connection has been rejected" msgstr "" -#: ../app/ui.js:1242 +#: ../app/ui.js:1269 msgid "Credentials are required" msgstr "" -#: ../vnc.html:55 +#: ../vnc.html:106 msgid "noVNC encountered an error:" msgstr "" -#: ../vnc.html:65 +#: ../vnc.html:116 msgid "Hide/Show the control bar" msgstr "" -#: ../vnc.html:74 +#: ../vnc.html:125 msgid "Drag" msgstr "" -#: ../vnc.html:74 +#: ../vnc.html:125 msgid "Move/Drag viewport" msgstr "" -#: ../vnc.html:80 +#: ../vnc.html:131 msgid "Keyboard" msgstr "" -#: ../vnc.html:80 +#: ../vnc.html:131 msgid "Show keyboard" msgstr "" -#: ../vnc.html:85 +#: ../vnc.html:136 msgid "Extra keys" msgstr "" -#: ../vnc.html:85 +#: ../vnc.html:136 msgid "Show extra keys" msgstr "" -#: ../vnc.html:90 +#: ../vnc.html:141 msgid "Ctrl" msgstr "" -#: ../vnc.html:90 +#: ../vnc.html:141 msgid "Toggle Ctrl" msgstr "" -#: ../vnc.html:93 +#: ../vnc.html:144 msgid "Alt" msgstr "" -#: ../vnc.html:93 +#: ../vnc.html:144 msgid "Toggle Alt" msgstr "" -#: ../vnc.html:96 +#: ../vnc.html:147 msgid "Toggle Windows" msgstr "" -#: ../vnc.html:96 +#: ../vnc.html:147 msgid "Windows" msgstr "" -#: ../vnc.html:99 +#: ../vnc.html:150 msgid "Send Tab" msgstr "" -#: ../vnc.html:99 +#: ../vnc.html:150 msgid "Tab" msgstr "" -#: ../vnc.html:102 +#: ../vnc.html:153 msgid "Esc" msgstr "" -#: ../vnc.html:102 +#: ../vnc.html:153 msgid "Send Escape" msgstr "" -#: ../vnc.html:105 +#: ../vnc.html:156 msgid "Ctrl+Alt+Del" msgstr "" -#: ../vnc.html:105 +#: ../vnc.html:156 msgid "Send Ctrl-Alt-Del" msgstr "" -#: ../vnc.html:112 +#: ../vnc.html:163 msgid "Shutdown/Reboot" msgstr "" -#: ../vnc.html:112 +#: ../vnc.html:163 msgid "Shutdown/Reboot..." msgstr "" -#: ../vnc.html:118 +#: ../vnc.html:169 msgid "Power" msgstr "" -#: ../vnc.html:120 +#: ../vnc.html:171 msgid "Shutdown" msgstr "" -#: ../vnc.html:121 +#: ../vnc.html:172 msgid "Reboot" msgstr "" -#: ../vnc.html:122 +#: ../vnc.html:173 msgid "Reset" msgstr "" -#: ../vnc.html:127 ../vnc.html:133 +#: ../vnc.html:178 ../vnc.html:184 msgid "Clipboard" msgstr "" -#: ../vnc.html:135 +#: ../vnc.html:186 msgid "Edit clipboard content in the textarea below." msgstr "" -#: ../vnc.html:143 +#: ../vnc.html:194 msgid "Full screen" msgstr "" -#: ../vnc.html:148 ../vnc.html:154 +#: ../vnc.html:199 ../vnc.html:205 msgid "Settings" msgstr "" -#: ../vnc.html:158 +#: ../vnc.html:211 msgid "Shared mode" msgstr "" -#: ../vnc.html:161 +#: ../vnc.html:218 msgid "View only" msgstr "" -#: ../vnc.html:165 +#: ../vnc.html:226 msgid "Clip to window" msgstr "" -#: ../vnc.html:168 +#: ../vnc.html:231 msgid "Scaling mode:" msgstr "" -#: ../vnc.html:170 +#: ../vnc.html:233 msgid "None" msgstr "" -#: ../vnc.html:171 +#: ../vnc.html:234 msgid "Local scaling" msgstr "" -#: ../vnc.html:172 +#: ../vnc.html:235 msgid "Remote resizing" msgstr "" -#: ../vnc.html:177 +#: ../vnc.html:240 msgid "Advanced" msgstr "" -#: ../vnc.html:180 +#: ../vnc.html:243 msgid "Quality:" msgstr "" -#: ../vnc.html:184 +#: ../vnc.html:247 msgid "Compression level:" msgstr "" -#: ../vnc.html:189 +#: ../vnc.html:252 msgid "Repeater ID:" msgstr "" -#: ../vnc.html:193 +#: ../vnc.html:256 msgid "WebSocket" msgstr "" -#: ../vnc.html:196 +#: ../vnc.html:261 msgid "Encrypt" msgstr "" -#: ../vnc.html:199 +#: ../vnc.html:266 msgid "Host:" msgstr "" -#: ../vnc.html:203 +#: ../vnc.html:270 msgid "Port:" msgstr "" -#: ../vnc.html:207 +#: ../vnc.html:274 msgid "Path:" msgstr "" -#: ../vnc.html:214 +#: ../vnc.html:283 msgid "Automatic reconnect" msgstr "" -#: ../vnc.html:217 +#: ../vnc.html:288 msgid "Reconnect delay (ms):" msgstr "" -#: ../vnc.html:222 +#: ../vnc.html:295 msgid "Show dot when no cursor" msgstr "" -#: ../vnc.html:227 +#: ../vnc.html:302 msgid "Logging:" msgstr "" -#: ../vnc.html:236 +#: ../vnc.html:311 msgid "Version:" msgstr "" -#: ../vnc.html:244 +#: ../vnc.html:319 msgid "Disconnect" msgstr "" -#: ../vnc.html:267 +#: ../vnc.html:342 msgid "Connect" msgstr "" -#: ../vnc.html:276 +#: ../vnc.html:351 msgid "Server identity" msgstr "" -#: ../vnc.html:279 +#: ../vnc.html:354 msgid "The server has provided the following identifying information:" msgstr "" -#: ../vnc.html:283 +#: ../vnc.html:357 msgid "Fingerprint:" msgstr "" -#: ../vnc.html:286 +#: ../vnc.html:361 msgid "" "Please verify that the information is correct and press \"Approve\". " "Otherwise press \"Reject\"." msgstr "" -#: ../vnc.html:291 +#: ../vnc.html:366 msgid "Approve" msgstr "" -#: ../vnc.html:292 +#: ../vnc.html:367 msgid "Reject" msgstr "" -#: ../vnc.html:300 +#: ../vnc.html:375 msgid "Credentials" msgstr "" -#: ../vnc.html:304 +#: ../vnc.html:379 msgid "Username:" msgstr "" -#: ../vnc.html:308 +#: ../vnc.html:383 msgid "Password:" msgstr "" -#: ../vnc.html:312 +#: ../vnc.html:387 msgid "Send credentials" msgstr "" -#: ../vnc.html:321 +#: ../vnc.html:396 msgid "Cancel" msgstr "" From 09fc4f7fb99d20a6f796297e990d8c4747eb20a2 Mon Sep 17 00:00:00 2001 From: Alexander Zeijlon Date: Fri, 14 Feb 2025 10:42:16 +0100 Subject: [PATCH 135/166] Update Swedish translations --- po/sv.po | 215 +++++++++++++++++++++++++++---------------------------- 1 file changed, 107 insertions(+), 108 deletions(-) diff --git a/po/sv.po b/po/sv.po index f86c1ef37..aff531adb 100644 --- a/po/sv.po +++ b/po/sv.po @@ -1,312 +1,308 @@ # Swedish translations for noVNC package # Svenska översättningar för paketet noVNC. -# Copyright (C) 2020 The noVNC authors +# 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.3.0\n" +"Project-Id-Version: noVNC 1.6.0\n" "Report-Msgid-Bugs-To: novnc@googlegroups.com\n" -"POT-Creation-Date: 2024-06-03 14:10+0200\n" -"PO-Revision-Date: 2024-06-18 13:52+0200\n" -"Last-Translator: Pierre Ossman \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.4.4\n" +"X-Generator: Poedit 3.5\n" -#: ../app/ui.js:69 +#: ../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:410 +#: ../app/ui.js:413 msgid "Connecting..." msgstr "Ansluter..." -#: ../app/ui.js:417 +#: ../app/ui.js:420 msgid "Disconnecting..." msgstr "Kopplar ner..." -#: ../app/ui.js:423 +#: ../app/ui.js:426 msgid "Reconnecting..." msgstr "Återansluter..." -#: ../app/ui.js:428 +#: ../app/ui.js:431 msgid "Internal error" msgstr "Internt fel" -#: ../app/ui.js:1026 -msgid "Must set host" -msgstr "Du måste specifiera en värd" - -#: ../app/ui.js:1052 +#: ../app/ui.js:1079 msgid "Failed to connect to server: " msgstr "Misslyckades att ansluta till servern: " -#: ../app/ui.js:1118 +#: ../app/ui.js:1145 msgid "Connected (encrypted) to " msgstr "Ansluten (krypterat) till " -#: ../app/ui.js:1120 +#: ../app/ui.js:1147 msgid "Connected (unencrypted) to " msgstr "Ansluten (okrypterat) till " -#: ../app/ui.js:1143 +#: ../app/ui.js:1170 msgid "Something went wrong, connection is closed" msgstr "Något gick fel, anslutningen avslutades" -#: ../app/ui.js:1146 +#: ../app/ui.js:1173 msgid "Failed to connect to server" msgstr "Misslyckades att ansluta till servern" -#: ../app/ui.js:1158 +#: ../app/ui.js:1185 msgid "Disconnected" msgstr "Frånkopplad" -#: ../app/ui.js:1173 +#: ../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:1176 +#: ../app/ui.js:1203 msgid "New connection has been rejected" msgstr "Ny anslutning har blivit nekad" -#: ../app/ui.js:1242 +#: ../app/ui.js:1269 msgid "Credentials are required" msgstr "Användaruppgifter krävs" -#: ../vnc.html:55 +#: ../vnc.html:106 msgid "noVNC encountered an error:" msgstr "noVNC stötte på ett problem:" -#: ../vnc.html:65 +#: ../vnc.html:116 msgid "Hide/Show the control bar" msgstr "Göm/Visa kontrollbaren" -#: ../vnc.html:74 +#: ../vnc.html:125 msgid "Drag" msgstr "Dra" -#: ../vnc.html:74 -msgid "Move/Drag Viewport" -msgstr "Flytta/Dra Vyn" +#: ../vnc.html:125 +msgid "Move/Drag viewport" +msgstr "Flytta/Dra vyn" -#: ../vnc.html:80 +#: ../vnc.html:131 msgid "Keyboard" msgstr "Tangentbord" -#: ../vnc.html:80 -msgid "Show Keyboard" -msgstr "Visa Tangentbord" +#: ../vnc.html:131 +msgid "Show keyboard" +msgstr "Visa tangentbord" -#: ../vnc.html:85 +#: ../vnc.html:136 msgid "Extra keys" msgstr "Extraknappar" -#: ../vnc.html:85 -msgid "Show Extra Keys" -msgstr "Visa Extraknappar" +#: ../vnc.html:136 +msgid "Show extra keys" +msgstr "Visa extraknappar" -#: ../vnc.html:90 +#: ../vnc.html:141 msgid "Ctrl" msgstr "Ctrl" -#: ../vnc.html:90 +#: ../vnc.html:141 msgid "Toggle Ctrl" msgstr "Växla Ctrl" -#: ../vnc.html:93 +#: ../vnc.html:144 msgid "Alt" msgstr "Alt" -#: ../vnc.html:93 +#: ../vnc.html:144 msgid "Toggle Alt" msgstr "Växla Alt" -#: ../vnc.html:96 +#: ../vnc.html:147 msgid "Toggle Windows" msgstr "Växla Windows" -#: ../vnc.html:96 +#: ../vnc.html:147 msgid "Windows" msgstr "Windows" -#: ../vnc.html:99 +#: ../vnc.html:150 msgid "Send Tab" msgstr "Skicka Tab" -#: ../vnc.html:99 +#: ../vnc.html:150 msgid "Tab" msgstr "Tab" -#: ../vnc.html:102 +#: ../vnc.html:153 msgid "Esc" msgstr "Esc" -#: ../vnc.html:102 +#: ../vnc.html:153 msgid "Send Escape" msgstr "Skicka Escape" -#: ../vnc.html:105 +#: ../vnc.html:156 msgid "Ctrl+Alt+Del" msgstr "Ctrl+Alt+Del" -#: ../vnc.html:105 +#: ../vnc.html:156 msgid "Send Ctrl-Alt-Del" msgstr "Skicka Ctrl-Alt-Del" -#: ../vnc.html:112 +#: ../vnc.html:163 msgid "Shutdown/Reboot" msgstr "Stäng av/Boota om" -#: ../vnc.html:112 +#: ../vnc.html:163 msgid "Shutdown/Reboot..." msgstr "Stäng av/Boota om..." -#: ../vnc.html:118 +#: ../vnc.html:169 msgid "Power" msgstr "Ström" -#: ../vnc.html:120 +#: ../vnc.html:171 msgid "Shutdown" msgstr "Stäng av" -#: ../vnc.html:121 +#: ../vnc.html:172 msgid "Reboot" msgstr "Boota om" -#: ../vnc.html:122 +#: ../vnc.html:173 msgid "Reset" msgstr "Återställ" -#: ../vnc.html:127 ../vnc.html:133 +#: ../vnc.html:178 ../vnc.html:184 msgid "Clipboard" msgstr "Urklipp" -#: ../vnc.html:135 +#: ../vnc.html:186 msgid "Edit clipboard content in the textarea below." msgstr "Redigera urklippets innehåll i fältet nedan." -#: ../vnc.html:143 +#: ../vnc.html:194 msgid "Full screen" msgstr "Fullskärm" -#: ../vnc.html:148 ../vnc.html:154 +#: ../vnc.html:199 ../vnc.html:205 msgid "Settings" msgstr "Inställningar" -#: ../vnc.html:158 -msgid "Shared Mode" -msgstr "Delat Läge" +#: ../vnc.html:211 +msgid "Shared mode" +msgstr "Delat läge" -#: ../vnc.html:161 -msgid "View Only" -msgstr "Endast Visning" +#: ../vnc.html:218 +msgid "View only" +msgstr "Endast visning" -#: ../vnc.html:165 -msgid "Clip to Window" -msgstr "Begränsa till Fönster" +#: ../vnc.html:226 +msgid "Clip to window" +msgstr "Begränsa till fönster" -#: ../vnc.html:168 +#: ../vnc.html:231 msgid "Scaling mode:" msgstr "Skalningsläge:" -#: ../vnc.html:170 +#: ../vnc.html:233 msgid "None" msgstr "Ingen" -#: ../vnc.html:171 -msgid "Local Scaling" -msgstr "Lokal Skalning" +#: ../vnc.html:234 +msgid "Local scaling" +msgstr "Lokal skalning" -#: ../vnc.html:172 -msgid "Remote Resizing" -msgstr "Ändra Storlek" +#: ../vnc.html:235 +msgid "Remote resizing" +msgstr "Ändra storlek" -#: ../vnc.html:177 +#: ../vnc.html:240 msgid "Advanced" msgstr "Avancerat" -#: ../vnc.html:180 +#: ../vnc.html:243 msgid "Quality:" msgstr "Kvalitet:" -#: ../vnc.html:184 +#: ../vnc.html:247 msgid "Compression level:" msgstr "Kompressionsnivå:" -#: ../vnc.html:189 +#: ../vnc.html:252 msgid "Repeater ID:" msgstr "Repeater-ID:" -#: ../vnc.html:193 +#: ../vnc.html:256 msgid "WebSocket" msgstr "WebSocket" -#: ../vnc.html:196 +#: ../vnc.html:261 msgid "Encrypt" msgstr "Kryptera" -#: ../vnc.html:199 +#: ../vnc.html:266 msgid "Host:" msgstr "Värd:" -#: ../vnc.html:203 +#: ../vnc.html:270 msgid "Port:" msgstr "Port:" -#: ../vnc.html:207 +#: ../vnc.html:274 msgid "Path:" msgstr "Sökväg:" -#: ../vnc.html:214 -msgid "Automatic Reconnect" -msgstr "Automatisk Återanslutning" +#: ../vnc.html:283 +msgid "Automatic reconnect" +msgstr "Automatisk återanslutning" -#: ../vnc.html:217 +#: ../vnc.html:288 msgid "Reconnect delay (ms):" msgstr "Fördröjning (ms):" -#: ../vnc.html:222 +#: ../vnc.html:295 msgid "Show dot when no cursor" msgstr "Visa prick när ingen muspekare finns" -#: ../vnc.html:227 +#: ../vnc.html:302 msgid "Logging:" msgstr "Loggning:" -#: ../vnc.html:236 +#: ../vnc.html:311 msgid "Version:" msgstr "Version:" -#: ../vnc.html:244 +#: ../vnc.html:319 msgid "Disconnect" msgstr "Koppla från" -#: ../vnc.html:267 +#: ../vnc.html:342 msgid "Connect" msgstr "Anslut" -#: ../vnc.html:276 +#: ../vnc.html:351 msgid "Server identity" msgstr "Server-identitet" -#: ../vnc.html:279 +#: ../vnc.html:354 msgid "The server has provided the following identifying information:" msgstr "Servern har gett följande identifierande information:" -#: ../vnc.html:283 +#: ../vnc.html:357 msgid "Fingerprint:" msgstr "Fingeravtryck:" -#: ../vnc.html:286 +#: ../vnc.html:361 msgid "" "Please verify that the information is correct and press \"Approve\". " "Otherwise press \"Reject\"." @@ -314,34 +310,37 @@ msgstr "" "Kontrollera att informationen är korrekt och tryck sedan \"Godkänn\". Tryck " "annars \"Neka\"." -#: ../vnc.html:291 +#: ../vnc.html:366 msgid "Approve" msgstr "Godkänn" -#: ../vnc.html:292 +#: ../vnc.html:367 msgid "Reject" msgstr "Neka" -#: ../vnc.html:300 +#: ../vnc.html:375 msgid "Credentials" msgstr "Användaruppgifter" -#: ../vnc.html:304 +#: ../vnc.html:379 msgid "Username:" msgstr "Användarnamn:" -#: ../vnc.html:308 +#: ../vnc.html:383 msgid "Password:" msgstr "Lösenord:" -#: ../vnc.html:312 -msgid "Send Credentials" -msgstr "Skicka Användaruppgifter" +#: ../vnc.html:387 +msgid "Send credentials" +msgstr "Skicka användaruppgifter" -#: ../vnc.html:321 +#: ../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" From c3b8cbd3d28c0ceec74c1f5693c6d9cb9c609638 Mon Sep 17 00:00:00 2001 From: Alexander Zeijlon Date: Fri, 14 Feb 2025 11:22:24 +0100 Subject: [PATCH 136/166] Update README.md with H.264 encoding support --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6976a7469..3c831be6f 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ profits such as: RSA-AES, Tight, VeNCrypt Plain, XVP, Apple's Diffie-Hellman, UltraVNC's MSLogonII * Supported VNC encodings: raw, copyrect, rre, hextile, tight, tightPNG, - ZRLE, JPEG, Zlib + ZRLE, JPEG, Zlib, H.264 * Supports scaling, clipping and resizing the desktop * Supports back & forward mouse buttons * Local cursor rendering From b45f35c6d7b06c1d8641ceb8e7f5ee00c23484eb Mon Sep 17 00:00:00 2001 From: Alexander Zeijlon Date: Fri, 14 Feb 2025 10:47:44 +0100 Subject: [PATCH 137/166] noVNC 1.6.0 beta --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 907cf6309..69ba9f5cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@novnc/novnc", - "version": "1.5.0", + "version": "1.6.0-beta", "description": "An HTML5 VNC client", "browser": "lib/rfb", "directories": { From b25675e05290817024fa5ba89581d9cdc12f8992 Mon Sep 17 00:00:00 2001 From: Alexander Zeijlon Date: Fri, 28 Feb 2025 14:42:52 +0100 Subject: [PATCH 138/166] Upgrade to websockify 0.13.0 in snap package --- snap/snapcraft.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 82d52de4d..f743a0148 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -42,7 +42,7 @@ parts: - jq websockify: - source: https://github.com/novnc/websockify/archive/v0.12.0.tar.gz + source: https://github.com/novnc/websockify/archive/v0.13.0.tar.gz plugin: python stage-packages: - python3-numpy From 045a0ba15893030fd5e96d9aefe810e14ba6dec7 Mon Sep 17 00:00:00 2001 From: Alexander Zeijlon Date: Mon, 10 Mar 2025 11:51:49 +0100 Subject: [PATCH 139/166] Let browser handle parsing of URLs before relaying We don't want to assign a path directly to url.pathname that contains a search query, since this causes '?' at the beginning of the query to be URL-encoded to '%3F'. Instead use URL() to parse the path for us. --- app/ui.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/ui.js b/app/ui.js index fd23c8007..51e57bd3b 100644 --- a/app/ui.js +++ b/app/ui.js @@ -1058,7 +1058,12 @@ const UI = { if (port) { url.port = port; } - url.pathname = '/' + path; + + // "./" is needed to force URL() to interpret the path-variable as + // a path and not as an URL. This is relevant if for example path + // starts with more than one "/", in which case it would be + // interpreted as a host name instead. + url = new URL("./" + path, url); } else { // Current (May 2024) browsers support relative WebSocket // URLs natively, but we need to support older browsers for From 6002d57a88820e500dcfb70042602de86d58736c Mon Sep 17 00:00:00 2001 From: Martine & Philippe Date: Mon, 17 Feb 2025 10:04:00 +0100 Subject: [PATCH 140/166] Update French translation --- app/locale/fr.json | 30 ++++--- po/fr.po | 217 +++++++++++++++++++++++++++------------------ 2 files changed, 151 insertions(+), 96 deletions(-) diff --git a/app/locale/fr.json b/app/locale/fr.json index be9a4f422..c8607cebe 100644 --- a/app/locale/fr.json +++ b/app/locale/fr.json @@ -1,9 +1,10 @@ { + "Running without HTTPS is not recommended, crashes or other issues are likely.": "Lancer sans HTTPS n'est pas recommandé, crashs ou autres problèmes en vue.", "Connecting...": "En cours de connexion...", "Disconnecting...": "Déconnexion en cours...", "Reconnecting...": "Reconnexion en cours...", "Internal error": "Erreur interne", - "Must set host": "Doit définir l'hôte", + "Failed to connect to server: ": "Échec de connexion au serveur ", "Connected (encrypted) to ": "Connecté (chiffré) à ", "Connected (unencrypted) to ": "Connecté (non chiffré) à ", "Something went wrong, connection is closed": "Quelque chose s'est mal passé, la connexion a été fermée", @@ -15,7 +16,7 @@ "noVNC encountered an error:": "noVNC a rencontré une erreur :", "Hide/Show the control bar": "Masquer/Afficher la barre de contrôle", "Drag": "Faire glisser", - "Move/Drag Viewport": "Déplacer/faire glisser le Viewport", + "Move/Drag viewport": "Déplacer la fenêtre de visualisation", "Keyboard": "Clavier", "Show keyboard": "Afficher le clavier", "Extra keys": "Touches supplémentaires", @@ -25,9 +26,9 @@ "Alt": "Alt", "Toggle Alt": "Basculer Alt", "Toggle Windows": "Basculer Windows", - "Windows": "Windows", - "Send Tab": "Envoyer l'onglet", - "Tab": "l'onglet", + "Windows": "Fenêtre", + "Send Tab": "Envoyer Tab", + "Tab": "Tabulation", "Esc": "Esc", "Send Escape": "Envoyer Escape", "Ctrl+Alt+Del": "Ctrl+Alt+Del", @@ -39,12 +40,12 @@ "Reboot": "Redémarrer", "Reset": "Réinitialiser", "Clipboard": "Presse-papiers", - "Clear": "Effacer", - "Fullscreen": "Plein écran", + "Edit clipboard content in the textarea below.": "Editer le contenu du presse-papier dans la zone ci-dessous.", + "Full screen": "Plein écran", "Settings": "Paramètres", "Shared mode": "Mode partagé", "View only": "Afficher uniquement", - "Clip to window": "Clip à fenêtre", + "Clip to window": "Ajuster à la fenêtre", "Scaling mode:": "Mode mise à l'échelle :", "None": "Aucun", "Local scaling": "Mise à l'échelle locale", @@ -58,15 +59,24 @@ "Host:": "Hôte :", "Port:": "Port :", "Path:": "Chemin :", - "Automatic reconnect": "Reconnecter automatiquemen", + "Automatic reconnect": "Reconnecter automatiquement", "Reconnect delay (ms):": "Délai de reconnexion (ms) :", "Show dot when no cursor": "Afficher le point lorsqu'il n'y a pas de curseur", "Logging:": "Se connecter :", "Version:": "Version :", "Disconnect": "Déconnecter", "Connect": "Connecter", + "Server identity": "Identité du serveur", + "The server has provided the following identifying information:": "Le serveur a fourni l'identification suivante :", + "Fingerprint:": "Empreinte digitale :", + "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "SVP, verifiez que l'information est correcte et pressez \"Accepter\". Sinon pressez \"Refuser\".", + "Approve": "Accepter", + "Reject": "Refuser", + "Credentials": "Envoyer les identifiants", "Username:": "Nom d'utilisateur :", "Password:": "Mot de passe :", "Send credentials": "Envoyer les identifiants", - "Cancel": "Annuler" + "Cancel": "Annuler", + "Must set host": "Doit définir l'hôte", + "Clear": "Effacer" } \ No newline at end of file diff --git a/po/fr.po b/po/fr.po index fa3619cc3..a750599b8 100644 --- a/po/fr.po +++ b/po/fr.po @@ -7,294 +7,339 @@ # msgid "" msgstr "" -"Project-Id-Version: noVNC 1.2.0\n" +"Project-Id-Version: noVNC 1.6.0\n" "Report-Msgid-Bugs-To: novnc@googlegroups.com\n" -"POT-Creation-Date: 2020-07-03 16:11+0200\n" -"PO-Revision-Date: 2022-04-25 23:40+0200\n" -"Last-Translator: Lowxorx \n" +"POT-Creation-Date: 2025-02-14 10:14+0100\n" +"PO-Revision-Date: 2025-02-17 10:04+0100\n" +"Last-Translator: Martine & Philippe \n" "Language-Team: French\n" "Language: fr\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:394 +#: ../app/ui.js:84 +msgid "" +"Running without HTTPS is not recommended, crashes or other issues are likely." +msgstr "" +"Lancer sans HTTPS n'est pas recommandé, crashs ou autres problèmes en vue." + +#: ../app/ui.js:413 msgid "Connecting..." msgstr "En cours de connexion..." -#: ../app/ui.js:401 +#: ../app/ui.js:420 msgid "Disconnecting..." msgstr "Déconnexion en cours..." -#: ../app/ui.js:407 +#: ../app/ui.js:426 msgid "Reconnecting..." msgstr "Reconnexion en cours..." -#: ../app/ui.js:412 +#: ../app/ui.js:431 msgid "Internal error" msgstr "Erreur interne" -#: ../app/ui.js:1008 -msgid "Must set host" -msgstr "Doit définir l'hôte" +#: ../app/ui.js:1079 +msgid "Failed to connect to server: " +msgstr "Échec de connexion au serveur " -#: ../app/ui.js:1090 +#: ../app/ui.js:1145 msgid "Connected (encrypted) to " msgstr "Connecté (chiffré) à " -#: ../app/ui.js:1092 +#: ../app/ui.js:1147 msgid "Connected (unencrypted) to " msgstr "Connecté (non chiffré) à " -#: ../app/ui.js:1115 +#: ../app/ui.js:1170 msgid "Something went wrong, connection is closed" msgstr "Quelque chose s'est mal passé, la connexion a été fermée" -#: ../app/ui.js:1118 +#: ../app/ui.js:1173 msgid "Failed to connect to server" msgstr "Échec de connexion au serveur" -#: ../app/ui.js:1128 +#: ../app/ui.js:1185 msgid "Disconnected" msgstr "Déconnecté" -#: ../app/ui.js:1143 +#: ../app/ui.js:1200 msgid "New connection has been rejected with reason: " msgstr "Une nouvelle connexion a été rejetée avec motif : " -#: ../app/ui.js:1146 +#: ../app/ui.js:1203 msgid "New connection has been rejected" msgstr "Une nouvelle connexion a été rejetée" -#: ../app/ui.js:1181 +#: ../app/ui.js:1269 msgid "Credentials are required" msgstr "Les identifiants sont requis" -#: ../vnc.html:74 +#: ../vnc.html:106 msgid "noVNC encountered an error:" msgstr "noVNC a rencontré une erreur :" -#: ../vnc.html:84 +#: ../vnc.html:116 msgid "Hide/Show the control bar" msgstr "Masquer/Afficher la barre de contrôle" -#: ../vnc.html:91 +#: ../vnc.html:125 msgid "Drag" msgstr "Faire glisser" -#: ../vnc.html:91 -msgid "Move/Drag Viewport" -msgstr "Déplacer/faire glisser le Viewport" +#: ../vnc.html:125 +msgid "Move/Drag viewport" +msgstr "Déplacer la fenêtre de visualisation" -#: ../vnc.html:97 +#: ../vnc.html:131 msgid "Keyboard" msgstr "Clavier" -#: ../vnc.html:97 +#: ../vnc.html:131 msgid "Show keyboard" msgstr "Afficher le clavier" -#: ../vnc.html:102 +#: ../vnc.html:136 msgid "Extra keys" msgstr "Touches supplémentaires" -#: ../vnc.html:102 +#: ../vnc.html:136 msgid "Show extra keys" msgstr "Afficher les touches supplémentaires" -#: ../vnc.html:107 +#: ../vnc.html:141 msgid "Ctrl" msgstr "Ctrl" -#: ../vnc.html:107 +#: ../vnc.html:141 msgid "Toggle Ctrl" msgstr "Basculer Ctrl" -#: ../vnc.html:110 +#: ../vnc.html:144 msgid "Alt" msgstr "Alt" -#: ../vnc.html:110 +#: ../vnc.html:144 msgid "Toggle Alt" msgstr "Basculer Alt" -#: ../vnc.html:113 +#: ../vnc.html:147 msgid "Toggle Windows" msgstr "Basculer Windows" -#: ../vnc.html:113 +#: ../vnc.html:147 msgid "Windows" -msgstr "Windows" +msgstr "Fenêtre" -#: ../vnc.html:116 +#: ../vnc.html:150 msgid "Send Tab" -msgstr "Envoyer l'onglet" +msgstr "Envoyer Tab" -#: ../vnc.html:116 +#: ../vnc.html:150 msgid "Tab" -msgstr "l'onglet" +msgstr "Tabulation" -#: ../vnc.html:119 +#: ../vnc.html:153 msgid "Esc" msgstr "Esc" -#: ../vnc.html:119 +#: ../vnc.html:153 msgid "Send Escape" msgstr "Envoyer Escape" -#: ../vnc.html:122 +#: ../vnc.html:156 msgid "Ctrl+Alt+Del" msgstr "Ctrl+Alt+Del" -#: ../vnc.html:122 +#: ../vnc.html:156 msgid "Send Ctrl-Alt-Del" msgstr "Envoyer Ctrl-Alt-Del" -#: ../vnc.html:129 +#: ../vnc.html:163 msgid "Shutdown/Reboot" msgstr "Arrêter/Redémarrer" -#: ../vnc.html:129 +#: ../vnc.html:163 msgid "Shutdown/Reboot..." msgstr "Arrêter/Redémarrer..." -#: ../vnc.html:135 +#: ../vnc.html:169 msgid "Power" msgstr "Alimentation" -#: ../vnc.html:137 +#: ../vnc.html:171 msgid "Shutdown" msgstr "Arrêter" -#: ../vnc.html:138 +#: ../vnc.html:172 msgid "Reboot" msgstr "Redémarrer" -#: ../vnc.html:139 +#: ../vnc.html:173 msgid "Reset" msgstr "Réinitialiser" -#: ../vnc.html:144 ../vnc.html:150 +#: ../vnc.html:178 ../vnc.html:184 msgid "Clipboard" msgstr "Presse-papiers" -#: ../vnc.html:154 -msgid "Clear" -msgstr "Effacer" +#: ../vnc.html:186 +msgid "Edit clipboard content in the textarea below." +msgstr "Editer le contenu du presse-papier dans la zone ci-dessous." -#: ../vnc.html:160 -msgid "Fullscreen" +#: ../vnc.html:194 +msgid "Full screen" msgstr "Plein écran" -#: ../vnc.html:165 ../vnc.html:172 +#: ../vnc.html:199 ../vnc.html:205 msgid "Settings" msgstr "Paramètres" -#: ../vnc.html:175 +#: ../vnc.html:211 msgid "Shared mode" msgstr "Mode partagé" -#: ../vnc.html:178 +#: ../vnc.html:218 msgid "View only" msgstr "Afficher uniquement" -#: ../vnc.html:182 +#: ../vnc.html:226 msgid "Clip to window" -msgstr "Clip à fenêtre" +msgstr "Ajuster à la fenêtre" -#: ../vnc.html:185 +#: ../vnc.html:231 msgid "Scaling mode:" msgstr "Mode mise à l'échelle :" -#: ../vnc.html:187 +#: ../vnc.html:233 msgid "None" msgstr "Aucun" -#: ../vnc.html:188 +#: ../vnc.html:234 msgid "Local scaling" msgstr "Mise à l'échelle locale" -#: ../vnc.html:189 +#: ../vnc.html:235 msgid "Remote resizing" msgstr "Redimensionnement à distance" -#: ../vnc.html:194 +#: ../vnc.html:240 msgid "Advanced" msgstr "Avancé" -#: ../vnc.html:197 +#: ../vnc.html:243 msgid "Quality:" msgstr "Qualité :" -#: ../vnc.html:201 +#: ../vnc.html:247 msgid "Compression level:" msgstr "Niveau de compression :" -#: ../vnc.html:206 +#: ../vnc.html:252 msgid "Repeater ID:" msgstr "ID Répéteur :" -#: ../vnc.html:210 +#: ../vnc.html:256 msgid "WebSocket" msgstr "WebSocket" -#: ../vnc.html:213 +#: ../vnc.html:261 msgid "Encrypt" msgstr "Chiffrer" -#: ../vnc.html:216 +#: ../vnc.html:266 msgid "Host:" msgstr "Hôte :" -#: ../vnc.html:220 +#: ../vnc.html:270 msgid "Port:" msgstr "Port :" -#: ../vnc.html:224 +#: ../vnc.html:274 msgid "Path:" msgstr "Chemin :" -#: ../vnc.html:231 +#: ../vnc.html:283 msgid "Automatic reconnect" -msgstr "Reconnecter automatiquemen" +msgstr "Reconnecter automatiquement" -#: ../vnc.html:234 +#: ../vnc.html:288 msgid "Reconnect delay (ms):" msgstr "Délai de reconnexion (ms) :" -#: ../vnc.html:239 +#: ../vnc.html:295 msgid "Show dot when no cursor" msgstr "Afficher le point lorsqu'il n'y a pas de curseur" -#: ../vnc.html:244 +#: ../vnc.html:302 msgid "Logging:" msgstr "Se connecter :" -#: ../vnc.html:253 +#: ../vnc.html:311 msgid "Version:" msgstr "Version :" -#: ../vnc.html:261 +#: ../vnc.html:319 msgid "Disconnect" msgstr "Déconnecter" -#: ../vnc.html:280 +#: ../vnc.html:342 msgid "Connect" msgstr "Connecter" -#: ../vnc.html:290 +#: ../vnc.html:351 +msgid "Server identity" +msgstr "Identité du serveur" + +#: ../vnc.html:354 +msgid "The server has provided the following identifying information:" +msgstr "Le serveur a fourni l'identification suivante :" + +#: ../vnc.html:357 +msgid "Fingerprint:" +msgstr "Empreinte digitale :" + +#: ../vnc.html:361 +msgid "" +"Please verify that the information is correct and press \"Approve\". " +"Otherwise press \"Reject\"." +msgstr "" +"SVP, verifiez que l'information est correcte et pressez \"Accepter\". Sinon " +"pressez \"Refuser\"." + +#: ../vnc.html:366 +msgid "Approve" +msgstr "Accepter" + +#: ../vnc.html:367 +msgid "Reject" +msgstr "Refuser" + +#: ../vnc.html:375 +msgid "Credentials" +msgstr "Envoyer les identifiants" + +#: ../vnc.html:379 msgid "Username:" msgstr "Nom d'utilisateur :" -#: ../vnc.html:294 +#: ../vnc.html:383 msgid "Password:" msgstr "Mot de passe :" -#: ../vnc.html:298 +#: ../vnc.html:387 msgid "Send credentials" msgstr "Envoyer les identifiants" -#: ../vnc.html:308 +#: ../vnc.html:396 msgid "Cancel" msgstr "Annuler" + +#~ msgid "Must set host" +#~ msgstr "Doit définir l'hôte" + +#~ msgid "Clear" +#~ msgstr "Effacer" From 09440da5c1deccf579020292cfeeb8f7dc23f65b Mon Sep 17 00:00:00 2001 From: Harold Horsman Date: Mon, 3 Mar 2025 18:20:00 +0100 Subject: [PATCH 141/166] Update Dutch translation --- app/locale/nl.json | 72 +++++++---- po/nl.po | 310 ++++++++++++++++++++++++++++----------------- 2 files changed, 241 insertions(+), 141 deletions(-) diff --git a/app/locale/nl.json b/app/locale/nl.json index ea6335bd8..61f2df3c7 100644 --- a/app/locale/nl.json +++ b/app/locale/nl.json @@ -1,36 +1,32 @@ { - "Connecting...": "Verbinden...", - "Disconnecting...": "Verbinding verbreken...", + "Running without HTTPS is not recommended, crashes or other issues are likely.": "Het is niet aan te raden om zonder HTTPS te werken, crashes of andere problemen zijn dan waarschijnlijk.", + "Connecting...": "Aan het verbinden…", + "Disconnecting...": "Bezig om verbinding te verbreken...", "Reconnecting...": "Opnieuw verbinding maken...", "Internal error": "Interne fout", - "Must set host": "Host moeten worden ingesteld", + "Failed to connect to server: ": "Verbinding maken met server is mislukt", "Connected (encrypted) to ": "Verbonden (versleuteld) met ", "Connected (unencrypted) to ": "Verbonden (onversleuteld) met ", "Something went wrong, connection is closed": "Er iets fout gelopen, verbinding werd verbroken", "Failed to connect to server": "Verbinding maken met server is mislukt", "Disconnected": "Verbinding verbroken", - "New connection has been rejected with reason: ": "Nieuwe verbinding is geweigerd omwille van de volgende reden: ", + "New connection has been rejected with reason: ": "Nieuwe verbinding is geweigerd met de volgende reden: ", "New connection has been rejected": "Nieuwe verbinding is geweigerd", - "Password is required": "Wachtwoord is vereist", + "Credentials are required": "Inloggegevens zijn nodig", "noVNC encountered an error:": "noVNC heeft een fout bemerkt:", "Hide/Show the control bar": "Verberg/Toon de bedieningsbalk", - "Move/Drag Viewport": "Verplaats/Versleep Kijkvenster", - "viewport drag": "kijkvenster slepen", - "Active Mouse Button": "Actieve Muisknop", - "No mousebutton": "Geen muisknop", - "Left mousebutton": "Linker muisknop", - "Middle mousebutton": "Middelste muisknop", - "Right mousebutton": "Rechter muisknop", + "Drag": "Sleep", + "Move/Drag viewport": "Verplaats/Versleep Kijkvenster", "Keyboard": "Toetsenbord", - "Show Keyboard": "Toon Toetsenbord", + "Show keyboard": "Toon Toetsenbord", "Extra keys": "Extra toetsen", - "Show Extra Keys": "Toon Extra Toetsen", + "Show extra keys": "Toon Extra Toetsen", "Ctrl": "Ctrl", "Toggle Ctrl": "Ctrl omschakelen", "Alt": "Alt", "Toggle Alt": "Alt omschakelen", - "Toggle Windows": "Windows omschakelen", - "Windows": "Windows", + "Toggle Windows": "Vensters omschakelen", + "Windows": "Vensters", "Send Tab": "Tab Sturen", "Tab": "Tab", "Esc": "Esc", @@ -44,30 +40,56 @@ "Reboot": "Herstarten", "Reset": "Resetten", "Clipboard": "Klembord", - "Clear": "Wissen", - "Fullscreen": "Volledig Scherm", + "Edit clipboard content in the textarea below.": "Edit de inhoud van het klembord in het tekstveld hieronder", + "Full screen": "Volledig Scherm", "Settings": "Instellingen", - "Shared Mode": "Gedeelde Modus", - "View Only": "Alleen Kijken", + "Shared mode": "Gedeelde Modus", + "View only": "Alleen Kijken", "Clip to window": "Randen buiten venster afsnijden", "Scaling mode:": "Schaalmodus:", "None": "Geen", - "Local Scaling": "Lokaal Schalen", - "Remote Resizing": "Op Afstand Formaat Wijzigen", + "Local scaling": "Lokaal Schalen", + "Remote resizing": "Op Afstand Formaat Wijzigen", "Advanced": "Geavanceerd", + "Quality:": "Kwaliteit:", + "Compression level:": "Compressieniveau:", "Repeater ID:": "Repeater ID:", "WebSocket": "WebSocket", "Encrypt": "Versleutelen", "Host:": "Host:", "Port:": "Poort:", "Path:": "Pad:", - "Automatic Reconnect": "Automatisch Opnieuw Verbinden", - "Reconnect Delay (ms):": "Vertraging voor Opnieuw Verbinden (ms):", + "Automatic reconnect": "Automatisch Opnieuw Verbinden", + "Reconnect delay (ms):": "Vertraging voor Opnieuw Verbinden (ms):", "Show dot when no cursor": "Geef stip weer indien geen cursor", "Logging:": "Logmeldingen:", + "Version:": "Versie:", "Disconnect": "Verbinding verbreken", "Connect": "Verbinden", + "Server identity": "Serveridentiteit", + "The server has provided the following identifying information:": "De server geeft de volgende identificerende informatie:", + "Fingerprint:": "Vingerafdruk:", + "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Verifieer dat de informatie is correct en druk “OK”. Druk anders op “Afwijzen”.", + "Approve": "OK", + "Reject": "Afwijzen", + "Credentials": "Inloggegevens", + "Username:": "Gebruikersnaam:", "Password:": "Wachtwoord:", + "Send credentials": "Stuur inloggegevens", + "Cancel": "Annuleren", + "Must set host": "Host moeten worden ingesteld", + "Password is required": "Wachtwoord is vereist", + "viewport drag": "kijkvenster slepen", + "Active Mouse Button": "Actieve Muisknop", + "No mousebutton": "Geen muisknop", + "Left mousebutton": "Linker muisknop", + "Middle mousebutton": "Middelste muisknop", + "Right mousebutton": "Rechter muisknop", + "Clear": "Wissen", "Send Password": "Verzend Wachtwoord:", - "Cancel": "Annuleren" + "Disconnect timeout": "Timeout tijdens verbreken van verbinding", + "Local Downscaling": "Lokaal Neerschalen", + "Local Cursor": "Lokale Cursor", + "Canvas not supported.": "Canvas wordt niet ondersteund.", + "Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "''Clipping mode' ingeschakeld, omdat schuifbalken in volledige-scherm-modus in IE niet worden ondersteund" } \ No newline at end of file diff --git a/po/nl.po b/po/nl.po index 80f9d6f4d..80b264301 100644 --- a/po/nl.po +++ b/po/nl.po @@ -6,302 +6,380 @@ # msgid "" msgstr "" -"Project-Id-Version: noVNC 1.1.0\n" +"Project-Id-Version: noVNC 1.6.0\n" "Report-Msgid-Bugs-To: novnc@googlegroups.com\n" -"POT-Creation-Date: 2019-04-09 11:06+0100\n" -"PO-Revision-Date: 2019-04-09 17:17+0100\n" -"Last-Translator: Arend Lapere \n" +"POT-Creation-Date: 2025-02-14 10:14+0100\n" +"PO-Revision-Date: 2025-03-03 18:20+0100\n" +"Last-Translator: Harold Horsman \n" "Language-Team: none\n" "Language: nl\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:383 +#: ../app/ui.js:84 +msgid "" +"Running without HTTPS is not recommended, crashes or other issues are likely." +msgstr "" +"Het is niet aan te raden om zonder HTTPS te werken, crashes of andere " +"problemen zijn dan waarschijnlijk." + +#: ../app/ui.js:413 msgid "Connecting..." -msgstr "Verbinden..." +msgstr "Aan het verbinden…" -#: ../app/ui.js:390 +#: ../app/ui.js:420 msgid "Disconnecting..." -msgstr "Verbinding verbreken..." +msgstr "Bezig om verbinding te verbreken..." -#: ../app/ui.js:396 +#: ../app/ui.js:426 msgid "Reconnecting..." msgstr "Opnieuw verbinding maken..." -#: ../app/ui.js:401 +#: ../app/ui.js:431 msgid "Internal error" msgstr "Interne fout" -#: ../app/ui.js:991 -msgid "Must set host" -msgstr "Host moeten worden ingesteld" +#: ../app/ui.js:1079 +#, fuzzy +msgid "Failed to connect to server: " +msgstr "Verbinding maken met server is mislukt" -#: ../app/ui.js:1073 +#: ../app/ui.js:1145 msgid "Connected (encrypted) to " msgstr "Verbonden (versleuteld) met " -#: ../app/ui.js:1075 +#: ../app/ui.js:1147 msgid "Connected (unencrypted) to " msgstr "Verbonden (onversleuteld) met " -#: ../app/ui.js:1098 +#: ../app/ui.js:1170 msgid "Something went wrong, connection is closed" msgstr "Er iets fout gelopen, verbinding werd verbroken" -#: ../app/ui.js:1101 +#: ../app/ui.js:1173 msgid "Failed to connect to server" msgstr "Verbinding maken met server is mislukt" -#: ../app/ui.js:1111 +#: ../app/ui.js:1185 msgid "Disconnected" msgstr "Verbinding verbroken" -#: ../app/ui.js:1124 +#: ../app/ui.js:1200 msgid "New connection has been rejected with reason: " -msgstr "Nieuwe verbinding is geweigerd omwille van de volgende reden: " +msgstr "Nieuwe verbinding is geweigerd met de volgende reden: " -#: ../app/ui.js:1127 +#: ../app/ui.js:1203 msgid "New connection has been rejected" msgstr "Nieuwe verbinding is geweigerd" -#: ../app/ui.js:1147 -msgid "Password is required" -msgstr "Wachtwoord is vereist" +#: ../app/ui.js:1269 +msgid "Credentials are required" +msgstr "Inloggegevens zijn nodig" -#: ../vnc.html:80 +#: ../vnc.html:106 msgid "noVNC encountered an error:" msgstr "noVNC heeft een fout bemerkt:" -#: ../vnc.html:90 +#: ../vnc.html:116 msgid "Hide/Show the control bar" msgstr "Verberg/Toon de bedieningsbalk" -#: ../vnc.html:97 -msgid "Move/Drag Viewport" -msgstr "Verplaats/Versleep Kijkvenster" - -#: ../vnc.html:97 -msgid "viewport drag" -msgstr "kijkvenster slepen" +#: ../vnc.html:125 +msgid "Drag" +msgstr "Sleep" -#: ../vnc.html:103 ../vnc.html:106 ../vnc.html:109 ../vnc.html:112 -msgid "Active Mouse Button" -msgstr "Actieve Muisknop" - -#: ../vnc.html:103 -msgid "No mousebutton" -msgstr "Geen muisknop" - -#: ../vnc.html:106 -msgid "Left mousebutton" -msgstr "Linker muisknop" - -#: ../vnc.html:109 -msgid "Middle mousebutton" -msgstr "Middelste muisknop" - -#: ../vnc.html:112 -msgid "Right mousebutton" -msgstr "Rechter muisknop" +#: ../vnc.html:125 +#, fuzzy +msgid "Move/Drag viewport" +msgstr "Verplaats/Versleep Kijkvenster" -#: ../vnc.html:115 +#: ../vnc.html:131 msgid "Keyboard" msgstr "Toetsenbord" -#: ../vnc.html:115 -msgid "Show Keyboard" +#: ../vnc.html:131 +#, fuzzy +msgid "Show keyboard" msgstr "Toon Toetsenbord" -#: ../vnc.html:121 +#: ../vnc.html:136 msgid "Extra keys" msgstr "Extra toetsen" -#: ../vnc.html:121 -msgid "Show Extra Keys" +#: ../vnc.html:136 +#, fuzzy +msgid "Show extra keys" msgstr "Toon Extra Toetsen" -#: ../vnc.html:126 +#: ../vnc.html:141 msgid "Ctrl" msgstr "Ctrl" -#: ../vnc.html:126 +#: ../vnc.html:141 msgid "Toggle Ctrl" msgstr "Ctrl omschakelen" -#: ../vnc.html:129 +#: ../vnc.html:144 msgid "Alt" msgstr "Alt" -#: ../vnc.html:129 +#: ../vnc.html:144 msgid "Toggle Alt" msgstr "Alt omschakelen" -#: ../vnc.html:132 +#: ../vnc.html:147 msgid "Toggle Windows" -msgstr "Windows omschakelen" +msgstr "Vensters omschakelen" -#: ../vnc.html:132 +#: ../vnc.html:147 msgid "Windows" -msgstr "Windows" +msgstr "Vensters" -#: ../vnc.html:135 +#: ../vnc.html:150 msgid "Send Tab" msgstr "Tab Sturen" -#: ../vnc.html:135 +#: ../vnc.html:150 msgid "Tab" msgstr "Tab" -#: ../vnc.html:138 +#: ../vnc.html:153 msgid "Esc" msgstr "Esc" -#: ../vnc.html:138 +#: ../vnc.html:153 msgid "Send Escape" msgstr "Escape Sturen" -#: ../vnc.html:141 +#: ../vnc.html:156 msgid "Ctrl+Alt+Del" msgstr "Ctrl-Alt-Del" -#: ../vnc.html:141 +#: ../vnc.html:156 msgid "Send Ctrl-Alt-Del" msgstr "Ctrl-Alt-Del Sturen" -#: ../vnc.html:149 +#: ../vnc.html:163 msgid "Shutdown/Reboot" msgstr "Uitschakelen/Herstarten" -#: ../vnc.html:149 +#: ../vnc.html:163 msgid "Shutdown/Reboot..." msgstr "Uitschakelen/Herstarten..." -#: ../vnc.html:155 +#: ../vnc.html:169 msgid "Power" msgstr "Systeem" -#: ../vnc.html:157 +#: ../vnc.html:171 msgid "Shutdown" msgstr "Uitschakelen" -#: ../vnc.html:158 +#: ../vnc.html:172 msgid "Reboot" msgstr "Herstarten" -#: ../vnc.html:159 +#: ../vnc.html:173 msgid "Reset" msgstr "Resetten" -#: ../vnc.html:164 ../vnc.html:170 +#: ../vnc.html:178 ../vnc.html:184 msgid "Clipboard" msgstr "Klembord" -#: ../vnc.html:174 -msgid "Clear" -msgstr "Wissen" +#: ../vnc.html:186 +msgid "Edit clipboard content in the textarea below." +msgstr "Edit de inhoud van het klembord in het tekstveld hieronder" -#: ../vnc.html:180 -msgid "Fullscreen" +#: ../vnc.html:194 +#, fuzzy +msgid "Full screen" msgstr "Volledig Scherm" -#: ../vnc.html:185 ../vnc.html:192 +#: ../vnc.html:199 ../vnc.html:205 msgid "Settings" msgstr "Instellingen" -#: ../vnc.html:195 -msgid "Shared Mode" +#: ../vnc.html:211 +#, fuzzy +msgid "Shared mode" msgstr "Gedeelde Modus" -#: ../vnc.html:198 -msgid "View Only" +#: ../vnc.html:218 +#, fuzzy +msgid "View only" msgstr "Alleen Kijken" -#: ../vnc.html:202 +#: ../vnc.html:226 msgid "Clip to window" msgstr "Randen buiten venster afsnijden" -#: ../vnc.html:205 +#: ../vnc.html:231 msgid "Scaling mode:" msgstr "Schaalmodus:" -#: ../vnc.html:207 +#: ../vnc.html:233 msgid "None" msgstr "Geen" -#: ../vnc.html:208 -msgid "Local Scaling" +#: ../vnc.html:234 +#, fuzzy +msgid "Local scaling" msgstr "Lokaal Schalen" -#: ../vnc.html:209 -msgid "Remote Resizing" +#: ../vnc.html:235 +#, fuzzy +msgid "Remote resizing" msgstr "Op Afstand Formaat Wijzigen" -#: ../vnc.html:214 +#: ../vnc.html:240 msgid "Advanced" msgstr "Geavanceerd" -#: ../vnc.html:217 +#: ../vnc.html:243 +msgid "Quality:" +msgstr "Kwaliteit:" + +#: ../vnc.html:247 +msgid "Compression level:" +msgstr "Compressieniveau:" + +#: ../vnc.html:252 msgid "Repeater ID:" msgstr "Repeater ID:" -#: ../vnc.html:221 +#: ../vnc.html:256 msgid "WebSocket" msgstr "WebSocket" -#: ../vnc.html:224 +#: ../vnc.html:261 msgid "Encrypt" msgstr "Versleutelen" -#: ../vnc.html:227 +#: ../vnc.html:266 msgid "Host:" msgstr "Host:" -#: ../vnc.html:231 +#: ../vnc.html:270 msgid "Port:" msgstr "Poort:" -#: ../vnc.html:235 +#: ../vnc.html:274 msgid "Path:" msgstr "Pad:" -#: ../vnc.html:242 -msgid "Automatic Reconnect" +#: ../vnc.html:283 +#, fuzzy +msgid "Automatic reconnect" msgstr "Automatisch Opnieuw Verbinden" -#: ../vnc.html:245 -msgid "Reconnect Delay (ms):" +#: ../vnc.html:288 +#, fuzzy +msgid "Reconnect delay (ms):" msgstr "Vertraging voor Opnieuw Verbinden (ms):" -#: ../vnc.html:250 +#: ../vnc.html:295 msgid "Show dot when no cursor" msgstr "Geef stip weer indien geen cursor" -#: ../vnc.html:255 +#: ../vnc.html:302 msgid "Logging:" msgstr "Logmeldingen:" -#: ../vnc.html:267 +#: ../vnc.html:311 +msgid "Version:" +msgstr "Versie:" + +#: ../vnc.html:319 msgid "Disconnect" msgstr "Verbinding verbreken" -#: ../vnc.html:286 +#: ../vnc.html:342 msgid "Connect" msgstr "Verbinden" -#: ../vnc.html:296 +#: ../vnc.html:351 +msgid "Server identity" +msgstr "Serveridentiteit" + +#: ../vnc.html:354 +msgid "The server has provided the following identifying information:" +msgstr "De server geeft de volgende identificerende informatie:" + +#: ../vnc.html:357 +msgid "Fingerprint:" +msgstr "Vingerafdruk:" + +#: ../vnc.html:361 +msgid "" +"Please verify that the information is correct and press \"Approve\". " +"Otherwise press \"Reject\"." +msgstr "" +"Verifieer dat de informatie is correct en druk “OK”. Druk anders op " +"“Afwijzen”." + +#: ../vnc.html:366 +msgid "Approve" +msgstr "OK" + +#: ../vnc.html:367 +msgid "Reject" +msgstr "Afwijzen" + +#: ../vnc.html:375 +msgid "Credentials" +msgstr "Inloggegevens" + +#: ../vnc.html:379 +msgid "Username:" +msgstr "Gebruikersnaam:" + +#: ../vnc.html:383 msgid "Password:" msgstr "Wachtwoord:" -#: ../vnc.html:300 -msgid "Send Password" -msgstr "Verzend Wachtwoord:" +# Translated by Harold Horsman +#: ../vnc.html:387 +msgid "Send credentials" +msgstr "Stuur inloggegevens" -#: ../vnc.html:310 +#: ../vnc.html:396 msgid "Cancel" msgstr "Annuleren" +#~ msgid "Must set host" +#~ msgstr "Host moeten worden ingesteld" + +#~ msgid "Password is required" +#~ msgstr "Wachtwoord is vereist" + +#~ msgid "viewport drag" +#~ msgstr "kijkvenster slepen" + +#~ msgid "Active Mouse Button" +#~ msgstr "Actieve Muisknop" + +#~ msgid "No mousebutton" +#~ msgstr "Geen muisknop" + +#~ msgid "Left mousebutton" +#~ msgstr "Linker muisknop" + +#~ msgid "Middle mousebutton" +#~ msgstr "Middelste muisknop" + +#~ msgid "Right mousebutton" +#~ msgstr "Rechter muisknop" + +#~ msgid "Clear" +#~ msgstr "Wissen" + +#~ msgid "Send Password" +#~ msgstr "Verzend Wachtwoord:" + #~ msgid "Disconnect timeout" #~ msgstr "Timeout tijdens verbreken van verbinding" From 3947b4ca843a21c00bc995be4fa7069ed5337734 Mon Sep 17 00:00:00 2001 From: Alexander Zeijlon Date: Wed, 12 Mar 2025 20:11:37 +0100 Subject: [PATCH 142/166] Update generated json files for new translations --- app/locale/de.json | 7 +++++- app/locale/el.json | 23 ++++++++++++++++- app/locale/pl.json | 13 +++++++++- app/locale/sv.json | 26 ++++++++++--------- app/locale/zh_CN.json | 58 ++++++++++++++++++++++++++++++------------- 5 files changed, 95 insertions(+), 32 deletions(-) diff --git a/app/locale/de.json b/app/locale/de.json index e92825a8b..fa1546377 100644 --- a/app/locale/de.json +++ b/app/locale/de.json @@ -65,5 +65,10 @@ "Connect": "Verbinden", "Password:": "Passwort:", "Cancel": "Abbrechen", - "Canvas not supported.": "Canvas nicht unterstützt." + "Canvas not supported.": "Canvas nicht unterstützt.", + "Disconnect timeout": "Zeitüberschreitung beim Trennen", + "Local Downscaling": "Lokales herunterskalieren", + "Local Cursor": "Lokaler Mauszeiger", + "Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "'Clipping-Modus' aktiviert, Scrollbalken in 'IE-Vollbildmodus' werden nicht unterstützt", + "True Color": "True Color" } \ No newline at end of file diff --git a/app/locale/el.json b/app/locale/el.json index 4df3e03c4..57d67316c 100644 --- a/app/locale/el.json +++ b/app/locale/el.json @@ -41,6 +41,7 @@ "Reset": "Επαναφορά", "Clipboard": "Πρόχειρο", "Edit clipboard content in the textarea below.": "Επεξεργαστείτε το περιεχόμενο του πρόχειρου στην περιοχή κειμένου παρακάτω.", + "Full Screen": "Πλήρης Οθόνη", "Settings": "Ρυθμίσεις", "Shared Mode": "Κοινόχρηστη Λειτουργία", "View Only": "Μόνο Θέαση", @@ -75,5 +76,25 @@ "Username:": "Κωδικός Χρήστη:", "Password:": "Κωδικός Πρόσβασης:", "Send Credentials": "Αποστολή Διαπιστευτηρίων", - "Cancel": "Ακύρωση" + "Cancel": "Ακύρωση", + "Password is required": "Απαιτείται ο κωδικός πρόσβασης", + "viewport drag": "σύρσιμο θεατού πεδίου", + "Active Mouse Button": "Ενεργό Πλήκτρο Ποντικιού", + "No mousebutton": "Χωρίς Πλήκτρο Ποντικιού", + "Left mousebutton": "Αριστερό Πλήκτρο Ποντικιού", + "Middle mousebutton": "Μεσαίο Πλήκτρο Ποντικιού", + "Right mousebutton": "Δεξί Πλήκτρο Ποντικιού", + "Clear": "Καθάρισμα", + "Canvas not supported.": "Δεν υποστηρίζεται το στοιχείο Canvas", + "Disconnect timeout": "Παρέλευση χρονικού ορίου αποσύνδεσης", + "Local Downscaling": "Τοπική Συρρίκνωση", + "Local Cursor": "Τοπικός Δρομέας", + "Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "Εφαρμογή λειτουργίας αποκοπής αφού δεν υποστηρίζονται οι λωρίδες κύλισης σε πλήρη οθόνη στον IE", + "True Color": "Πραγματικά Χρώματα", + "Style:": "Στυλ:", + "default": "προεπιλεγμένο", + "Apply": "Εφαρμογή", + "Connection": "Σύνδεση", + "Token:": "Διακριτικό:", + "Send Password": "Αποστολή Κωδικού Πρόσβασης" } \ No newline at end of file diff --git a/app/locale/pl.json b/app/locale/pl.json index 865f90ca1..87c169056 100644 --- a/app/locale/pl.json +++ b/app/locale/pl.json @@ -65,5 +65,16 @@ "Connect": "Połącz", "Password:": "Hasło:", "Cancel": "Anuluj", - "Canvas not supported.": "Element Canvas nie jest wspierany." + "Canvas not supported.": "Element Canvas nie jest wspierany.", + "Disconnect timeout": "Timeout rozłączenia", + "Local Downscaling": "Downscaling lokalny", + "Local Cursor": "Lokalny kursor", + "Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "Wymuszam clipping mode ponieważ paski przewijania nie są wspierane przez IE w trybie pełnoekranowym", + "True Color": "True Color", + "Style:": "Styl:", + "default": "domyślny", + "Apply": "Zapisz", + "Connection": "Połączenie", + "Token:": "Token:", + "Send Password": "Wyślij Hasło" } \ No newline at end of file diff --git a/app/locale/sv.json b/app/locale/sv.json index 83385cd24..67f6675aa 100644 --- a/app/locale/sv.json +++ b/app/locale/sv.json @@ -4,7 +4,6 @@ "Disconnecting...": "Kopplar ner...", "Reconnecting...": "Återansluter...", "Internal error": "Internt fel", - "Must set host": "Du måste specifiera en värd", "Failed to connect to server: ": "Misslyckades att ansluta till servern: ", "Connected (encrypted) to ": "Ansluten (krypterat) till ", "Connected (unencrypted) to ": "Ansluten (okrypterat) till ", @@ -17,11 +16,11 @@ "noVNC encountered an error:": "noVNC stötte på ett problem:", "Hide/Show the control bar": "Göm/Visa kontrollbaren", "Drag": "Dra", - "Move/Drag Viewport": "Flytta/Dra Vyn", + "Move/Drag viewport": "Flytta/Dra vyn", "Keyboard": "Tangentbord", - "Show Keyboard": "Visa Tangentbord", + "Show keyboard": "Visa tangentbord", "Extra keys": "Extraknappar", - "Show Extra Keys": "Visa Extraknappar", + "Show extra keys": "Visa extraknappar", "Ctrl": "Ctrl", "Toggle Ctrl": "Växla Ctrl", "Alt": "Alt", @@ -44,13 +43,13 @@ "Edit clipboard content in the textarea below.": "Redigera urklippets innehåll i fältet nedan.", "Full screen": "Fullskärm", "Settings": "Inställningar", - "Shared Mode": "Delat Läge", - "View Only": "Endast Visning", - "Clip to Window": "Begränsa till Fönster", + "Shared mode": "Delat läge", + "View only": "Endast visning", + "Clip to window": "Begränsa till fönster", "Scaling mode:": "Skalningsläge:", "None": "Ingen", - "Local Scaling": "Lokal Skalning", - "Remote Resizing": "Ändra Storlek", + "Local scaling": "Lokal skalning", + "Remote resizing": "Ändra storlek", "Advanced": "Avancerat", "Quality:": "Kvalitet:", "Compression level:": "Kompressionsnivå:", @@ -60,7 +59,7 @@ "Host:": "Värd:", "Port:": "Port:", "Path:": "Sökväg:", - "Automatic Reconnect": "Automatisk Återanslutning", + "Automatic reconnect": "Automatisk återanslutning", "Reconnect delay (ms):": "Fördröjning (ms):", "Show dot when no cursor": "Visa prick när ingen muspekare finns", "Logging:": "Loggning:", @@ -76,6 +75,9 @@ "Credentials": "Användaruppgifter", "Username:": "Användarnamn:", "Password:": "Lösenord:", - "Send Credentials": "Skicka Användaruppgifter", - "Cancel": "Avbryt" + "Send credentials": "Skicka användaruppgifter", + "Cancel": "Avbryt", + "Must set host": "Du måste specifiera en värd", + "HTTPS is required for full functionality": "HTTPS krävs för full funktionalitet", + "Clear": "Rensa" } \ No newline at end of file diff --git a/app/locale/zh_CN.json b/app/locale/zh_CN.json index 63014c017..2898d2c1d 100644 --- a/app/locale/zh_CN.json +++ b/app/locale/zh_CN.json @@ -1,31 +1,33 @@ { + "Running without HTTPS is not recommended, crashes or other issues are likely.": "不建议在没有 HTTPS 的情况下运行,可能会出现崩溃或出现其他问题。", "Connecting...": "连接中...", + "Disconnecting...": "正在断开连接...", + "Reconnecting...": "重新连接中...", + "Internal error": "内部错误", + "Must set host": "必须设置主机", + "Failed to connect to server: ": "无法连接到服务器:", "Connected (encrypted) to ": "已连接(已加密)到", "Connected (unencrypted) to ": "已连接(未加密)到", - "Disconnecting...": "正在断开连接...", + "Something went wrong, connection is closed": "出了点问题,连接已关闭", + "Failed to connect to server": "无法连接到服务器", "Disconnected": "已断开连接", - "Must set host": "必须设置主机", - "Reconnecting...": "重新连接中...", - "Password is required": "请提供密码", - "Disconnect timeout": "超时断开", + "New connection has been rejected with reason: ": "新连接被拒绝,原因如下:", + "New connection has been rejected": "新连接已被拒绝", + "Credentials are required": "需要凭证", "noVNC encountered an error:": "noVNC 遇到一个错误:", "Hide/Show the control bar": "显示/隐藏控制栏", + "Drag": "拖动", "Move/Drag viewport": "移动/拖动窗口", - "viewport drag": "窗口拖动", - "Active Mouse Button": "启动鼠标按键", - "No mousebutton": "禁用鼠标按键", - "Left mousebutton": "鼠标左键", - "Middle mousebutton": "鼠标中键", - "Right mousebutton": "鼠标右键", "Keyboard": "键盘", "Show keyboard": "显示键盘", "Extra keys": "额外按键", "Show extra keys": "显示额外按键", "Ctrl": "Ctrl", "Toggle Ctrl": "切换 Ctrl", - "Edit clipboard content in the textarea below.": "在下面的文本区域中编辑剪贴板内容。", "Alt": "Alt", "Toggle Alt": "切换 Alt", + "Toggle Windows": "切换窗口", + "Windows": "窗口", "Send Tab": "发送 Tab 键", "Tab": "Tab", "Esc": "Esc", @@ -39,31 +41,53 @@ "Reboot": "重启", "Reset": "重置", "Clipboard": "剪贴板", - "Clear": "清除", - "Fullscreen": "全屏", + "Edit clipboard content in the textarea below.": "在下面的文本区域中编辑剪贴板内容。", + "Full screen": "全屏", "Settings": "设置", - "Encrypt": "加密", "Shared mode": "分享模式", "View only": "仅查看", "Clip to window": "限制/裁切窗口大小", "Scaling mode:": "缩放模式:", "None": "无", "Local scaling": "本地缩放", - "Local Downscaling": "降低本地尺寸", "Remote resizing": "远程调整大小", "Advanced": "高级", - "Local Cursor": "本地光标", + "Quality:": "品质:", + "Compression level:": "压缩级别:", "Repeater ID:": "中继站 ID", "WebSocket": "WebSocket", + "Encrypt": "加密", "Host:": "主机:", "Port:": "端口:", "Path:": "路径:", "Automatic reconnect": "自动重新连接", "Reconnect delay (ms):": "重新连接间隔 (ms):", + "Show dot when no cursor": "无光标时显示点", "Logging:": "日志级别:", + "Version:": "版本:", "Disconnect": "断开连接", "Connect": "连接", + "Server identity": "服务器身份", + "The server has provided the following identifying information:": "服务器提供了以下识别信息:", + "Fingerprint:": "指纹:", + "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "请核实信息是否正确,并按 “同意”,否则按 “拒绝”。", + "Approve": "同意", + "Reject": "拒绝", + "Credentials": "凭证", + "Username:": "用户名:", "Password:": "密码:", + "Send credentials": "发送凭证", "Cancel": "取消", + "Password is required": "请提供密码", + "Disconnect timeout": "超时断开", + "viewport drag": "窗口拖动", + "Active Mouse Button": "启动鼠标按键", + "No mousebutton": "禁用鼠标按键", + "Left mousebutton": "鼠标左键", + "Middle mousebutton": "鼠标中键", + "Right mousebutton": "鼠标右键", + "Clear": "清除", + "Local Downscaling": "降低本地尺寸", + "Local Cursor": "本地光标", "Canvas not supported.": "不支持 Canvas。" } \ No newline at end of file From a8dfd6a3ea3c74244f5ebdaa5a7f1023007a7820 Mon Sep 17 00:00:00 2001 From: Alexander Zeijlon Date: Wed, 12 Mar 2025 20:25:30 +0100 Subject: [PATCH 143/166] noVNC 1.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 69ba9f5cc..bfe16a542 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@novnc/novnc", - "version": "1.6.0-beta", + "version": "1.6.0", "description": "An HTML5 VNC client", "browser": "lib/rfb", "directories": { From f0a39cd357a5995673149b95951d4c1261b69571 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Mon, 24 Mar 2025 22:14:43 +0100 Subject: [PATCH 144/166] Fix appearance of extra key buttons Since the extra keys panel is quite narrow in width, a max-width style resulted in the buttons almost disappearing. That rule was only intended for elements inside the settings panel. Broken by commit 14f9ea5880f32f2a4867006d46c8e871942c698e. Another minor error that is also fixed by this commit is that the clipboard textarea no longer incorrectly gets a left margin of 6px. Fixes #1946. --- app/styles/base.css | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index 87bfb45c5..af67cf281 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -475,15 +475,6 @@ html { margin: 5px; } -.noVNC_panel button, -.noVNC_panel select, -.noVNC_panel textarea, -.noVNC_panel input:not([type=checkbox]):not([type=radio]) { - margin-left: 6px; - /* Prevent inputs in panels from being too wide */ - max-width: calc(100% - 6px - var(--input-xpadding) * 2); -} - .noVNC_panel .noVNC_heading { background-color: var(--novnc-blue); border-radius: 6px; @@ -621,6 +612,15 @@ html { list-style: none; padding: 0px; } +#noVNC_settings button, +#noVNC_settings select, +#noVNC_settings textarea, +#noVNC_settings input:not([type=checkbox]):not([type=radio]) { + margin-left: 6px; + /* Prevent inputs in panels from being too wide */ + max-width: calc(100% - 6px - var(--input-xpadding) * 2); +} + #noVNC_setting_port { width: 80px; } From 6010c9da041e64cf49be85dc8d257a83597e9669 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Mon, 24 Mar 2025 22:33:09 +0100 Subject: [PATCH 145/166] Update comment reference missed in previous commit Should have been part of f0a39cd357a5995673149b95951d4c1261b69571 --- app/styles/base.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/styles/base.css b/app/styles/base.css index af67cf281..33f0f3596 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -617,7 +617,7 @@ html { #noVNC_settings textarea, #noVNC_settings input:not([type=checkbox]):not([type=radio]) { margin-left: 6px; - /* Prevent inputs in panels from being too wide */ + /* Prevent inputs in settings from being too wide */ max-width: calc(100% - 6px - var(--input-xpadding) * 2); } From 154653523c2c9846242c02baebc2f8598721f1a3 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 13 Mar 2025 09:03:35 +0100 Subject: [PATCH 146/166] Only include valid translations in .json files Fuzzy translations might be incorrect, and obsolete translations aren't used anywhere. --- po/po2js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/po/po2js b/po/po2js index e74b1d4f4..6347e1ea5 100755 --- a/po/po2js +++ b/po/po2js @@ -32,6 +32,8 @@ let po = pofile.parse(data); const bodyPart = po.items .filter(item => item.msgid !== "") .filter(item => item.msgstr[0] !== "") + .filter(item => !item.flags.fuzzy) + .filter(item => !item.obsolete) .map(item => " " + JSON.stringify(item.msgid) + ": " + JSON.stringify(item.msgstr[0])) .join(",\n"); From 8edb3d282eb9ebb138b0f9a4baacb90bb4c4427e Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 8 Apr 2025 15:42:20 +0200 Subject: [PATCH 147/166] Close VideoFrame after H.264 detection Otherwise browser will complain when it is garbage collected. --- core/util/browser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/util/browser.js b/core/util/browser.js index fc1b77f9e..63596d211 100644 --- a/core/util/browser.js +++ b/core/util/browser.js @@ -117,7 +117,7 @@ async function _checkWebCodecsH264DecodeSupport() { let error = null; let decoder = new VideoDecoder({ - output: (frame) => { gotframe = true; }, + output: (frame) => { gotframe = true; frame.close(); }, error: (e) => { error = e; }, }); let chunk = new EncodedVideoChunk({ From 980a49e5c5f4d14103a9e90a82d721c4f5578faf Mon Sep 17 00:00:00 2001 From: Liao Peiyuan Date: Thu, 10 Apr 2025 12:29:19 -0700 Subject: [PATCH 148/166] Fix typo in error-handler.js --- app/error-handler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/error-handler.js b/app/error-handler.js index 5f6ffb674..ef1cb64c7 100644 --- a/app/error-handler.js +++ b/app/error-handler.js @@ -6,7 +6,7 @@ * See README.md for usage and integration instructions. */ -// Fallback for all uncought errors +// Fallback for all uncaught errors function handleError(event, err) { try { const msg = document.getElementById('noVNC_fallback_errormsg'); From 6f9edb1d4a94ad2faa7d45f6d520351444670650 Mon Sep 17 00:00:00 2001 From: leandro ostruka Date: Sat, 12 Apr 2025 11:36:30 -0300 Subject: [PATCH 149/166] Fix typo in Portuguese translation for "Show extra keys" --- app/locale/pt_BR.json | 2 +- po/pt_BR.po | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/locale/pt_BR.json b/app/locale/pt_BR.json index 224b86efd..9c2f7cec7 100644 --- a/app/locale/pt_BR.json +++ b/app/locale/pt_BR.json @@ -19,7 +19,7 @@ "Keyboard": "Teclado", "Show keyboard": "Mostrar teclado", "Extra keys": "Teclas adicionais", - "Show extra keys": "Mostar teclas adicionais", + "Show extra keys": "Mostrar teclas adicionais", "Ctrl": "Ctrl", "Toggle Ctrl": "Pressionar/soltar Ctrl", "Alt": "Alt", diff --git a/po/pt_BR.po b/po/pt_BR.po index 422b94266..fb3368b75 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -100,7 +100,7 @@ msgstr "Teclas adicionais" #: ../vnc.html:89 msgid "Show extra keys" -msgstr "Mostar teclas adicionais" +msgstr "Mostrar teclas adicionais" #: ../vnc.html:94 msgid "Ctrl" From a22857c99c130b9a4d7dd647b42c086b56fe6cfd Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 28 Apr 2025 09:55:49 +0200 Subject: [PATCH 150/166] Document new behaviour of host/port/encrypt/path This changed in 96c76f7, but we forgot to adjust the documentation for the parameters. --- docs/EMBEDDING.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/EMBEDDING.md b/docs/EMBEDDING.md index b411de82b..732859930 100644 --- a/docs/EMBEDDING.md +++ b/docs/EMBEDDING.md @@ -37,13 +37,18 @@ query string. Currently the following options are available: * `reconnect_delay` - How long to wait in milliseconds before attempting to reconnect. -* `host` - The WebSocket host to connect to. +* `host` - The WebSocket host to connect to. This setting is deprecated + in favor of specifying a URL in `path`. -* `port` - The WebSocket port to connect to. +* `port` - The WebSocket port to connect to. This setting is deprecated + in favor of specifying a URL in `path`. -* `encrypt` - If TLS should be used for the WebSocket connection. +* `encrypt` - If TLS should be used for the WebSocket connection. This + setting is deprecated in favor of specifying a URL in `path`. -* `path` - The WebSocket path to use. +* `path` - The WebSocket URL to use. It can be either an absolute URL, + or a URL relative vnc.html. If `host` is specified, then `path` will + be interpreted as the path component in the URL instead. * `password` - The password sent to the server, if required. From 88749fc0f9da7c58396ab22cdd24bf5676ede366 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 28 Apr 2025 09:56:52 +0200 Subject: [PATCH 151/166] Document new JSON settings files This mechanism was added in 438e5b3, but we forgot to document it. --- docs/EMBEDDING.md | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/docs/EMBEDDING.md b/docs/EMBEDDING.md index 732859930..9e927d0d3 100644 --- a/docs/EMBEDDING.md +++ b/docs/EMBEDDING.md @@ -25,8 +25,27 @@ server and setting up a WebSocket proxy to the VNC server. ## Parameters -The noVNC application can be controlled by including certain settings in the -query string. Currently the following options are available: +The noVNC application can be controlled via a number of settings. All of +them are available in the UI for the user to change, but they can also +be set via other means: + +* Via the URL, either as a query parameter: + + ``` + https://www.example.com/vnc.html?reconnect=0&shared=1 + ``` + + or as a fragment: + + ``` + https://www.example.com/vnc.html#reconnect=0&shared=1 + ``` + + The latter might be preferred as it is not sent to the server. + +* Via the files `defaults.json` and `mandatory.json` + +Currently, the following options are available: * `autoconnect` - Automatically connect as soon as the page has finished loading. From 243d7fdd5fe2160f66096f567b78807ddbc13a75 Mon Sep 17 00:00:00 2001 From: Alexander Zeijlon Date: Wed, 7 May 2025 09:14:47 +0200 Subject: [PATCH 152/166] Disable setting showDotCursor in RFB constructor This has been deprecated for around six years now. Let's remove the deprecation warning and disable setting showDotCursor via the options parameter. --- core/rfb.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index e3266cc8d..80011e4a1 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -300,10 +300,6 @@ export default class RFB extends EventTargetMixin { this._resizeSession = false; this._showDotCursor = false; - if (options.showDotCursor !== undefined) { - Log.Warn("Specifying showDotCursor as a RFB constructor argument is deprecated"); - this._showDotCursor = options.showDotCursor; - } this._qualityLevel = 6; this._compressionLevel = 2; 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 153/166] 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 000000000..a2165c669 --- /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 154/166] 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 51e57bd3b..2542e0591 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 dcf5ba446..5572c9401 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 155/166] 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 a70122717..70c8a2c74 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 156/166] 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 ef42ac666..4efd6f4b9 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 5844ce172..528b19064 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 157/166] 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 ae17a4409..ee8a4bc42 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 62bcbfa59..110e6ad07 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 158/166] 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 5fd1e0564..f79846034 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 159/166] Remove deprecated "directories" entry --- package.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/package.json b/package.json index bfe16a542..19d0b51cb 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 160/166] 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 19d0b51cb..3e60b63e2 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 6347e1ea5..38effd300 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 f5ba57cc5..3fcaacd43 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 617f4ed65..000000000 --- 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 b10240ec4..b41c40ebd 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 eaf5b704d8ba2cf2e934c915916a38fed6569e9e Mon Sep 17 00:00:00 2001 From: Alexander Zeijlon Date: Mon, 20 Oct 2025 14:20:04 +0200 Subject: [PATCH 161/166] 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 6b55504a5..6a4b11a50 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 162/166] 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 6a4b11a50..2f29a772b 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 163/166] 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 000000000..ae3cad155 --- /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 80011e4a1..1073a878b 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 63596d211..12f47a76a 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 692cc23b2..6c9bc568e 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 000000000..1c173a8d7 --- /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 2a7bbeaab..7aa54cd0c 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 164/166] 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 2542e0591..2b936c020 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 165/166] 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 5b41548ed..1d0a1104f 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 013a9fc3737115d3b3152f9da6a068ce4d99de4e Mon Sep 17 00:00:00 2001 From: Mikhail Loginov Date: Tue, 28 Oct 2025 10:59:18 +0000 Subject: [PATCH 166/166] fix code style --- app/codio-utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/codio-utils.js b/app/codio-utils.js index 68220666a..66df3cb88 100644 --- a/app/codio-utils.js +++ b/app/codio-utils.js @@ -1,6 +1,6 @@ export const overrideDefaults = (defaults) => { if (!defaults['autoconnect']) { - defaults['autoconnect'] = 'true' + defaults['autoconnect'] = 'true'; } if (!defaults['host']) { defaults['host'] = window.location.hostname; @@ -16,4 +16,4 @@ export const overrideDefaults = (defaults) => { } defaults['port'] = port; } -}; \ No newline at end of file +};