This commit is contained in:
Maximus 2025-10-22 22:16:48 +01:00 committed by GitHub
commit aeaafed2e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 2080 additions and 5 deletions

View File

@ -1758,6 +1758,70 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
if (parent.parent.taskManager != null) { parent.parent.taskManager.agentAction(command, obj); }
break;
}
case 'annotationcaps': {
try {
const nodeid = obj.dbNodeKey;
if (!nodeid) { console.log('annotationcaps: missing obj.dbNodeKey'); break; }
parent.db.Get(nodeid, function (err, doc) {
if (err || !doc) { console.log('annotationcaps: db.Get failed', err); return; }
// db.Get may return a single doc or an array in some paths — normalize.
const node = Array.isArray(doc) ? doc[0] : doc;
node.caps = node.caps || {};
if (typeof command.annotation === 'boolean') {
node.caps.annotation = command.annotation;
}
if (typeof command.annotationPermission === 'string') {
node.annotationPermission = (command.annotationPermission === 'granted') ? 'granted' : 'denied';
}
parent.db.Set(node);
const targets = ['*', 'server-ids', node.meshid, node._id];
const ev = {
etype: 'node',
action: 'changenode',
nodeid: node._id,
meshid: node.meshid,
domain: node.domain || (obj.domain && obj.domain.id),
node: parent.CloneSafeNode ? parent.CloneSafeNode(node) : node
};
parent.parent.DispatchEvent(targets, obj, ev);
});
} catch (e) {
console.log('annotationcaps handler error:', e);
}
break;
}
case 'annotationAck': {
try {
const nodeid = obj.dbNodeKey;
const meshid = obj.dbMeshKey;
if (nodeid) {
const targets = ['*', 'server-ids', meshid, nodeid];
const ev = {
etype: 'node',
action: 'annotationAck',
nodeid: nodeid,
meshid: meshid,
domain: (obj.domain && obj.domain.id),
supported: command.supported,
permission: command.permission,
op: command.op,
event: command.event,
ok: command.ok,
error: command.error
};
parent.parent.DispatchEvent(targets, obj, ev);
}
} catch (e) {
console.log('annotationAck handler error:', e);
}
break;
}
default: {
parent.agentStats.unknownAgentActionCount++;
parent.parent.debug('agent', 'Unknown agent action (' + obj.remoteaddrport + '): ' + JSON.stringify(command) + '.');

View File

@ -5558,6 +5558,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
'adduserbatch': serverCommandAddUserBatch,
'addusertousergroup': serverCommandAddUserToUserGroup,
'agentdisconnect': serverCommandAgentDisconnect,
'annotation': serverCommandAnnotation,
'authcookie': serverCommandAuthCookie,
'changeemail': serverCommandChangeEmail,
'changelang': serverCommandChangeLang,
@ -6237,6 +6238,141 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
});
}
function serverCommandAnnotation(command) {
const allowedOps = new Set([
'start','stop','clear','style',
'strokeStart','strokeMove','strokeEnd',
'path','rect','circle','arrow','remove','probe'
]);
let err = null;
try {
let nodeids = [];
if (Array.isArray(command.nodeids)) {
if (common.validateStrArray(command.nodeids, 1, 256) == false) { err = 'Invalid nodeids'; }
else nodeids = command.nodeids;
} else if (typeof command.nodeid === 'string') {
nodeids = [ command.nodeid ];
} else {
err = 'Missing nodeid(s)';
}
if (err == null) {
const norm = [];
for (let i in nodeids) {
const nid = nodeids[i];
norm.push((nid.indexOf('/') === -1) ? ('node/' + domain.id + '/' + nid) : nid);
}
command.nodeids = norm;
}
if (command.payload && (typeof command.payload === 'object')) {
for (const k in command.payload) { if (command[k] === undefined) { command[k] = command.payload[k]; } }
delete command.payload;
}
if (!err) {
if (common.validateString(command.op, 3, 32) === false || !allowedOps.has(command.op)) {
err = 'Invalid op';
}
}
} catch (ex) { err = 'Validation exception: ' + ex; }
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action:'annotation', responseid:command.responseid, result:err })); } catch (ex) { } }
return;
}
let anyForwarded = false;
for (let i in command.nodeids) {
const nodeid = command.nodeids[i];
parent.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
if ((node == null) || (visible === false)) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action:'annotation', responseid:command.responseid, nodeid:nodeid, result:'Invalid device id' })); } catch (ex) { } }
return;
}
if (((rights & MESHRIGHT_REMOTECONTROL) == 0) && ((rights & MESHRIGHT_REMOTEVIEWONLY) == 0)) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action:'annotation', responseid:command.responseid, nodeid:node._id, result:'Access Denied' })); } catch (ex) { } }
return;
}
const caps = node.caps || {};
//console.log('anno caps =', caps);
if (!(node.agent && node.agent.id === 14) || caps.annotation !== true) {
if (command.responseid != null) {
try { ws.send(JSON.stringify({ action:'annotation', responseid:command.responseid, nodeid:node._id, result:'Not supported' })); } catch (ex) {}
}
return;
}
switch (command.op) {
case 'rect':
if (command.w == null && (typeof command.x2 === 'number')) command.w = command.x2;
if (command.h == null && (typeof command.y2 === 'number')) command.h = command.y2;
break;
case 'circle':
if (command.cx == null && (typeof command.x === 'number')) command.cx = command.x;
if (command.cy == null && (typeof command.y === 'number')) command.cy = command.y;
break;
case 'strokeStart':
case 'strokeMove':
break;
case 'path':
break;
case 'style':
break;
}
const agentCmd = {
action: 'annotation',
op: command.op,
color: command.color,
width: command.width,
x: command.x, y: command.y,
x2: command.x2, y2: command.y2,
w: command.w, h: command.h,
r: command.r,
cx: command.cx, cy: command.cy,
points: command.points,
norm: (command.norm === true),
ttlMs: command.ttlMs,
sessionid: ws.sessionId,
username: user.name,
userid: user._id
};
const routedCmd = Object.assign({ nodeid: node._id }, agentCmd);
const routed = routeCommandToNode(routedCmd);
if (routed === true) { anyForwarded = true; }
try {
parent.parent.DispatchEvent(
['*', node._id, user._id],
obj,
{ etype:'node', action:'annotation', nodeid:node._id, userid:user._id, username:user.name, domain:domain.id, op:command.op }
);
} catch (e) { console.log('SystemCommandAnnotation() - meshuser - WARN : ' + e) }
});
}
if (command.responseid != null) {
try { ws.send(JSON.stringify({ action:'annotation', responseid:command.responseid, result:'ok' })); } catch (ex) { }
}
}
function serverCommandAuthCookie(command) {
try {
ws.send(JSON.stringify({

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 708 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
public/images/pen16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

BIN
public/images/pen24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 B

BIN
public/images/pen32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -156,6 +156,10 @@ var CreateAgentRemoteDesktop = function (canvasid, scrolldiv) {
else if (Msg[1] == 2) { obj.Canvas.drawImage(Msg[2], obj.rotX(Msg[3], Msg[4]), obj.rotY(Msg[3], Msg[4])); delete Msg[2]; }
obj.PendingOperations.splice(i, 1);
obj.TilesDrawn++;
if (obj.TilesDrawn == 1 && obj.onFirstTileDrawn) {
obj.onFirstTileDrawn();
}
if ((obj.TilesDrawn == obj.tilesReceived) && (obj.KillDraw < obj.TilesDrawn)) { obj.KillDraw = obj.TilesDrawn = obj.tilesReceived = 0; }
return true;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,200 @@
/* === Annotation overlay (scoped, no impact on existing UI) === */
#AnnoOverlay {
position: absolute;
inset: 0;
z-index: 10; /* Above #Desk, below HUD */
pointer-events: none; /* toggled to 'auto' while drawing */
}
#annotationMenu {
position: absolute;
bottom: 45px;
background-color: #eee;
z-index: 100;
border-radius: 5px;
box-shadow: 1px 0px 10px 5px #333;
padding: 5px;
width: 115px;
}
/* Tool section layout */
.annoToolsSection {
width: 114px;
margin-bottom: 5px;
overflow: hidden; /* Clear floats */
}
/* Tool icons */
.annoSelector1, .annoSelector2, .annoSelector3, .annoSelector4, .annoSelector5, .annoSelector6, .annoSelector7, .annoClear {
margin: 2px;
height: 28px;
width: 28px;
background: url(../images/icon-meshtoolbox.png) center center no-repeat;
background-size: 20px 20px;
}
.annoSelector1{
background: url(../images/pen24.png) center center no-repeat;
}
.annoSelector2{
background: url(../images/icon-round-rectangle24.png) center center no-repeat;
}
.annoSelector3{
background: url(../images/icon-circle24.png) center center no-repeat;
}
.annoSelector4{
background: url(../images/icon-arrow24.png) center center no-repeat;
}
.annoSelector5{
background: url(../images/icon-eraser24.png) center center no-repeat;
}
.annoSelector6{
background: url(../images/icon-colorpicker24.png) center center no-repeat;
}
.annoSelector7{
background: url(../images/icon-TTL24.png) center center no-repeat;
}
.annoClear{
background: url(../images/icon-clear24.png) center center no-repeat;
}
/* Separator line */
.annoSeparator {
width: 100%;
height: 1px;
background: linear-gradient(to right, transparent 0%, #bbb 20%, #bbb 80%, transparent 100%);
clear: both;
margin: 9px 0 9px 0;
}
/* Color presets section */
.annoColorsSection {
display: flex;
align-items: center;
gap: 3px;
width: 90%;
clear: both;
margin: 3px;
}
.annoColorPreset {
width: 16px;
height: 16px;
border-radius: 2px;
border: 1px solid #bbb;
cursor: pointer;
flex-shrink: 0;
}
.annoColorPreset:hover {
transform: scale(1.1);
}
#ttlMenuSection {
display: none;
}
#annotationMenu.ttl-active #mainMenuSection {
display: none;
}
#annotationMenu.ttl-active #ttlMenuSection {
display: block;
}
.ttl-options-header {
font-size: 11px;
text-align: center;
margin-bottom: 5px;
color: #666;
}
/* TTL selector styling - matches annoSelector pattern */
.ttlSelector0, .ttlSelector1, .ttlSelector2, .ttlSelector3, .ttlSelector4, .ttlBack {
margin: 2px;
height: 28px;
width: 28px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: bold;
color: #333;
background-color: #f5f5f5;
border: 1px solid #ccc;
border-radius: 3px;
}
.ttlSelector0 {
font-size: 14px;
}
.ttlBack {
font-size: 16px;
background-color: #e8e8e8;
}
/* Selected state for TTL options */
.uiSelector.selected .ttlSelector0,
.uiSelector.selected .ttlSelector1,
.uiSelector.selected .ttlSelector2,
.uiSelector.selected .ttlSelector3,
.uiSelector.selected .ttlSelector4 {
background-color: #4CAF50;
color: white;
border-color: #45a049;
}
.ttl-indicator {
position: absolute;
bottom: 2px;
right: 2px;
background: rgba(0,0,0,0.8);
color: white;
font-size: 9px;
font-weight: bold;
padding: 1px 3px;
border-radius: 2px;
pointer-events: none;
z-index: 1;
}
/* Ensure the parent button has relative positioning */
#annoToolButton7 {
position: relative;
}
.annoColorPreset.selected-color {
border: 2px solid #818181 !important;
box-shadow: 2px 2px 3px rgb(129 129 129 / 37%);
transform: scale(1.1);
}
.custom-color {
border: 2px solid #333 !important; /* Distinguish custom colors */
}
.annotationDefault3{
padding: 0px 2px 3px 2px;
}
.mobileAnnoMenuFix{
margin: auto;
width: 40px;
}
.ttl-indicator-mobile {
position: absolute;
bottom: 2px;
background: rgba(0, 0, 0, 0.8);
color: white;
font-size: 9px;
font-weight: bold;
padding: 1px 3px;
border-radius: 2px;
pointer-events: none;
z-index: 1;
}

View File

@ -2659,7 +2659,7 @@ a {
-ms-grid-row: 4;
}
#DeskRunButton, #DeskChatButton, #DeskNotifyButton, #DeskOpenWebButton, #DeskBackgroundButton, #DeskSaveImageButton, #DeskRecordButton, #DeskClipboardInButton, #DeskClipboardOutButton, #DeskRefreshButton, #DeskLockButton, #DeskInputLockedButton, #DeskInputUnLockedButton, #DeskGuestShareButton, #DeskMonitorSelectionSpan {
#DeskRunButton, #DeskChatButton, #DeskNotifyButton, #DeskOpenWebButton, #DeskBackgroundButton, #DeskSaveImageButton, #DeskRecordButton, #DeskClipboardInButton, #DeskClipboardOutButton, #DeskRefreshButton, #DeskLockButton, #DeskInputLockedButton, #DeskInputUnLockedButton, #DeskGuestShareButton, #DeskMonitorSelectionSpan,#DeskAnnotateButton,#DeskAnnotateMenuButton {
float: right;
margin-top: 1px;
margin-right: 4px;

View File

@ -13,6 +13,7 @@
<link rel="icon" type="image/png" sizes="32x32" href="{{{domainurl}}}favicon-32x32.png">
<link rel="apple-touch-icon" href="/favicon-303x303.png" />
<link type="text/css" href="styles/xterm.css" media="screen" rel="stylesheet" title="CSS" />
<link type="text/css" href="styles/annotation-style.css" media="screen" rel="stylesheet" title="CSS" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="#ffffff">
<meta name="apple-mobile-web-app-title" content="{{{title}}}">
@ -30,6 +31,7 @@
<script type="text/javascript" src="scripts/zlib-adler32{{{min}}}.js"></script>
<script type="text/javascript" src="scripts/zlib-crc32{{{min}}}.js"></script>
<script keeplink=1 type="text/javascript" src="scripts/filesaver.min.js"></script>
<script src="scripts/annotation-ui.js"></script>
<meta name="msapplication-TileColor" content="#00aba9">
<meta name="theme-color" content="#ffffff">
<title>{{{title}}}</title>
@ -944,6 +946,75 @@
<div id=deskarea4 style="position:absolute;bottom:0px;width:100%;height:32px">
<div style=padding-top:2px;padding-bottom:2px;background:#C0C0C0>
<div style=float:right;text-align:right;padding-right:2px>
<span id=DeskAnnotateMenuButton title="Annotations menu"><img class="desktopButtons" id=DeskAnnotateMenuButtonImage src='images/icon-meshtoolbox.png' onclick="AnnotationUI && AnnotationUI.toggleAnnotateMenu()" height=16 width=16 />
<div id=annotationMenu style="display:none">
<!-- Main tools section - 3x2 grid -->
<div id="mainMenuSection">
<div class="annoToolsSection">
<div tabindex=0 id=annoToolButton1 class=uiSelector onclick="AnnotationUI && AnnotationUI.selectAnnotationTool(1)" title="Pen tool">
<div class="annoSelector1"></div>
</div>
<div tabindex=0 id=annoToolButton2 class=uiSelector onclick="AnnotationUI && AnnotationUI.selectAnnotationTool(2)" title="Rectangle tool">
<div class="annoSelector2"></div>
</div>
<div tabindex=0 id=annoToolButton3 class=uiSelector onclick="AnnotationUI && AnnotationUI.selectAnnotationTool(3)" title="Circle tool">
<div class="annoSelector3"></div>
</div>
<div tabindex=0 id=annoToolButton4 class=uiSelector onclick="AnnotationUI && AnnotationUI.selectAnnotationTool(4)" title="Arrow tool">
<div class="annoSelector4"></div>
</div>
<div tabindex=0 id=annoToolButton5 class=uiSelector onclick="AnnotationUI && AnnotationUI.selectAnnotationTool(5)" title="Eraser tool">
<div class="annoSelector5"></div>
</div>
<div tabindex=0 id=annoToolButton6 class=uiSelector onclick="AnnotationUI && AnnotationUI.selectAnnotationTool(6)" title="Color picker">
<div class="annoSelector6"></div>
</div>
<div tabindex=0 id=annoToolButton7 class=uiSelector onclick="AnnotationUI && AnnotationUI.selectAnnotationTool(7)" title="Timer/TTL">
<div class="annoSelector7"></div>
<div id="ttlIndicator" class="ttl-indicator">5s</div>
</div>
<div tabindex=0 id=annoClearButton class=uiSelector onclick="AnnotationUI && AnnotationUI.clearAnnotations()" title="Clear all">
<div class="annoClear"></div>
</div>
</div>
</div>
<div id="ttlMenuSection">
<div class="ttl-options-header">Annotation Duration</div>
<div id="ttltoolssection" class="annoToolsSection"> <!-- Reuse the same grid layout -->
<div tabindex=0 id=ttlOption0 class=uiSelector onclick="AnnotationUI._setTtlOption(0)" title="Permanent">
<div class="ttlSelector0">∞</div>
</div>
<div tabindex=0 id=ttlOption1 class=uiSelector onclick="AnnotationUI._setTtlOption(1)" title="5 seconds">
<div class="ttlSelector1">5s</div>
</div>
<div tabindex=0 id=ttlOption2 class=uiSelector onclick="AnnotationUI._setTtlOption(2)" title="10 seconds">
<div class="ttlSelector2">10s</div>
</div>
<div tabindex=0 id=ttlOption3 class=uiSelector onclick="AnnotationUI._setTtlOption(3)" title="15 seconds">
<div class="ttlSelector3">15s</div>
</div>
<div tabindex=0 id=ttlOption4 class=uiSelector onclick="AnnotationUI._setTtlOption(4)" title="20 seconds">
<div class="ttlSelector4">20s</div>
</div>
<div tabindex=0 id=ttlBackButton class=uiSelector onclick="AnnotationUI._showMainMenu()" title="Back to main menu">
<div class="ttlBack">←</div>
</div>
</div>
</div>
<!-- Separator line -->
<div class="annoSeparator"></div>
<!-- Color presets section - horizontal row -->
<div class="annoColorsSection">
<div tabindex=0 id=annoColorPreset1 class="annoColorPreset selected-color" onclick="AnnotationUI && AnnotationUI.selectAnnotationColor('#215587')" title="Default Blue" style="background:#215587"></div>
<div tabindex=0 id=annoColorPreset2 class="annoColorPreset" onclick="AnnotationUI && AnnotationUI.selectAnnotationColor('#27ae60')" title="Green" style="background:#27ae60"></div>
<div tabindex=0 id=annoColorPreset3 class="annoColorPreset" onclick="AnnotationUI && AnnotationUI.selectAnnotationColor('#c0392b')" title="Red" style="background:#c0392b"></div>
<div tabindex=0 id=annoColorPreset4 class="annoColorPreset" onclick="AnnotationUI && AnnotationUI.selectAnnotationColor('#d35400')" title="Orange" style="background:#d35400"></div>
</div>
</div>
</span>
<span id=DeskAnnotateButton title="On-screen Annotations"><img class="desktopButtons" id=DeskAnnotateButtonImage src='images/icon-annotate-tool.png' onclick="AnnotationUI && AnnotationUI.toggle()" height=16 width=16 /></span>
<span id=DeskLockButton><img src='images/icon-lock.png' onclick=deviceLockFunction() height=16 width=16 style=padding-top:5px;cursor:pointer /></span>
<span id=DeskChatButton><img src='images/icon-chat.png' onclick=deviceChat(event) height=16 width=16 style=padding-top:5px;cursor:pointer /></span>&nbsp;
<span id=DeskToastButton><img src='images/icon-notify.png' onclick=deviceToastFunction() height=16 width=16 style=padding-top:5px;cursor:pointer /></span>&nbsp;
@ -1285,6 +1356,7 @@
<audio id="chimes"><source src="sounds/chimes.mp3" type="audio/mp3" /></audio>
<iframe name="fileUploadFrame" style=display:none></iframe>
<script>
if(AnnotationUI){AnnotationUI.setMobileMode(true);}
'use strict';
var random = '{{{randomlength}}}' // Random length string for BREACH mitigation
@ -2101,6 +2173,8 @@
if (currentNode == node) { updateDeviceDetails(); }
//if ((currentNode == node) && (xxdialogMode != null) && (xxdialogTag == '@xxmap')) { p10showNodeLocationDialog(); }
if (message.event.node.caps != null) { node.caps = message.event.node.caps; }
if (message.event.node.annotationPermission != null) { node.annotationPermission = message.event.node.annotationPermission; }
}
break;
}
@ -2208,6 +2282,13 @@
// TODO: Disconnect
break;
}
case 'annotationAck': {
// Forward annotation acknowledgments to annotation UI
if (typeof AnnotationUI !== 'undefined' && AnnotationUI._onAnnoAck) {
AnnotationUI._onAnnoAck(message);
}
break;
}
default:
//console.log('Unknown message.event.action', message.event.action);
break;
@ -4620,6 +4701,26 @@
QV('deskActionsBtn', meshrights & 8);
Q('DeskControl').checked = ((meshrights & 8) != 0);
if (online == false) QV('DeskTools', false);
// Annotation button (Android only, requires online connection and desktop session)
var showAnnotateButton = false;
if (currentNode && currentNode.agent && currentNode.agent.id === 14) { // Android
if (online && (deskState == 3) && (desktop != null) && (desktop.contype == 1)) {
// Only show if annotation caps available AND screen capture is actually streaming
if (currentNode.caps && currentNode.caps.annotation === true && desktop.m._AnnotateCanStart) {
showAnnotateButton = true;
if (Q('DeskAnnotateMenuButton')) QC('DeskAnnotateMenuButton').add('annotationDefault3');
if (Q('DeskAnnotateButton')) QC('DeskAnnotateButton').add('annotationDefault3');
}
}
}
QV('DeskAnnotateButton', showAnnotateButton);
QE('DeskAnnotateButton', showAnnotateButton); // Enable/disable based on same logic
if(showAnnotateButton && AnnotationUI && AnnotationUI.isActive()){
QV('DeskAnnotateMenuButton', true);
}else{
QV('DeskAnnotateMenuButton',false);
}
}
// Used to translate incoming agent console messages
@ -4632,6 +4733,38 @@
return r.split('\n').join('<br />') + '<br /><br />';
}
function bindAnnotations(viewer) {
if (!window.AnnotationUI) return;
// Keep everything centralized in AnnotationUI
AnnotationUI.setContext({
getCurrentNode: () => currentNode,
send: (o) => meshserver.send(o),
getViewer: () => viewer,
getCanvas: () => Q('Desk') // the canvas id used by your viewer here
});
AnnotationUI.hookDesktop(viewer);
try {
const prev = viewer.onStateChanged;
viewer.onStateChanged = function () {
// call existing handler first
if (typeof prev === 'function') { try { prev.apply(this, arguments); } catch {} }
// if viewer is gone or state is disconnected, unhook safely
try {
const st = viewer && viewer.State;
if (!viewer || st === 0) { AnnotationUI.unhook(); }
} catch {}
};
} catch {}
if (typeof connectivity === 'number' && typeof meshrights === 'number') {
AnnotationUI.updateNodeContext(currentNode, connectivity, meshrights);
} else {
AnnotationUI.updateNodeContext(currentNode);
}
}
function connectDesktop(e, contype, tsid, consent) {
setSessionActivity();
QV('p11DeskSessionSelector', false);
@ -4684,6 +4817,12 @@
desktop.m.FrameRateTimer = desktopsettings.framerate;
desktop.m.onDisplayinfo = deskDisplayInfo;
desktop.m.onScreenSizeChange = function (o, x, y) { if (fullscreen) { QS('deskarea3').width = (x * fullscreenzoom) + 'px'; QS('deskarea3').height = (y * fullscreenzoom) + 'px'; } deskAdjust(); }
desktop.m.onFirstTileDrawn = function() {
if (desktop.m._AnnotateCanStart) return; // Only run once
desktop.m._AnnotateCanStart = true;
bindAnnotations(desktop);
updateDesktopButtons();
};
desktop.Start(desktopNode._id);
desktop.contype = 1;
} else if (contype == 3) {
@ -4691,9 +4830,11 @@
meshserver.send({ action: 'msg', type: 'userSessions', nodeid: currentNode._id, tag: consent });
}
} else {
// Disconnect and clean up the remote desktop
desktop.Stop();
desktopNode = desktop = null;
if (window.AnnotationUI) { AnnotationUI.unhook(); }
}
}

View File

@ -15,6 +15,7 @@
<link type="text/css" href="styles/xterm.css" media="screen" rel="stylesheet" title="CSS" />
<link type="text/css" href="styles/flatpickr.min.css" media="screen" rel="stylesheet" title="CSS">
<link type="text/css" href="styles/custom.css" media="screen" rel="stylesheet" title="CSS" />
<link type="text/css" href="styles/annotation-style.css" media="screen" rel="stylesheet" title="CSS" />
<link rel="apple-touch-icon" href="/favicon-303x303.png" />
<script type="text/javascript" src="scripts/common-0.0.1{{{min}}}.js"></script>
<script type="text/javascript" src="scripts/meshcentral{{{min}}}.js"></script>
@ -51,6 +52,7 @@
<script keeplink=1 type="text/javascript" src="scripts/purify{{{min}}}.js"></script>
<script keeplink=1 type="text/javascript" src="scripts/marked{{{min}}}.js"></script>
<script type="text/javascript" src="scripts/custom.js"></script>
<script src="scripts/annotation-ui.js"></script>
<title>{{{title}}}</title>
</head>
<body id="body" oncontextmenu="handleContextMenu(event)" style="display:none;min-width:495px" onload="if (typeof(startup) !== 'undefined') startup();">
@ -771,6 +773,75 @@
<span id=DeskInputLockedButton title="Remote input is locked"><img class="desktopButtons" id=DeskInputLockedButtonImage src='images/icon-keylock-red.png' onclick=deskInputLockFunction(0) height=16 width=16 /></span>
<span id=DeskInputUnLockedButton title="Remote input is unlocked"><img class="desktopButtons" id=DeskInputUnLockedButtonImage src='images/icon-keylock.png' onclick=deskInputLockFunction(1) height=16 width=16 /></span>
<span id=DeskGuestShareButton title="Share the device with a guest"><img class="desktopButtons" id=DeskGuestShareButtonImage src='images/icon-share2.png' onclick=showShareDevice() height=16 width=16 /></span>
<span id=DeskAnnotateButton title="On-screen Annotations"><img class="desktopButtons" id=DeskAnnotateButtonImage src='images/icon-annotate-tool.png' onclick="AnnotationUI && AnnotationUI.toggle()" height=16 width=16 /></span>
<span id=DeskAnnotateMenuButton title="Annotations menu"><img class="desktopButtons" id=DeskAnnotateMenuButtonImage src='images/icon-meshtoolbox.png' onclick="AnnotationUI && AnnotationUI.toggleAnnotateMenu()" height=16 width=16 />
<div id=annotationMenu style="display:none">
<!-- Main tools section - 3x2 grid -->
<div id="mainMenuSection">
<div class="annoToolsSection">
<div tabindex=0 id=annoToolButton1 class=uiSelector onclick="AnnotationUI && AnnotationUI.selectAnnotationTool(1)" title="Pen tool">
<div class="annoSelector1"></div>
</div>
<div tabindex=0 id=annoToolButton2 class=uiSelector onclick="AnnotationUI && AnnotationUI.selectAnnotationTool(2)" title="Rectangle tool">
<div class="annoSelector2"></div>
</div>
<div tabindex=0 id=annoToolButton3 class=uiSelector onclick="AnnotationUI && AnnotationUI.selectAnnotationTool(3)" title="Circle tool">
<div class="annoSelector3"></div>
</div>
<div tabindex=0 id=annoToolButton4 class=uiSelector onclick="AnnotationUI && AnnotationUI.selectAnnotationTool(4)" title="Arrow tool">
<div class="annoSelector4"></div>
</div>
<div tabindex=0 id=annoToolButton5 class=uiSelector onclick="AnnotationUI && AnnotationUI.selectAnnotationTool(5)" title="Eraser tool">
<div class="annoSelector5"></div>
</div>
<div tabindex=0 id=annoToolButton6 class=uiSelector onclick="AnnotationUI && AnnotationUI.selectAnnotationTool(6)" title="Color picker">
<div class="annoSelector6"></div>
</div>
<div tabindex=0 id=annoToolButton7 class=uiSelector onclick="AnnotationUI && AnnotationUI.selectAnnotationTool(7)" title="Timer/TTL">
<div class="annoSelector7"></div>
<div id="ttlIndicator" class="ttl-indicator">5s</div>
</div>
<div tabindex=0 id=annoClearButton class=uiSelector onclick="AnnotationUI && AnnotationUI.clearAnnotations()" title="Clear all">
<div class="annoClear"></div>
</div>
</div>
</div>
<div id="ttlMenuSection">
<div class="ttl-options-header">Annotation Duration</div>
<div id="ttltoolssection" class="annoToolsSection"> <!-- Reuse the same grid layout -->
<div tabindex=0 id=ttlOption0 class=uiSelector onclick="AnnotationUI._setTtlOption(0)" title="Permanent">
<div class="ttlSelector0">∞</div>
</div>
<div tabindex=0 id=ttlOption1 class=uiSelector onclick="AnnotationUI._setTtlOption(1)" title="5 seconds">
<div class="ttlSelector1">5s</div>
</div>
<div tabindex=0 id=ttlOption2 class=uiSelector onclick="AnnotationUI._setTtlOption(2)" title="10 seconds">
<div class="ttlSelector2">10s</div>
</div>
<div tabindex=0 id=ttlOption3 class=uiSelector onclick="AnnotationUI._setTtlOption(3)" title="15 seconds">
<div class="ttlSelector3">15s</div>
</div>
<div tabindex=0 id=ttlOption4 class=uiSelector onclick="AnnotationUI._setTtlOption(4)" title="20 seconds">
<div class="ttlSelector4">20s</div>
</div>
<div tabindex=0 id=ttlBackButton class=uiSelector onclick="AnnotationUI._showMainMenu()" title="Back to main menu">
<div class="ttlBack">←</div>
</div>
</div>
</div>
<!-- Separator line -->
<div class="annoSeparator"></div>
<!-- Color presets section - horizontal row -->
<div class="annoColorsSection">
<div tabindex=0 id=annoColorPreset1 class="annoColorPreset selected-color" onclick="AnnotationUI && AnnotationUI.selectAnnotationColor('#215587')" title="Default Blue" style="background:#215587"></div>
<div tabindex=0 id=annoColorPreset2 class="annoColorPreset" onclick="AnnotationUI && AnnotationUI.selectAnnotationColor('#27ae60')" title="Green" style="background:#27ae60"></div>
<div tabindex=0 id=annoColorPreset3 class="annoColorPreset" onclick="AnnotationUI && AnnotationUI.selectAnnotationColor('#c0392b')" title="Red" style="background:#c0392b"></div>
<div tabindex=0 id=annoColorPreset4 class="annoColorPreset" onclick="AnnotationUI && AnnotationUI.selectAnnotationColor('#d35400')" title="Orange" style="background:#d35400"></div>
</div>
</div>
</span>
</div>
<div class="toright2"><span id=DeskMonitorSelectionSpan></span></div>
<div>
@ -3629,6 +3700,10 @@
if ((xxcurrentView == 20) && (currentMesh != null) && (currentMesh._id == node.meshid)) { mainUpdate(4096); }
if ((currentNode == node) && (xxdialogMode != null) && (xxdialogTag == '@xxmap')) { p10showNodeLocationDialog(); }
// If annotation permissions exist add - update those
if (message.event.node.caps != null) { node.caps = message.event.node.caps; }
if (message.event.node.annotationPermission != null) { node.annotationPermission = message.event.node.annotationPermission; }
}
break;
}
@ -3868,6 +3943,13 @@
try { pluginHandler[message.event.plugin][message.event.pluginaction](message); } catch (e) { console.log("PluginHandler could not event message: ", e); }
break;
}
case 'annotationAck': {
// Forward annotation acknowledgments to annotation UI
if (typeof AnnotationUI !== 'undefined' && AnnotationUI._onAnnoAck) {
AnnotationUI._onAnnoAck(message);
}
break;
}
default:
//console.log('Unknown message.event.action', message.event.action);
break;
@ -7815,6 +7897,8 @@
} else {
QV('DeskGuestShareButton', false);
}
}
if ((node.mtype == 4) && (connectivity & 1)) {
if (node.porttype == 'PDU') {
@ -8540,6 +8624,7 @@
xxdialogTag = { rangeStart: rangeStartTime, rangeEnd: rangeEndTime, start: startTime };
}
function showShareDeviceValidate() {
if (currentNode.agent.caps & 1) { QV('d2userConsentSelector', Q('d2shareType').value < 8); }
QV('d2httpPortSelector', Q('d2shareType').value == 8);
@ -9325,8 +9410,27 @@
QV('DeskRefreshButton', (deskState == 3) && (desktop.contype == 1))
if (rights & 8) { Q('DeskControl').checked = (getstore('DeskControl', 1) == 1); } else { Q('DeskControl').checked = false; }
QS('DeskControlSpan').color = Q('DeskControl').checked?null:'red';
if (online == false) QV('DeskTools', false);
// Annotation button (Android only, requires online connection and desktop session)
var showAnnotateButton = false;
if (currentNode && currentNode.agent && currentNode.agent.id === 14) { // Android
if (online && (deskState == 3) && (desktop != null) && (desktop.contype == 1)) {
// Only show if annotation caps available AND screen capture is actually streaming
if (currentNode.caps && currentNode.caps.annotation === true && desktop.m._AnnotateCanStart) {
showAnnotateButton = true;
}
}
}
QV('DeskAnnotateButton', showAnnotateButton);
QE('DeskAnnotateButton', showAnnotateButton); // Enable/disable based on same logic
if(showAnnotateButton && AnnotationUI && AnnotationUI.isActive()){
QV('DeskAnnotateMenuButton', true);
}else{
QV('DeskAnnotateMenuButton',false);
}
}
// Debug
@ -9342,7 +9446,40 @@
if (msgid && (msgid < agentConsoleMessages.length)) { r = EscapeHtml(format(agentConsoleMessages[msgid], (msgargs[0]), (msgargs[1]), (msgargs[2]))); } else { r = EscapeHtml(msg); }
return r.split('\n').join('<br />') + '<br /><br />';
}
function bindAnnotations(viewer) {
if (!window.AnnotationUI) return;
// Keep everything centralized in AnnotationUI
AnnotationUI.setContext({
getCurrentNode: () => currentNode,
send: (o) => meshserver.send(o),
getViewer: () => viewer,
getCanvas: () => Q('Desk') // the canvas id used by your viewer here
});
AnnotationUI.hookDesktop(viewer);
try {
const prev = viewer.onStateChanged;
viewer.onStateChanged = function () {
// call existing handler first
if (typeof prev === 'function') { try { prev.apply(this, arguments); } catch {} }
// if viewer is gone or state is disconnected, unhook safely
try {
const st = viewer && viewer.State;
if (!viewer || st === 0) { AnnotationUI.unhook(); }
} catch {}
};
} catch {}
if (typeof connectivity === 'number' && typeof meshrights === 'number') {
AnnotationUI.updateNodeContext(currentNode, connectivity, meshrights);
} else {
AnnotationUI.updateNodeContext(currentNode);
}
}
function connectDesktop(e, contype, tsid, consent) {
if (xxdialogMode) return;
if ((e != null) && (e.shiftKey != false) && (contype == 3)) { contype = 1; } // If the shift key is not pressed, don't try to ask for session list.
@ -9482,7 +9619,13 @@
if (desktopsettings.remotekeymap == true) { desktop.m.remoteKeyMap = desktopsettings.remotekeymap; }
//desktop.m.onDisplayinfo = deskDisplayInfo;
desktop.m.onScreenSizeChange = deskAdjust;
desktop.Start(desktopNode._id);
desktop.m.onFirstTileDrawn = function() {
if (desktop.m._AnnotateCanStart) return; // Only run once
desktop.m._AnnotateCanStart = true;
bindAnnotations(desktop);
updateDesktopButtons();
};
desktop.Start(desktopNode._id);
desktop.latency.callback = function(ms) { /* console.log('latency', ms); */ updateSessionTime(); };
desktop.contype = 1;
} else if (contype == 3) {
@ -9514,8 +9657,10 @@
desktop.Stop();
webRtcDesktopReset();
desktopNode = desktop = null;
if (window.AnnotationUI) { AnnotationUI.unhook(); }
if (pluginHandler != null) { pluginHandler.callHook('onDesktopDisconnect'); }
}
}
function askRdpCredentials() {

View File

@ -19,6 +19,7 @@
<link href="styles/select2.min.css" rel="stylesheet" title="CSS">
<link href="styles/select2-bootstrap-5-theme.min.css" rel="stylesheet" title="CSS">
<link type="text/css" href="styles/custom.css" media="screen" rel="stylesheet" title="CSS" />
<link type="text/css" href="styles/annotation-style.css" media="screen" rel="stylesheet" title="CSS" />
<link rel="apple-touch-icon" href="/favicon-303x303.png" />
<script type="text/javascript" src="scripts/common-0.0.1{{{min}}}.js"></script>
<script type="text/javascript" src="scripts/meshcentral{{{min}}}.js"></script>
@ -60,6 +61,7 @@
<script keeplink=1 type="text/javascript" src="scripts/purify{{{min}}}.js"></script>
<script keeplink=1 type="text/javascript" src="scripts/marked{{{min}}}.js"></script>
<script type="text/javascript" src="scripts/custom.js"></script>
<script src="scripts/annotation-ui.js"></script>
<title>{{{title}}}</title>
</head>
@ -976,6 +978,75 @@
<span id="DeskLatency" style="width:70px" class="text-center" title="Desktop Session Latency"></span>
<span id="DeskTimer" style="width:70px" class="text-center" title="Session time"></span>
<input id=DeskToolsButton type=button class="btn btn-primary btn-sm mx-1" value=Tools title="Toggle tools view" onkeypress="return false" onkeydown="return false" onclick="toggleDeskTools()" />
<span id=DeskAnnotateMenuButton title="Annotations menu"><img class="desktopButtons" id=DeskAnnotateMenuButtonImage src='images/icon-meshtoolbox.png' onclick="AnnotationUI && AnnotationUI.toggleAnnotateMenu()" height=16 width=16 />
<div id=annotationMenu style="display:none">
<!-- Main tools section - 3x2 grid -->
<div id="mainMenuSection">
<div class="annoToolsSection">
<div tabindex=0 id=annoToolButton1 class=uiSelector onclick="AnnotationUI && AnnotationUI.selectAnnotationTool(1)" title="Pen tool">
<div class="annoSelector1"></div>
</div>
<div tabindex=0 id=annoToolButton2 class=uiSelector onclick="AnnotationUI && AnnotationUI.selectAnnotationTool(2)" title="Rectangle tool">
<div class="annoSelector2"></div>
</div>
<div tabindex=0 id=annoToolButton3 class=uiSelector onclick="AnnotationUI && AnnotationUI.selectAnnotationTool(3)" title="Circle tool">
<div class="annoSelector3"></div>
</div>
<div tabindex=0 id=annoToolButton4 class=uiSelector onclick="AnnotationUI && AnnotationUI.selectAnnotationTool(4)" title="Arrow tool">
<div class="annoSelector4"></div>
</div>
<div tabindex=0 id=annoToolButton5 class=uiSelector onclick="AnnotationUI && AnnotationUI.selectAnnotationTool(5)" title="Eraser tool">
<div class="annoSelector5"></div>
</div>
<div tabindex=0 id=annoToolButton6 class=uiSelector onclick="AnnotationUI && AnnotationUI.selectAnnotationTool(6)" title="Color picker">
<div class="annoSelector6"></div>
</div>
<div tabindex=0 id=annoToolButton7 class=uiSelector onclick="AnnotationUI && AnnotationUI.selectAnnotationTool(7)" title="Timer/TTL">
<div class="annoSelector7"></div>
<div id="ttlIndicator" class="ttl-indicator">5s</div>
</div>
<div tabindex=0 id=annoClearButton class=uiSelector onclick="AnnotationUI && AnnotationUI.clearAnnotations()" title="Clear all">
<div class="annoClear"></div>
</div>
</div>
</div>
<div id="ttlMenuSection">
<div class="ttl-options-header">Annotation Duration</div>
<div id="ttltoolssection" class="annoToolsSection"> <!-- Reuse the same grid layout -->
<div tabindex=0 id=ttlOption0 class=uiSelector onclick="AnnotationUI._setTtlOption(0)" title="Permanent">
<div class="ttlSelector0">∞</div>
</div>
<div tabindex=0 id=ttlOption1 class=uiSelector onclick="AnnotationUI._setTtlOption(1)" title="5 seconds">
<div class="ttlSelector1">5s</div>
</div>
<div tabindex=0 id=ttlOption2 class=uiSelector onclick="AnnotationUI._setTtlOption(2)" title="10 seconds">
<div class="ttlSelector2">10s</div>
</div>
<div tabindex=0 id=ttlOption3 class=uiSelector onclick="AnnotationUI._setTtlOption(3)" title="15 seconds">
<div class="ttlSelector3">15s</div>
</div>
<div tabindex=0 id=ttlOption4 class=uiSelector onclick="AnnotationUI._setTtlOption(4)" title="20 seconds">
<div class="ttlSelector4">20s</div>
</div>
<div tabindex=0 id=ttlBackButton class=uiSelector onclick="AnnotationUI._showMainMenu()" title="Back to main menu">
<div class="ttlBack">←</div>
</div>
</div>
</div>
<!-- Separator line -->
<div class="annoSeparator"></div>
<!-- Color presets section - horizontal row -->
<div class="annoColorsSection">
<div tabindex=0 id=annoColorPreset1 class="annoColorPreset selected-color" onclick="AnnotationUI && AnnotationUI.selectAnnotationColor('#215587')" title="Default Blue" style="background:#215587"></div>
<div tabindex=0 id=annoColorPreset2 class="annoColorPreset" onclick="AnnotationUI && AnnotationUI.selectAnnotationColor('#27ae60')" title="Green" style="background:#27ae60"></div>
<div tabindex=0 id=annoColorPreset3 class="annoColorPreset" onclick="AnnotationUI && AnnotationUI.selectAnnotationColor('#c0392b')" title="Red" style="background:#c0392b"></div>
<div tabindex=0 id=annoColorPreset4 class="annoColorPreset" onclick="AnnotationUI && AnnotationUI.selectAnnotationColor('#d35400')" title="Orange" style="background:#d35400"></div>
</div>
</div>
</span>
<span id=DeskAnnotateButton title="On-screen Annotations"><img class="desktopButtons" id=DeskAnnotateButtonImage src='images/icon-annotate-tool.png' onclick="AnnotationUI && AnnotationUI.toggle()" height=16 width=16 /></span>
<span id=DeskGuestShareButton title="Share the device with a guest" role="button">
<i class="fa-solid fa-fw fa-share-nodes" id=DeskInputUnLockedButtonImage onclick=showShareDevice()></i>
</span>
@ -4153,7 +4224,8 @@
if ((xxcurrentView == 20) && (currentMesh != null) && (currentMesh._id == node.meshid)) { mainUpdate(4096); }
if ((currentNode == node) && (xxdialogMode != null) && (xxdialogTag == '@xxmap')) { p10showNodeLocationDialog(); }
}
if (message.event.node.caps != null) { node.caps = message.event.node.caps; }
if (message.event.node.annotationPermission != null) { node.annotationPermission = message.event.node.annotationPermission; } }
break;
}
case 'nodemeshchange': {
@ -4392,6 +4464,13 @@
try { pluginHandler[message.event.plugin][message.event.pluginaction](message); } catch (e) { console.log("PluginHandler could not event message: ", e); }
break;
}
case 'annotationAck': {
// Forward annotation acknowledgments to annotation UI
if (typeof AnnotationUI !== 'undefined' && AnnotationUI._onAnnoAck) {
AnnotationUI._onAnnoAck(message);
}
break;
}
default:
//console.log('Unknown message.event.action', message.event.action);
break;
@ -10151,7 +10230,28 @@
if (rights & 8) { Q('DeskControl').checked = (getstore('DeskControl', 1) == 1); } else { Q('DeskControl').checked = false; }
QS('DeskControlSpan').color = Q('DeskControl').checked ? null : 'red';
if (online == false) QV('DeskTools', false);
if (online == false) QV('DeskTools', false);
// Annotation button (Android only, requires online connection and desktop session)
var showAnnotateButton = false;
if (currentNode && currentNode.agent && currentNode.agent.id === 14) { // Android
if (online && (deskState == 3) && (desktop != null) && (desktop.contype == 1)) {
// Only show if annotation caps available AND screen capture is actually streaming
if (currentNode.caps && currentNode.caps.annotation === true && desktop.m._AnnotateCanStart) {
showAnnotateButton = true;
if (Q('DeskAnnotateMenuButton')) QC('DeskAnnotateMenuButton').add('annotationDefault3');
if (Q('DeskAnnotateButton')) QC('DeskAnnotateButton').add('annotationDefault3');
}
}
}
QV('DeskAnnotateButton', showAnnotateButton);
QE('DeskAnnotateButton', showAnnotateButton); // Enable/disable based on same logic
if(showAnnotateButton && AnnotationUI && AnnotationUI.isActive()){
QV('DeskAnnotateMenuButton', true);
}else{
QV('DeskAnnotateMenuButton',false);
}
}
// Debug
@ -10168,6 +10268,39 @@
return r.split('\n').join('<br />') + '<br /><br />';
}
function bindAnnotations(viewer) {
if (!window.AnnotationUI) return;
// Keep everything centralized in AnnotationUI
AnnotationUI.setContext({
getCurrentNode: () => currentNode,
send: (o) => meshserver.send(o),
getViewer: () => viewer,
getCanvas: () => Q('Desk') // the canvas id used by your viewer here
});
AnnotationUI.hookDesktop(viewer);
try {
const prev = viewer.onStateChanged;
viewer.onStateChanged = function () {
// call existing handler first
if (typeof prev === 'function') { try { prev.apply(this, arguments); } catch {} }
// if viewer is gone or state is disconnected, unhook safely
try {
const st = viewer && viewer.State;
if (!viewer || st === 0) { AnnotationUI.unhook(); }
} catch {}
};
} catch {}
if (typeof connectivity === 'number' && typeof meshrights === 'number') {
AnnotationUI.updateNodeContext(currentNode, connectivity, meshrights);
} else {
AnnotationUI.updateNodeContext(currentNode);
}
}
function connectDesktop(e, contype, tsid, consent) {
if (xxdialogMode) return;
if ((e != null) && (e.shiftKey != false) && (contype == 3)) { contype = 1; } // If the shift key is not pressed, don't try to ask for session list.
@ -10307,6 +10440,13 @@
if (desktopsettings.remotekeymap == true) { desktop.m.remoteKeyMap = desktopsettings.remotekeymap; }
//desktop.m.onDisplayinfo = deskDisplayInfo;
desktop.m.onScreenSizeChange = deskAdjust;
desktop.m.onFirstTileDrawn = function() {
if (desktop.m._AnnotateCanStart) return; // Only run once
desktop.m._AnnotateCanStart = true;
bindAnnotations(desktop);
updateDesktopButtons();
};
desktop.Start(desktopNode._id);
desktop.latency.callback = function (ms) { /* console.log('latency', ms); */ updateSessionTime(); };
desktop.contype = 1;
@ -10339,6 +10479,7 @@
desktop.Stop();
webRtcDesktopReset();
desktopNode = desktop = null;
if (window.AnnotationUI) { AnnotationUI.unhook(); }
if (pluginHandler != null) { pluginHandler.callHook('onDesktopDisconnect'); }
}
}