4
<head>
5
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
6
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
7
- <meta name="viewport" content="user-scalable=1.0,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0" />
7
+ <meta name="viewport" content="user-scalable=yes,initial-scale=1.0,minimum-scale=1.0" />
8
<meta name="apple-mobile-web-app-capable" content="yes" />
9
<meta name="format-detection" content="telephone=no" />
10
<meta name="robots" content="noindex,nofollow">
67
68
<body id="body" oncontextmenu="handleContextMenu(event)" style="display:none;min-width:495px"
69
onload="if (typeof(startup) !== 'undefined') startup();">
70
+ <script>
71
+ if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
72
+ document.body.classList.add('is-mobile');
73
+ document.documentElement.classList.add('is-mobile');
74
+ }
75
+ // Auto-fullscreen on landscape: when in the desktop view and connected,
76
+ // rotating to landscape triggers deskToggleFull() so the existing fulldesk
77
+ // CSS maximises the canvas without any new layout code.
78
+ // A flag tracks whether WE triggered fullscreen so we can auto-exit on
79
+ // rotating back to portrait (but not if the user exited fullscreen manually).
80
+ var _mobileAutoFullscreen = false;
81
+ var _mobileSavedAspectRatio = 0;
82
+ var _mobileZoomMode = 'fit'; // 'native' = 1:1 scrollable | 'fit' = fill viewport
83
+ var _nativePinchStartDist = 0, _nativePinchStartW = 0, _nativePinchStartH = 0;
84
+ var _nativePinchMidX = 0, _nativePinchMidY = 0;
85
+ var _prevHadFulldesk = false;
86
+
87
+ // Switch between native 1:1 (scrollable, full precision) and fit-to-viewport.
88
+ // In native mode we suspend desktop.m.onScreenSizeChange (= deskAdjust) so
89
+ // it can't override the explicit pixel dimensions we set on the canvas.
90
+ function _mobileSetZoom(mode) {
91
+ _mobileZoomMode = mode;
92
+ var desk = document.getElementById('Desk');
93
+ var dp = document.getElementById('DeskParent');
94
+ if (!desk || !dp) return;
95
+ var dm = (typeof desktop !== 'undefined') && desktop && desktop.m;
96
+ if (mode === 'native') {
97
+ // CSS custom properties lock canvas at native resolution.
98
+ // Panning is handled by dtouchmove JS (scrollLeft/Top on deskarea3x).
99
+ // Touch listeners stay active so trackpad mode keeps working.
100
+ var w = (dm && dm.ScreenWidth) || desk.width || 640;
101
+ var h = (dm && dm.ScreenHeight) || desk.height || 480;
102
+ document.documentElement.style.setProperty('--native-desk-w', w + 'px');
103
+ document.documentElement.style.setProperty('--native-desk-h', h + 'px');
104
+ document.body.classList.add('native-zoom');
105
+ if (dm && !dm._savedOnScreenSizeChange) {
106
+ dm._savedOnScreenSizeChange = dm.onScreenSizeChange;
107
+ dm.onScreenSizeChange = function(obj, sw, sh) {
108
+ document.documentElement.style.setProperty('--native-desk-w', sw + 'px');
109
+ document.documentElement.style.setProperty('--native-desk-h', sh + 'px');
110
+ };
111
+ }
112
+ } else {
113
+ document.body.classList.remove('native-zoom');
114
+ if (dm && dm._savedOnScreenSizeChange) {
115
+ dm.onScreenSizeChange = dm._savedOnScreenSizeChange;
116
+ delete dm._savedOnScreenSizeChange;
117
+ }
118
+ desk.style.width = '100%';
119
+ desk.style.height = '100%';
120
+ if (typeof deskAdjust === 'function') deskAdjust();
121
+ }
122
+ }
123
+
124
+ // Watch body class for fulldesk being added/removed and apply the right zoom
125
+ if (document.body.classList.contains('is-mobile')) {
126
+ new MutationObserver(function(muts) {
127
+ muts.forEach(function(m) {
128
+ if (m.attributeName !== 'class') return;
129
+ var inFull = document.body.classList.contains('fulldesk');
130
+ if (inFull && !_prevHadFulldesk) {
131
+ // Just entered fullscreen: portrait->native, landscape->fit
132
+ setTimeout(function() {
133
+ var landscape = window.matchMedia('(orientation: landscape)').matches;
134
+ _mobileSetZoom(landscape ? 'fit' : 'native');
135
+ }, 200);
136
+ } else if (!inFull && _prevHadFulldesk) {
137
+ _mobileSetZoom('fit'); // always reset to fit on exit
138
+ }
139
+ _prevHadFulldesk = inFull;
140
+ });
141
+ }).observe(document.body, { attributes: true });
142
+ }
143
+ function _doOrientationUpdate() {
144
+ if (!document.body.classList.contains('is-mobile')) return;
145
+ var landscape = window.matchMedia('(orientation: landscape)').matches;
146
+ document.body.classList.toggle('is-landscape', landscape);
147
+ // Check DOM state — JS variables like xxcurrentView / fullscreen are
148
+ // in a later function scope and not accessible from this early script.
149
+ var p11 = document.getElementById('p11');
150
+ var inDesktop = p11 && p11.style.display !== 'none';
151
+ var alreadyFullscreen = document.body.classList.contains('fulldesk');
152
+ if (landscape && inDesktop && !alreadyFullscreen) {
153
+ // Auto-enter fullscreen in landscape using the global function
154
+ if (typeof deskToggleFull === 'function') {
155
+ _mobileAutoFullscreen = true;
156
+ _mobileSavedAspectRatio = (typeof deskAspectRatio !== 'undefined') ? deskAspectRatio : 0;
157
+ deskToggleFull();
158
+ }
159
+ // CSS rule body.is-mobile.is-landscape.fulldesk #Desk { width/height: 100% !important }
160
+ // handles the canvas fill automatically — no JS override needed here.
161
+ } else if (!landscape && alreadyFullscreen && _mobileAutoFullscreen) {
162
+ _mobileAutoFullscreen = false;
163
+ if (typeof deskToggleFull === 'function') deskToggleFull();
164
+ // Let deskAdjust() recalculate naturally for portrait (no forced CSS override)
165
+ setTimeout(function() { if (typeof deskAdjust === 'function') deskAdjust(); }, 400);
166
+ } else {
167
+ // Not entering/exiting fullscreen but still call deskAdjust so the
168
+ // canvas recalculates for the new viewport dimensions
169
+ if (typeof deskAdjust === 'function') {
170
+ requestAnimationFrame(function() { deskAdjust(); });
171
+ }
172
+ }
173
+ }
174
+ function _updateOrientation() {
175
+ setTimeout(_doOrientationUpdate, 300);
176
+ }
177
+ window.addEventListener('orientationchange', _updateOrientation);
178
+ window.addEventListener('resize', _updateOrientation);
179
+
180
+ // Use Popper strategy:'fixed' so dropdown menus escape overflow:hidden parents and canvas layers.
181
+ window.addEventListener('load', function() {
182
+ if (typeof bootstrap === 'undefined') return;
183
+ document.querySelectorAll('[data-bs-toggle="dropdown"]').forEach(function(el) {
184
+ var ex = bootstrap.Dropdown.getInstance(el);
185
+ if (ex) ex.dispose();
186
+ new bootstrap.Dropdown(el, {
187
+ popperConfig: function(cfg) { cfg.strategy = 'fixed'; return cfg; }
188
+ });
189
+ });
190
+ });
191
+ </script>
192
<!-- right click menu -->
193
<div id="contextMenu" class="contextMenu noselect" style="display:none">
194
<div id="cxinfo" class="cmtext" onclick="cmaction(1,event)"><b>Information</b></div>
511
</div>
512
<div id=p1 style="display:none">
513
<div id="p1title" class="d-flex justify-content-between align-items-center">
392
- <div>
393
- <h1>My Devices</h1>
394
- </div>
514
+ <div class="fs-4 fw-bold">My Devices</div>
515
<div style="display:none" id="devListToolbarViewIcons">
516
<div id=devViewPageState style="float:left;line-height:32px;height:32px;font-size:16px;display:none"></div>
517
<div tabindex=0 id=devViewPageButton1 class=viewSelector onclick=onDeviceViewPageChange(1)
778
</div>
779
</div>
780
<div id=p3 style="display:none">
661
- <div id="p3title">
662
- <h1>My Events</h1>
781
+ <div id="p3title" class="d-flex align-items-center">
782
+ <div class="fs-4 fw-bold">My Events</div>
783
</div>
784
<table class="pTable">
785
<tr>
813
<div id=p3events></div>
814
</div>
815
<div id=p4 style="display:none">
696
- <div id="p4title">
697
- <h1>My Users</h1>
816
+ <div id="p4title" class="d-flex align-items-center">
817
+ <div class="fs-4 fw-bold">My Users</div>
818
</div>
819
<table class="pTable">
820
<tr>
701
- <td class="style14 d-flex align-items-center flex-wrap p-1">
821
+ <td id="p4toolbarActions" class="style14 d-flex align-items-center flex-wrap p-1">
822
<div class="d-flex align-items-center">
823
<input type=button id=UsersSelectAllButton class="btn btn-secondary btn-sm me-1" onclick="p3usersSelectallButtonFunction()" value="Select All" />
824
<input type=button id=UsersGroupActionButton class="btn btn-primary btn-sm me-1" disabled="disabled" value="Group Action" onclick=p3usersGroupActionFunction() />
825
<input id=UserNewAccountButton class="btn btn-success btn-sm me-1" type=button onclick=showCreateNewAccountDialog() value="New Account..." />
826
+ <div class="btn-group me-1 mobileActionDropdown">
827
+ <button type="button" class="btn btn-secondary btn-sm" data-bs-toggle="dropdown" aria-expanded="false">More <i class="fa-solid fa-chevron-down fa-xs"></i></button>
828
+ <div class="dropdown-menu">
829
+ <button type="button" class="dropdown-item" onclick="showUserBroadcastDialog()">Broadcast</button>
830
+ <button type="button" class="dropdown-item" onclick="p4downloadUserInfo()">Download user information</button>
831
+ <button type="button" id=p4MobileUserBatchCreate class="dropdown-item" style="display:none" onclick="p4batchAccountCreate()">Batch create accounts</button>
832
+ <button type="button" class="dropdown-item" onclick="onUsersViewSettings()">Settings</button>
833
+ </div>
834
+ </div>
835
<div class="d-inline-flex align-items-center">
836
<input id=UserSearchInput type=search class="form-control-sm me-1" placeholder=Filter onchange=onUserSearchInputChanged() onkeyup=onUserSearchInputChanged() autocomplete=off onfocus=onUserSearchFocus(1) onblur=onUserSearchFocus(0) />
837
</div>
838
</div>
710
- <div class="d-flex align-items-center ms-auto">
839
+ <div class="d-flex align-items-center ms-auto desktopAction">
840
<input type=button class="btn btn-primary btn-sm me-2"onclick=showUserBroadcastDialog() style=margin-right:6px value="Broadcast" />
841
<a href=# onclick=p4downloadUserInfo()><i role=button class="fa-solid fa-download me-2" title="Download user information"></i></a>
842
<a href=# onclick=p4batchAccountCreate()><i role=button id=p4UserBatchCreate style="display:none" title="Batch create many user accounts" class="fa-solid fa-upload me-1"></i></a>
848
<div id="p3users"></div>
849
</div>
850
<div id=p5 style="display:none">
722
- <div id="p5title">
723
- <h1>My Files</h1>
851
+ <div id="p5title" class="d-flex align-items-center">
852
+ <div class="fs-4 fw-bold">My Files</div>
853
</div>
854
<table id="p5toolbar" class="table" cellpadding="0" cellspacing="0">
855
<tr>
861
<input type=button id=p5RenameFileButton class="btn btn-primary btn-sm me-1" disabled="disabled" value="Rename" onclick="p5renamefile();" />
862
<input type=button id=p5DeleteFileButton class="btn btn-primary btn-sm me-1" disabled="disabled" value="Delete" onclick="p5deletefile();" />
863
<input type=button id=p5ViewFileButton class="btn btn-primary btn-sm me-1" disabled="disabled" value="Edit" onclick="p5viewfile()" />
735
- <input type=button id=p5NewFolderButton class="btn btn-primary btn-sm me-1" disabled="disabled" value="New Folder" onclick="p5createfolder();" />
736
- <input type=button id=p5UploadButton class="btn btn-primary btn-sm me-1" disabled="disabled" value="Upload" onclick="p5uploadFile()" />
737
- <input type=button id=p5DownloadButton class="btn btn-primary btn-sm me-1" disabled="disabled" value="Download" onclick="p5downloadButton()" />
738
- <input type=button id=p5CutButton class="btn btn-primary btn-sm me-1" disabled="disabled" value="Cut" onclick="p5copyFile(1)" />
739
- <input type=button id=p5CopyButton class="btn btn-primary btn-sm me-1" disabled="disabled" value="Copy" onclick="p5copyFile(0)" />
740
- <input type=button id=p5PasteButton class="btn btn-primary btn-sm me-1" disabled="disabled" value="Paste" onclick="p5pasteFile()" />
864
+ <input type=button id=p5NewFolderButton class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="New Folder" onclick="p5createfolder();" />
865
+ <input type=button id=p5UploadButton class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Upload" onclick="p5uploadFile()" />
866
+ <input type=button id=p5DownloadButton class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Download" onclick="p5downloadButton()" />
867
+ <input type=button id=p5CutButton class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Cut" onclick="p5copyFile(1)" />
868
+ <input type=button id=p5CopyButton class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Copy" onclick="p5copyFile(0)" />
869
+ <input type=button id=p5PasteButton class="btn btn-primary btn-sm me-1 desktopAction" disabled="disabled" value="Paste" onclick="p5pasteFile()" />
870
+ <div class="btn-group me-1 mobileActionDropdown">
871
+ <button type="button" class="btn btn-secondary btn-sm" data-bs-toggle="dropdown" aria-expanded="false">More <i class="fa-solid fa-chevron-down fa-xs"></i></button>
872
+ <div class="dropdown-menu">
873
+ <button type="button" disabled="disabled" id=p5MobileNewFolderButton class="dropdown-item" onclick="p5createfolder()">New Folder</button>
874
+ <button type="button" disabled="disabled" id=p5MobileUploadButton class="dropdown-item" onclick="p5uploadFile()">Upload</button>
875
+ <button type="button" disabled="disabled" id=p5MobileDownloadButton class="dropdown-item" onclick="p5downloadButton()">Download</button>
876
+ <button type="button" disabled="disabled" id=p5MobileCutButton class="dropdown-item" onclick="p5copyFile(1)">Cut</button>
877
+ <button type="button" disabled="disabled" id=p5MobileCopyButton class="dropdown-item" onclick="p5copyFile(0)">Copy</button>
878
+ <button type="button" disabled="disabled" id=p5MobilePasteButton class="dropdown-item" onclick="p5pasteFile()">Paste</button>
879
+ </div>
880
+ </div>
881
+ </div>
882
</td>
883
</tr>
884
<tr>
920
</div>
921
<div id=p6 style="display:none">
922
<div id="p6info" style="overflow-y:auto">
782
- <div id="p6title">
783
- <img id=MainMeshImage src="serverpic.ashx">
784
- <h1>My Server</h1>
923
+ <div id="p6title" class="d-flex align-items-center">
924
+ <div class="fs-4 fw-bold">My Server</div>
925
+ <img id=MainMeshImage src="serverpic.ashx" class="ms-auto">
926
</div>
927
<div id="p2ServerActions">
928
<p><strong>Server actions</strong></p>
986
<div id="p10BackButton" class="pe-2">
987
<i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex=0 onclick=goBack() title="Back" onkeypress="if (event.key == 'Enter') goBack()"></i>
988
</div>
848
- <div class='fs-4 fw-bold'>General - <span id=p10deviceName></span></div>
989
+ <div class='fs-4 fw-bold'><i class="fa-solid fa-circle-info fa-sm me-2 text-secondary"></i>General - <span id=p10deviceName></span></div>
990
</div>
991
<div id=p10info style="overflow-y:auto" class="pt-3">
992
<table style="width:100%" cellpadding="0" cellspacing="0">
1029
</div>
1030
</div>
1031
<div id=p11 class="noselect" style="display:none">
1032
+ <!-- softKeyboard: lives at p11 level (outside deskarea4) so it stays
1033
+ focusable even when the toolbar is hidden in fullscreen/landscape.
1034
+ position:fixed keeps it off-screen while remaining a valid focus target. -->
1035
+ <input id="softKeyboard" autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false"
1036
+ type="text" inputmode="text"
1037
+ style="position:fixed;opacity:0;width:1px;height:1px;left:-9999px;top:-9999px"
1038
+ oninput="onSoftKeyboardInput(this)"
1039
+ onblur="onSoftKeyboardFocusChange(false)"
1040
+ onfocus="onSoftKeyboardFocusChange(true)" />
1041
+ <!-- Trackpad cursor -->
1042
+ <svg id="trackpadCursor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 20" width="18" height="30"
1043
+ style="display:none;position:fixed;pointer-events:none;z-index:9999;transform-origin:0 0;filter:drop-shadow(1px 1px 2px rgba(0,0,0,0.8))">
1044
+ <path d="M0,0 L0,16 L4,12 L6.5,18 L8.5,17 L6,11 L11,11 Z" fill="white" stroke="black" stroke-width="1.2" stroke-linejoin="round"/>
1045
+ </svg>
1046
<div id="p11title" class="d-flex align-items-center">
1047
<div id="p11BackButton" class="pe-2">
1048
<i class="fa-solid fa-2xl fa-square-caret-left" role="button" tabindex=0 onclick=goBack() title="Back" onkeypress="if (event.key == 'Enter') goBack()"></i>
1049
</div>
895
- <div class='fs-4 fw-bold'>Desktop - <span id=p11deviceName></span></div>
1050
+ <div class='fs-4 fw-bold'><i class="fa-solid fa-display fa-sm me-2 text-secondary"></i>Desktop - <span id=p11deviceName></span></div>
1051
<div id="p11KeyboardLights" style="font-size:x-small;color:black" class="ms-auto pe-2">
1052
<div id="p11numlock" style="display:inline-block;margin-left:1px;border-radius:5px;background-color:#A3FFB8;padding:2px">NUM</div>
1053
<div id="p11capslock" style="display:inline-block;margin-left:1px;border-radius:5px;background-color:#A3FFB8;padding:2px">CAPS</div>
1128
<div id=DeskParent>
1129
<canvas id=Desk width=640 height=480 oncontextmenu="return false"
1130
onmousedown=dmousedown(event) onmouseup=dmouseup(event) onmousemove=dmousemove(event)
976
- onmousewheel=dmousewheel(event)></canvas>
1131
+ onmousewheel=dmousewheel(event)
1132
+ ontouchstart=dtouchstart(event) ontouchmove=dtouchmove(event) ontouchend=dtouchend(event)></canvas>
1133
</div>
1134
<div id=DeskTools>
1135
<div id=deskToolsAreaTop>
1167
<div id=deskarea4 class="areaFoot d-flex flex-wrap">
1168
<div class="d-flex align-items-center">
1169
<select id="deskkeys" class="form-select-sm me-1" cmenu=deskKeyShortcutContextMenu></select>
1014
- <input id="DeskWD" type=button class="btn btn-primary btn-sm me-1" value="Send" onkeypress="return false" onkeydown="return false" onclick="deskSendKeys()" />
1170
+ <button id="DeskWD" class="btn btn-primary btn-sm me-1" onkeypress="return false" onkeydown="return false" onclick="deskSendKeys()">Send</button>
1171
<input id="DeskESC" style="display:none" type="button" class="btn btn-secondary btn-sm me-1" value="ESC" onkeypress="return false" onkeydown="return false" onclick="sendDeskEsc()" />
1172
<input id="DeskClip" type="button" class="btn btn-secondary btn-sm me-1" value="Clipboard" onkeypress="return false" onkeydown="return false" onclick="showDeskClip()" />
1173
<input id="DeskType" class="btn btn-secondary btn-sm me-1" cmenu="deskPreConfigShortcutContextMenu" type="button" value="Type" onkeypress="return false" onkeydown="return false" onclick="showDeskType()" />
1174
+ <!-- Mobile-only: toggle button for the soft keyboard -->
1175
+ <button id="DeskSoftKbdBtn" class="btn btn-secondary btn-sm me-1" type="button"
1176
+ onclick="toggleMobileKeyboard()" title="Show/hide on-screen keyboard"
1177
+ style="display:none">
1178
+ <i class="fa-solid fa-keyboard"></i>
1179
+ </button>
1180
<div id="DeskControlSpan" title="Toggle mouse and keyboard input" class="form-check"><input id="DeskControl" type="checkbox" class="form-check-input" onkeypress="return false" onkeydown="return false" onclick="toggleKvmControl()"><label class="form-check-label" for="DeskControl">Input</label></div>
1181
</div>
1020
- <div class="d-flex ms-auto align-items-center">
1182
+ <!-- Mobile-only "More" button — opens a fixed-position action panel -->
1183
+ <div id="deskMobileActions" class="ms-auto align-items-center" style="display:none">
1184
+ <button class="btn btn-secondary btn-sm" type="button" onclick="deskToggleMobileActions()" title="More actions">
1185
+ More <i class="fa-solid fa-chevron-up fa-xs"></i>
1186
+ </button>
1187
+ </div>
1188
+ <div id="deskarea4Icons" class="d-flex ms-auto align-items-center">
1189
<span id=DeskMonitorSelectionSpan></span>
1190
<span id="DeskLatency" style="width:70px" class="text-center" title="Desktop Session Latency"></span>
1191
<span id="DeskTimer" style="width:70px" class="text-center" title="Session time"></span>
1232
<span id=DeskRunButton cmenu="deskPreConfigScriptContextMenu" class="deskarea" title="Run a script on this computer" role="button">
1233
<i class="fa-solid fa-fw fa-play" onclick=runDeviceCmd()></i>
1234
</span>
1067
- </div>
1235
+ </div><!-- /deskarea4Icons -->
1236
+ </div>
1237
+ <!-- Mobile action panel: fixed above the tab bar, shown via deskToggleMobileActions() -->
1238
+ <div id="deskMobileActionsBackdrop" onclick="deskCloseMobileActions()" style="display:none;position:fixed;inset:0;z-index:1998"></div>
1239
+ <div id="deskMobileActionsPanel" style="display:none;position:fixed;right:8px;bottom:82px;z-index:1999;min-width:220px;border-radius:12px;overflow-y:auto;overflow-x:hidden;max-height:calc(100dvh - 100px);box-shadow:0 4px 24px rgba(0,0,0,0.28)">
1240
+ <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="toggleMobileKeyboard();deskCloseMobileActions()"><i class="fa-solid fa-keyboard fa-fw me-2 text-primary"></i>On-screen Keyboard</a>
1241
+ <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="showDeskType();deskCloseMobileActions()"><i class="fa-solid fa-font fa-fw me-2 text-primary"></i>Type Text</a>
1242
+ <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deviceActionFunction();deskCloseMobileActions()"><i class="fa-solid fa-bolt fa-fw me-2 text-primary"></i>Actions</a>
1243
+ <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="showDesktopSettings();deskCloseMobileActions()"><i class="fa-solid fa-gear fa-fw me-2 text-primary"></i>Settings</a>
1244
+ <a id="deskTrackpadItem" class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deskToggleTrackpad();deskCloseMobileActions()"><i class="fa-solid fa-computer-mouse fa-fw me-2 text-primary"></i>Enable Trackpad Mode</a>
1245
+ <a id="deskRightClickItem" class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deskToggleRightClickMode();deskCloseMobileActions()"><i class="fa-solid fa-hand-pointer fa-fw me-2 text-primary"></i>Enable Right-click Mode</a>
1246
+ <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deskCustomizeKeys();deskCloseMobileActions()"><i class="fa-solid fa-sliders fa-fw me-2 text-primary"></i>Customize Shortcuts</a>
1247
+ <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deskRefreshFunction();deskCloseMobileActions()"><i class="fa-solid fa-rotate fa-fw me-2 text-primary"></i>Refresh Desktop</a>
1248
+ <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="showDeskClip();deskCloseMobileActions()"><i class="fa-solid fa-paste fa-fw me-2 text-primary"></i>Clipboard</a>
1249
+ <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deskClipboardOutFunction();deskCloseMobileActions()"><i class="fa-solid fa-clipboard fa-fw me-2 text-primary"></i>Upload Clipboard</a>
1250
+ <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deviceUrlFunction();deskCloseMobileActions()"><i class="fa-solid fa-globe fa-fw me-2 text-primary"></i>Open URL on Remote</a>
1251
+ <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deviceLockFunction();deskCloseMobileActions()"><i class="fa-solid fa-lock fa-fw me-2 text-primary"></i>Lock Remote</a>
1252
+ <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deviceToastFunction();deskCloseMobileActions()"><i class="fa-solid fa-bell fa-fw me-2 text-primary"></i>Send Notification</a>
1253
+ <a class="d-block px-3 py-2 text-decoration-none border-bottom" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="deviceChat(event);deskCloseMobileActions()"><i class="fa-solid fa-message fa-fw me-2 text-primary"></i>Chat</a>
1254
+ <a class="d-block px-3 py-2 text-decoration-none" style="font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)" onclick="runDeviceCmd();deskCloseMobileActions()"><i class="fa-solid fa-play fa-fw me-2 text-primary"></i>Run Script</a>
1255
+ </div>
1256
+ <!-- Fullscreen floating button - replaces all toolbars when in fulldesk mode -->
1257
+ <div id="deskFsBtn" onclick="deskToggleMobileActions()" style="display:none;position:fixed;bottom:54px;right:12px;z-index:2000;width:48px;height:48px;border-radius:50%;background:rgba(0,0,0,0.55);color:#fff;align-items:center;justify-content:center;cursor:pointer;backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);border:1.5px solid rgba(255,255,255,0.22);font-size:20px">
1258
+ <i class="fa-solid fa-bars"></i>
1259
</div>
1260
</div>
1261
</div>
1264
<div id="p12BackButton" class="pe-2">
1265
<i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex=0 onclick=goBack() title="Back" onkeypress="if (event.key == 'Enter') goBack()"></i>
1266
</div>
1076
- <div class='fs-4 fw-bold'>Terminal - <span id=p12deviceName></span></div>
1267
+ <div class='fs-4 fw-bold'><i class="fa-solid fa-terminal fa-sm me-2 text-secondary"></i>Terminal - <span id=p12deviceName></span></div>
1268
<div id="devListToolbarViewIcons2" class="ms-auto">
1269
<i class="fa-solid fa-xl fa-maximize fa-border" role="button" onclick=deskToggleFull(event) title="Full Screen. Hold shift to browser full screen."></i>
1270
</div>
1357
<input type=button onkeypress="return false" onkeydown="return false"
1358
class="btn btn-primary me-1 btn-sm" id="escbutton"
1359
value="ESC" onclick="termSendKey(27,'escbutton')" />
1360
+ <input type=button onkeypress="return false" onkeydown="return false"
1361
+ class="btn btn-primary me-1 btn-sm" id="tabbutton"
1362
+ value="Tab" onclick="termSendKey(9,'tabbutton')" style="display:none" />
1363
<input type=button onkeypress="return false" onkeydown="return false"
1364
class="btn btn-primary me-1 btn-sm" id="bsbutton"
1365
value="Backspace" onclick="termSendKey(8,'bsbutton')" style="display:none" />
1405
<div id="p13BackButton" class="pe-2">
1406
<i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex=0 onclick=goBack() title="Back" onkeypress="if (event.key == 'Enter') goBack()"></i>
1407
</div>
1214
- <div class='fs-4 fw-bold'>Files - <span id=p13deviceName></span></div>
1408
+ <div class='fs-4 fw-bold'><i class="fa-solid fa-folder-open fa-sm me-2 text-secondary"></i>Files - <span id=p13deviceName></span></div>
1409
</div>
1410
<table id="p13toolbar" cellpadding="0" cellspacing="0">
1411
<tr>
1452
<input type=button disabled="disabled" id=p13RenameFileButton class="btn btn-primary me-1 btn-sm" value="Rename" onclick="p13renamefile()" />
1453
<input type=button disabled="disabled" id=p13DeleteFileButton class="btn btn-primary me-1 btn-sm" value="Delete" onclick="p13deletefile()" />
1454
<input type=button disabled="disabled" id=p13ViewFileButton class="btn btn-primary me-1 btn-sm" value="Edit" onclick="p13viewfile()" />
1261
- <input type=button disabled="disabled" id=p13NewFolderButton class="btn btn-primary me-1 btn-sm" value="New Folder" onclick="p13createfolder()" />
1262
- <input type=button disabled="disabled" id=p13NewFileButton class="btn btn-primary me-1 btn-sm" value="New File" onclick="p13createfile()" />
1263
- <input type=button disabled="disabled" id=p13UploadButton class="btn btn-primary me-1 btn-sm" value="Upload" onclick="p13uploadFile()" />
1264
- <input type=button disabled="disabled" id=p13DownloadButton class="btn btn-primary me-1 btn-sm" value="Download" onclick="p13downloadButton()" />
1265
- <input type=button disabled="disabled" id=p13CutButton class="btn btn-primary me-1 btn-sm" value="Cut" onclick="p13copyFile(1)" />
1266
- <input type=button disabled="disabled" id=p13CopyButton class="btn btn-primary me-1 btn-sm" value="Copy" onclick="p13copyFile(0)" />
1267
- <input type=button disabled="disabled" id=p13PasteButton class="btn btn-primary me-1 btn-sm" value="Paste" onclick="p13pasteFile()" />
1268
- <input type=button disabled="disabled" id=p13ZipButton class="btn btn-primary me-1 btn-sm" value="Zip" onclick="p13zipFiles()" />
1269
- <input type=button disabled="disabled" id=p13UnzipButton class="btn btn-primary me-1 btn-sm" value="Unzip" onclick="p13unzipFile()" />
1270
- <input type=button disabled="disabled" id=p13RefreshButton class="btn btn-primary me-1 btn-sm" value="Refresh" onclick="p13folderup(9999)" />
1271
- <input type=button disabled="disabled" id=p13FindButton class="btn btn-primary me-1 btn-sm" value="Find" onclick="p13findfile()" />
1272
- <input type=button disabled="disabled" id=p13GoToFolderButton class="btn btn-primary me-1 btn-sm" value="GoTo" onclick="p13gotofolder()" />
1273
- <input type=button disabled="disabled" id=p13OpenButton class="btn btn-primary me-1 btn-sm" value="Open" onclick="p13openfilefolder()" />
1455
+ <input type=button disabled="disabled" id=p13NewFolderButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="New Folder" onclick="p13createfolder()" />
1456
+ <input type=button disabled="disabled" id=p13NewFileButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="New File" onclick="p13createfile()" />
1457
+ <input type=button disabled="disabled" id=p13UploadButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="Upload" onclick="p13uploadFile()" />
1458
+ <input type=button disabled="disabled" id=p13DownloadButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="Download" onclick="p13downloadButton()" />
1459
+ <input type=button disabled="disabled" id=p13CutButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="Cut" onclick="p13copyFile(1)" />
1460
+ <input type=button disabled="disabled" id=p13CopyButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="Copy" onclick="p13copyFile(0)" />
1461
+ <input type=button disabled="disabled" id=p13PasteButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="Paste" onclick="p13pasteFile()" />
1462
+ <input type=button disabled="disabled" id=p13ZipButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="Zip" onclick="p13zipFiles()" />
1463
+ <input type=button disabled="disabled" id=p13UnzipButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="Unzip" onclick="p13unzipFile()" />
1464
+ <input type=button disabled="disabled" id=p13RefreshButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="Refresh" onclick="p13folderup(9999)" />
1465
+ <input type=button disabled="disabled" id=p13FindButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="Find" onclick="p13findfile()" />
1466
+ <input type=button disabled="disabled" id=p13GoToFolderButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="GoTo" onclick="p13gotofolder()" />
1467
+ <input type=button disabled="disabled" id=p13OpenButton class="btn btn-primary me-1 btn-sm desktopFileAction" value="Open" onclick="p13openfilefolder()" />
1468
+ <div class="btn-group me-1 mobileFileActionDropdown">
1469
+ <button type="button" class="btn btn-secondary btn-sm" data-bs-toggle="dropdown" aria-expanded="false">More <i class="fa-solid fa-chevron-down fa-xs"></i></button>
1470
+ <div class="dropdown-menu">
1471
+ <button type="button" disabled="disabled" id=p13MobileNewFolderButton class="dropdown-item" onclick="p13createfolder()">New Folder</button>
1472
+ <button type="button" disabled="disabled" id=p13MobileNewFileButton class="dropdown-item" onclick="p13createfile()">New File</button>
1473
+ <button type="button" disabled="disabled" id=p13MobileUploadButton class="dropdown-item" onclick="p13uploadFile()">Upload</button>
1474
+ <button type="button" disabled="disabled" id=p13MobileDownloadButton class="dropdown-item" onclick="p13downloadButton()">Download</button>
1475
+ <button type="button" disabled="disabled" id=p13MobileCutButton class="dropdown-item" onclick="p13copyFile(1)">Cut</button>
1476
+ <button type="button" disabled="disabled" id=p13MobileCopyButton class="dropdown-item" onclick="p13copyFile(0)">Copy</button>
1477
+ <button type="button" disabled="disabled" id=p13MobilePasteButton class="dropdown-item" onclick="p13pasteFile()">Paste</button>
1478
+ <button type="button" disabled="disabled" id=p13MobileZipButton class="dropdown-item" onclick="p13zipFiles()">Zip</button>
1479
+ <button type="button" disabled="disabled" id=p13MobileUnzipButton class="dropdown-item" onclick="p13unzipFile()">Unzip</button>
1480
+ <button type="button" disabled="disabled" id=p13MobileRefreshButton class="dropdown-item" onclick="p13folderup(9999)">Refresh</button>
1481
+ <button type="button" disabled="disabled" id=p13MobileFindButton class="dropdown-item" onclick="p13findfile()">Find</button>
1482
+ <button type="button" disabled="disabled" id=p13MobileGoToFolderButton class="dropdown-item" onclick="p13gotofolder()">GoTo</button>
1483
+ <button type="button" disabled="disabled" id=p13MobileOpenButton class="dropdown-item" onclick="p13openfilefolder()">Open</button>
1484
+ </div>
1485
+ </div>
1486
</div>
1487
</td>
1488
</tr>
1594
<div id="p16BackButton" class="pe-2">
1595
<i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex=0 onclick=goBack() title="Back" onkeypress="if (event.key == 'Enter') goBack()"></i>
1596
</div>
1385
- <div class='fs-4 fw-bold'>Events - <span id=p16deviceName></span></div>
1597
+ <div class='fs-4 fw-bold'><i class="fa-solid fa-calendar fa-sm me-2 text-secondary"></i>Events - <span id=p16deviceName></span></div>
1598
</div>
1599
<table class="pTable">
1600
<tr>
1631
<div id="p17BackButton" class="pe-2">
1632
<i class="fa-solid fa-square-caret-left fa-2xl" role="button" tabindex=0 onclick=goBack() title="Back" onkeypress="if (event.key == 'Enter') goBack()"></i>
1633
</div>
1422
- <div class='fs-4 fw-bold'>Details - <span id=p17deviceName></span></div>
1634
+ <div class='fs-4 fw-bold'><i class="fa-solid fa-list fa-sm me-2 text-secondary"></i>Details - <span id=p17deviceName></span></div>
1635
<div id="devListToolbarViewIcons3" class="ms-auto">
1636
<i class="fa-solid fa-rotate-right fa-xl fa-border" role="button" onclick=refreshDetails(event) title="Refresh details information."></i>
1637
<i class="fa-solid fa-chart-line fa-xl fa-border" role="button" onclick=deskToggleCpuGraph(event) title="Show device CPU and memory usage."></i>
1825
<div id=p31events style=""></div>
1826
</div>
1827
<div id=p40 style="display:none">
1616
- <div id="p40title">
1617
- <h1>My Server Stats</h1>
1828
+ <div id="p40title" class="d-flex align-items-center">
1829
+ <div class="fs-4 fw-bold">My Server Stats</div>
1830
</div>
1831
<div class="areaHead d-flex align-items-center flex-wrap p-1">
1832
<div class="d-flex align-items-center">
1855
<canvas id=serverMainStats style=""></canvas>
1856
</div>
1857
<div id=p41 style="display:none">
1646
- <div id="p41title">
1647
- <h1>My Server Tracing</h1>
1858
+ <div id="p41title" class="d-flex align-items-center">
1859
+ <div class="fs-4 fw-bold">My Server Tracing</div>
1860
</div>
1861
<div class="areaHead d-flex align-items-center flex-wrap p-1">
1862
<div class="d-flex align-items-center">
1878
<div id=p41events style=""></div>
1879
</div>
1880
<div id=p42 style="display:none">
1669
- <h1>My Server Plugins</h1>
1881
+ <div id="p42title" class="d-flex align-items-center">
1882
+ <div class="fs-4 fw-bold">My Server Plugins</div>
1883
+ </div>
1884
<div class="areaHead">
1885
<div class="toright2">
1886
</div>
1924
style="width:100%;height:calc(100vh - 245px);max-height:calc(100vh - 245px)"></iframe>
1925
</div>
1926
<div id=p50 style="display:none">
1713
- <div id="p50title">
1714
- <h1>My User Groups</h1>
1927
+ <div id="p50title" class="d-flex align-items-center">
1928
+ <div class="fs-4 fw-bold">My User Groups</div>
1929
</div>
1930
<table class="pTable">
1931
<tr>
1970
</div>
1971
</div>
1972
<div id=p52 style="display:none">
1759
- <div id="p52title">
1760
- <h1>My User Recordings</h1>
1973
+ <div id="p52title" class="d-flex align-items-center">
1974
+ <div class="fs-4 fw-bold">My User Recordings</div>
1975
</div>
1976
<table class="pTable">
1977
<tr>
1988
<div id=p52recordings style="overflow-y:auto"></div>
1989
</div>
1990
<div id=p60 style="display:none">
1777
- <div id="p60title">
1778
- <h1>My Reports</h1>
1991
+ <div id="p60title" class="d-flex align-items-center">
1992
+ <div class="fs-4 fw-bold">My Reports</div>
1993
</div>
1994
<table class="pTable">
1995
<tr>
2587
2588
// If SSPI or LDAP authentication not used, allow batch account creation.
2589
QV('p4UserBatchCreate', (features & 0x00080000) == 0);
2590
+ QV('p4MobileUserBatchCreate', (features & 0x00080000) == 0);
2591
2592
// Set the file editor
2593
d4EditWrapVal = Number(getstore('editorWrap', 0));
5231
if (document.querySelector('.modal.show')) {
5232
return;
5233
}
5234
+ mobileKbdGotKeypress = true; // tells onSoftKeyboardInput not to double-send
5235
setSessionActivity();
5236
if (!xxdialogMode && (xxcurrentView == 11) && desktop && Q('DeskControl').checked) {
5237
// Check what keys we are allows to send
5325
}
5326
5327
function ondockeyup(e) {
5328
+ mobileKbdGotKeypress = false;
5329
+ var sk = Q('softKeyboard'); if (sk && document.activeElement === sk) { sk.value = ''; }
5330
setSessionActivity();
5331
if (!xxdialogMode && (xxcurrentView == 11) && desktop && Q('DeskControl').checked) {
5332
// Check what keys we are allows to send
8800
if (nname.length == 0) { nname = '<i>' + "None" + '</i>'; }
8801
if (((meshrights & 4) != 0) && ((!mesh.flags) || ((mesh.flags & 2) == 0 || (mesh.flags & 16)))) { nname = '<span tabindex=0 title="' + "Click here to edit the server-side device name" + '" onclick=showEditNodeValueDialog(0) onkeyup="if (event.key == \'Enter\') showEditNodeValueDialog(0)" role="button">' + nname + ' <i class="fa-solid fa-pencil fa-2xs"/></i></span>'; }
8802
nnameEx = nname;
8585
- if (mesh) { nname += '<span style=color:#AAA;font-size:small> - ' + EscapeHtml(mesh.name) + '</span>'; }
8803
+ if (mesh) { nname += '<span class="deviceGroupName" style=color:#AAA;font-size:small> - ' + EscapeHtml(mesh.name) + '</span>'; }
8804
QH('p10deviceName', nname);
8805
QH('p11deviceName', nname);
8806
QH('p12deviceName', nname);
9069
9070
x += '</table><br />';
9071
// Show action button, only show if we have permissions 4, 8, 64
8854
- if (((meshrights & (4 + 8 + 64 + 262144)) != 0) && (node.mtype < 3) && ((node.agent == null) || (node.agent.id != 34))) { x += '<input type=button class="btn btn-primary btn-sm me-2" value="' + "Actions" + '" title="' + "Perform power actions on the device" + '" onclick=deviceActionFunction() />'; }
8855
- x += '<input type=button class="btn btn-primary btn-sm me-2" value="' + "Notes" + '" title="' + "View notes about this device" + '" onclick=showNotes(' + ((meshrights & 128) == 0) + ',"' + encodeURIComponentEx(node._id) + '") />';
8856
- x += '<input type=button class="btn btn-primary btn-sm me-2" value="' + "Log Event" + '" title="' + "Write an event for this device" + '" onclick=writeDeviceEvent("' + encodeURIComponentEx(node._id) + '") />';
9072
+ if (((meshrights & (4 + 8 + 64 + 262144)) != 0) && (node.mtype < 3) && ((node.agent == null) || (node.agent.id != 34))) { x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Actions" + '" title="' + "Perform power actions on the device" + '" onclick=deviceActionFunction() />'; }
9073
+ x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Notes" + '" title="' + "View notes about this device" + '" onclick=showNotes(' + ((meshrights & 128) == 0) + ',"' + encodeURIComponentEx(node._id) + '") />';
9074
+ x += '<input type=button class="btn btn-primary btn-sm me-2 mb-2" value="' + "Log Event" + '" title="' + "Write an event for this device" + '" onclick=writeDeviceEvent("' + encodeURIComponentEx(node._id) + '") />';
9075
if ((node.mtype == 2) && (connectivity & 1) && ((meshrights & 131072) != 0)) { x += '<input type=button class="btn btn-primary btn-sm me-2" cmenu=deskPreConfigScriptContextMenu value="' + "Run" + '" title="' + "Run commands on this device" + '" onclick=runDeviceCmd("' + encodeURIComponentEx(node._id) + '") />'; }
9076
if (node.mtype != 4) {
9077
if ((meshrights & 8) && (connectivity & 1) && ((meshrights & 16384) != 0) || ((node.pmt == 1) && ((features2 & 2) != 0))) { x += '<input type=button class="btn btn-primary btn-sm me-2" value="' + "Message" + '" title="' + "Display a text message on the remote device" + '" onclick=deviceMessageFunction() />'; }
9724
desktop.m.SendRefresh();
9725
}
9726
9727
+ // Adapted from Classic Mobile's deskChangeMouseButton()
9728
+ function deskMobileToggleZoom() {
9729
+ _mobileSetZoom(_mobileZoomMode === 'native' ? 'fit' : 'native');
9730
+ }
9731
+
9732
+ function deskToggleRightClickMode() {
9733
+ if (xxdialogMode || desktop == null) return;
9734
+ desktop.m.SwapMouse = !desktop.m.SwapMouse;
9735
+ }
9736
+
9737
+ function deskToggleMobileActions() {
9738
+ var panel = Q('deskMobileActionsPanel'), backdrop = Q('deskMobileActionsBackdrop');
9739
+ if (!panel) return;
9740
+ var show = (panel.style.display === 'none');
9741
+ if (show) {
9742
+ if (document.body.classList.contains('fulldesk')) {
9743
+ // Fullscreen mode: rebuild panel with all controls
9744
+ _buildFullscreenPanel(panel);
9745
+ } else {
9746
+ // Normal mode: update dynamic labels to reflect current state
9747
+ var rc = Q('deskRightClickItem');
9748
+ if (rc) {
9749
+ var active = desktop && desktop.m && desktop.m.SwapMouse;
9750
+ rc.innerHTML = '<i class="fa-solid fa-hand-pointer fa-fw me-2 text-primary"></i>'
9751
+ + (active ? 'Disable Right-click Mode' : 'Enable Right-click Mode');
9752
+ }
9753
+ var tp = Q('deskTrackpadItem');
9754
+ if (tp) {
9755
+ tp.innerHTML = '<i class="fa-solid fa-computer-mouse fa-fw me-2 text-primary"></i>'
9756
+ + (trackpadMode ? 'Disable Trackpad Mode' : 'Enable Trackpad Mode');
9757
+ }
9758
+ }
9759
+ }
9760
+ QV('deskMobileActionsPanel', show);
9761
+ QV('deskMobileActionsBackdrop', show);
9762
+ // Toggle floating button icon: when closed -> ✕ when open
9763
+ var fsBtn = Q('deskFsBtn');
9764
+ if (fsBtn) fsBtn.innerHTML = show ? '<i class="fa-solid fa-xmark"></i>' : '<i class="fa-solid fa-bars"></i>';
9765
+ }
9766
+
9767
+ function _buildFullscreenPanel(panel) {
9768
+ var inputOn = Q('DeskControl') && Q('DeskControl').checked;
9769
+ var swap = desktop && desktop.m && desktop.m.SwapMouse;
9770
+ var trackpad = (typeof trackpadMode !== 'undefined') && trackpadMode;
9771
+ var s = 'font-size:15px;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)';
9772
+ var cls = 'd-block px-3 py-2 text-decoration-none border-bottom';
9773
+ var end = 'd-block px-3 py-2 text-decoration-none'; // last item, no border
9774
+ function row(c, icon, label, action, extra) {
9775
+ return '<a class="' + c + '" style="' + s + '" onclick="' + action + '">'
9776
+ + '<i class="fa-solid ' + icon + ' fa-fw me-2 ' + (extra || 'text-primary') + '"></i>'
9777
+ + label + '</a>';
9778
+ }
9779
+ panel.innerHTML =
9780
+ row(cls, 'fa-compress', 'Exit Fullscreen', 'deskToggleFull();deskCloseMobileActions()', 'text-danger') +
9781
+ row(cls, (_mobileZoomMode==='native'?'fa-compress-arrows-alt':'fa-expand-arrows-alt'),
9782
+ (_mobileZoomMode==='native'?'Scale to Fit':'Native Size (Scrollable)'), 'deskMobileToggleZoom();deskCloseMobileActions()') +
9783
+ row(cls, 'fa-keyboard', 'Input <i class="fa-solid ' + (inputOn?'fa-toggle-on text-success':'fa-toggle-off text-secondary') + ' ms-1"></i>', "Q('DeskControl').click();deskCloseMobileActions()") +
9784
+ row(cls, 'fa-keyboard', 'On-screen Keyboard', 'toggleMobileKeyboard();deskCloseMobileActions()') +
9785
+ row(cls, 'fa-computer-mouse', (trackpad?'Disable':'Enable')+' Trackpad', 'deskToggleTrackpad();deskCloseMobileActions()') +
9786
+ row(cls, 'fa-hand-pointer', (swap?'Disable':'Enable')+' Right-click', 'deskToggleRightClickMode();deskCloseMobileActions()') +
9787
+ row(cls, 'fa-font', 'Type Text', 'showDeskType();deskCloseMobileActions()') +
9788
+ row(cls, 'fa-terminal', 'Ctrl + Alt + Del', 'if(desktop&&desktop.m)desktop.m.sendcad();deskCloseMobileActions()') +
9789
+ row(cls, 'fa-keyboard', 'Send ESC', 'sendDeskEsc();deskCloseMobileActions()') +
9790
+ row(cls, 'fa-sliders', 'Keyboard Shortcuts', 'deskCustomizeKeys();deskCloseMobileActions()') +
9791
+ row(cls, 'fa-paste', 'Clipboard', 'showDeskClip();deskCloseMobileActions()') +
9792
+ row(cls, 'fa-clipboard', 'Upload Clipboard', 'deskClipboardOutFunction();deskCloseMobileActions()') +
9793
+ row(cls, 'fa-rotate-right', 'Rotate Right', 'drotate(1);deskCloseMobileActions()') +
9794
+ row(cls, 'fa-rotate-left', 'Rotate Left', 'drotate(-1);deskCloseMobileActions()') +
9795
+ row(cls, 'fa-rotate', 'Refresh Desktop', 'deskRefreshFunction();deskCloseMobileActions()') +
9796
+ row(cls, 'fa-gear', 'Desktop Settings', 'showDesktopSettings();deskCloseMobileActions()') +
9797
+ row(end, 'fa-plug-circle-xmark', 'Disconnect', 'connectDesktop(null,0);deskCloseMobileActions()', 'text-danger');
9798
+ }
9799
+ function deskCloseMobileActions() {
9800
+ QV('deskMobileActionsPanel', false);
9801
+ QV('deskMobileActionsBackdrop', false);
9802
+ var fsBtn = Q('deskFsBtn');
9803
+ if (fsBtn) fsBtn.innerHTML = '<i class="fa-solid fa-bars"></i>';
9804
+ }
9805
+
9806
function deviceToastFunction() {
9807
if (xxdialogMode) return;
9808
var x = '<select id=d2deviceop style=width:100%;margin-bottom:4px class=form-select><option value=2>' + "Toast Notification" + '</option><option value=1>' + "Message Box" + '</option><option value=3>' + "Alert Box" + '</option></select>';
10653
delete multiDesktop[currentNode._id];
10654
} else {
10655
// Device is not already connected, just setup a blank canvas
10359
- if (desktop == null) { QH('DeskParent', '<canvas id=Desk oncontextmenu="return false" onmousedown=dmousedown(event) onmouseup=dmouseup(event) onmousemove=dmousemove(event)></canvas>'); }
10656
+ if (desktop == null) { QH('DeskParent', '<canvas id=Desk oncontextmenu="return false" onmousedown=dmousedown(event) onmouseup=dmouseup(event) onmousemove=dmousemove(event) ontouchstart=dtouchstart(event) ontouchmove=dtouchmove(event) ontouchend=dtouchend(event)></canvas>'); }
10657
desktopNode = currentNode;
10658
}
10659
// Setup the mouse wheel
10664
if (desktop) { desktop.m.onDisplayinfo = deskDisplayInfo; deskDisplayInfo(desktop.m, desktop.m.displays, desktop.m.selectedDisplay); }
10665
updateDesktopButtons();
10666
deskAdjust();
10667
+
10668
updateMetadata(desktop, 'deskmetadata');
10669
}
10670
10721
QE('connectbutton1h', hwonline);
10722
QV('deskFocusBtn', (desktop != null) && (desktop.contype == 2) && (deskState != 0) && (desktopsettings.showfocus));
10723
QE('DeskClip', deskState == 3);
10724
+ QE('DeskSoftKbdBtn', deskState == 3);
10725
QV('DeskClip', (inputAllowed) && (currentNode.agent) && ((features2 & 0x1800) != 0x1800) && (currentNode.agent.id != 11) && (currentNode.agent.id != 16) && ((desktop == null) || (desktop.contype != 2)) && ((desktopsettings.autoclipboard != true) || (navigator.clipboard == null) || (navigator.clipboard.readText == null))); // Clipboard not supported on macOS
10726
QE('DeskESC', (deskState == 3) && (desktop.contype != 4));
10727
QV('DeskESC', browserfullscreen && inputAllowed);
10920
if (desktopsettings.remotekeymap == true) { desktop.m.remoteKeyMap = desktopsettings.remotekeymap; }
10921
//desktop.m.onDisplayinfo = deskDisplayInfo;
10922
desktop.m.onScreenSizeChange = deskAdjust;
10923
+ if (document.body.classList.contains('is-mobile')) {
10924
+ desktop.m.GetPositionOfControl = function(canvas) {
10925
+ var r = canvas.getBoundingClientRect();
10926
+ return [r.left + window.scrollX, r.top + window.scrollY];
10927
+ };
10928
+ var _origGrab = desktop.m.GrabMouseInput;
10929
+ var _origUngrab = desktop.m.UnGrabMouseInput;
10930
+ desktop.m.GrabMouseInput = function() {
10931
+ _origGrab.call(desktop.m);
10932
+ var c = desktop.m.CanvasId;
10933
+ c.ontouchstart = null;
10934
+ c.ontouchmove = null;
10935
+ c.ontouchend = null;
10936
+ c.addEventListener('touchstart', dtouchstart, { passive: false });
10937
+ c.addEventListener('touchmove', dtouchmove, { passive: false });
10938
+ c.addEventListener('touchend', dtouchend, { passive: false });
10939
+ };
10940
+ desktop.m.UnGrabMouseInput = function() {
10941
+ var c = desktop.m.CanvasId;
10942
+ c.removeEventListener('touchstart', dtouchstart);
10943
+ c.removeEventListener('touchmove', dtouchmove);
10944
+ c.removeEventListener('touchend', dtouchend);
10945
+ _origUngrab.call(desktop.m);
10946
+ };
10947
+ // Override SendMouseMsg to fix the bounds check for rotated canvas.
10948
+ // After setRotation(1), canvas.width=1080 & canvas.height=1920 (swapped).
10949
+ // The original check is X<=canvas.width (1080), but our rotation-corrected
10950
+ // X (= remote.x) can be up to 1920 — clicks in the lower half of the
10951
+ // rotated view are silently dropped. We replace the check with the correct
10952
+ // unrotated remote dimensions.
10953
+ (function() {
10954
+ var dm = desktop.m, _origSMM = dm.SendMouseMsg;
10955
+ dm.SendMouseMsg = function(Action, event) {
10956
+ var rot = this.rotation;
10957
+ if (!rot) return _origSMM.call(this, Action, event);
10958
+ if (Action == null || !this.Canvas) return;
10959
+ var cw = this.Canvas.canvas.width, ch = this.Canvas.canvas.height;
10960
+ var ScaleW = cw / this.CanvasId.clientWidth;
10961
+ var ScaleH = ch / this.CanvasId.clientHeight;
10962
+ var off = this.GetPositionOfControl(this.Canvas.canvas);
10963
+ var X = (event.pageX - off[0]) * ScaleW;
10964
+ var Y = (event.pageY - off[1]) * ScaleH;
10965
+ if (event.addx) X += event.addx;
10966
+ if (event.addy) Y += event.addy;
10967
+ // Correct max: for rot=1,3 remote.x<=ch and remote.y<=cw (unrotated dims)
10968
+ var maxX = (rot===1||rot===3) ? ch : cw;
10969
+ var maxY = (rot===1||rot===3) ? cw : ch;
10970
+ if (X<0||X>maxX||Y<0||Y>maxY) return;
10971
+ var Button = 0;
10972
+ if (Action===this.KeyAction.UP||Action===this.KeyAction.DOWN) {
10973
+ if (event.which) { Button=(event.which===1)?this.MouseButton.LEFT:((event.which===2)?this.MouseButton.MIDDLE:this.MouseButton.RIGHT); }
10974
+ else if (typeof event.button==='number') { Button=(event.button===0)?this.MouseButton.LEFT:((event.button===1)?this.MouseButton.MIDDLE:this.MouseButton.RIGHT); }
10975
+ }
10976
+ if (this.SwapMouse) { if(Button===this.MouseButton.LEFT){Button=this.MouseButton.RIGHT;}else if(Button===this.MouseButton.RIGHT){Button=this.MouseButton.LEFT;} }
10977
+ var msg = String.fromCharCode(0x00,this.InputType.MOUSE,0x00,0x0A,0x00,
10978
+ (Action===this.KeyAction.DBLCLICK?0x88:Action===this.KeyAction.DOWN?Button:(Button*2)&0xFF),
10979
+ ((X/256)&0xFF),(X&0xFF),((Y/256)&0xFF),(Y&0xFF));
10980
+ if (this.State===3) {
10981
+ if (Action===this.KeyAction.NONE) { if(this.Alternate===0||this.ipad){this.send(msg);this.Alternate=1;}else{this.Alternate=0;} }
10982
+ else { this.send(msg); }
10983
+ }
10984
+ };
10985
+ })();
10986
+ }
10987
desktop.Start(desktopNode._id);
10988
desktop.latency.callback = function (ms) { /* console.log('latency', ms); */ updateSessionTime(); };
10989
desktop.contype = 1;
11183
QV('DeskInputUnLockedButton', false);
11184
deskFocusBtn.value = "All Focus";
11185
if (fullscreen == true) { deskToggleFull(); }
11186
+ _mobileAutoFullscreen = false; // reset so reconnecting in landscape re-triggers
11187
+ // Hide trackpad cursor and reset trackpad state on disconnect
11188
+ if (typeof trackpadMode !== 'undefined') {
11189
+ trackpadMode = false;
11190
+ var _tpc = document.getElementById('trackpadCursor');
11191
+ if (_tpc) _tpc.style.display = 'none';
11192
+ }
11193
webRtcDesktopReset();
11194
deskPreferedStickyDisplay = 0;
11195
break;
11208
deskLastClipboardSent = null;
11209
}
11210
if (updateSessionTimer == null) { updateSessionTimer = setInterval(updateSessionTime, 1000); }
11211
+ // On mobile, wake the agent to start streaming.
11212
+ // SendRefresh alone is sometimes ignored before the first mouse event.
11213
+ // Synthetic mousedown+mouseup guarantees the agent starts sending frames.
11214
+ if (document.body.classList.contains('is-mobile')) {
11215
+ // Re-trigger landscape fullscreen if phone is already in landscape
11216
+ // when reconnecting (no orientationchange event fires in this case)
11217
+ setTimeout(function() { if (typeof _doOrientationUpdate === 'function') _doOrientationUpdate(); }, 600);
11218
+ setTimeout(function() {
11219
+ if (!desktop || desktop.State !== 3) return;
11220
+ desktop.m.SendRefresh();
11221
+ var dm = desktop.m;
11222
+ if (dm && dm.CanvasId) {
11223
+ var r = dm.CanvasId.getBoundingClientRect();
11224
+ if (r.width > 0) {
11225
+ var ev = { pageX: r.left + window.scrollX + r.width / 2, pageY: r.top + window.scrollY + r.height / 2, button: 0, buttons: 0, which: 1 };
11226
+ dm.mousedown(ev);
11227
+ dm.mouseup(ev);
11228
+ }
11229
+ }
11230
+ }, 500);
11231
+ }
11232
break;
11233
default:
11234
//console.log('Unknown onDesktopStateChange state', state);
11443
QS('termTable')['max-height'] = '100%';
11444
}
11445
11055
- // If shift is pressed, enter browser full screen.
11056
- if (e.shiftKey == true) {
11446
+ // On mobile or when shift is pressed, enter browser full screen.
11447
+ if (e.shiftKey == true || document.body.classList.contains('is-mobile')) {
11448
enterBrowserFullscreen(Q('body'));
11449
browserfullscreen = true;
11450
}
11615
// Customize keyboard shortcuts
11616
function deskCustomizeKeys() {
11617
if (xxdialogMode) return;
11227
- var x = '<div id=d2shortcuts style="width:100%;height:180px;padding:4px;overflow-y:auto;border:1px solid gray"></div><div style=width:100%;padding:5px>';
11228
- x += '<label><input id=d1kshift type=checkbox class="form-check-input me-2" /> ' + "Shift" + '</label><label> <input id=d1kalt type=checkbox class="form-check-input me-2" /> ' + "Alt" + '</label><label> <input id=d1kctrl type=checkbox class="form-check-input me-2" /> ' + "Ctrl" + '</label> <input id=d1kwin type=checkbox class="form-check-input me-2" /> ' + "Win" + '</label>';
11229
- x += ' <select id=d2keySelect>';
11618
+ // Shortcut list
11619
+ var x = '<div id="d2shortcuts" class="list-group mb-3" style="max-height:200px;overflow-y:auto"></div>';
11620
+ // Modifier checkboxes
11621
+ x += '<div class="d-flex flex-wrap gap-3 mb-2">';
11622
+ x += '<div class="form-check"><input id="d1kshift" type="checkbox" class="form-check-input"><label class="form-check-label" for="d1kshift">Shift</label></div>';
11623
+ x += '<div class="form-check"><input id="d1kalt" type="checkbox" class="form-check-input"><label class="form-check-label" for="d1kalt">Alt</label></div>';
11624
+ x += '<div class="form-check"><input id="d1kctrl" type="checkbox" class="form-check-input"><label class="form-check-label" for="d1kctrl">Ctrl</label></div>';
11625
+ x += '<div class="form-check"><input id="d1kwin" type="checkbox" class="form-check-input"><label class="form-check-label" for="d1kwin">Win</label></div>';
11626
+ x += '</div>';
11627
+ // Key select + Add button
11628
+ x += '<div class="d-flex gap-2 mb-3">';
11629
+ x += '<select id="d2keySelect" class="form-select form-select-sm">';
11630
for (var i in keyStrings) { x += '<option value=' + i + '>' + keyStrings[i] + '</option>'; }
11631
for (var i = 1; i <= 12; i++) { x += '<option value=' + (i + 111) + '>F' + i + '</option>'; }
11632
for (var i = 0; i < 10; i++) { x += '<option value=' + (i + 48) + '>' + i + '</option>'; }
11633
for (var i = 0; i < 26; i++) { x += '<option value=' + (i + 65) + '>' + String.fromCharCode(i + 65) + '</option>'; }
11234
- x += '</select> <input type=button value=' + "Add" + ' onclick=addDeskCustomizeKey() /></div>';
11235
- x += '<div style=width:100%;padding:2px;text-align:center><input type=button value="' + "Restore Default Keyboard Shortcuts" + '" onclick=restoreDeskCustomizeKey() /></div>';
11236
- setModalContent('xxAddAgent', "Keyboard Shortcuts Customization", x);
11634
+ x += '</select>';
11635
+ x += '<button type="button" class="btn btn-primary btn-sm flex-shrink-0" onclick="addDeskCustomizeKey()">Add</button>';
11636
+ x += '</div>';
11637
+ // Restore defaults
11638
+ x += '<button type="button" class="btn btn-outline-secondary btn-sm w-100" onclick="restoreDeskCustomizeKey()">Restore Default Shortcuts</button>';
11639
+ setModalContent('xxAddAgent', "Keyboard Shortcuts", x);
11640
showModal('xxAddAgentModal', 'idx_dlgOkButton', () => deskCustomizeKeysEx());
11641
deskUpdateShortcutList();
11642
}
11647
}
11648
11649
function deskUpdateShortcutList() {
11247
- var x = '';
11650
+ var x = '', last = deskKeyboardShortcuts.length - 1;
11651
for (var i in deskKeyboardShortcuts) {
11249
- var kt = keyShortcutTotext(deskKeyboardShortcuts[i]), orderButtons = '';
11250
- if (i != (deskKeyboardShortcuts.length - 1)) { orderButtons += '<img width=8 height=8 style=float:right;cursor:pointer;padding:3px src="images/c2.png" onclick=deskCustomizeKeyDown(' + deskKeyboardShortcuts[i] + ')>'; }
11251
- if (i != 0) { orderButtons += '<img width=8 height=8 style=float:right;cursor:pointer;padding:3px src="images/c3.png" onclick=deskCustomizeKeyUp(' + deskKeyboardShortcuts[i] + ')>'; }
11252
- x += '<div style="width:100%;background-color:#AAA;border-radius:4px;margin-bottom:4px;padding:4px;text-align:left;box-sizing:border-box" value=' + deskKeyboardShortcuts[i] + '>' + kt + '<img width=10 height=10 style=float:right;cursor:pointer;padding:2px;margin-left:8px src="images/trash.png" onclick=removeDeskCustomizeKey(' + deskKeyboardShortcuts[i] + ')>' + orderButtons + '</div>';
11652
+ var k = deskKeyboardShortcuts[i];
11653
+ x += '<div class="list-group-item d-flex align-items-center justify-content-between py-2 px-3 gap-2">';
11654
+ x += '<span class="flex-grow-1">' + keyShortcutTotext(k) + '</span>';
11655
+ x += '<div class="d-flex gap-1 flex-shrink-0">';
11656
+ if (parseInt(i) > 0) {
11657
+ x += '<button type="button" class="btn btn-outline-secondary btn-sm" onclick="deskCustomizeKeyUp(' + k + ')" title="Move up"><i class="fa-solid fa-arrow-up fa-xs"></i></button>';
11658
+ }
11659
+ if (parseInt(i) < last) {
11660
+ x += '<button type="button" class="btn btn-outline-secondary btn-sm" onclick="deskCustomizeKeyDown(' + k + ')" title="Move down"><i class="fa-solid fa-arrow-down fa-xs"></i></button>';
11661
+ }
11662
+ x += '<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeDeskCustomizeKey(' + k + ')" title="Remove"><i class="fa-solid fa-trash fa-xs"></i></button>';
11663
+ x += '</div></div>';
11664
}
11254
- if (x == '') { x = '<i>' + "No keyboard shortcuts defined" + '</i>'; }
11665
+ if (x == '') { x = '<div class="list-group-item text-muted fst-italic">No keyboard shortcuts defined</div>'; }
11666
QH('d2shortcuts', x);
11667
}
11668
11702
if (Q('d1kalt').checked) { k |= 0x020000; }
11703
if (Q('d1kctrl').checked) { k |= 0x080000; }
11704
if (Q('d1kwin').checked) { k |= 0x100000; }
11294
- if ((k > 0) && (deskKeyboardShortcuts.indexOf(k) == -1)) { deskKeyboardShortcuts.push(k); deskUpdateShortcutList(); }
11705
+ if ((k > 0) && (deskKeyboardShortcuts.indexOf(k) == -1)) {
11706
+ deskKeyboardShortcuts.push(k);
11707
+ deskUpdateShortcutList();
11708
+ // Reset form so the next shortcut starts fresh
11709
+ Q('d1kshift').checked = false;
11710
+ Q('d1kalt').checked = false;
11711
+ Q('d1kctrl').checked = false;
11712
+ Q('d1kwin').checked = false;
11713
+ Q('d2keySelect').selectedIndex = 0;
11714
+ }
11715
}
11716
11717
// Customize keyboard strings
12241
function dmousemove(e) { setSessionActivity(); e.addx = Q('DeskParent').scrollLeft; e.addy = Q('DeskParent').scrollTop; if (!xxdialogMode && desktop != null && Q('DeskControl').checked) { Q('Desk').style.cursor = ''; if ((webRtcDesktop != null) && (webRtcDesktop.softdesktop != null)) { webRtcDesktop.softdesktop.m.mousemove(e); desktop.m.sendKeepAlive(); } else { desktop.m.mousemove(e); } } else if (!xxdialogMode && desktop != null && !Q('DeskControl').checked) { Q('Desk').style.cursor = 'not-allowed'; } }
12242
function dmousewheel(e) { setSessionActivity(); e.addx = Q('DeskParent').scrollLeft; e.addy = Q('DeskParent').scrollTop; if (!xxdialogMode && desktop != null && Q('DeskControl').checked) { if ((webRtcDesktop != null) && (webRtcDesktop.softdesktop != null)) { webRtcDesktop.softdesktop.m.mousewheel(e); desktop.m.sendKeepAlive(); } else { if (desktop.m.mousewheel) { desktop.m.mousewheel(e); } } haltEvent(e); return true; } return false; }
12243
function drotate(x) { if (!xxdialogMode && desktop != null) { desktop.m.setRotation(desktop.m.rotation + x); deskAdjust(); deskAdjust(); } }
12244
+
12245
+ // Mobile touch input for remote desktop.
12246
+ // 1-finger tap = left click on remote
12247
+ // Direct mode — 1-finger tap=click, long-press=right-click, drag=cursor move
12248
+ // Trackpad mode — 1-finger drag moves visible cursor; tap clicks at cursor position
12249
+ // Both modes — 2-finger scroll = mouse wheel on remote
12250
+ var dtouchStartX = 0, dtouchStartY = 0, dtouchLastX = 0, dtouchLastY = 0;
12251
+ var dtouchMoved = false;
12252
+ var dtouchLastTapTime = 0, dtouchLastTapX = 0, dtouchLastTapY = 0; // double-tap tracking
12253
+ var dtouchLongPressTimer = null;
12254
+ var dtouchTwoFingerLastY = 0;
12255
+ // Trackpad state (mirrors Classic Mobile)
12256
+ var trackpadMode = false;
12257
+ var trackpadLastX = null, trackpadLastY = null;
12258
+ var trackpadCurX = 0, trackpadCurY = 0;
12259
+ var trackpadDown = false;
12260
+ var trackpadTapTimer = null;
12261
+ var trackpadTapMoved = false;
12262
+ var trackpadEdgeTimer = null;
12263
+ var trackpadEdgeDX = 0, trackpadEdgeDY = 0;
12264
+
12265
+ function dtouchMakeEvent(clientX, clientY, button) {
12266
+ // When the canvas is rotated, crotX/crotY (agent-desktop-0.0.2.js) convert
12267
+ // canvas-pixel space -> remote-desktop space. We apply that correction here by
12268
+ // computing adjusted clientX/Y that will produce the correct remote coords after
12269
+ // SendMouseMsg's (pageX - offset) * scale calculation.
12270
+ var adjX = clientX, adjY = clientY;
12271
+ var dm = dtouchGetModule();
12272
+ if (dm && dm.rotation && dm.CanvasId && dm.Canvas && dm.crotX && dm.crotY) {
12273
+ var r = dm.CanvasId.getBoundingClientRect();
12274
+ if (r.width > 0 && r.height > 0) {
12275
+ var cw = dm.Canvas.canvas.width, ch = dm.Canvas.canvas.height;
12276
+ var px = (clientX - r.left) * cw / r.width;
12277
+ var py = (clientY - r.top) * ch / r.height;
12278
+ var rx = dm.crotX(px, py), ry = dm.crotY(px, py);
12279
+ adjX = r.left + rx * r.width / cw;
12280
+ adjY = r.top + ry * r.height / ch;
12281
+ }
12282
+ }
12283
+ return { pageX: adjX + window.scrollX, pageY: adjY + window.scrollY, button: button || 0, buttons: 0, which: (button === 2) ? 3 : 1 };
12284
+ }
12285
+
12286
+ function dtouchGetModule() {
12287
+ return (webRtcDesktop && webRtcDesktop.softdesktop) ? webRtcDesktop.softdesktop.m : (desktop ? desktop.m : null);
12288
+ }
12289
+
12290
+ // Trackpad mode (ported from Classic Mobile)
12291
+ function deskToggleTrackpad() {
12292
+ trackpadMode = !trackpadMode;
12293
+ if (trackpadMode) {
12294
+ var dp = Q('DeskParent'), r = dp.getBoundingClientRect();
12295
+ trackpadMoveCursor(r.left + r.width / 2, r.top + r.height / 2);
12296
+ } else {
12297
+ trackpadStopEdgeScroll();
12298
+ Q('trackpadCursor').style.display = 'none';
12299
+ trackpadCurX = 0; trackpadCurY = 0;
12300
+ }
12301
+ }
12302
+
12303
+ function trackpadMoveCursor(clientX, clientY) {
12304
+ var deskRect = Q('Desk').getBoundingClientRect();
12305
+ // Clamp cursor within canvas bounds
12306
+ var cx = Math.max(0, Math.min(deskRect.width, clientX - deskRect.left));
12307
+ var cy = Math.max(0, Math.min(deskRect.height, clientY - deskRect.top));
12308
+ var cursor = Q('trackpadCursor');
12309
+ // position:fixed -> coordinates are viewport-relative (never clipped by overflow:hidden)
12310
+ cursor.style.left = (deskRect.left + cx) + 'px';
12311
+ cursor.style.top = (deskRect.top + cy) + 'px';
12312
+ // Rotate cursor to match canvas rotation so it always points "up" in the current view
12313
+ var dm = dtouchGetModule();
12314
+ cursor.style.transform = 'rotate(' + ((dm ? (dm.rotation || 0) : 0) * 90) + 'deg)';
12315
+ cursor.style.display = 'block';
12316
+ trackpadCurX = cx; trackpadCurY = cy;
12317
+ }
12318
+
12319
+ function trackpadMakeEvent(button) {
12320
+ // Build event at cursor position (not finger position); dtouchMakeEvent handles rotation
12321
+ var rect = Q('Desk').getBoundingClientRect();
12322
+ return dtouchMakeEvent(rect.left + trackpadCurX, rect.top + trackpadCurY, button);
12323
+ }
12324
+
12325
+ function trackpadSendMove() {
12326
+ var dm = dtouchGetModule();
12327
+ if (!xxdialogMode && dm) dm.mousemove(trackpadMakeEvent(0));
12328
+ }
12329
+
12330
+ function trackpadSendClick(button) {
12331
+ var dm = dtouchGetModule();
12332
+ if (!xxdialogMode && dm) { dm.mousedown(trackpadMakeEvent(button)); dm.mouseup(trackpadMakeEvent(button)); }
12333
+ }
12334
+
12335
+ function trackpadStopEdgeScroll() {
12336
+ if (trackpadEdgeTimer) { clearInterval(trackpadEdgeTimer); trackpadEdgeTimer = null; }
12337
+ trackpadEdgeDX = 0; trackpadEdgeDY = 0;
12338
+ }
12339
+
12340
+ function trackpadStartEdgeScroll() {
12341
+ if (trackpadEdgeTimer) return;
12342
+ trackpadEdgeTimer = setInterval(function() {
12343
+ if (!trackpadMode || (trackpadEdgeDX === 0 && trackpadEdgeDY === 0)) { trackpadStopEdgeScroll(); return; }
12344
+ var dp = Q('DeskParent'); dp.scrollLeft += trackpadEdgeDX; dp.scrollTop += trackpadEdgeDY;
12345
+ }, 16);
12346
+ }
12347
+
12348
+ function trackpadCheckEdge() {
12349
+ var deskRect = Q('Desk').getBoundingClientRect();
12350
+ // Use cursor position relative to canvas for edge detection
12351
+ var cx = deskRect.left + trackpadCurX, cy = deskRect.top + trackpadCurY, zone = 60;
12352
+ trackpadEdgeDX = cx < deskRect.left + zone ? -Math.round((zone-(cx-deskRect.left))/4) :
12353
+ cx > deskRect.right - zone ? Math.round((zone-(deskRect.right-cx))/4) : 0;
12354
+ trackpadEdgeDY = cy < deskRect.top + zone ? -Math.round((zone-(cy-deskRect.top))/4) :
12355
+ cy > deskRect.bottom - zone ? Math.round((zone-(deskRect.bottom-cy))/4) : 0;
12356
+ if (trackpadEdgeDX || trackpadEdgeDY) trackpadStartEdgeScroll(); else trackpadStopEdgeScroll();
12357
+ }
12358
+
12359
+ // Mobile soft-keyboard: focusing #softKeyboard shows the OS keyboard.
12360
+ // Key events bubble to document handlers (backspace, enter, etc.).
12361
+ // Mobile keyboards fire 'input' instead of keypress -> SendKeyUnicode.
12362
+ var mobileKbdGotKeypress = false;
12363
+
12364
+ function onSoftKeyboardInput(el) {
12365
+ if (!desktop || !desktop.m || !Q('DeskControl').checked) { el.value = ''; return; }
12366
+ if (mobileKbdGotKeypress) { el.value = ''; return; } // desktop keyboard — already handled by keypress
12367
+ var str = el.value; el.value = '';
12368
+ if (str && desktop.m.SendKeyUnicode) {
12369
+ for (var i = 0; i < str.length; i++) {
12370
+ var c = str.charCodeAt(i);
12371
+ desktop.m.SendKeyUnicode(desktop.m.KeyAction.DOWN, c);
12372
+ desktop.m.SendKeyUnicode(desktop.m.KeyAction.UP, c);
12373
+ }
12374
+ }
12375
+ }
12376
+
12377
+ function onSoftKeyboardFocusChange(focused) {
12378
+ // Update the toggle button icon: solid keyboard = open, regular = closed
12379
+ var btn = Q('DeskSoftKbdBtn');
12380
+ if (btn) {
12381
+ var icon = btn.querySelector('i');
12382
+ if (icon) {
12383
+ icon.className = focused ? 'fa-solid fa-keyboard text-warning' : 'fa-solid fa-keyboard';
12384
+ }
12385
+ }
12386
+ }
12387
+
12388
+ function toggleMobileKeyboard() {
12389
+ var kb = Q('softKeyboard');
12390
+ if (!kb) return;
12391
+ if (document.activeElement === kb) { kb.blur(); } else { kb.focus(); }
12392
+ }
12393
+
12394
+ function dtouchSendClick(clientX, clientY, button) {
12395
+ var ev = dtouchMakeEvent(clientX, clientY, button);
12396
+ var dm = dtouchGetModule();
12397
+ if (dm) { dm.mousedown(ev); dm.mouseup(ev); }
12398
+ }
12399
+
12400
+ function dtouchstart(e) {
12401
+ setSessionActivity();
12402
+ deskCloseMobileActions();
12403
+ dtouchMoved = false;
12404
+ if (dtouchLongPressTimer) { clearTimeout(dtouchLongPressTimer); dtouchLongPressTimer = null; }
12405
+ if (xxdialogMode || desktop == null) return;
12406
+ e.preventDefault();
12407
+ // Trackpad mode
12408
+ if (trackpadMode) {
12409
+ trackpadStopEdgeScroll();
12410
+ if (e.touches.length !== 1) return;
12411
+ var t = e.touches[0];
12412
+ trackpadLastX = t.clientX; trackpadLastY = t.clientY;
12413
+ trackpadTapMoved = false;
12414
+ trackpadTapTimer = setTimeout(function() { trackpadTapTimer = null; }, 250);
12415
+ trackpadDown = false;
12416
+ return;
12417
+ }
12418
+ // Direct mode — 2 fingers
12419
+ if (e.touches.length === 2) {
12420
+ var fdx = e.touches[0].clientX - e.touches[1].clientX;
12421
+ var fdy = e.touches[0].clientY - e.touches[1].clientY;
12422
+ dtouchTwoFingerLastY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
12423
+ if (_mobileZoomMode === 'native') {
12424
+ // Record pinch start for zoom gesture
12425
+ _nativePinchStartDist = Math.sqrt(fdx*fdx + fdy*fdy);
12426
+ var sc = Q('deskarea3x');
12427
+ _nativePinchStartW = sc ? sc.scrollWidth : 640;
12428
+ _nativePinchStartH = sc ? sc.scrollHeight : 480;
12429
+ _nativePinchMidX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
12430
+ _nativePinchMidY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
12431
+ }
12432
+ return;
12433
+ }
12434
+ if (e.touches.length !== 1) return;
12435
+ var t = e.touches[0];
12436
+ dtouchStartX = dtouchLastX = t.clientX;
12437
+ dtouchStartY = dtouchLastY = t.clientY;
12438
+ dtouchLongPressTimer = setTimeout(function() {
12439
+ dtouchLongPressTimer = null;
12440
+ if (dtouchMoved || xxdialogMode || desktop == null || !Q('DeskControl').checked) return;
12441
+ dtouchSendClick(dtouchLastX, dtouchLastY, 2);
12442
+ dtouchMoved = true;
12443
+ }, 500);
12444
+ }
12445
+
12446
+ function dtouchmove(e) {
12447
+ setSessionActivity();
12448
+ if (xxdialogMode || desktop == null) return;
12449
+ e.preventDefault();
12450
+ // Trackpad mode
12451
+ if (trackpadMode) {
12452
+ if (e.touches.length !== 1) return;
12453
+ var t = e.touches[0];
12454
+ if (trackpadLastX === null) { trackpadLastX = t.clientX; trackpadLastY = t.clientY; return; }
12455
+ var dx = t.clientX - trackpadLastX, dy = t.clientY - trackpadLastY;
12456
+ trackpadLastX = t.clientX; trackpadLastY = t.clientY;
12457
+ if (Math.abs(dx) > 1 || Math.abs(dy) > 1) trackpadTapMoved = true;
12458
+ var rect = Q('Desk').getBoundingClientRect();
12459
+ trackpadMoveCursor(rect.left + trackpadCurX + dx * 1.5, rect.top + trackpadCurY + dy * 1.5);
12460
+ trackpadSendMove();
12461
+ trackpadCheckEdge();
12462
+ return;
12463
+ }
12464
+ // Native zoom mode — 1 finger: pan by adjusting scrollLeft/Top
12465
+ if (_mobileZoomMode === 'native' && e.touches.length === 1) {
12466
+ var t = e.touches[0];
12467
+ var dx = t.clientX - dtouchLastX, dy = t.clientY - dtouchLastY;
12468
+ dtouchLastX = t.clientX; dtouchLastY = t.clientY;
12469
+ if (Math.abs(dx) > 1 || Math.abs(dy) > 1) {
12470
+ dtouchMoved = true;
12471
+ var sc = Q('deskarea3x');
12472
+ if (sc) { sc.scrollLeft -= dx; sc.scrollTop -= dy; }
12473
+ }
12474
+ return;
12475
+ }
12476
+ // Native zoom mode — 2 fingers: pinch to resize canvas (zoom in/out)
12477
+ if (_mobileZoomMode === 'native' && e.touches.length === 2 && _nativePinchStartDist > 0) {
12478
+ var fdx = e.touches[0].clientX - e.touches[1].clientX;
12479
+ var fdy = e.touches[0].clientY - e.touches[1].clientY;
12480
+ var newDist = Math.sqrt(fdx*fdx + fdy*fdy);
12481
+ var ratio = newDist / _nativePinchStartDist;
12482
+ var newW = Math.round(_nativePinchStartW * ratio);
12483
+ var newH = Math.round(_nativePinchStartH * ratio);
12484
+ // Clamp between 25% and 400% of the native remote resolution
12485
+ var dm = dtouchGetModule();
12486
+ var minW = Math.round((dm ? dm.ScreenWidth : 640) * 0.25);
12487
+ var maxW = Math.round((dm ? dm.ScreenWidth : 640) * 4);
12488
+ var minH = Math.round((dm ? dm.ScreenHeight : 480) * 0.25);
12489
+ var maxH = Math.round((dm ? dm.ScreenHeight : 480) * 4);
12490
+ newW = Math.max(minW, Math.min(maxW, newW));
12491
+ newH = Math.max(minH, Math.min(maxH, newH));
12492
+ document.documentElement.style.setProperty('--native-desk-w', newW + 'px');
12493
+ document.documentElement.style.setProperty('--native-desk-h', newH + 'px');
12494
+ // Keep pinch midpoint fixed on screen by adjusting scroll
12495
+ var sc = Q('deskarea3x');
12496
+ if (sc) {
12497
+ var scRect = sc.getBoundingClientRect();
12498
+ var midViewX = _nativePinchMidX - scRect.left;
12499
+ var midViewY = _nativePinchMidY - scRect.top;
12500
+ sc.scrollLeft = (sc.scrollLeft + midViewX) * ratio - midViewX;
12501
+ sc.scrollTop = (sc.scrollTop + midViewY) * ratio - midViewY;
12502
+ // Update start values for incremental updates each frame
12503
+ _nativePinchStartDist = newDist;
12504
+ _nativePinchStartW = newW;
12505
+ _nativePinchStartH = newH;
12506
+ }
12507
+ dtouchMoved = true;
12508
+ return;
12509
+ }
12510
+ // Direct mode
12511
+ if (e.touches.length === 2) {
12512
+ var midX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
12513
+ var midY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
12514
+ var dy = dtouchTwoFingerLastY - midY;
12515
+ dtouchTwoFingerLastY = midY;
12516
+ if (Math.abs(dy) > 1 && Q('DeskControl').checked) {
12517
+ var dm = dtouchGetModule();
12518
+ if (dm && dm.SendMouseMsg && dm.KeyAction) {
12519
+ var wev = dtouchMakeEvent(midX, midY, 0);
12520
+ wev.wheelDelta = -dy * 4;
12521
+ dm.SendMouseMsg(dm.KeyAction.SCROLL, wev);
12522
+ }
12523
+ }
12524
+ return;
12525
+ }
12526
+ if (e.touches.length !== 1) return;
12527
+ var t = e.touches[0];
12528
+ dtouchLastX = t.clientX; dtouchLastY = t.clientY;
12529
+ if (Math.abs(t.clientX - dtouchStartX) > 8 || Math.abs(t.clientY - dtouchStartY) > 8) {
12530
+ dtouchMoved = true;
12531
+ if (dtouchLongPressTimer) { clearTimeout(dtouchLongPressTimer); dtouchLongPressTimer = null; }
12532
+ }
12533
+ }
12534
+
12535
+ function dtouchend(e) {
12536
+ setSessionActivity();
12537
+ if (dtouchLongPressTimer) { clearTimeout(dtouchLongPressTimer); dtouchLongPressTimer = null; }
12538
+ if (xxdialogMode || desktop == null) return;
12539
+ e.preventDefault();
12540
+ // Trackpad mode
12541
+ if (trackpadMode) {
12542
+ trackpadStopEdgeScroll();
12543
+ trackpadLastX = null; trackpadLastY = null; trackpadDown = false;
12544
+ if (trackpadTapTimer !== null && !trackpadTapMoved) {
12545
+ clearTimeout(trackpadTapTimer); trackpadTapTimer = null;
12546
+ if (Q('DeskControl').checked) trackpadSendClick(0);
12547
+ }
12548
+ return;
12549
+ }
12550
+ // Direct mode
12551
+ if (!Q('DeskControl').checked) return;
12552
+ if (e.changedTouches.length !== 1) return;
12553
+ var t = e.changedTouches[0];
12554
+ // 30px threshold (not dtouchMoved's 8px) — finger pressure causes micro-movement
12555
+ // that would block taps; only real drags exceed 30px.
12556
+ var totalDx = Math.abs(t.clientX - dtouchStartX);
12557
+ var totalDy = Math.abs(t.clientY - dtouchStartY);
12558
+ if (totalDx > 30 || totalDy > 30) return; // real drag — no click
12559
+ var now = Date.now();
12560
+ var dx = Math.abs(t.clientX - dtouchLastTapX), dy = Math.abs(t.clientY - dtouchLastTapY);
12561
+ if ((now - dtouchLastTapTime) < 400 && dx < 30 && dy < 30) {
12562
+ // Use first tap's position: Windows requires both clicks within 4 remote px;
12563
+ // a 2px finger shift × ~5x scale = 10 remote px, breaking detection.
12564
+ dtouchSendClick(dtouchLastTapX, dtouchLastTapY, 0);
12565
+ dtouchLastTapTime = 0; // reset so a 3rd tap doesn't re-trigger
12566
+ } else {
12567
+ dtouchSendClick(t.clientX, t.clientY, 0);
12568
+ dtouchLastTapTime = now;
12569
+ dtouchLastTapX = t.clientX;
12570
+ dtouchLastTapY = t.clientY;
12571
+ }
12572
+ }
12573
function stopProcess(id, name) {
12574
setModalContent('xxAddAgent', "Process Control", format("Stop process #{0} \"{1}\"?", id, name));
12575
showModal('xxAddAgentModal', 'idx_dlgOkButton', () => stopProcessEx(3, (id + '|' + name)));
12919
xterm.loadAddon(xtermimage);
12920
xterm.open(Q('termarea3xdiv')); // termarea3x
12921
xterm.onData(function (data) { if (terminal != null) { if (terminal.urlname == 'sshterminalrelay.ashx') { terminal.socket.send('~' + data); } else { terminal.sendText(data); } } })
12922
+ if (document.body.classList.contains('is-mobile')) {
12923
+ Q('termarea3xdiv').addEventListener('touchend', function() { xterm.focus(); });
12924
+ }
12925
if (xtermfit) { xtermfit.fit(); }
12926
xterm.onTitleChange(function (title) { QH('termtitle', ' - ' + EscapeHtml(title)); });
12927
xterm.onResize(function (size) {
13600
QV('p13ZipButton', filesNode.mtype != 3);
13601
QV('p13UnzipButton', filesNode.mtype != 3);
13602
QV('p13PasteButton', filesNode.mtype != 3);
13603
+ [
13604
+ ['p13NewFolderButton', 'p13MobileNewFolderButton'],
13605
+ ['p13NewFileButton', 'p13MobileNewFileButton'],
13606
+ ['p13UploadButton', 'p13MobileUploadButton'],
13607
+ ['p13DownloadButton', 'p13MobileDownloadButton'],
13608
+ ['p13CutButton', 'p13MobileCutButton'],
13609
+ ['p13CopyButton', 'p13MobileCopyButton'],
13610
+ ['p13PasteButton', 'p13MobilePasteButton'],
13611
+ ['p13ZipButton', 'p13MobileZipButton'],
13612
+ ['p13UnzipButton', 'p13MobileUnzipButton'],
13613
+ ['p13RefreshButton', 'p13MobileRefreshButton'],
13614
+ ['p13FindButton', 'p13MobileFindButton'],
13615
+ ['p13GoToFolderButton', 'p13MobileGoToFolderButton'],
13616
+ ['p13OpenButton', 'p13MobileOpenButton']
13617
+ ].forEach(function (x) { QE(x[1], !Q(x[0]).disabled); });
13618
}
13619
13620
function p13getFileSelCount(includeDirs) { var cc = 0; var checkboxes = document.getElementsByName('fd'); for (var i = 0; i < checkboxes.length; i++) { if ((checkboxes[i].checked) && ((includeDirs != false) || (checkboxes[i].attributes.file.value == '3'))) cc++; } return cc; }
15104
QH('p15statetext', '');
15105
QH('p15coreName', '');
15106
QV('p15outputselecttd', false);
15107
+ QV('p15BackButton', false); // no "back" navigation from server console
15108
+ QC('p15title').add('no-back-btn'); // add left-padding since no back button
15109
15110
if (samenode == false) {
15111
QH('p15agentConsoleText', consoleServerText);
15112
Q('p15agentConsoleText').scrollTop = Q('p15agentConsoleText').scrollHeight;
15113
}
15114
} else {
14346
- // Setup the console
15115
+ // Setup device console
15116
var samenode = (consoleNode == currentNode);
15117
consoleNode = currentNode;
15118
17811
QE('p5CutButton', (sfc > 0) && (cc == sfc));
17812
QE('p5CopyButton', (sfc > 0) && (cc == sfc));
17813
QE('p5PasteButton', (p5clipboard != null) && (p5clipboard.length > 0) && (filetreelocation.length > 0));
17814
+ [
17815
+ ['p5NewFolderButton', 'p5MobileNewFolderButton'],
17816
+ ['p5UploadButton', 'p5MobileUploadButton'],
17817
+ ['p5DownloadButton', 'p5MobileDownloadButton'],
17818
+ ['p5CutButton', 'p5MobileCutButton'],
17819
+ ['p5CopyButton', 'p5MobileCopyButton'],
17820
+ ['p5PasteButton', 'p5MobilePasteButton']
17821
+ ].forEach(function (x) { QE(x[1], !Q(x[0]).disabled); });
17822
}
17823
17824
function getFileSelCount(includeDirs) { var cc = 0, checkboxes = document.getElementsByName('fc'); for (var i = 0; i < checkboxes.length; i++) { if ((checkboxes[i].checked) && ((includeDirs != false) || (checkboxes[i].attributes.file.value == '3'))) cc++; } return cc; }
22022
QV('UsersSubMenuSpan', x == 4 || x == 50 || x == 52);
22023
QV('EventsSubMenuSpan', (x == 3) || (x == 60));
22024
var panels = { 3: 'EventsLive', 4: 'UsersGeneral', 10: 'MainDev', 11: 'MainDevDesktop', 12: 'MainDevTerminal', 13: 'MainDevFiles', 14: 'MainDevAmt', 15: 'MainDevConsole', 16: 'MainDevEvents', 17: 'MainDevInfo', 18: 'MainDevApps', 19: 'MainDevPlugins', 20: 'MeshGeneral', 21: 'MeshSummary', 30: 'UserGeneral', 31: 'UserEvents', 6: 'ServerGeneral', 40: 'ServerStats', 41: 'ServerTrace', 42: 'ServerPlugins', 50: 'UsersGroups', 52: 'UsersRecordings', 60: 'EventsReport', 115: 'ServerConsole' };
22025
+
22026
+ // Classic Mobile tab shuffle: on mobile, hide the active device sub-tab from
22027
+ // the bottom bar so all remaining tabs fit without scrolling.
22028
+ if (document.body.classList.contains('is-mobile')) {
22029
+ var _devTabIds = ['MainDev','MainDevDesktop','MainDevTerminal','MainDevFiles','MainDevAmt','MainDevConsole','MainDevEvents','MainDevInfo','MainDevPlugins'];
22030
+ for (var _dti = 0; _dti < _devTabIds.length; _dti++) {
22031
+ var _dtEl = document.getElementById(_devTabIds[_dti]);
22032
+ if (_dtEl) _dtEl.classList.remove('mob-active-tab');
22033
+ }
22034
+ if (x >= 10 && x < 20 && panels[x]) {
22035
+ var _dtActive = document.getElementById(panels[x]);
22036
+ if (_dtActive) _dtActive.classList.add('mob-active-tab');
22037
+ }
22038
+ }
22039
+
22040
for (var i in panels) {
22041
QC(panels[i]).remove('style3x');
22042
QC(panels[i]).remove('style3sel');