435 lines
17 KiB
JavaScript
435 lines
17 KiB
JavaScript
import RFB from "../core/rfb.js";
|
|
import * as WebUtil from "./webutil.js";
|
|
import { isTouchDevice, isSafari, hasScrollbarGutter, dragThreshold, supportsBinaryClipboard, isFirefox, isWindows, isIOS, supportsPointerLock }
|
|
from '../core/util/browser.js';
|
|
import { MouseButtonMapper, XVNC_BUTTONS } from "../core/mousebuttonmapper.js";
|
|
import * as Log from '../core/util/logging.js';
|
|
|
|
const UI = {
|
|
connected: false,
|
|
screenID: null,
|
|
screen: {},
|
|
screens: [],
|
|
supportsBroadcastChannel: (typeof BroadcastChannel !== "undefined"),
|
|
controlChannel: null,
|
|
draggingTab: false,
|
|
//Initial Loading of the UI
|
|
prime() {
|
|
this.start();
|
|
},
|
|
|
|
//Render default UI
|
|
start() {
|
|
window.addEventListener("unload", (e) => {
|
|
if (UI.rfb) {
|
|
UI.disconnect();
|
|
}
|
|
});
|
|
|
|
// Settings with immediate effects
|
|
UI.initSetting('logging', 'warn');
|
|
UI.updateLogging();
|
|
|
|
UI.addDefaultHandlers();
|
|
UI.updateVisualState('disconnected');
|
|
|
|
const webui_mode = window.localStorage.getItem('theme')?.toLowerCase() || 'dark'
|
|
document.getElementById('screen').classList.add(webui_mode);
|
|
|
|
},
|
|
|
|
addDefaultHandlers() {
|
|
document.getElementById('noVNC_connect_button').addEventListener('click', UI.connect);
|
|
// Control panel events
|
|
document.getElementById('toggleMenu').addEventListener('click', UI.toggleMenu);
|
|
document.getElementById('closeMenu').addEventListener('click', UI.toggleMenu);
|
|
document.getElementById('fullscreenTrigger').addEventListener('click', UI.fullscreenTrigger);
|
|
document.getElementById('menuTab').addEventListener('mousemove', UI.dragTab);
|
|
document.getElementById('menuTab').addEventListener('mouseup', UI.dragEnd);
|
|
document.getElementById('menuTab').addEventListener('touchmove', UI.touchDragTab);
|
|
document.getElementById('dragHandler').addEventListener('mousedown', UI.dragStart);
|
|
document.getElementById('dragHandler').addEventListener('touchstart', UI.dragStart);
|
|
document.getElementById('dragHandler').addEventListener('mouseup', UI.dragEnd);
|
|
document.getElementById('dragHandler').addEventListener('touchend', UI.dragEnd);
|
|
document.getElementById('menuTab').addEventListener('mouseleave', UI.dragEnd);
|
|
// End control panel events
|
|
},
|
|
|
|
dragStart(e) {
|
|
UI.draggingTab = true
|
|
},
|
|
dragEnd(e) {
|
|
document.getElementById('menuTab').classList.remove('dragging')
|
|
UI.draggingTab = false
|
|
},
|
|
|
|
dragTab(e) {
|
|
if (UI.draggingTab) {
|
|
document.getElementById('menuTab').style.top = (e.clientY - 10) + 'px'
|
|
document.getElementById('menuTab').classList.add('dragging')
|
|
}
|
|
},
|
|
touchDragTab(e) {
|
|
if (UI.draggingTab) {
|
|
e.preventDefault()
|
|
const touch = e.touches[0]
|
|
document.getElementById('menuTab').style.top = (touch.clientY - 10) + 'px'
|
|
document.getElementById('menuTab').classList.add('dragging')
|
|
}
|
|
},
|
|
|
|
fullscreenTrigger() {
|
|
if (document.fullscreenElement || // alternative standard method
|
|
document.mozFullScreenElement || // currently working methods
|
|
document.webkitFullscreenElement ||
|
|
document.msFullscreenElement) {
|
|
if (document.exitFullscreen) {
|
|
document.exitFullscreen();
|
|
} else if (document.mozCancelFullScreen) {
|
|
document.mozCancelFullScreen();
|
|
} else if (document.webkitExitFullscreen) {
|
|
document.webkitExitFullscreen();
|
|
} else if (document.msExitFullscreen) {
|
|
document.msExitFullscreen();
|
|
}
|
|
} else {
|
|
if (document.documentElement.requestFullscreen) {
|
|
document.documentElement.requestFullscreen();
|
|
} else if (document.documentElement.mozRequestFullScreen) {
|
|
document.documentElement.mozRequestFullScreen();
|
|
} else if (document.documentElement.webkitRequestFullscreen) {
|
|
document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
|
|
} else if (document.body.msRequestFullscreen) {
|
|
document.body.msRequestFullscreen();
|
|
}
|
|
}
|
|
|
|
},
|
|
|
|
toggleMenu() {
|
|
document.getElementById('mySidenav').classList.toggle('show_nav')
|
|
const show = document.getElementById('mySidenav').classList.contains('show_nav')
|
|
if (show) {
|
|
document.getElementById('toggleMenuIcon').classList.add('rotate-180')
|
|
} else {
|
|
document.getElementById('toggleMenuIcon').classList.remove('rotate-180')
|
|
}
|
|
},
|
|
|
|
getSetting(name, isBool, default_value) {
|
|
let val = WebUtil.readSetting(name);
|
|
if ((val === 'undefined' || val === null) && default_value !== 'undefined' && default_value !== null) {
|
|
val = default_value;
|
|
}
|
|
if (typeof val !== 'undefined' && val !== null && isBool) {
|
|
if (val.toString().toLowerCase() in {'0': 1, 'no': 1, 'false': 1}) {
|
|
val = false;
|
|
} else {
|
|
val = true;
|
|
}
|
|
}
|
|
return val;
|
|
},
|
|
|
|
connect() {
|
|
|
|
let details = null
|
|
const initialAutoPlacementValue = window.localStorage.getItem('autoPlacement')
|
|
if (initialAutoPlacementValue === null) {
|
|
details = {
|
|
left: window.screenLeft,
|
|
top: window.screenTop
|
|
}
|
|
}
|
|
|
|
if (!UI.rfb) {
|
|
UI.rfb = new RFB(document.getElementById('noVNC_container'),
|
|
document.getElementById('noVNC_keyboardinput'),
|
|
"", //URL
|
|
{
|
|
shared: UI.getSetting('shared', true),
|
|
repeaterID: UI.getSetting('repeaterID', false),
|
|
credentials: { password: null },
|
|
hiDpi: UI.getSetting('enable_hidpi', true, false)
|
|
},
|
|
false // Not a primary display
|
|
);
|
|
}
|
|
|
|
|
|
UI.rfb.addEventListener("connect", UI.connectFinished);
|
|
//UI.rfb.addEventListener("disconnect", UI.disconnectFinished);
|
|
//TODO: add support for forced static resolution for multiple monitors
|
|
//UI.rfb.forcedResolutionX = UI.getSetting('forced_resolution_x', false);
|
|
//UI.rfb.forcedResolutionY = UI.getSetting('forced_resolution_y', false);
|
|
const resize_setting = UI.getSetting('resize', false, 'remote');
|
|
UI.rfb.clipViewport = resize_setting !== 'off';
|
|
UI.rfb.scaleViewport = resize_setting === 'scale';
|
|
UI.rfb.resizeSession = resize_setting === 'remote';
|
|
UI.rfb.qualityLevel = parseInt(UI.getSetting('quality'));
|
|
UI.rfb.dynamicQualityMin = parseInt(UI.getSetting('dynamic_quality_min'));
|
|
UI.rfb.dynamicQualityMax = parseInt(UI.getSetting('dynamic_quality_max'));
|
|
UI.rfb.jpegVideoQuality = parseInt(UI.getSetting('jpeg_video_quality'));
|
|
UI.rfb.webpVideoQuality = parseInt(UI.getSetting('webp_video_quality'));
|
|
UI.rfb.videoArea = parseInt(UI.getSetting('video_area'));
|
|
UI.rfb.videoTime = parseInt(UI.getSetting('video_time'));
|
|
UI.rfb.videoOutTime = parseInt(UI.getSetting('video_out_time'));
|
|
UI.rfb.videoScaling = parseInt(UI.getSetting('video_scaling'));
|
|
UI.rfb.treatLossless = parseInt(UI.getSetting('treat_lossless'));
|
|
UI.rfb.maxVideoResolutionX = parseInt(UI.getSetting('max_video_resolution_x'));
|
|
UI.rfb.maxVideoResolutionY = parseInt(UI.getSetting('max_video_resolution_y'));
|
|
UI.rfb.frameRate = parseInt(UI.getSetting('framerate'));
|
|
UI.rfb.compressionLevel = parseInt(UI.getSetting('compression'));
|
|
UI.rfb.showDotCursor = UI.getSetting('show_dot', true);
|
|
UI.rfb.idleDisconnect = UI.getSetting('idle_disconnect');
|
|
UI.rfb.pointerRelative = UI.getSetting('pointer_relative');
|
|
UI.rfb.videoQuality = parseInt(UI.getSetting('video_quality'));
|
|
UI.rfb.antiAliasing = UI.getSetting('anti_aliasing');
|
|
UI.rfb.clipboardUp = UI.getSetting('clipboard_up', true, true);
|
|
UI.rfb.clipboardDown = UI.getSetting('clipboard_down', true, true);
|
|
UI.rfb.clipboardSeamless = UI.getSetting('clipboard_seamless', true, true);
|
|
UI.rfb.keyboard.enableIME = UI.getSetting('enable_ime', true, false);
|
|
UI.rfb.clipboardBinary = supportsBinaryClipboard() && UI.rfb.clipboardSeamless;
|
|
UI.rfb.enableWebRTC = UI.getSetting('enable_webrtc', true, false);
|
|
UI.rfb.mouseButtonMapper = UI.initMouseButtonMapper();
|
|
if (UI.rfb.videoQuality === 5) {
|
|
UI.rfb.enableQOI = true;
|
|
}
|
|
|
|
if (UI.supportsBroadcastChannel) {
|
|
UI.controlChannel = new BroadcastChannel(UI.rfb.connectionID);
|
|
UI.controlChannel.addEventListener('message', UI.handleControlMessage)
|
|
}
|
|
|
|
//attach this secondary display to the primary display
|
|
const screen = UI.rfb.attachSecondaryDisplay(details);
|
|
UI.screenID = screen.screenID
|
|
UI.screen = screen
|
|
document.querySelector('title').textContent = 'Display ' + UI.screenID
|
|
window.name = UI.screenID
|
|
|
|
if (supportsBinaryClipboard()) {
|
|
// explicitly request permission to the clipboard
|
|
navigator.permissions.query({ name: "clipboard-read" }).then((result) => { Log.Debug('binary clipboard enabled') });
|
|
}
|
|
},
|
|
|
|
handleControlMessage(event) {
|
|
switch (event.data.eventType) {
|
|
case 'identify':
|
|
UI.identify(event.data)
|
|
break;
|
|
case 'secondarydisconnected':
|
|
UI.updateVisualState('disconnected');
|
|
break;
|
|
}
|
|
},
|
|
|
|
updateVisualState(state) {
|
|
document.documentElement.classList.remove("noVNC_connecting");
|
|
document.documentElement.classList.remove("noVNC_connected");
|
|
document.documentElement.classList.remove("noVNC_disconnecting");
|
|
document.documentElement.classList.remove("noVNC_reconnecting");
|
|
document.documentElement.classList.remove("noVNC_disconnected");
|
|
|
|
const transitionElem = document.getElementById("noVNC_transition_text");
|
|
if (WebUtil.isInsideKasmVDI())
|
|
{
|
|
parent.postMessage({ action: 'connection_state', value: state}, '*' );
|
|
}
|
|
|
|
let connect_el = document.getElementById('noVNC_connect_dlg');
|
|
|
|
switch (state) {
|
|
case 'init':
|
|
break;
|
|
case 'connecting':
|
|
transitionElem.textContent = _("Connecting...");
|
|
document.documentElement.classList.add("noVNC_connecting");
|
|
break;
|
|
case 'connected':
|
|
document.documentElement.classList.add("noVNC_connected");
|
|
if (!connect_el.classList.contains("noVNC_hidden")) {
|
|
connect_el.classList.add('noVNC_hidden');
|
|
}
|
|
break;
|
|
case 'disconnecting':
|
|
transitionElem.textContent = _("Disconnecting...");
|
|
document.documentElement.classList.add("noVNC_disconnecting");
|
|
break;
|
|
case 'disconnected':
|
|
document.documentElement.classList.add("noVNC_disconnected");
|
|
if (connect_el.classList.contains("noVNC_hidden")) {
|
|
connect_el.classList.remove('noVNC_hidden');
|
|
}
|
|
UI.disconnect()
|
|
break;
|
|
case 'reconnecting':
|
|
transitionElem.textContent = _("Reconnecting...");
|
|
document.documentElement.classList.add("noVNC_reconnecting");
|
|
break;
|
|
default:
|
|
Log.Error("Invalid visual state: " + state);
|
|
UI.showStatus(_("Internal error"), 'error');
|
|
return;
|
|
}
|
|
},
|
|
|
|
identify(data) {
|
|
UI.screens = data.screens
|
|
const screen = data.screens.find(el => el.id === UI.screenID)
|
|
if (screen) {
|
|
document.getElementById('noVNC_identify_monitor').innerHTML = screen.num
|
|
document.getElementById('noVNC_identify_monitor').classList.add("show")
|
|
document.querySelector('title').textContent = 'Display ' + screen.num + ' - ' + UI.screenID
|
|
setTimeout(() => {
|
|
document.getElementById('noVNC_identify_monitor').classList.remove("show")
|
|
}, 3500)
|
|
}
|
|
},
|
|
|
|
|
|
showStatus(text, statusType, time, kasm = false) {
|
|
// If inside the full Kasm CDI framework, don't show messages unless explicitly told to
|
|
if (WebUtil.isInsideKasmVDI() && !kasm) {
|
|
return;
|
|
}
|
|
|
|
const statusElem = document.getElementById('noVNC_status');
|
|
|
|
if (typeof statusType === 'undefined') {
|
|
statusType = 'normal';
|
|
}
|
|
|
|
// Don't overwrite more severe visible statuses and never
|
|
// errors. Only shows the first error.
|
|
if (statusElem.classList.contains("noVNC_open")) {
|
|
if (statusElem.classList.contains("noVNC_status_error")) {
|
|
return;
|
|
}
|
|
if (statusElem.classList.contains("noVNC_status_warn") &&
|
|
statusType === 'normal') {
|
|
return;
|
|
}
|
|
}
|
|
|
|
clearTimeout(UI.statusTimeout);
|
|
|
|
switch (statusType) {
|
|
case 'error':
|
|
statusElem.classList.remove("noVNC_status_warn");
|
|
statusElem.classList.remove("noVNC_status_normal");
|
|
statusElem.classList.add("noVNC_status_error");
|
|
break;
|
|
case 'warning':
|
|
case 'warn':
|
|
statusElem.classList.remove("noVNC_status_error");
|
|
statusElem.classList.remove("noVNC_status_normal");
|
|
statusElem.classList.add("noVNC_status_warn");
|
|
break;
|
|
case 'normal':
|
|
case 'info':
|
|
default:
|
|
statusElem.classList.remove("noVNC_status_error");
|
|
statusElem.classList.remove("noVNC_status_warn");
|
|
statusElem.classList.add("noVNC_status_normal");
|
|
break;
|
|
}
|
|
|
|
statusElem.textContent = text;
|
|
statusElem.classList.add("noVNC_open");
|
|
|
|
// If no time was specified, show the status for 1.5 seconds
|
|
if (typeof time === 'undefined') {
|
|
time = 1500;
|
|
}
|
|
|
|
// Error messages do not timeout
|
|
if (statusType !== 'error') {
|
|
UI.statusTimeout = window.setTimeout(UI.hideStatus, time);
|
|
}
|
|
},
|
|
|
|
hideStatus() {
|
|
clearTimeout(UI.statusTimeout);
|
|
document.getElementById('noVNC_status').classList.remove("noVNC_open");
|
|
},
|
|
|
|
disconnect() {
|
|
if (UI.rfb) {
|
|
UI.rfb.disconnect();
|
|
if (UI.supportsBroadcastChannel) {
|
|
UI.controlChannel.removeEventListener('message', UI.handleControlMessage);
|
|
UI.rfb.removeEventListener("connect", UI.connectFinished);
|
|
}
|
|
}
|
|
},
|
|
|
|
connectFinished(e) {
|
|
UI.connected = true;
|
|
UI.inhibitReconnect = false;
|
|
|
|
UI.showStatus("Secondary Screen Connected");
|
|
UI.updateVisualState('connected');
|
|
|
|
// Do this last because it can only be used on rendered elements
|
|
UI.rfb.focus();
|
|
},
|
|
|
|
initMouseButtonMapper() {
|
|
const mouseButtonMapper = new MouseButtonMapper();
|
|
|
|
const settings = WebUtil.readSetting("mouseButtonMapper");
|
|
if (settings) {
|
|
mouseButtonMapper.load(settings);
|
|
return mouseButtonMapper;
|
|
}
|
|
|
|
mouseButtonMapper.set(0, XVNC_BUTTONS.LEFT_BUTTON);
|
|
mouseButtonMapper.set(1, XVNC_BUTTONS.MIDDLE_BUTTON);
|
|
mouseButtonMapper.set(2, XVNC_BUTTONS.RIGHT_BUTTON);
|
|
mouseButtonMapper.set(3, XVNC_BUTTONS.BACK_BUTTON);
|
|
mouseButtonMapper.set(4, XVNC_BUTTONS.FORWARD_BUTTON);
|
|
WebUtil.writeSetting("mouseButtonMapper", mouseButtonMapper.dump());
|
|
|
|
return mouseButtonMapper;
|
|
},
|
|
|
|
updateLogging() {
|
|
WebUtil.initLogging(UI.getSetting('logging'));
|
|
},
|
|
|
|
// Initial page load read/initialization of settings
|
|
initSetting(name, defVal) {
|
|
// Check Query string followed by cookie
|
|
let val = WebUtil.getConfigVar(name);
|
|
if (val === null) {
|
|
val = WebUtil.readSetting(name, defVal);
|
|
}
|
|
WebUtil.setSetting(name, val);
|
|
return val;
|
|
},
|
|
|
|
// Apply remote resizing or local scaling
|
|
applyResizeMode() {
|
|
if (!UI.rfb) return;
|
|
const resize_setting = UI.getSetting('resize');
|
|
UI.rfb.clipViewport = resize_setting !== 'off';
|
|
UI.rfb.scaleViewport = resize_setting === 'scale';
|
|
UI.rfb.resizeSession = resize_setting === 'remote' || UI.rfb.forcedResolutionX && UI.rfb.forcedResolutionY;
|
|
UI.rfb.idleDisconnect = UI.getSetting('idle_disconnect');
|
|
UI.rfb.videoQuality = UI.getSetting('video_quality');
|
|
UI.rfb.enableWebP = UI.getSetting('enable_webp');
|
|
UI.rfb.enableHiDpi = UI.getSetting('enable_hidpi');
|
|
},
|
|
|
|
}
|
|
|
|
UI.prime();
|
|
const initialAutoPlacementValue = window.localStorage.getItem('autoPlacement')
|
|
|
|
if ('getScreenDetails' in window && initialAutoPlacementValue === null) {
|
|
UI.connect();
|
|
}
|
|
export default UI;
|