diff --git a/meshcentral-config-schema.json b/meshcentral-config-schema.json
index b43e4fdd..5c5bf358 100644
--- a/meshcentral-config-schema.json
+++ b/meshcentral-config-schema.json
@@ -1173,6 +1173,11 @@
"default": 2,
"description": "Valid numbers are 1 and 2, changes the style of the login page and some secondary pages."
},
+ "showModernUIToggle": {
+ "type": "boolean",
+ "default": false,
+ "description": "When set to true, the user will be able to toggle between the modern and classic UI."
+ },
"title": {
"type": "string",
"default": "MeshCentral",
diff --git a/public/styles/style-bootstrap.css b/public/styles/style-bootstrap.css
index fb00e82c..cf6fe678 100644
--- a/public/styles/style-bootstrap.css
+++ b/public/styles/style-bootstrap.css
@@ -266,6 +266,16 @@ body {
right: 3px;
}
+.textnewui {
+ color: white;
+ font-weight: bold;
+ padding-top: 5px;
+ cursor: pointer;
+ position: absolute;
+ right: 0;
+ margin-right: 10px;
+}
+
.LogoffLinkColor {
color:white;
}
@@ -2877,7 +2887,7 @@ nav .lbbuttonsel2 {
width: 28px;
}
-.viewSelector3 {
+.viewSelector3, .uiSelector7 {
margin-left: 2px;
margin-top: 2px;
background: url(../images/views.png) -56px 0px;
@@ -2977,6 +2987,13 @@ nav .lbbuttonsel2 {
background-color: #AAA;
}
+.uiSelector_end {
+ width: 32px;
+ height: 32px;
+ float: left;
+ margin: 3px;
+}
+
.uiSelectorSel {
background-color: #BBB;
opacity: 0.8;
diff --git a/public/styles/style.css b/public/styles/style.css
index b60847a2..4df3ac40 100644
--- a/public/styles/style.css
+++ b/public/styles/style.css
@@ -263,6 +263,16 @@ body {
right: 3px;
}
+.textnewui {
+ color: white;
+ font-weight: bold;
+ padding-top: 5px;
+ cursor: pointer;
+ position: absolute;
+ right: 0;
+ margin-right: 10px;
+}
+
.LogoffLinkColor {
color:white;
}
@@ -2896,7 +2906,7 @@ a {
width: 28px;
}
-.viewSelector3 {
+.viewSelector3, .uiSelector7 {
margin-left: 2px;
margin-top: 2px;
background: url(../images/views.png) -56px 0px;
@@ -2996,6 +3006,13 @@ a {
background-color: #AAA;
}
+.uiSelector_end {
+ width: 32px;
+ height: 32px;
+ float: left;
+ margin: 3px;
+}
+
.uiSelectorSel {
background-color: #BBB;
opacity: 0.8;
diff --git a/sample-config-advanced.json b/sample-config-advanced.json
index 936b0875..13c53654 100644
--- a/sample-config-advanced.json
+++ b/sample-config-advanced.json
@@ -186,6 +186,7 @@
"domains": {
"": {
"_siteStyle": 2,
+ "_showModernUIToggle": true,
"title": "MyServer",
"title2": "Servername",
"_titlePicture": "title-sample.png",
diff --git a/views/default.handlebars b/views/default.handlebars
index dc73f027..6dc33569 100644
--- a/views/default.handlebars
+++ b/views/default.handlebars
@@ -164,6 +164,9 @@
0
+
+ Try the new MeshCentral UI
+
@@ -199,11 +202,13 @@
+
+
|
@@ -1723,6 +1728,9 @@
});
}
+ // Show the modern ui switcher
+ QV('textnewui', ((features2 & 0x40000000) == 0) ? false : true);
+
// Connect to the mesh server
meshserver = MeshServerCreateControl(domainUrl);
meshserver.onStateChanged = onStateChanged;
@@ -2183,6 +2191,24 @@
QV('body', true);
}
+ function saveUserInterfaceMode() {
+ var nUiViewMode = 2;
+ if (Q('ui1').checked) { nUiViewMode = 3; }
+ if (getstore('uiViewMode', 2) != nUiViewMode) {
+ putstore('uiViewMode', nUiViewMode);
+ reload();
+ }
+ }
+
+ function toggleBootstrapUIMode() {
+ if (xxdialogMode) return;
+ var uiViewMode = getstore('uiViewMode', 2);
+ var x = '
';
+ x += '
';
+ setDialogMode(2, "User Interface", 3, saveUserInterfaceMode, x);
+ QV('uiMenu', false);
+ }
+
function getNodeFromId(id) { if (nodes != null) { for (var i in nodes) { if (nodes[i]._id == id) return nodes[i]; } } return null; }
function reload() {
var x = window.location.href;
diff --git a/views/default3.handlebars b/views/default3.handlebars
index b34bf161..83fd67d5 100644
--- a/views/default3.handlebars
+++ b/views/default3.handlebars
@@ -153,6 +153,9 @@
+
+ Try the new MeshCentral UI
+
@@ -190,6 +193,11 @@
onkeypress="if (event.key == 'Enter') userInterfaceSelectMenu(3)">
+
+
|
@@ -2149,6 +2158,9 @@
});
}
+ // Show the modern ui switcher
+ QV('textnewui', ((features2 & 0x40000000) == 0) ? false : true);
+
// Connect to the mesh server
meshserver = MeshServerCreateControl(domainUrl);
meshserver.onStateChanged = onStateChanged;
@@ -2622,6 +2634,31 @@
QV('body', true);
}
+ function saveUserInterfaceMode() {
+ var nUiViewMode = 3;
+ if (Q('ui0').checked) { nUiViewMode = 2; }
+ if (getstore('uiViewMode', 3) != nUiViewMode) {
+ putstore('uiViewMode', nUiViewMode);
+ // Check if the URL includes 'sitestyle=' and remove it
+ var url = new URL(window.location.href);
+ if (url.searchParams.has('sitestyle')) {
+ url.searchParams.delete('sitestyle');
+ window.history.replaceState({}, document.title, url.toString());
+ }
+ reload();
+ }
+ }
+
+ function toggleBootstrapUIMode() {
+ if (xxdialogMode) return;
+ var uiViewMode = getstore('uiViewMode', 3);
+ var x = '';
+ x += '';
+ setModalContent('xxAddAgent', "User Interface", x);
+ showModal('xxAddAgentModal', 'idx_dlgOkButton', saveUserInterfaceMode);
+ QV('uiMenu', false);
+ }
+
function getNodeFromId(id) { if (nodes != null) { for (var i in nodes) { if (nodes[i]._id == id) return nodes[i]; } } return null; }
function reload() {
var x = window.location.href;
diff --git a/webserver.js b/webserver.js
index 879701d7..4d469d8d 100644
--- a/webserver.js
+++ b/webserver.js
@@ -3184,8 +3184,14 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
if (obj.parent.config.settings && obj.parent.config.settings.webrtcconfig && (typeof obj.parent.config.settings.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(obj.parent.config.settings.webrtcconfig)).replace(/'/g, '%27'); }
else if (args.webrtcconfig && (typeof args.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(args.webrtcconfig)).replace(/'/g, '%27'); }
+ // Load default page style or new modern ui
+ var uiViewMode = 'default';
+ var webstateJSON = JSON.parse(webstate);
+ if (webstateJSON && webstateJSON.uiViewMode == 3) { uiViewMode = 'default3'; }
+ if (domain.sitestyle == 3) { uiViewMode = 'default3'; }
+ if (req.query.sitestyle == 3) { uiViewMode = 'default3'; }
// Refresh the session
- render(dbGetFunc.req, dbGetFunc.res, getRenderPage(((domain.sitestyle == 3) || (req.query.sitestyle == 3) ? 'default3' : 'default'), dbGetFunc.req, domain), getRenderArgs({
+ render(dbGetFunc.req, dbGetFunc.res, getRenderPage(uiViewMode, dbGetFunc.req, domain), getRenderArgs({
authCookie: authCookie,
authRelayCookie: authRelayCookie,
viewmode: viewmode,
@@ -3335,6 +3341,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
if (domain.scrolltotop == true) { features2 += 0x08000000; } // Show the "Scroll to top" button
if (domain.devicesearchbargroupname === true) { features2 += 0x10000000; } // Search bar will find by group name too
if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.duo2factor != false)) && (typeof domain.duo2factor == 'object') && (typeof domain.duo2factor.integrationkey == 'string') && (typeof domain.duo2factor.secretkey == 'string') && (typeof domain.duo2factor.apihostname == 'string')) { features2 += 0x20000000; } // using Duo for 2FA is allowed
+ if (domain.showmodernuitoggle == true) { features2 += 0x40000000; } // Indicates that the new UI should be shown
return { features: features, features2: features2 };
}
@@ -9045,7 +9052,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
}
// Filter the user web site and only output state that we need to keep
- const acceptableUserWebStateStrings = ['webPageStackMenu', 'notifications', 'deviceView', 'nightMode', 'webPageFullScreen', 'search', 'showRealNames', 'sort', 'deskAspectRatio', 'viewsize', 'DeskControl', 'uiMode', 'footerBar','loctag','theme','lastThemes'];
+ const acceptableUserWebStateStrings = ['webPageStackMenu', 'notifications', 'deviceView', 'nightMode', 'webPageFullScreen', 'search', 'showRealNames', 'sort', 'deskAspectRatio', 'viewsize', 'DeskControl', 'uiMode', 'footerBar','loctag','theme','lastThemes','uiViewMode'];
const acceptableUserWebStateDesktopStrings = ['encoding', 'showfocus', 'showmouse', 'showcad', 'limitFrameRate', 'noMouseRotate', 'quality', 'scaling', 'agentencoding']
obj.filterUserWebState = function (state) {
if (typeof state == 'string') { try { state = JSON.parse(state); } catch (ex) { return null; } }