[mirotalk] - improve collaborative whiteboard, update dep
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
# ====================================================
|
||||
# MiroTalk P2P v.1.6.65 - Environment Configuration
|
||||
# MiroTalk P2P v.1.6.66 - Environment Configuration
|
||||
# ====================================================
|
||||
|
||||
# App environment
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
/**
|
||||
* ==============================================
|
||||
* MiroTalk P2P v.1.6.65 - Configuration File
|
||||
* MiroTalk P2P v.1.6.66 - Configuration File
|
||||
* ==============================================
|
||||
*
|
||||
* Branding and customizations require a license:
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ dependencies: {
|
||||
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
|
||||
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
|
||||
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
||||
* @version 1.6.65
|
||||
* @version 1.6.66
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
Generated
+27
-26
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "mirotalk",
|
||||
"version": "1.6.65",
|
||||
"version": "1.6.66",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mirotalk",
|
||||
"version": "1.6.65",
|
||||
"version": "1.6.66",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@mattermost/client": "11.1.0",
|
||||
"@ngrok/ngrok": "1.6.0",
|
||||
"@sentry/node": "^10.27.0",
|
||||
"@sentry/node": "^10.28.0",
|
||||
"axios": "^1.13.2",
|
||||
"chokidar": "^5.0.0",
|
||||
"colors": "^1.4.0",
|
||||
@@ -20,7 +20,7 @@
|
||||
"crypto-js": "^4.2.0",
|
||||
"dompurify": "^3.3.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.1.0",
|
||||
"express": "^5.2.1",
|
||||
"express-openid-connect": "^2.19.3",
|
||||
"express-rate-limit": "^8.2.1",
|
||||
"he": "^1.2.0",
|
||||
@@ -1104,18 +1104,18 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@sentry/core": {
|
||||
"version": "10.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.27.0.tgz",
|
||||
"integrity": "sha512-Zc68kdH7tWTDtDbV1zWIbo3Jv0fHAU2NsF5aD2qamypKgfSIMSbWVxd22qZyDBkaX8gWIPm/0Sgx6aRXRBXrYQ==",
|
||||
"version": "10.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.28.0.tgz",
|
||||
"integrity": "sha512-9yFIPxyfWkDzt+IaRjboeNiXOKi22ZRGG3ELmZlLak8JCC+vA+q/+AmF/8Jnw59WlL3/KVC1Q8+t8bLCkxlswg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/node": {
|
||||
"version": "10.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.27.0.tgz",
|
||||
"integrity": "sha512-1cQZ4+QqV9juW64Jku1SMSz+PoZV+J59lotz4oYFvCNYzex8hRAnDKvNiKW1IVg5mEEkz98mg1fvcUtiw7GTiQ==",
|
||||
"version": "10.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.28.0.tgz",
|
||||
"integrity": "sha512-aih3iqagUU/9Xa6RObgdS9cKL3q5eerYNMJoO9SflMgeyhHBM5BRqo0IPSMQ9nuogrDBp443sgtW450VXYO7Bg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
@@ -1148,9 +1148,9 @@
|
||||
"@opentelemetry/sdk-trace-base": "^2.2.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.37.0",
|
||||
"@prisma/instrumentation": "6.19.0",
|
||||
"@sentry/core": "10.27.0",
|
||||
"@sentry/node-core": "10.27.0",
|
||||
"@sentry/opentelemetry": "10.27.0",
|
||||
"@sentry/core": "10.28.0",
|
||||
"@sentry/node-core": "10.28.0",
|
||||
"@sentry/opentelemetry": "10.28.0",
|
||||
"import-in-the-middle": "^2",
|
||||
"minimatch": "^9.0.0"
|
||||
},
|
||||
@@ -1159,14 +1159,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/node-core": {
|
||||
"version": "10.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.27.0.tgz",
|
||||
"integrity": "sha512-Dzo1I64Psb7AkpyKVUlR9KYbl4wcN84W4Wet3xjLmVKMgrCo2uAT70V4xIacmoMH5QLZAx0nGfRy9yRCd4nzBg==",
|
||||
"version": "10.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.28.0.tgz",
|
||||
"integrity": "sha512-OOmNtMSPHjiVb+dmTC9Lq+uIrC2FplZSdst033mH+ucBF7xjyY1/WAk02pw+hqNVFQKwaItqhGNFTmC7aST60Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@apm-js-collab/tracing-hooks": "^0.3.1",
|
||||
"@sentry/core": "10.27.0",
|
||||
"@sentry/opentelemetry": "10.27.0",
|
||||
"@sentry/core": "10.28.0",
|
||||
"@sentry/opentelemetry": "10.28.0",
|
||||
"import-in-the-middle": "^2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1183,12 +1183,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/opentelemetry": {
|
||||
"version": "10.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.27.0.tgz",
|
||||
"integrity": "sha512-z2vXoicuGiqlRlgL9HaYJgkin89ncMpNQy0Kje6RWyhpzLe8BRgUXlgjux7WrSrcbopDdC1OttSpZsJ/Wjk7fg==",
|
||||
"version": "10.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.28.0.tgz",
|
||||
"integrity": "sha512-SiSLN294vlxipDG0/FvMYIFmyXEffXmPvvdyp5DUqY8NyJytYPPUJ3DuQhc9XRVyEd9XeOgra661nxNIKPr1pg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "10.27.0"
|
||||
"@sentry/core": "10.28.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -2736,19 +2736,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
|
||||
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.0",
|
||||
"body-parser": "^2.2.1",
|
||||
"content-disposition": "^1.0.0",
|
||||
"content-type": "^1.0.5",
|
||||
"cookie": "^0.7.1",
|
||||
"cookie-signature": "^1.2.1",
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mirotalk",
|
||||
"version": "1.6.65",
|
||||
"version": "1.6.66",
|
||||
"description": "A free WebRTC browser-based video call",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
@@ -43,7 +43,7 @@
|
||||
"dependencies": {
|
||||
"@mattermost/client": "11.1.0",
|
||||
"@ngrok/ngrok": "1.6.0",
|
||||
"@sentry/node": "^10.27.0",
|
||||
"@sentry/node": "^10.28.0",
|
||||
"axios": "^1.13.2",
|
||||
"chokidar": "^5.0.0",
|
||||
"colors": "^1.4.0",
|
||||
@@ -52,7 +52,7 @@
|
||||
"crypto-js": "^4.2.0",
|
||||
"dompurify": "^3.3.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.1.0",
|
||||
"express": "^5.2.1",
|
||||
"express-openid-connect": "^2.19.3",
|
||||
"express-rate-limit": "^8.2.1",
|
||||
"he": "^1.2.0",
|
||||
|
||||
@@ -186,3 +186,198 @@
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Whiteboard Shortcuts Styles */
|
||||
#whiteboardShortcutsContent {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wb-shortcuts-container {
|
||||
text-align: left;
|
||||
font-family: 'Segoe UI', Arial, sans-serif;
|
||||
background: var(--body-bg);
|
||||
padding: 1.5rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.wb-shortcuts-title {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.8rem;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: #ffd700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid rgba(255, 215, 0, 0.3);
|
||||
}
|
||||
|
||||
.wb-shortcuts-title:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.wb-shortcuts-title i {
|
||||
color: #ffa500;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.wb-shortcuts-code {
|
||||
background: var(--body-bg);
|
||||
padding: 1rem 1rem;
|
||||
border-radius: 8px;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.8;
|
||||
color: var(--text-color);
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
filter: brightness(0.85);
|
||||
}
|
||||
|
||||
.wb-shortcuts-text {
|
||||
color: #66beff;
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
letter-spacing: 0.03em;
|
||||
background: linear-gradient(135deg, rgba(102, 190, 255, 0.15), rgba(102, 190, 255, 0.08));
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
display: inline-block;
|
||||
box-shadow: 0 2px 8px rgba(102, 190, 255, 0.1);
|
||||
}
|
||||
|
||||
.wb-shortcuts-list {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
background: var(--body-bg);
|
||||
border-radius: 8px;
|
||||
padding: 0.8rem;
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
.wb-shortcuts-list li {
|
||||
margin: 0.6rem 0;
|
||||
padding: 0.5rem 0.8rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 6px;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.wb-shortcuts-list li:hover {
|
||||
background: rgba(255, 215, 0, 0.15);
|
||||
border-left: 3px solid #ffd700;
|
||||
}
|
||||
|
||||
/* Sticky Note Dialog Styles */
|
||||
.sticky-note-form {
|
||||
display: flex;
|
||||
background: var(--body-bg);
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
padding: 1.2rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
border: 1px solid rgba(255, 215, 0, 0.18);
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sticky-note-colors-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sticky-note-textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid rgba(255, 215, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 235, 59, 0.07);
|
||||
color: var(--text-color, #ffffff);
|
||||
font-size: 1rem;
|
||||
font-family: 'Segoe UI', Arial, sans-serif;
|
||||
resize: vertical;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 8px rgba(255, 215, 0, 0.08);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sticky-note-color-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.sticky-note-color-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-color, #ffffff);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.sticky-note-color-input {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
border: none !important;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 1px 4px rgba(255, 215, 0, 0.07);
|
||||
}
|
||||
|
||||
.sticky-note-color-input:hover {
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
.sticky-note-color-input::-webkit-color-swatch-wrapper {
|
||||
padding: 4px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.sticky-note-color-input::-webkit-color-swatch {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.sticky-note-color-input::-moz-color-swatch {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Responsive styles for small screens */
|
||||
@media (max-width: 600px) {
|
||||
.sticky-note-form {
|
||||
padding: 0.7rem;
|
||||
border-radius: 8px;
|
||||
gap: 0.7rem;
|
||||
}
|
||||
.sticky-note-colors-row {
|
||||
flex-direction: column;
|
||||
gap: 0.7rem;
|
||||
}
|
||||
.sticky-note-color-group {
|
||||
min-width: 0;
|
||||
}
|
||||
.sticky-note-textarea {
|
||||
font-size: 0.95rem;
|
||||
padding: 8px;
|
||||
}
|
||||
.sticky-note-color-input {
|
||||
height: 38px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ let brand = {
|
||||
},
|
||||
about: {
|
||||
imageUrl: '../images/mirotalk-logo.gif',
|
||||
title: 'WebRTC P2P v1.6.65',
|
||||
title: 'WebRTC P2P v1.6.66',
|
||||
html: `
|
||||
<button
|
||||
id="support-button"
|
||||
|
||||
+429
-15
@@ -15,7 +15,7 @@
|
||||
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
|
||||
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
|
||||
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
||||
* @version 1.6.65
|
||||
* @version 1.6.66
|
||||
*
|
||||
*/
|
||||
|
||||
@@ -391,7 +391,9 @@ const wbDrawingColorEl = getId('wbDrawingColorEl');
|
||||
const whiteboardGhostButton = getId('whiteboardGhostButton');
|
||||
const wbBackgroundColorEl = getId('wbBackgroundColorEl');
|
||||
const whiteboardPencilBtn = getId('whiteboardPencilBtn');
|
||||
const whiteboardVanishingBtn = getId('whiteboardVanishingBtn');
|
||||
const whiteboardObjectBtn = getId('whiteboardObjectBtn');
|
||||
const whiteboardEraserBtn = getId('whiteboardEraserBtn');
|
||||
const whiteboardUndoBtn = getId('whiteboardUndoBtn');
|
||||
const whiteboardRedoBtn = getId('whiteboardRedoBtn');
|
||||
const whiteboardDropDownMenuBtn = getId('whiteboardDropDownMenuBtn');
|
||||
@@ -400,16 +402,18 @@ const whiteboardImgFileBtn = getId('whiteboardImgFileBtn');
|
||||
const whiteboardPdfFileBtn = getId('whiteboardPdfFileBtn');
|
||||
const whiteboardImgUrlBtn = getId('whiteboardImgUrlBtn');
|
||||
const whiteboardTextBtn = getId('whiteboardTextBtn');
|
||||
const whiteboardStickyNoteBtn = getId('whiteboardStickyNoteBtn');
|
||||
const whiteboardLineBtn = getId('whiteboardLineBtn');
|
||||
const whiteboardRectBtn = getId('whiteboardRectBtn');
|
||||
const whiteboardTriangleBtn = getId('whiteboardTriangleBtn');
|
||||
const whiteboardCircleBtn = getId('whiteboardCircleBtn');
|
||||
const whiteboardSaveBtn = getId('whiteboardSaveBtn');
|
||||
const whiteboardEraserBtn = getId('whiteboardEraserBtn');
|
||||
const whiteboardCleanBtn = getId('whiteboardCleanBtn');
|
||||
const whiteboardLockBtn = getId('whiteboardLockBtn');
|
||||
const whiteboardUnlockBtn = getId('whiteboardUnlockBtn');
|
||||
const whiteboardCloseBtn = getId('whiteboardCloseBtn');
|
||||
const whiteboardShortcutsBtn = getId('whiteboardShortcutsBtn');
|
||||
const whiteboardShortcutsContent = getId('whiteboardShortcutsContent');
|
||||
|
||||
// Room actions buttons
|
||||
const captionEveryoneBtn = getId('captionEveryoneBtn');
|
||||
@@ -661,8 +665,10 @@ let wbIsDrawing = false;
|
||||
let wbIsOpen = false;
|
||||
let wbIsRedoing = false;
|
||||
let wbIsEraser = false;
|
||||
let wbIsVanishing = false;
|
||||
let wbIsBgTransparent = false;
|
||||
let wbPop = [];
|
||||
let wbVanishingObjects = [];
|
||||
let isWhiteboardFs = false;
|
||||
|
||||
// file transfer
|
||||
@@ -824,7 +830,9 @@ function setButtonsToolTip() {
|
||||
setTippy(whiteboardGhostButton, 'Toggle transparent background', 'bottom');
|
||||
setTippy(wbBackgroundColorEl, 'Background color', 'bottom');
|
||||
setTippy(whiteboardPencilBtn, 'Drawing mode', 'bottom');
|
||||
setTippy(whiteboardVanishingBtn, 'Vanishing pen (disappears in 5s)', 'bottom');
|
||||
setTippy(whiteboardObjectBtn, 'Object mode', 'bottom');
|
||||
setTippy(whiteboardEraserBtn, 'Eraser mode', 'bottom');
|
||||
setTippy(whiteboardUndoBtn, 'Undo', 'bottom');
|
||||
setTippy(whiteboardRedoBtn, 'Redo', 'bottom');
|
||||
// Suspend/Hide File transfer buttons
|
||||
@@ -5923,6 +5931,12 @@ function setMyWhiteboardBtn() {
|
||||
whiteboardObjectBtn.addEventListener('click', (e) => {
|
||||
whiteboardIsDrawingMode(false);
|
||||
});
|
||||
whiteboardStickyNoteBtn.addEventListener('click', (e) => {
|
||||
whiteboardAddObj('stickyNote');
|
||||
});
|
||||
whiteboardVanishingBtn.addEventListener('click', (e) => {
|
||||
whiteboardIsVanishingMode(true);
|
||||
});
|
||||
whiteboardUndoBtn.addEventListener('click', (e) => {
|
||||
whiteboardAction(getWhiteboardAction('undo'));
|
||||
});
|
||||
@@ -5988,6 +6002,10 @@ function setMyWhiteboardBtn() {
|
||||
//setWhiteboardBgColor(wbIsBgTransparent ? 'rgba(0, 0, 0, 0.100)' : wbBackgroundColorEl.value);
|
||||
wbIsBgTransparent ? wbCanvasBackgroundColor('rgba(0, 0, 0, 0.100)') : setTheme();
|
||||
});
|
||||
whiteboardShortcutsBtn.addEventListener('click', (e) => {
|
||||
showWhiteboardShortcuts();
|
||||
});
|
||||
|
||||
// Hide the whiteboard dropdown menu if clicked outside
|
||||
document.addEventListener('click', (event) => {
|
||||
if (!whiteboardDropDownMenuBtn.contains(event.target) && !whiteboardDropDownMenuBtn.contains(event.target)) {
|
||||
@@ -11177,7 +11195,9 @@ function toggleWhiteboard() {
|
||||
function setupWhiteboard() {
|
||||
setupWhiteboardCanvas();
|
||||
setupWhiteboardCanvasSize();
|
||||
setupWhiteboardLocalListners();
|
||||
setupWhiteboardLocalListeners();
|
||||
setupWhiteboardShortcuts();
|
||||
setupWhiteboardDragAndDrop();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -11249,11 +11269,34 @@ function whiteboardIsDrawingMode(status) {
|
||||
wbCanvas.isDrawingMode = status;
|
||||
if (status) {
|
||||
setColor(whiteboardPencilBtn, 'green');
|
||||
setColor(whiteboardVanishingBtn, 'white');
|
||||
setColor(whiteboardObjectBtn, 'white');
|
||||
setColor(whiteboardEraserBtn, 'white');
|
||||
wbIsEraser = false;
|
||||
wbIsVanishing = false;
|
||||
} else {
|
||||
setColor(whiteboardPencilBtn, 'white');
|
||||
setColor(whiteboardVanishingBtn, 'white');
|
||||
setColor(whiteboardObjectBtn, 'green');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whiteboard: vanishing mode
|
||||
* @param {boolean} status if vanishing mode on
|
||||
*/
|
||||
function whiteboardIsVanishingMode(status) {
|
||||
wbCanvas.isDrawingMode = status;
|
||||
wbIsVanishing = status;
|
||||
if (status) {
|
||||
setColor(whiteboardVanishingBtn, 'green');
|
||||
setColor(whiteboardPencilBtn, 'white');
|
||||
setColor(whiteboardObjectBtn, 'white');
|
||||
setColor(whiteboardEraserBtn, 'white');
|
||||
wbIsEraser = false;
|
||||
} else {
|
||||
setColor(whiteboardPencilBtn, 'white');
|
||||
setColor(whiteboardVanishingBtn, 'white');
|
||||
wbCanvas.isDrawingMode = false;
|
||||
setColor(whiteboardObjectBtn, 'green');
|
||||
}
|
||||
}
|
||||
@@ -11263,9 +11306,18 @@ function whiteboardIsDrawingMode(status) {
|
||||
* @param {boolean} status if eraser on
|
||||
*/
|
||||
function whiteboardIsEraser(status) {
|
||||
whiteboardIsDrawingMode(false);
|
||||
if (status) {
|
||||
wbCanvas.isDrawingMode = false;
|
||||
wbIsVanishing = false;
|
||||
setColor(whiteboardPencilBtn, 'white');
|
||||
setColor(whiteboardVanishingBtn, 'white');
|
||||
setColor(whiteboardObjectBtn, 'white');
|
||||
setColor(whiteboardEraserBtn, 'green');
|
||||
} else {
|
||||
setColor(whiteboardEraserBtn, 'white');
|
||||
setColor(whiteboardObjectBtn, 'green');
|
||||
}
|
||||
wbIsEraser = status;
|
||||
setColor(whiteboardEraserBtn, wbIsEraser ? 'green' : 'white');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -11311,6 +11363,9 @@ function whiteboardAddObj(type) {
|
||||
case 'pdfFile':
|
||||
setupFileSelection('Select the PDF', wbPdfInput, renderPdfToCanvas);
|
||||
break;
|
||||
case 'stickyNote':
|
||||
createStickyNote();
|
||||
break;
|
||||
case 'text':
|
||||
const text = new fabric.IText('Lorem Ipsum', {
|
||||
top: 0,
|
||||
@@ -11370,6 +11425,175 @@ function whiteboardAddObj(type) {
|
||||
}
|
||||
}
|
||||
|
||||
function whiteboardEraseObject() {
|
||||
if (wbCanvas && typeof wbCanvas.getActiveObjects === 'function') {
|
||||
const activeObjects = wbCanvas.getActiveObjects();
|
||||
if (activeObjects && activeObjects.length > 0) {
|
||||
// Remove all selected objects
|
||||
activeObjects.forEach((obj) => {
|
||||
wbCanvas.remove(obj);
|
||||
});
|
||||
wbCanvas.discardActiveObject();
|
||||
wbCanvas.requestRenderAll();
|
||||
wbCanvasToJson();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function whiteboardCloneObject() {
|
||||
if (wbCanvas && typeof wbCanvas.getActiveObjects === 'function') {
|
||||
const activeObjects = wbCanvas.getActiveObjects();
|
||||
if (activeObjects && activeObjects.length > 0) {
|
||||
activeObjects.forEach((obj, idx) => {
|
||||
obj.clone((cloned) => {
|
||||
// Offset each clone for visibility
|
||||
cloned.set({
|
||||
left: obj.left + 30 + idx * 10,
|
||||
top: obj.top + 30 + idx * 10,
|
||||
evented: true,
|
||||
});
|
||||
wbCanvas.add(cloned);
|
||||
wbCanvas.setActiveObject(cloned);
|
||||
wbCanvasToJson();
|
||||
});
|
||||
});
|
||||
wbCanvas.requestRenderAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function wbHandleVanishingObjects() {
|
||||
if (wbIsVanishing && wbCanvas._objects.length > 0) {
|
||||
const obj = wbCanvas._objects[wbCanvas._objects.length - 1];
|
||||
if (obj && obj.type === 'path') {
|
||||
wbVanishingObjects.push(obj);
|
||||
const fadeDuration = 1000,
|
||||
vanishTimeout = 5000;
|
||||
setTimeout(() => {
|
||||
const start = performance.now();
|
||||
function fade(ts) {
|
||||
const p = Math.min((ts - start) / fadeDuration, 1);
|
||||
obj.set('opacity', 1 - p);
|
||||
wbCanvas.requestRenderAll();
|
||||
if (p < 1) requestAnimationFrame(fade);
|
||||
}
|
||||
requestAnimationFrame(fade);
|
||||
}, vanishTimeout - fadeDuration);
|
||||
setTimeout(() => {
|
||||
wbCanvas.remove(obj);
|
||||
wbCanvas.renderAll();
|
||||
wbCanvasToJson();
|
||||
wbVanishingObjects.splice(wbVanishingObjects.indexOf(obj), 1);
|
||||
}, vanishTimeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createStickyNote() {
|
||||
Swal.fire({
|
||||
background: swBg,
|
||||
title: 'Create Sticky Note',
|
||||
html: `
|
||||
<div class="sticky-note-form">
|
||||
<textarea id="stickyNoteText" class="sticky-note-textarea" rows="4" placeholder="Type your note here...">Note</textarea>
|
||||
<div class="sticky-note-colors-row">
|
||||
<div class="sticky-note-color-group">
|
||||
<label for="stickyNoteColor" class="sticky-note-color-label">
|
||||
<i class="fas fa-palette"></i> Background
|
||||
</label>
|
||||
<input id="stickyNoteColor" type="color" value="#FFEB3B" class="sticky-note-color-input">
|
||||
</div>
|
||||
<div class="sticky-note-color-group">
|
||||
<label for="stickyNoteTextColor" class="sticky-note-color-label">
|
||||
<i class="fas fa-font"></i> Text
|
||||
</label>
|
||||
<input id="stickyNoteTextColor" type="color" value="#000000" class="sticky-note-color-input">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Create',
|
||||
cancelButtonText: 'Cancel',
|
||||
showClass: { popup: 'animate__animated animate__fadeInDown' },
|
||||
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
|
||||
preConfirm: () => {
|
||||
return {
|
||||
text: getId('stickyNoteText').value,
|
||||
color: getId('stickyNoteColor').value,
|
||||
textColor: getId('stickyNoteTextColor').value,
|
||||
};
|
||||
},
|
||||
didOpen: () => {
|
||||
// Focus textarea for quick typing
|
||||
setTimeout(() => {
|
||||
getId('stickyNoteText').focus();
|
||||
}, 100);
|
||||
},
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
const noteData = result.value;
|
||||
|
||||
// Create sticky note background (rectangle)
|
||||
const noteRect = new fabric.Rect({
|
||||
left: 100,
|
||||
top: 100,
|
||||
width: 220,
|
||||
height: 160,
|
||||
fill: noteData.color,
|
||||
shadow: 'rgba(0,0,0,0.18) 0px 4px 12px',
|
||||
rx: 14,
|
||||
ry: 14,
|
||||
});
|
||||
|
||||
// Create text for sticky note
|
||||
const noteText = new fabric.Textbox(noteData.text, {
|
||||
left: 110,
|
||||
top: 110,
|
||||
width: 200,
|
||||
fontSize: 18,
|
||||
fontFamily: 'Segoe UI, Arial, sans-serif',
|
||||
fill: noteData.textColor,
|
||||
textAlign: 'left',
|
||||
editable: true,
|
||||
fontWeight: 'bold',
|
||||
shadow: new fabric.Shadow({
|
||||
color: 'rgba(255,255,255,0.18)',
|
||||
blur: 2,
|
||||
offsetX: 1,
|
||||
offsetY: 1,
|
||||
}),
|
||||
padding: 8,
|
||||
cornerSize: 8,
|
||||
});
|
||||
|
||||
// Group rectangle and text together
|
||||
const stickyNoteGroup = new fabric.Group([noteRect, noteText], {
|
||||
left: 100,
|
||||
top: 100,
|
||||
selectable: true,
|
||||
hasControls: true,
|
||||
hoverCursor: 'pointer',
|
||||
});
|
||||
|
||||
// Make the text editable by handling double-click events
|
||||
stickyNoteGroup.on('mousedblclick', function () {
|
||||
noteText.enterEditing();
|
||||
noteText.hiddenTextarea && noteText.hiddenTextarea.focus();
|
||||
});
|
||||
|
||||
// Exit editing when clicking outside the noteText
|
||||
wbCanvas.on('mouse:down', function (e) {
|
||||
if (noteText.isEditing && e.target !== noteText) {
|
||||
noteText.exitEditing();
|
||||
}
|
||||
});
|
||||
|
||||
addWbCanvasObj(stickyNoteGroup);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Canvas file selections
|
||||
* @param {string} title
|
||||
@@ -11569,13 +11793,15 @@ function addWbCanvasObj(obj) {
|
||||
wbCanvas.add(obj).setActiveObject(obj);
|
||||
whiteboardIsDrawingMode(false);
|
||||
wbCanvasToJson();
|
||||
} else {
|
||||
console.error('Invalid input. Expected an obj of canvas elements');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whiteboard: Local listners
|
||||
*/
|
||||
function setupWhiteboardLocalListners() {
|
||||
function setupWhiteboardLocalListeners() {
|
||||
wbCanvas.on('mouse:down', function (e) {
|
||||
mouseDown(e);
|
||||
});
|
||||
@@ -11598,6 +11824,10 @@ function setupWhiteboardLocalListners() {
|
||||
function mouseDown(e) {
|
||||
wbIsDrawing = true;
|
||||
if (wbIsEraser && e.target) {
|
||||
// Don't add vanishing objects to redo stack
|
||||
if (!wbVanishingObjects.includes(e.target)) {
|
||||
wbPop.push(e.target); // To allow redo
|
||||
}
|
||||
wbCanvas.remove(e.target);
|
||||
return;
|
||||
}
|
||||
@@ -11631,6 +11861,7 @@ function mouseMove() {
|
||||
function objectAdded() {
|
||||
if (!wbIsRedoing) wbPop = [];
|
||||
wbIsRedoing = false;
|
||||
wbHandleVanishingObjects();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -11649,7 +11880,11 @@ function wbCanvasBackgroundColor(color) {
|
||||
*/
|
||||
function wbCanvasUndo() {
|
||||
if (wbCanvas._objects.length > 0) {
|
||||
wbPop.push(wbCanvas._objects.pop());
|
||||
const obj = wbCanvas._objects.pop();
|
||||
// Don't add vanishing objects to redo stack
|
||||
if (!wbVanishingObjects.includes(obj)) {
|
||||
wbPop.push(obj);
|
||||
}
|
||||
wbCanvas.renderAll();
|
||||
}
|
||||
}
|
||||
@@ -11727,12 +11962,13 @@ async function wbUpdate() {
|
||||
* Whiteboard: json to canvas objects
|
||||
* @param {object} config data
|
||||
*/
|
||||
function handleJsonToWbCanvas(config) {
|
||||
function JsonToWbCanvas(config) {
|
||||
if (!wbIsOpen) toggleWhiteboard();
|
||||
|
||||
wbCanvas.loadFromJSON(config.wbCanvasJson);
|
||||
wbCanvas.renderAll();
|
||||
|
||||
wbIsRedoing = true;
|
||||
wbCanvas.loadFromJSON(config.wbCanvasJson, function () {
|
||||
wbCanvas.renderAll();
|
||||
wbIsRedoing = false;
|
||||
});
|
||||
if (!isPresenter && !wbCanvas.isDrawingMode && wbIsLock) {
|
||||
wbDrawing(false);
|
||||
}
|
||||
@@ -11760,7 +11996,7 @@ function confirmCleanBoard() {
|
||||
Swal.fire({
|
||||
background: swBg,
|
||||
imageUrl: images.delete,
|
||||
position: 'center',
|
||||
position: 'top',
|
||||
title: 'Clean the board',
|
||||
text: 'Are you sure you want to clean the board?',
|
||||
showDenyButton: true,
|
||||
@@ -11810,6 +12046,7 @@ function handleWhiteboardAction(config, logMe = true) {
|
||||
break;
|
||||
case 'clear':
|
||||
wbCanvas.clear();
|
||||
wbCanvas.renderAll();
|
||||
break;
|
||||
case 'toggle':
|
||||
toggleWhiteboard();
|
||||
@@ -11850,6 +12087,183 @@ function wbDrawing(status) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show whiteboard shortcuts
|
||||
*/
|
||||
function showWhiteboardShortcuts() {
|
||||
if (!whiteboardShortcutsContent) {
|
||||
console.error('Whiteboard shortcuts content not found');
|
||||
return;
|
||||
}
|
||||
Swal.fire({
|
||||
background: swBg,
|
||||
position: 'center',
|
||||
title: 'Whiteboard Shortcuts',
|
||||
html: whiteboardShortcutsContent.innerHTML,
|
||||
confirmButtonText: 'Got it!',
|
||||
showClass: { popup: 'animate__animated animate__fadeInDown' },
|
||||
hideClass: { popup: 'animate__animated animate__fadeOutUp' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup whiteboard drag and drop
|
||||
* @returns void
|
||||
*/
|
||||
function setupWhiteboardDragAndDrop() {
|
||||
if (!wbCanvas) return;
|
||||
|
||||
// Prevent default drag behaviors
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach((eventName) => {
|
||||
wbCanvas.upperCanvasEl.addEventListener(eventName, preventDefaults, false);
|
||||
});
|
||||
|
||||
function preventDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Highlight drop area
|
||||
['dragenter', 'dragover'].forEach((eventName) => {
|
||||
wbCanvas.upperCanvasEl.addEventListener(
|
||||
eventName,
|
||||
() => {
|
||||
wbCanvas.upperCanvasEl.style.border = '1px dashed #fff';
|
||||
},
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach((eventName) => {
|
||||
wbCanvas.upperCanvasEl.addEventListener(
|
||||
eventName,
|
||||
() => {
|
||||
wbCanvas.upperCanvasEl.style.border = '';
|
||||
},
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
// Handle dropped files
|
||||
wbCanvas.upperCanvasEl.addEventListener('drop', handleWhiteboardDrop, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle whiteboard drop
|
||||
* @param {object} e event
|
||||
*/
|
||||
function handleWhiteboardDrop(e) {
|
||||
const dt = e.dataTransfer;
|
||||
const files = dt.files;
|
||||
|
||||
if (files.length === 0) return;
|
||||
|
||||
const file = files[0];
|
||||
const fileType = file.type;
|
||||
|
||||
switch (true) {
|
||||
case fileType.startsWith('image/'):
|
||||
renderImageToCanvas(file);
|
||||
break;
|
||||
case fileType === 'application/pdf':
|
||||
renderPdfToCanvas(file);
|
||||
break;
|
||||
default:
|
||||
userLog('warning', `Unsupported file type: ${fileType}. Please drop an image or PDF file.`, 6000);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup whiteboard keyboard shortcuts
|
||||
*/
|
||||
function setupWhiteboardShortcuts() {
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (!wbIsOpen) return;
|
||||
|
||||
// Whiteboard clone shortcut: Cmd+C/Ctrl+C
|
||||
if ((event.key === 'c' || event.key === 'C') && (event.ctrlKey || event.metaKey)) {
|
||||
whiteboardCloneObject();
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
// Whiteboard erase shortcut: Cmd+X/Ctrl+X
|
||||
if ((event.key === 'x' || event.key === 'X') && (event.ctrlKey || event.metaKey)) {
|
||||
whiteboardEraseObject();
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// Whiteboard undo shortcuts: Cmd+Z/Ctrl+Z
|
||||
if ((event.key === 'z' || event.key === 'Z') && (event.ctrlKey || event.metaKey) && !event.shiftKey) {
|
||||
whiteboardAction(getWhiteboardAction('undo'));
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
// Whiteboard Redo shortcuts: Cmd+Shift+Z/Ctrl+Shift+Z or Cmd+Y/Ctrl+Y
|
||||
if (
|
||||
((event.key === 'z' || event.key === 'Z') && (event.ctrlKey || event.metaKey) && event.shiftKey) ||
|
||||
((event.key === 'y' || event.key === 'Y') && (event.ctrlKey || event.metaKey))
|
||||
) {
|
||||
whiteboardAction(getWhiteboardAction('redo'));
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// Use event.code and check for Alt+Meta (Mac) or Alt+Ctrl (Windows/Linux)
|
||||
if (event.code && event.altKey && (event.ctrlKey || event.metaKey) && !event.shiftKey) {
|
||||
switch (event.code) {
|
||||
case 'KeyT': // Text
|
||||
whiteboardAddObj('text');
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'KeyL': // Line
|
||||
whiteboardAddObj('line');
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'KeyC': // Circle
|
||||
whiteboardAddObj('circle');
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'KeyR': // Rectangle
|
||||
whiteboardAddObj('rect');
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'KeyG': // Triangle (G for Geometry)
|
||||
whiteboardAddObj('triangle');
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'KeyN': // Sticky Note
|
||||
whiteboardAddObj('stickyNote');
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'KeyU': // Image (from URL)
|
||||
whiteboardAddObj('imgUrl');
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'KeyV': // Vanishing Pen
|
||||
whiteboardIsVanishingMode(!wbIsVanishing);
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'KeyI': // Image (from file)
|
||||
whiteboardAddObj('imgFile');
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'KeyP': // PDF (from file)
|
||||
whiteboardAddObj('pdfFile');
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'KeyQ': // Clear Board
|
||||
confirmCleanBoard();
|
||||
event.preventDefault();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create File Sharing Data Channel
|
||||
* @param {string} peer_id socket.id
|
||||
@@ -12662,7 +13076,7 @@ function showAbout() {
|
||||
Swal.fire({
|
||||
background: swBg,
|
||||
position: 'center',
|
||||
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.6.65',
|
||||
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.6.66',
|
||||
imageUrl: brand.about?.imageUrl && brand.about.imageUrl.trim() !== '' ? brand.about.imageUrl : images.about,
|
||||
customClass: { image: 'img-about' },
|
||||
html: `
|
||||
|
||||
@@ -962,7 +962,9 @@ access to use this app.
|
||||
<input id="wbBackgroundColorEl" class="whiteboardColorPicker" type="color" value="#000000" />
|
||||
<input id="wbDrawingColorEl" class="whiteboardColorPicker" type="color" value="#FFFFFF" />
|
||||
<button id="whiteboardPencilBtn" class="fas fa-pencil-alt"></button>
|
||||
<button id="whiteboardVanishingBtn" class="fas fa-highlighter"></button>
|
||||
<button id="whiteboardObjectBtn" class="fas fa-mouse-pointer"></button>
|
||||
<button id="whiteboardEraserBtn" class="fas fa-eraser"></button>
|
||||
<button id="whiteboardUndoBtn" class="fas fa-undo"></button>
|
||||
<button id="whiteboardRedoBtn" class="fas fa-redo"></button>
|
||||
<div class="whiteboard-dropdown">
|
||||
@@ -984,6 +986,9 @@ access to use this app.
|
||||
<button id="whiteboardImgUrlBtn"><i class="fas fa-link"></i> Add image URL</button>
|
||||
<button id="whiteboardPdfFileBtn"><i class="far fa-file-pdf"></i> Add pdf file</button>
|
||||
<button id="whiteboardTextBtn"><i class="fas fa-spell-check"></i> Add text</button>
|
||||
<button id="whiteboardStickyNoteBtn">
|
||||
<i class="fas fa-sticky-note"></i> Add sticky note
|
||||
</button>
|
||||
<button id="whiteboardLineBtn"><i class="fas fa-slash"></i> Add line</button>
|
||||
<button id="whiteboardRectBtn"><i class="far fa-square"></i> Add rectangle</button>
|
||||
<button id="whiteboardTriangleBtn">
|
||||
@@ -1000,14 +1005,46 @@ access to use this app.
|
||||
</button>
|
||||
<button id="whiteboardCircleBtn"><i class="far fa-circle"></i> Add circle</button>
|
||||
<button id="whiteboardSaveBtn"><i class="fas fa-save"></i> Save</button>
|
||||
<button id="whiteboardEraserBtn"><i class="fas fa-eraser"></i> Eraser</button>
|
||||
<button id="whiteboardCleanBtn"><i class="fas fa-trash"></i> Clean</button>
|
||||
<button id="whiteboardShortcutsBtn"><i class="fas fa-keyboard"></i> Shortcuts</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<canvas id="wbCanvas"></canvas>
|
||||
<div id="whiteboardShortcutsContent">
|
||||
<div class="wb-shortcuts-container">
|
||||
<h3 class="wb-shortcuts-title"><i class="fas fa-keyboard"></i> General</h3>
|
||||
<pre class="wb-shortcuts-code">
|
||||
Clone: ⌘/Ctrl + C
|
||||
Erase: ⌘/Ctrl + X
|
||||
Undo: ⌘/Ctrl + Z
|
||||
Redo: ⌘/Ctrl + Shift + Z or ⌘/Ctrl + Y</pre
|
||||
>
|
||||
|
||||
<h3 class="wb-shortcuts-title"><i class="fas fa-pencil-alt"></i> Create Objects</h3>
|
||||
<p class="wb-shortcuts-text"><strong>Hold:</strong></p>
|
||||
<ul class="wb-shortcuts-list">
|
||||
<li>• Alt + Cmd (Mac)</li>
|
||||
<li>• Alt + Ctrl (Win/Linux)</li>
|
||||
</ul>
|
||||
<p class="wb-shortcuts-text"><strong>Then press:</strong></p>
|
||||
<pre class="wb-shortcuts-code">
|
||||
T = Text
|
||||
L = Line
|
||||
C = Circle
|
||||
R = Rectangle
|
||||
G = Triangle
|
||||
N = Sticky Note
|
||||
U = Image URL
|
||||
I = Image
|
||||
P = PDF
|
||||
V = Vanishing Pen
|
||||
Q = Clear all</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user