diff --git a/app/styles/base.css b/app/styles/base.css index b6e49f9c..f0b7eebd 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -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; diff --git a/app/styles/screen.css b/app/styles/screen.css new file mode 100644 index 00000000..48b15c71 --- /dev/null +++ b/app/styles/screen.css @@ -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 */; +} \ No newline at end of file diff --git a/app/ui.js b/app/ui.js index 3dfeddad..6dfb13c5 100644 --- a/app/ui.js +++ b/app/ui.js @@ -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,9 +1910,52 @@ 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(); - ctx.roundRect(x, y, w, h, 5); + 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) diff --git a/app/ui_screen.js b/app/ui_screen.js index 9f3bc890..9d0ccfd0 100644 --- a/app/ui_screen.js +++ b/app/ui_screen.js @@ -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; diff --git a/core/rfb.js b/core/rfb.js index cc518f60..44b98bc5 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -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); diff --git a/screen.html b/screen.html index 66ddd034..7c5299d7 100644 --- a/screen.html +++ b/screen.html @@ -44,6 +44,7 @@ + @@ -58,7 +59,7 @@ -
+