KASM-5411 Use windows placement api (#86)

Automatic placement of new displays using the Windows API if available. Control panel for secondary displays.
This commit is contained in:
KodeStar 2024-01-16 13:01:06 +00:00 committed by GitHub
parent 246dbd4999
commit 4dac080460
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 498 additions and 26 deletions

View File

@ -1371,6 +1371,12 @@ a:visited {
#noVNC_setting_enable_hidpi_option.show {
display: flex!important;
}
#noVNC_auto_placement_option {
display: none!important;
}
#noVNC_auto_placement_option.show {
display: flex!important;
}
#noVNC_refreshMonitors {
position: absolute;
top: 20px;

213
app/styles/screen.css Normal file
View File

@ -0,0 +1,213 @@
:root {
--text-color: rgb(147, 166, 188);
--bg: rgb(33 39 63 / 0.98);
--gray-500: 107 114 128;
}
.light {
--text-color: rgb(22, 45, 72);
--bg: rgba(234, 235, 240, 0.98);
--gray-500: 209 213 219;
}
#mySidenav {
top: 0;
bottom: 0;
left: 0;
margin-top: auto;
margin-bottom: auto;
position: fixed;
z-index: 999999;
transition: 0.5s;
border-radius: 0;
width: 100%;
max-width: 400px;
height: 100%;
display: flex;
flex-direction: column;
transform: translateX(calc(-100% - 30px));
color: var(--text-color);
}
#mySidenav * {
box-sizing: border-box;
}
#mySidenav button {
border: 0;
color: var(--text-color);
}
.sidenavnew {
top: 0;
bottom: 0;
left: 0;
margin-top: auto;
margin-bottom: auto;
position: fixed;
z-index: 999999;
background-color: var(--bg);
overflow-x: hidden;
transition: 0.5s;
border-radius: 0;
width: 100%;
overflow-y: auto;
height: 100%;
display: flex;
padding: 30px;
flex-direction: column;
}
#mySidenav .sidebar-icons-list .sidebar-icons {
width: 100%;
padding: 10px 12px;
background-color: transparent;
margin: 0;
border-radius: 5px;
margin-bottom: 0;
display: flex;
align-items: center;
height: auto;
gap: 12px;
text-align: initial;
}
#mySidenav.loadin {
transform: translateX(-100%);
}
#mySidenav .innertab {
display: flex;
align-items: center;
justify-content: center;
padding: 15px 5px;
}
#mySidenav.loadin.show_nav {
left: 0;
transform: translateX(0);
}
#mySidenav .tab {
position: absolute;
right: -30px;
top: calc(50% - 30px);
white-space: nowrap;
}
#menuTab.dragging {
padding-top: 5rem/* 80px */;
padding-bottom: 5rem/* 80px */;
margin-top: -5rem/* -80px */;
padding-right: 2rem/* 32px */;
margin-right: -2rem/* -32px */;
}
.sidebar-icon-container {
border-radius: 5px;
display: flex;
justify-content: center;
align-items: center;
width: 36px;
height: 36px;
flex: 0 0 36px;
}
.text-white {
--text-opacity: 1;
color: rgb(255 255 255 / var(--text-opacity));
}
.bg-gray-500 {
--tw-bg-opacity: 1;
background-color: rgb(var(--gray-500) / var(--tw-bg-opacity));
}
svg:not(:root).svg-inline--fa, svg:not(:host).svg-inline--fa {
overflow: visible;
box-sizing: content-box;
}
.svg-inline--fa.fa-lg {
vertical-align: -0.2em;
}
.fa-lg {
font-size: 1.25em;
line-height: 0.05em;
vertical-align: -0.075em;
}
.svg-inline--fa {
display: var(--fa-display, inline-block);
height: 1em;
overflow: visible;
vertical-align: -0.125em;
}
.bg-emerald-500 {
--bg-opacity: 1;
background-color: rgb(16 185 129 / var(--bg-opacity));
}
.bg-transparent {
background-color: transparent;
}
.touch-none {
touch-action: none;
}
.rotate-180 {
transform: rotate(180deg);
}
#mySidenav .tabinner {
background: var(--bg);
width: 30px;
}
.justify-between {
justify-content: space-between;
}
.rounded-r-lg {
border-top-right-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
}
.absolute {
position: absolute;
}
.overflow-hidden {
overflow: hidden;
}
.flex-col {
flex-direction: column;
}
.flex {
display: flex;
}
.py-1 {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.bg-black\/5 {
background-color: rgb(0 0 0 / 0.05);
}
.rounded-tr-lg {
border-top-right-radius: 0.5rem;
}
.justify-center {
justify-content: center;
}
.items-center {
align-items: center;
}
.cursor-move {
cursor: move;
}
.cursor-pointer {
cursor: pointer;
}
.py-5 {
padding-top: 1.25rem;
padding-bottom: 1.25rem;
}
.grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.grid {
display: grid;
}
.font-bold {
font-weight: 700;
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
.text-xs {
font-size: 0.75rem/* 12px */;
line-height: 1rem/* 16px */;
}

View File

@ -66,6 +66,7 @@ const UI = {
sortedMonitors: [],
selectedMonitor: null,
refreshRotation: 0,
currentDisplay: null,
supportsBroadcastChannel: (typeof BroadcastChannel !== "undefined"),
@ -196,6 +197,15 @@ const UI = {
UI.addOption(document.getElementById('noVNC_setting_logging'), llevels[i], llevels[i]);
}
if ('getScreenDetails' in window) {
document.getElementById('noVNC_auto_placement_option').classList.add("show");
}
const initialAutoPlacementValue = window.localStorage.getItem('autoPlacement')
if (initialAutoPlacementValue === null) {
document.getElementById("noVNC_auto_placement").checked = true
}
// Settings with immediate effects
UI.initSetting('logging', 'warn');
UI.updateLogging();
@ -507,6 +517,7 @@ const UI = {
UI.addClickHandle('noVNC_settings_button', UI.toggleSettingsPanel);
document.getElementById("noVNC_setting_enable_perf_stats").addEventListener('click', UI.showStats);
document.getElementById("noVNC_auto_placement").addEventListener('change', UI.setAutoPlacement);
UI.addSettingChangeHandler('encrypt');
UI.addSettingChangeHandler('resize');
@ -597,6 +608,14 @@ const UI = {
}
},
setAutoPlacement(e) {
if (e.target.checked === false) {
window.localStorage.setItem('autoPlacement', false)
} else {
window.localStorage.removeItem('autoPlacement')
}
},
/*addMultiMonitorAddHandler() {
if (UI.supportsBroadcastChannel) {
UI.addClickHandle('noVNC_addmonitor_button', UI.addSecondaryMonitor);
@ -1891,10 +1910,53 @@ const UI = {
UI.draw()
},
addSecondaryMonitor() {
normalizePlacementValues(details) {
},
increaseCurrentDisplay(details) {
const max = details.screens.length
const thisIndex = details.screens.findIndex(el => el === details.currentScreen)
if (max === 1) {
return 0
}
if (UI.currentDisplay === null) {
UI.currentDisplay = thisIndex
}
UI.currentDisplay += 1
if (UI.currentDisplay === thisIndex) {
UI.currentDisplay += 1
}
if (UI.currentDisplay >= max) {
UI.currentDisplay = 0
}
return UI.currentDisplay
},
async addSecondaryMonitor() {
let new_display_path = window.location.pathname.replace(/[^/]*$/, '')
let new_display_url = `${window.location.protocol}//${window.location.host}${new_display_path}screen.html`;
const auto_placement = document.getElementById('noVNC_auto_placement').checked
if (auto_placement && 'getScreenDetails' in window) {
let permission = false;
try {
const { state } = await navigator.permissions.query({ name: 'window-management' });
permission = (state === 'granted' || state === 'prompt');
if (permission && window.screen.isExtended) {
const details = await window.getScreenDetails()
const current = UI.increaseCurrentDisplay(details)
let screen = details.screens[current]
const options = 'left='+screen.availLeft+',top='+screen.availTop+',width='+screen.availWidth+',height='+screen.availHeight+',fullscreen'
window.open(new_display_url, '_blank', options);
return
}
} catch (e) {
console.log(e)
// Nothing.
}
}
Log.Debug(`Opening a secondary display ${new_display_url}`)
window.open(new_display_url, '_blank', 'toolbar=0,location=0,menubar=0');
},
@ -1997,7 +2059,12 @@ const UI = {
rect(ctx, x, y, w, h) {
ctx.beginPath();
if (typeof ctx.roundRect !== 'undefined') {
ctx.roundRect(x, y, w, h, 5);
} else {
// fallback for old browsers
ctx.rect(x, y, w, h);
}
ctx.stroke();
ctx.closePath();
ctx.fill();
@ -2066,7 +2133,6 @@ const UI = {
for (var i = 0; i < monitors.length; i++) {
var monitor = monitors[i];
var a = sortedMonitors.find(el => el.id === monitor.id)
console.log(a)
screens.push({
screenID: a.id,
serverHeight: Math.round(a.h * scale),
@ -2080,8 +2146,6 @@ const UI = {
serverWidth: Math.round(width * scale),
screens
}
console.log('setScreenPlan')
console.log(screenPlan)
UI.rfb.applyScreenPlan(screenPlan);
},
@ -2839,11 +2903,19 @@ const UI = {
screenRegistered(e) {
console.log('screen registered')
// Get the current screen plan
// When a new display is added, it is defaulted to be placed to the far right relative to existing displays and to the top
if (UI.rfb) {
let screenPlan = UI.rfb.getScreenPlan();
console.log(screenPlan)
if (e && e.detail) {
const { left, top, screenID } = e.detail
const current = screenPlan.screens.findIndex(el => el.screenID === screenID)
if (current) {
screenPlan.screens[current].x = left
screenPlan.screens[current].y = top
}
}
UI.updateMonitors(screenPlan)
UI._identify(UI.monitors)

View File

@ -12,15 +12,14 @@ const UI = {
screens: [],
supportsBroadcastChannel: (typeof BroadcastChannel !== "undefined"),
controlChannel: null,
draggingTab: false,
//Initial Loading of the UI
prime() {
console.log('prime')
this.start();
},
//Render default UI
start() {
console.log('start')
window.addEventListener("unload", (e) => {
if (UI.rfb) {
UI.disconnect();
@ -33,10 +32,88 @@ const UI = {
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) {
@ -55,7 +132,16 @@ const UI = {
},
connect() {
console.log('connect')
let details = null
const initialAutoPlacementValue = window.localStorage.getItem('autoPlacement')
if (initialAutoPlacementValue === null) {
details = {
left: window.screenLeft,
top: window.screenTop
}
}
UI.rfb = new RFB(document.getElementById('noVNC_container'),
document.getElementById('noVNC_keyboardinput'),
"", //URL
@ -103,20 +189,17 @@ const UI = {
}
if (UI.supportsBroadcastChannel) {
console.log('add event listener')
UI.controlChannel = new BroadcastChannel(UI.rfb.connectionID);
UI.controlChannel.addEventListener('message', UI.handleControlMessage)
}
//attach this secondary display to the primary display
if (UI.screenID === null) {
const screen = UI.rfb.attachSecondaryDisplay();
const screen = UI.rfb.attachSecondaryDisplay(details);
UI.screenID = screen.screenID
UI.screen = screen
} else {
console.log('else reattach screens')
console.log(UI.screen)
UI.rfb.reattachSecondaryDisplay(UI.screen);
UI.rfb.reattachSecondaryDisplay(UI.screen, details);
}
document.querySelector('title').textContent = 'Display ' + UI.screenID
@ -171,7 +254,6 @@ const UI = {
document.documentElement.classList.add("noVNC_disconnecting");
break;
case 'disconnected':
console.log('disconnected')
document.documentElement.classList.add("noVNC_disconnected");
if (connect_el.classList.contains("noVNC_hidden")) {
connect_el.classList.remove('noVNC_hidden');
@ -191,7 +273,6 @@ const UI = {
identify(data) {
UI.screens = data.screens
console.log('identify')
const screen = data.screens.find(el => el.id === UI.screenID)
if (screen) {
document.getElementById('noVNC_identify_monitor').innerHTML = screen.num
@ -274,7 +355,6 @@ const UI = {
if (UI.rfb) {
UI.rfb.disconnect();
if (UI.supportsBroadcastChannel) {
console.log('remove event listeners')
UI.controlChannel.removeEventListener('message', UI.handleControlMessage);
UI.rfb.removeEventListener("connect", UI.connectFinished);
}
@ -329,5 +409,9 @@ const UI = {
}
UI.prime();
const initialAutoPlacementValue = window.localStorage.getItem('autoPlacement')
if ('getScreenDetails' in window && initialAutoPlacementValue === null) {
UI.connect();
}
export default UI;

View File

@ -743,16 +743,16 @@ export default class RFB extends EventTargetMixin {
// ===== PUBLIC METHODS =====
attachSecondaryDisplay() {
attachSecondaryDisplay(details) {
this._updateConnectionState('connecting');
const screen = this._registerSecondaryDisplay();
const screen = this._registerSecondaryDisplay(false, details);
this._updateConnectionState('connected');
return screen
}
reattachSecondaryDisplay(screen) {
reattachSecondaryDisplay(screen, details) {
this._updateConnectionState('connecting');
this._registerSecondaryDisplay(screen);
this._registerSecondaryDisplay(screen, details);
this._updateConnectionState('connected');
return screen
}
@ -1543,7 +1543,15 @@ export default class RFB extends EventTargetMixin {
size.serverWidth + 'x' + size.serverHeight);
} else if (this._display.screenIndex > 0) {
//re-register the secondary display with new resolution
this._registerSecondaryDisplay();
let details = null
const initialAutoPlacementValue = window.localStorage.getItem('autoPlacement')
if (initialAutoPlacementValue === null) {
details = {
left: window.screenLeft,
top: window.screenTop
}
}
this._registerSecondaryDisplay(false, details);
}
if (this._display.screens.length > 1) {
@ -1729,12 +1737,16 @@ export default class RFB extends EventTargetMixin {
let coords;
switch (event.data.eventType) {
case 'register':
const details = {
...event.data.details,
screenID: event.data.screenID
}
this._display.addScreen(event.data.screenID, event.data.width, event.data.height, event.data.pixelRatio, event.data.containerHeight, event.data.containerWidth);
size = this._screenSize();
RFB.messages.setDesktopSize(this._sock, size, this._screenFlags);
this._sendEncodings();
this._updateContinuousUpdates();
this.dispatchEvent(new CustomEvent("screenregistered", {}));
this.dispatchEvent(new CustomEvent("screenregistered", { detail: details }));
Log.Info(`Secondary monitor (${event.data.screenID}) has been registered.`);
break;
case 'reattach':
@ -1843,7 +1855,7 @@ export default class RFB extends EventTargetMixin {
}
_registerSecondaryDisplay(currentScreen = false) {
_registerSecondaryDisplay(currentScreen = false, details = null) {
if (!this._isPrimaryDisplay) {
//let screen = this._screenSize().screens[0];
//
@ -1864,7 +1876,8 @@ export default class RFB extends EventTargetMixin {
pixelRatio: screen.pixelRatio,
containerWidth: screen.containerWidth,
containerHeight: screen.containerHeight,
channel: null
channel: null,
details
}
this._controlChannel.postMessage(message);

View File

@ -44,6 +44,7 @@
<!-- Stylesheets -->
<link rel="stylesheet" href="app/styles/base.css">
<link rel="stylesheet" href="app/styles/screen.css">
<script src="app/error-handler.js"></script>
@ -58,7 +59,7 @@
<script type="module" crossorigin="use-credentials" src="app/ui_screen.js"></script>
</head>
<body>
<body id="screen">
<div id="noVNC_fallback_error" class="noVNC_center">
<div>
<div id="noVNC_close_error" onclick="document.getElementById('noVNC_fallback_error').remove()"></div>
@ -107,4 +108,81 @@
autocomplete="off" spellcheck="false" tabindex="-1"></textarea>
</div>
<div id="noVNC_identify_monitor">0</div>
<div id="mySidenav" class="loadin">
<div id="menuTab" class="tab touch-none" style="top: 49px;">
<div class="flex flex-col tabinner rounded-r-lg">
<div id="dragHandler" class="flex justify-center cursor-move items-center bg-black/5 rounded-tr-lg py-1">
<svg aria-hidden="true" focusable="false" data-prefix="fal" data-icon="grip-dots"
class="svg-inline--fa fa-grip-dots " role="img" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512">
<path fill="currentColor"
d="M384 192a16 16 0 1 0 0-32 16 16 0 1 0 0 32zm0-64a48 48 0 1 1 0 96 48 48 0 1 1 0-96zM224 192a16 16 0 1 0 0-32 16 16 0 1 0 0 32zm0-64a48 48 0 1 1 0 96 48 48 0 1 1 0-96zM48 176a16 16 0 1 0 32 0 16 16 0 1 0 -32 0zm64 0a48 48 0 1 1 -96 0 48 48 0 1 1 96 0zM384 352a16 16 0 1 0 0-32 16 16 0 1 0 0 32zm0-64a48 48 0 1 1 0 96 48 48 0 1 1 0-96zM208 336a16 16 0 1 0 32 0 16 16 0 1 0 -32 0zm64 0a48 48 0 1 1 -96 0 48 48 0 1 1 96 0zM64 352a16 16 0 1 0 0-32 16 16 0 1 0 0 32zm0-64a48 48 0 1 1 0 96 48 48 0 1 1 0-96z">
</path>
</svg>
</div>
<div id="toggleMenu" class="cursor-pointer relative rounded-br-lg innertab">
<div id="toggleMenuIcon" class="flex absolute">
<svg aria-hidden="true" focusable="false" data-prefix="fal" data-icon="chevron-right"
class="svg-inline--fa fa-chevron-right " role="img" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 320 512">
<path fill="currentColor"
d="M299.3 244.7c6.2 6.2 6.2 16.4 0 22.6l-192 192c-6.2 6.2-16.4 6.2-22.6 0s-6.2-16.4 0-22.6L265.4 256 84.7 75.3c-6.2-6.2-6.2-16.4 0-22.6s16.4-6.2 22.6 0l192 192z">
</path>
</svg>
</div>
</div>
</div>
</div>
<div class="sidenavnew">
<div class="flex justify-between">
<span class="text-xl font-bold">Control Panel</span>
<button id="closeMenu" class="bg-transparent">
<svg aria-hidden="true" focusable="false" data-prefix="fal" data-icon="xmark"
class="svg-inline--fa fa-xmark text-xl" role="img" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 384 512">
<path fill="currentColor"
d="M324.5 411.1c6.2 6.2 16.4 6.2 22.6 0s6.2-16.4 0-22.6L214.6 256 347.1 123.5c6.2-6.2 6.2-16.4 0-22.6s-16.4-6.2-22.6 0L192 233.4 59.5 100.9c-6.2-6.2-16.4-6.2-22.6 0s-6.2 16.4 0 22.6L169.4 256 36.9 388.5c-6.2 6.2-6.2 16.4 0 22.6s16.4 6.2 22.6 0L192 278.6 324.5 411.1z">
</path>
</svg>
</button>
</div>
<div class="sidebar-icons-list">
<div class="grid grid-cols-4 py-5">
<button id="fullscreenTrigger" class="sidebar-icons !bg-transparent flex flex-col">
<div id="fullscreenBg" class="sidebar-icon-container text-white bg-gray-500"><svg aria-hidden="true" focusable="false"
data-prefix="fal" data-icon="expand" class="svg-inline--fa fa-expand fa-lg " role="img"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path fill="currentColor"
d="M144 32c8.8 0 16 7.2 16 16s-7.2 16-16 16H32V176c0 8.8-7.2 16-16 16s-16-7.2-16-16V48c0-8.8 7.2-16 16-16H144zM0 336c0-8.8 7.2-16 16-16s16 7.2 16 16V448H144c8.8 0 16 7.2 16 16s-7.2 16-16 16H16c-8.8 0-16-7.2-16-16V336zM432 32c8.8 0 16 7.2 16 16V176c0 8.8-7.2 16-16 16s-16-7.2-16-16V64H304c-8.8 0-16-7.2-16-16s7.2-16 16-16H432zM416 336c0-8.8 7.2-16 16-16s16 7.2 16 16V464c0 8.8-7.2 16-16 16H304c-8.8 0-16-7.2-16-16s7.2-16 16-16H416V336z">
</path>
</svg></div>
<div class="flex flex-col">
<div class="font-bold text-xs">Fullscreen</div>
</div>
</button>
</div>
</div>
</div>
</div>
<script>
function fullscreenchanged(event) {
if (document.fullscreenElement) {
if ('keyboard' in navigator) {
navigator.keyboard.lock(["Escape"]);
}
document.getElementById('fullscreenBg').classList.remove('bg-gray-500')
document.getElementById('fullscreenBg').classList.add('bg-emerald-500')
} else {
if ('keyboard' in navigator) {
navigator.keyboard.unlock(["Escape"]);
}
document.getElementById('fullscreenBg').classList.remove('bg-emerald-500')
document.getElementById('fullscreenBg').classList.add('bg-gray-500')
}
}
document.addEventListener("fullscreenchange", fullscreenchanged);
</script>
</body>

View File

@ -553,6 +553,12 @@
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 448 512"><path fill="currentColor" d="M240 64c0-8.8-7.2-16-16-16s-16 7.2-16 16V240H32c-8.8 0-16 7.2-16 16s7.2 16 16 16H208V448c0 8.8 7.2 16 16 16s16-7.2 16-16V272H416c8.8 0 16-7.2 16-16s-7.2-16-16-16H240V64z"/></svg>
Add Monitor
</button>
<label id="noVNC_auto_placement_option" class="button">
<input style="margin: 0 10px 0 3px;" id="noVNC_auto_placement" type="checkbox" />
Auto placement
</label>
<label id="noVNC_setting_enable_hidpi_option" class="button">
<input style="margin: 0 10px 0 3px;" id="noVNC_setting_enable_hidpi" type="checkbox" />
Native Resolution