@underpostnet/underpost 2.98.1 → 2.99.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -3
- package/bin/build.js +5 -5
- package/bin/deploy.js +10 -1
- package/cli.md +109 -110
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +4 -4
- package/package.json +1 -2
- package/src/api/user/user.router.js +7 -40
- package/src/cli/baremetal.js +67 -71
- package/src/cli/cloud-init.js +11 -12
- package/src/cli/cluster.js +22 -24
- package/src/cli/db.js +43 -50
- package/src/cli/deploy.js +162 -60
- package/src/cli/env.js +20 -5
- package/src/cli/fs.js +19 -21
- package/src/cli/index.js +35 -32
- package/src/cli/lxd.js +5 -5
- package/src/cli/monitor.js +66 -88
- package/src/cli/repository.js +7 -6
- package/src/cli/run.js +369 -261
- package/src/cli/secrets.js +3 -3
- package/src/cli/ssh.js +31 -32
- package/src/cli/static.js +1 -1
- package/src/cli/test.js +6 -7
- package/src/client/components/core/Content.js +42 -40
- package/src/client/components/core/FullScreen.js +202 -9
- package/src/client/components/core/PanelForm.js +4 -2
- package/src/client/components/core/VanillaJs.js +80 -29
- package/src/index.js +49 -32
- package/src/runtime/express/Express.js +7 -6
- package/src/server/auth.js +6 -1
- package/src/server/backup.js +11 -1
- package/src/server/conf.js +4 -4
- package/src/{cli → server}/cron.js +56 -29
- package/src/server/dns.js +39 -31
- package/src/server/peer.js +2 -2
- package/src/server/process.js +2 -2
- package/src/server/proxy.js +8 -7
- package/src/server/runtime.js +4 -7
- package/src/server/start.js +28 -15
- package/src/ws/IoServer.js +2 -3
- package/src/cli/script.js +0 -85
- package/src/monitor.js +0 -34
package/src/cli/secrets.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import dotenv from 'dotenv';
|
|
8
8
|
import { shellExec } from '../server/process.js';
|
|
9
9
|
import fs from 'fs-extra';
|
|
10
|
-
import
|
|
10
|
+
import Underpost from '../index.js';
|
|
11
11
|
|
|
12
12
|
dotenv.config();
|
|
13
13
|
|
|
@@ -41,7 +41,7 @@ class UnderpostSecret {
|
|
|
41
41
|
createFromEnvFile(envPath) {
|
|
42
42
|
const envObj = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
|
|
43
43
|
for (const key of Object.keys(envObj)) {
|
|
44
|
-
|
|
44
|
+
Underpost.secret.docker.set(key, envObj[key]);
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
set(key, value) {
|
|
@@ -61,7 +61,7 @@ class UnderpostSecret {
|
|
|
61
61
|
createFromEnvFile(envPath) {
|
|
62
62
|
const envObj = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
|
|
63
63
|
for (const key of Object.keys(envObj)) {
|
|
64
|
-
|
|
64
|
+
Underpost.env.set(key, envObj[key]);
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
67
|
},
|
package/src/cli/ssh.js
CHANGED
|
@@ -5,11 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { generateRandomPasswordSelection } from '../client/components/core/CommonJs.js';
|
|
8
|
-
import Dns from '../server/dns.js';
|
|
9
8
|
import { pbcopy, shellExec } from '../server/process.js';
|
|
10
9
|
import { loggerFactory } from '../server/logger.js';
|
|
11
10
|
import fs from 'fs-extra';
|
|
12
|
-
import
|
|
11
|
+
import Underpost from '../index.js';
|
|
13
12
|
|
|
14
13
|
const logger = loggerFactory(import.meta);
|
|
15
14
|
|
|
@@ -252,17 +251,17 @@ EOF`);
|
|
|
252
251
|
|
|
253
252
|
// Set defaults
|
|
254
253
|
if (!options.user) options.user = 'root';
|
|
255
|
-
if (!options.host) options.host = await
|
|
254
|
+
if (!options.host) options.host = await Underpost.dns.getPublicIp();
|
|
256
255
|
if (!options.password) options.password = options.disablePassword ? '' : generateRandomPasswordSelection(16);
|
|
257
256
|
if (!options.groups) options.groups = 'wheel';
|
|
258
257
|
if (!options.port) options.port = 22; // Handle connect uri
|
|
259
258
|
|
|
260
|
-
const userHome =
|
|
259
|
+
const userHome = Underpost.ssh.getUserHome(options.user);
|
|
261
260
|
options.userHome = userHome;
|
|
262
261
|
|
|
263
262
|
// Load config and override password and host if user exists in config
|
|
264
263
|
if (options.deployId) {
|
|
265
|
-
const config =
|
|
264
|
+
const config = Underpost.ssh.loadConfigNode(options.deployId);
|
|
266
265
|
confNode = config.confNode;
|
|
267
266
|
confNodePath = config.confNodePath;
|
|
268
267
|
|
|
@@ -335,7 +334,7 @@ EOF`);
|
|
|
335
334
|
// Remove the private key copy folder and update config only if deployId is provided
|
|
336
335
|
if (options.deployId) {
|
|
337
336
|
if (!confNode) {
|
|
338
|
-
const config =
|
|
337
|
+
const config = Underpost.ssh.loadConfigNode(options.deployId);
|
|
339
338
|
confNode = config.confNode;
|
|
340
339
|
confNodePath = config.confNodePath;
|
|
341
340
|
}
|
|
@@ -347,7 +346,7 @@ EOF`);
|
|
|
347
346
|
}
|
|
348
347
|
|
|
349
348
|
delete confNode.users[options.user];
|
|
350
|
-
|
|
349
|
+
Underpost.ssh.saveConfigNode(confNodePath, confNode);
|
|
351
350
|
}
|
|
352
351
|
|
|
353
352
|
logger.info(`User removed`);
|
|
@@ -362,7 +361,7 @@ EOF`);
|
|
|
362
361
|
// If deployId is provided, check for existing config and backup keys
|
|
363
362
|
if (options.deployId) {
|
|
364
363
|
if (!confNode) {
|
|
365
|
-
const config =
|
|
364
|
+
const config = Underpost.ssh.loadConfigNode(options.deployId);
|
|
366
365
|
confNode = config.confNode;
|
|
367
366
|
confNodePath = config.confNodePath;
|
|
368
367
|
}
|
|
@@ -378,14 +377,14 @@ EOF`);
|
|
|
378
377
|
logger.info(`User ${options.user} already exists in config. Importing existing keys...`);
|
|
379
378
|
|
|
380
379
|
// Create system user if it doesn't exist
|
|
381
|
-
const userExists =
|
|
380
|
+
const userExists = Underpost.ssh.checkUserExists(options.user);
|
|
382
381
|
if (!userExists) {
|
|
383
|
-
|
|
382
|
+
Underpost.ssh.createSystemUser(options.user, options.password, options.groups);
|
|
384
383
|
}
|
|
385
384
|
|
|
386
|
-
const userHome =
|
|
385
|
+
const userHome = Underpost.ssh.getUserHome(options.user);
|
|
387
386
|
const sshDir = `${userHome}/.ssh`;
|
|
388
|
-
|
|
387
|
+
Underpost.ssh.ensureSSHDirectory(sshDir);
|
|
389
388
|
|
|
390
389
|
const userKeyPath = `${sshDir}/id_rsa`;
|
|
391
390
|
const userPubKeyPath = `${sshDir}/id_rsa.pub`;
|
|
@@ -394,10 +393,10 @@ EOF`);
|
|
|
394
393
|
fs.copyFileSync(privateKeyPath, userKeyPath);
|
|
395
394
|
fs.copyFileSync(publicKeyPath, userPubKeyPath);
|
|
396
395
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
396
|
+
Underpost.ssh.configureAuthorizedKeys(sshDir, userPubKeyPath, options.disablePassword);
|
|
397
|
+
Underpost.ssh.configureSudoAccess(options.user, options.password, options.disablePassword);
|
|
398
|
+
Underpost.ssh.configureKnownHosts(sshDir, options.port, options.host);
|
|
399
|
+
Underpost.ssh.setSSHFilePermissions(sshDir, options.user, userKeyPath, userPubKeyPath);
|
|
401
400
|
|
|
402
401
|
logger.info(`Keys imported from ${privateCopyDir} to ${sshDir}`);
|
|
403
402
|
logger.info(`User added with existing keys`);
|
|
@@ -406,11 +405,11 @@ EOF`);
|
|
|
406
405
|
}
|
|
407
406
|
|
|
408
407
|
// New user or no existing keys - create new user and generate keys
|
|
409
|
-
|
|
408
|
+
Underpost.ssh.createSystemUser(options.user, options.password, options.groups);
|
|
410
409
|
|
|
411
|
-
const userHome =
|
|
410
|
+
const userHome = Underpost.ssh.getUserHome(options.user);
|
|
412
411
|
const sshDir = `${userHome}/.ssh`;
|
|
413
|
-
|
|
412
|
+
Underpost.ssh.ensureSSHDirectory(sshDir);
|
|
414
413
|
|
|
415
414
|
const keyPath = `${sshDir}/id_rsa`;
|
|
416
415
|
const pubKeyPath = `${sshDir}/id_rsa.pub`;
|
|
@@ -421,10 +420,10 @@ EOF`);
|
|
|
421
420
|
);
|
|
422
421
|
}
|
|
423
422
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
423
|
+
Underpost.ssh.configureAuthorizedKeys(sshDir, pubKeyPath, options.disablePassword);
|
|
424
|
+
Underpost.ssh.configureSudoAccess(options.user, options.password, options.disablePassword);
|
|
425
|
+
Underpost.ssh.configureKnownHosts(sshDir, options.port, options.host);
|
|
426
|
+
Underpost.ssh.setSSHFilePermissions(sshDir, options.user, keyPath, pubKeyPath);
|
|
428
427
|
|
|
429
428
|
// Save a copy of the keys to the private folder only if deployId is provided
|
|
430
429
|
if (options.deployId) {
|
|
@@ -447,7 +446,7 @@ EOF`);
|
|
|
447
446
|
privateKeyCopyPath,
|
|
448
447
|
publicKeyCopyPath,
|
|
449
448
|
};
|
|
450
|
-
|
|
449
|
+
Underpost.ssh.saveConfigNode(confNodePath, confNode);
|
|
451
450
|
}
|
|
452
451
|
|
|
453
452
|
logger.info(`User added`);
|
|
@@ -457,7 +456,7 @@ EOF`);
|
|
|
457
456
|
// Handle config user listing (only with deployId)
|
|
458
457
|
if (options.deployId) {
|
|
459
458
|
if (!confNode) {
|
|
460
|
-
const config =
|
|
459
|
+
const config = Underpost.ssh.loadConfigNode(options.deployId);
|
|
461
460
|
confNode = config.confNode;
|
|
462
461
|
confNodePath = config.confNodePath;
|
|
463
462
|
}
|
|
@@ -472,7 +471,7 @@ EOF`);
|
|
|
472
471
|
|
|
473
472
|
// Handle generate root keys
|
|
474
473
|
if (options.generate)
|
|
475
|
-
|
|
474
|
+
Underpost.ssh.generateKeys({ user: options.user, password: options.password, host: options.host });
|
|
476
475
|
|
|
477
476
|
// Handle list operations
|
|
478
477
|
if (options.keysList) shellExec(`cat ${userHome}/.ssh/authorized_keys`);
|
|
@@ -489,8 +488,8 @@ EOF`);
|
|
|
489
488
|
|
|
490
489
|
// Handle start server
|
|
491
490
|
if (options.start) {
|
|
492
|
-
|
|
493
|
-
|
|
491
|
+
Underpost.ssh.chmod({ user: options.user });
|
|
492
|
+
Underpost.ssh.initService({ port: options.port });
|
|
494
493
|
}
|
|
495
494
|
|
|
496
495
|
// Handle status server
|
|
@@ -512,10 +511,10 @@ EOF`);
|
|
|
512
511
|
if (fs.existsSync(confNodePath)) {
|
|
513
512
|
const { users } = JSON.parse(fs.readFileSync(confNodePath, 'utf8'));
|
|
514
513
|
const { user, host, keyPath, port } = users[options.user];
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
514
|
+
Underpost.env.set('DEFAULT_SSH_USER', user);
|
|
515
|
+
Underpost.env.set('DEFAULT_SSH_HOST', host);
|
|
516
|
+
Underpost.env.set('DEFAULT_SSH_KEY_PATH', keyPath);
|
|
517
|
+
Underpost.env.set('DEFAULT_SSH_PORT', port);
|
|
519
518
|
} else logger.warn(`No SSH config found at ${confNodePath}`);
|
|
520
519
|
},
|
|
521
520
|
|
package/src/cli/static.js
CHANGED
|
@@ -412,7 +412,7 @@ class UnderpostStatic {
|
|
|
412
412
|
// Handle config template generation
|
|
413
413
|
if (options.generateConfig) {
|
|
414
414
|
const configPath = typeof options.generateConfig === 'string' ? options.generateConfig : './static-config.json';
|
|
415
|
-
return
|
|
415
|
+
return Underpost.static.generateConfigTemplate(configPath);
|
|
416
416
|
}
|
|
417
417
|
|
|
418
418
|
// Parse comma-separated options
|
package/src/cli/test.js
CHANGED
|
@@ -9,8 +9,7 @@ import { MariaDB } from '../db/mariadb/MariaDB.js';
|
|
|
9
9
|
import { getNpmRootPath } from '../server/conf.js';
|
|
10
10
|
import { actionInitLog, loggerFactory, setUpInfo } from '../server/logger.js';
|
|
11
11
|
import { pbcopy, shellExec } from '../server/process.js';
|
|
12
|
-
import
|
|
13
|
-
|
|
12
|
+
import Underpost from '../index.js';
|
|
14
13
|
const logger = loggerFactory(import.meta);
|
|
15
14
|
|
|
16
15
|
/**
|
|
@@ -67,10 +66,10 @@ class UnderpostTest {
|
|
|
67
66
|
options.podStatus &&
|
|
68
67
|
typeof options.podStatus === 'string'
|
|
69
68
|
)
|
|
70
|
-
return await
|
|
69
|
+
return await Underpost.test.statusMonitor(options.podName, options.podStatus, options.kindType);
|
|
71
70
|
|
|
72
71
|
if (options.sh === true || options.logs === true) {
|
|
73
|
-
const [pod] =
|
|
72
|
+
const [pod] = Underpost.deploy.get(deployList);
|
|
74
73
|
if (pod) {
|
|
75
74
|
if (options.sh) return pbcopy(`sudo kubectl exec -it ${pod.NAME} -- sh`);
|
|
76
75
|
if (options.logs) return shellExec(`sudo kubectl logs -f ${pod.NAME}`);
|
|
@@ -104,7 +103,7 @@ class UnderpostTest {
|
|
|
104
103
|
break;
|
|
105
104
|
}
|
|
106
105
|
else {
|
|
107
|
-
const pods =
|
|
106
|
+
const pods = Underpost.deploy.get(deployId);
|
|
108
107
|
if (pods.length > 0)
|
|
109
108
|
for (const deployData of pods) {
|
|
110
109
|
const { NAME } = deployData;
|
|
@@ -115,7 +114,7 @@ class UnderpostTest {
|
|
|
115
114
|
else logger.warn(`Couldn't find pods in deployment`, { deployId });
|
|
116
115
|
}
|
|
117
116
|
}
|
|
118
|
-
} else return
|
|
117
|
+
} else return Underpost.test.run();
|
|
119
118
|
},
|
|
120
119
|
/**
|
|
121
120
|
* @method statusMonitor
|
|
@@ -134,7 +133,7 @@ class UnderpostTest {
|
|
|
134
133
|
logger.info(`Loading instance`, { podName, status, kindType, deltaMs, maxAttempts });
|
|
135
134
|
const _monitor = async () => {
|
|
136
135
|
await timer(deltaMs);
|
|
137
|
-
const pods =
|
|
136
|
+
const pods = Underpost.deploy.get(podName, kindType);
|
|
138
137
|
let result = pods.find((p) => p.STATUS === status || (status === 'Running' && p.STATUS === 'Completed'));
|
|
139
138
|
logger.info(
|
|
140
139
|
`Testing pod ${podName}... ${result ? 1 : 0}/1 - elapsed time ${deltaMs * (index + 1)}s - attempt ${
|
|
@@ -8,50 +8,50 @@ import { DocumentService } from '../../services/document/document.service.js';
|
|
|
8
8
|
import { CoreService, getApiBaseUrl, headersFactory } from '../../services/core/core.service.js';
|
|
9
9
|
import { loggerFactory } from './Logger.js';
|
|
10
10
|
import { imageShimmer, renderChessPattern, renderCssAttr, styleFactory } from './Css.js';
|
|
11
|
-
import { getQueryParams } from './Router.js';
|
|
11
|
+
import { getQueryParams, setPath } from './Router.js';
|
|
12
12
|
|
|
13
13
|
const logger = loggerFactory(import.meta);
|
|
14
14
|
|
|
15
15
|
const attachMarkdownLinkHandlers = (containerSelector) => {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// Internal link - navigate normally
|
|
52
|
-
window.location.href = href;
|
|
16
|
+
const container = s(containerSelector);
|
|
17
|
+
if (!container || container.dataset.mdLinkHandler) return;
|
|
18
|
+
container.dataset.mdLinkHandler = 'true';
|
|
19
|
+
|
|
20
|
+
container.addEventListener('click', async (e) => {
|
|
21
|
+
const link = e.target.closest('.markdown-content a[href]');
|
|
22
|
+
if (!link) return;
|
|
23
|
+
|
|
24
|
+
const href = link.getAttribute('href');
|
|
25
|
+
if (!href || href.startsWith('#')) return;
|
|
26
|
+
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
const isExternal = href.startsWith('http://') || href.startsWith('https://');
|
|
29
|
+
|
|
30
|
+
if (isExternal) {
|
|
31
|
+
const result = await Modal.RenderConfirm({
|
|
32
|
+
id: `external-link-${s4()}`,
|
|
33
|
+
html: async () => html`
|
|
34
|
+
<div class="in section-mp" style="text-align: center; padding: 20px;">
|
|
35
|
+
<p>${Translate.Render('external-link-warning')}</p>
|
|
36
|
+
<p style="word-break: break-all; margin-top: 10px;"><strong>${href}</strong></p>
|
|
37
|
+
</div>
|
|
38
|
+
`,
|
|
39
|
+
icon: html`<i class="fas fa-external-link-alt"></i>`,
|
|
40
|
+
style: {
|
|
41
|
+
width: '350px',
|
|
42
|
+
height: '500px',
|
|
43
|
+
overflow: 'auto',
|
|
44
|
+
'z-index': '11',
|
|
45
|
+
resize: 'none',
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (result && result.status === 'confirm') {
|
|
50
|
+
window.open(href, '_blank', 'noopener,noreferrer');
|
|
53
51
|
}
|
|
54
|
-
}
|
|
52
|
+
} else {
|
|
53
|
+
setPath(href);
|
|
54
|
+
}
|
|
55
55
|
});
|
|
56
56
|
};
|
|
57
57
|
|
|
@@ -214,7 +214,9 @@ const Content = {
|
|
|
214
214
|
case 'md':
|
|
215
215
|
{
|
|
216
216
|
const content = await Content.getFileContent(file, options);
|
|
217
|
-
render += html`<div class="${options.class}" ${styleFactory(options.style)}
|
|
217
|
+
render += html`<div class="${options.class} markdown-content" ${styleFactory(options.style)}>
|
|
218
|
+
${marked.parse(content)}
|
|
219
|
+
</div>`;
|
|
218
220
|
}
|
|
219
221
|
break;
|
|
220
222
|
|
|
@@ -1,31 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility module for fullscreen mode management with cross-browser and PWA compatibility.
|
|
3
|
+
* Provides robust fullscreen detection, event handling, and UI synchronization.
|
|
4
|
+
* @module src/client/components/core/FullScreen.js
|
|
5
|
+
* @namespace FullScreenClient
|
|
6
|
+
*/
|
|
7
|
+
|
|
1
8
|
import { Responsive } from './Responsive.js';
|
|
2
9
|
import { ToggleSwitch } from './ToggleSwitch.js';
|
|
3
10
|
import { Translate } from './Translate.js';
|
|
4
11
|
import { checkFullScreen, fullScreenIn, fullScreenOut, s } from './VanillaJs.js';
|
|
5
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Manages fullscreen mode state, event handling, and UI synchronization.
|
|
15
|
+
* Supports all major browsers and PWA/Nativefier environments with comprehensive
|
|
16
|
+
* vendor-prefixed API detection.
|
|
17
|
+
* @memberof FullScreenClient
|
|
18
|
+
*/
|
|
6
19
|
const FullScreen = {
|
|
20
|
+
/**
|
|
21
|
+
* Internal state flag tracking the intended fullscreen mode.
|
|
22
|
+
* @type {boolean}
|
|
23
|
+
* @private
|
|
24
|
+
*/
|
|
25
|
+
_fullScreenSwitch: false,
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Flag indicating whether event listeners have been attached.
|
|
29
|
+
* Prevents duplicate event listener registration.
|
|
30
|
+
* @type {boolean}
|
|
31
|
+
* @private
|
|
32
|
+
*/
|
|
33
|
+
_eventListenersAdded: false,
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Flag preventing concurrent sync operations.
|
|
37
|
+
* Ensures state synchronization happens sequentially.
|
|
38
|
+
* @type {boolean}
|
|
39
|
+
* @private
|
|
40
|
+
*/
|
|
41
|
+
_syncInProgress: false,
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Checks if the browser is currently in fullscreen mode.
|
|
45
|
+
* Supports all vendor-prefixed fullscreen APIs for maximum compatibility:
|
|
46
|
+
* - Standard Fullscreen API
|
|
47
|
+
* - Webkit (Chrome, Safari, Opera)
|
|
48
|
+
* - Mozilla (Firefox)
|
|
49
|
+
* - Microsoft (IE/Edge)
|
|
50
|
+
* @private
|
|
51
|
+
* @memberof FullScreenClient.FullScreen
|
|
52
|
+
* @returns {boolean} True if currently in fullscreen mode, false otherwise.
|
|
53
|
+
*/
|
|
54
|
+
_isFullScreen: function () {
|
|
55
|
+
return !!(
|
|
56
|
+
document.fullscreenElement ||
|
|
57
|
+
document.webkitFullscreenElement ||
|
|
58
|
+
document.mozFullScreenElement ||
|
|
59
|
+
document.msFullscreenElement ||
|
|
60
|
+
document.fullscreen ||
|
|
61
|
+
document.webkitIsFullScreen ||
|
|
62
|
+
document.mozFullScreen
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Synchronizes the toggle switch UI state with the actual fullscreen state.
|
|
68
|
+
* Prevents race conditions using the _syncInProgress flag.
|
|
69
|
+
* Updates the toggle switch only if there's a mismatch between UI and actual state.
|
|
70
|
+
* @private
|
|
71
|
+
* @memberof FullScreenClient.FullScreen
|
|
72
|
+
* @returns {void}
|
|
73
|
+
*/
|
|
74
|
+
_syncToggleState: function () {
|
|
75
|
+
if (this._syncInProgress) return;
|
|
76
|
+
this._syncInProgress = true;
|
|
77
|
+
|
|
78
|
+
const actualFullScreen = this._isFullScreen();
|
|
79
|
+
|
|
80
|
+
// Only update if there's a mismatch
|
|
81
|
+
if (this._fullScreenSwitch !== actualFullScreen) {
|
|
82
|
+
this._fullScreenSwitch = actualFullScreen;
|
|
83
|
+
|
|
84
|
+
// Update toggle switch UI if it exists
|
|
85
|
+
const toggle = s('.fullscreen');
|
|
86
|
+
if (toggle && ToggleSwitch.Tokens[`fullscreen`]) {
|
|
87
|
+
const switchState = ToggleSwitch.Tokens[`fullscreen`].checked;
|
|
88
|
+
if (switchState !== actualFullScreen) {
|
|
89
|
+
ToggleSwitch.Tokens[`fullscreen`].click();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
setTimeout(() => {
|
|
95
|
+
this._syncInProgress = false;
|
|
96
|
+
}, 100);
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Event handler for fullscreen change events.
|
|
101
|
+
* Triggers UI state synchronization when fullscreen mode changes.
|
|
102
|
+
* @private
|
|
103
|
+
* @memberof FullScreenClient.FullScreen
|
|
104
|
+
* @returns {void}
|
|
105
|
+
*/
|
|
106
|
+
_handleFullScreenChange: function () {
|
|
107
|
+
this._syncToggleState();
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Attaches all necessary event listeners for fullscreen mode detection.
|
|
112
|
+
* Handles multiple scenarios:
|
|
113
|
+
* - Vendor-prefixed fullscreen change events (Chrome, Firefox, IE/Edge)
|
|
114
|
+
* - Window resize events (for PWA/Nativefier compatibility)
|
|
115
|
+
* - ESC key detection (fallback for manual fullscreen exit)
|
|
116
|
+
* Only attaches listeners once to prevent duplicates.
|
|
117
|
+
* @private
|
|
118
|
+
* @memberof FullScreenClient.FullScreen
|
|
119
|
+
* @returns {void}
|
|
120
|
+
*/
|
|
121
|
+
_addEventListeners: function () {
|
|
122
|
+
if (this._eventListenersAdded) return;
|
|
123
|
+
|
|
124
|
+
const events = ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange'];
|
|
125
|
+
|
|
126
|
+
events.forEach((eventName) => {
|
|
127
|
+
document.addEventListener(eventName, () => this._handleFullScreenChange(), false);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Additional check for PWA/Nativefier window resize events
|
|
131
|
+
window.addEventListener(
|
|
132
|
+
'resize',
|
|
133
|
+
() => {
|
|
134
|
+
setTimeout(() => this._syncToggleState(), 150);
|
|
135
|
+
},
|
|
136
|
+
false,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// ESC key detection fallback
|
|
140
|
+
document.addEventListener(
|
|
141
|
+
'keydown',
|
|
142
|
+
(e) => {
|
|
143
|
+
if (e.key === 'Escape' || e.keyCode === 27) {
|
|
144
|
+
setTimeout(() => this._syncToggleState(), 100);
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
false,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
this._eventListenersAdded = true;
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Enters fullscreen mode if not already in fullscreen.
|
|
155
|
+
* Updates internal state and triggers fullscreen API.
|
|
156
|
+
* Verifies the state change after a delay to ensure synchronization.
|
|
157
|
+
* @private
|
|
158
|
+
* @memberof FullScreenClient.FullScreen
|
|
159
|
+
* @returns {void}
|
|
160
|
+
*/
|
|
161
|
+
_enterFullScreen: function () {
|
|
162
|
+
if (this._isFullScreen()) return;
|
|
163
|
+
|
|
164
|
+
this._fullScreenSwitch = true;
|
|
165
|
+
fullScreenIn();
|
|
166
|
+
|
|
167
|
+
// Verify after attempt
|
|
168
|
+
setTimeout(() => this._syncToggleState(), 300);
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Exits fullscreen mode if currently in fullscreen.
|
|
173
|
+
* Updates internal state and triggers fullscreen exit API.
|
|
174
|
+
* Verifies the state change after a delay to ensure synchronization.
|
|
175
|
+
* @private
|
|
176
|
+
* @memberof FullScreenClient.FullScreen
|
|
177
|
+
* @returns {void}
|
|
178
|
+
*/
|
|
179
|
+
_exitFullScreen: function () {
|
|
180
|
+
if (!this._isFullScreen()) return;
|
|
181
|
+
|
|
182
|
+
this._fullScreenSwitch = false;
|
|
183
|
+
fullScreenOut();
|
|
184
|
+
|
|
185
|
+
// Verify after attempt
|
|
186
|
+
setTimeout(() => this._syncToggleState(), 300);
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Renders the fullscreen toggle setting UI component.
|
|
191
|
+
* Initializes fullscreen state detection, sets up event listeners,
|
|
192
|
+
* and creates a toggle switch for user interaction.
|
|
193
|
+
* Integrates with the Responsive system for dynamic updates.
|
|
194
|
+
* @memberof FullScreenClient.FullScreen
|
|
195
|
+
* @returns {Promise<string>} A promise resolving to the HTML string for the fullscreen setting component.
|
|
196
|
+
*/
|
|
7
197
|
RenderSetting: async function () {
|
|
8
|
-
|
|
198
|
+
// Initialize state from actual fullscreen status
|
|
199
|
+
this._fullScreenSwitch = this._isFullScreen();
|
|
200
|
+
|
|
201
|
+
// Setup event listeners once
|
|
202
|
+
this._addEventListeners();
|
|
203
|
+
|
|
204
|
+
// Update responsive event
|
|
9
205
|
Responsive.Event['full-screen-settings'] = () => {
|
|
10
|
-
|
|
11
|
-
if ((fullScreenSwitch && !fullScreenMode) || (!fullScreenSwitch && fullScreenMode))
|
|
12
|
-
if (s('.fullscreen')) ToggleSwitch.Tokens[`fullscreen`].click();
|
|
206
|
+
this._syncToggleState();
|
|
13
207
|
};
|
|
208
|
+
|
|
14
209
|
return html`<div class="in section-mp">
|
|
15
210
|
${await ToggleSwitch.Render({
|
|
16
211
|
wrapper: true,
|
|
17
212
|
wrapperLabel: html`<i class="fa-solid fa-expand"></i> ${Translate.Render('fullscreen')}`,
|
|
18
213
|
id: 'fullscreen',
|
|
19
214
|
disabledOnClick: true,
|
|
20
|
-
checked:
|
|
215
|
+
checked: this._fullScreenSwitch,
|
|
21
216
|
on: {
|
|
22
217
|
unchecked: () => {
|
|
23
|
-
|
|
24
|
-
if (checkFullScreen()) fullScreenOut();
|
|
218
|
+
this._exitFullScreen();
|
|
25
219
|
},
|
|
26
220
|
checked: () => {
|
|
27
|
-
|
|
28
|
-
if (!checkFullScreen()) fullScreenIn();
|
|
221
|
+
this._enterFullScreen();
|
|
29
222
|
},
|
|
30
223
|
},
|
|
31
224
|
})}
|
|
@@ -455,7 +455,9 @@ const PanelForm = {
|
|
|
455
455
|
|
|
456
456
|
const baseNewDoc = newInstance(data);
|
|
457
457
|
baseNewDoc.tags = tags.filter((t) => !prefixTags.includes(t));
|
|
458
|
-
baseNewDoc.mdFileId = hasMdContent
|
|
458
|
+
baseNewDoc.mdFileId = hasMdContent
|
|
459
|
+
? `<div class="markdown-content">${marked.parse(data.mdFileId)}</div>`
|
|
460
|
+
: null;
|
|
459
461
|
baseNewDoc.userId = Elements.Data.user?.main?.model?.user?._id;
|
|
460
462
|
|
|
461
463
|
// Ensure profileImageId is properly formatted as object with _id property
|
|
@@ -677,7 +679,7 @@ const PanelForm = {
|
|
|
677
679
|
mdPlain = await blobArray[0].text();
|
|
678
680
|
// Parse markdown with proper error handling
|
|
679
681
|
try {
|
|
680
|
-
parsedMarkdown = mdPlain ? marked.parse(mdPlain) : '';
|
|
682
|
+
parsedMarkdown = mdPlain ? `<div class="markdown-content">${marked.parse(mdPlain)}</div>` : '';
|
|
681
683
|
} catch (parseError) {
|
|
682
684
|
logger.error('Error parsing markdown for document:', documentObject._id, parseError);
|
|
683
685
|
parsedMarkdown = `<p><strong>Error rendering markdown:</strong> ${parseError.message}</p>`;
|