Add SnapDOM Integration and Refactor Dashboard Tab Functionality
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Introduced @zumer/snapdom for enhanced tab preview capture, replacing html2canvas-pro for improved performance and reliability. - Refactored DashboardTabPanes and DashboardTabs components to utilize the new tab preview logic, streamlining the rendering process. - Updated CSS styles for dashboard tab previews, enhancing visual feedback and layout consistency. - Removed deprecated preview capture logic and associated worker, simplifying the codebase. - Enhanced NavigationTabsContext to manage tab previews and capturing state more effectively.
This commit is contained in:
parent
fcad667516
commit
969f5fece2
@ -4022,43 +4022,36 @@ body.objectKanbanColumnResizing * {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-tab-pane {
|
.dashboard-tab-pane {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-tab-pane-active {
|
.dashboard-tab-pane-active {
|
||||||
position: relative;
|
|
||||||
left: 0;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-tab-pane-inactive {
|
.dashboard-tab-pane-inactive {
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: -100000px;
|
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
|
overflow: hidden;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
visibility: hidden;
|
||||||
|
transform: translate(-100%, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-tab-pane-inactive.dashboard-tab-pane-capturing {
|
.dashboard-tab-pane-inactive.dashboard-tab-pane-capturing {
|
||||||
position: fixed;
|
/* Stay translated off-screen so capture cannot cover the active tab.
|
||||||
top: 0;
|
Layout size stays 100% of the host for SnapDOM. */
|
||||||
left: calc(-1 * var(--dashboard-tab-capture-width) - 48px);
|
|
||||||
z-index: 2;
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: var(--dashboard-tab-capture-width);
|
|
||||||
height: var(--dashboard-tab-capture-height);
|
|
||||||
min-width: var(--dashboard-tab-capture-width);
|
|
||||||
min-height: var(--dashboard-tab-capture-height);
|
|
||||||
max-width: var(--dashboard-tab-capture-width);
|
|
||||||
max-height: var(--dashboard-tab-capture-height);
|
|
||||||
overflow: hidden;
|
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
opacity: 1;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-tab-preview-popover .ant-popover-inner {
|
.dashboard-tab-preview-popover .ant-popover-inner {
|
||||||
@ -4069,18 +4062,69 @@ body.objectKanbanColumnResizing * {
|
|||||||
.dashboard-tab-preview {
|
.dashboard-tab-preview {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 320px;
|
width: 320px;
|
||||||
height: var(--dashboard-tab-preview-height, 180px);
|
|
||||||
background: var(--tab-preview-render-background);
|
background: var(--tab-preview-render-background);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-tab-preview-image {
|
.dashboard-tab-preview-stack {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-tab-preview-svg {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-tab-preview-svg-current {
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-tab-preview-svg-previous {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
object-position: top center;
|
object-position: top center;
|
||||||
|
filter: blur(8px);
|
||||||
|
opacity: 1;
|
||||||
|
transition:
|
||||||
|
opacity 280ms ease,
|
||||||
|
filter 280ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-tab-preview-stack:has(.dashboard-tab-preview-loading):not(
|
||||||
|
:has(.dashboard-tab-preview-svg-current)
|
||||||
|
) {
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-tab-preview-svg-previous.is-revealing {
|
||||||
|
opacity: 0;
|
||||||
|
filter: blur(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-tab-preview-loading {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-tab-preview-loading-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--color-text-secondary, rgba(255, 255, 255, 0.92));
|
||||||
|
filter: drop-shadow(0 0 10px rgba(0, 0, 0, 0.45));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-tab-preview-placeholder {
|
.dashboard-tab-preview-placeholder {
|
||||||
|
|||||||
14
bun.lock
14
bun.lock
@ -43,6 +43,7 @@
|
|||||||
"@tsparticles/slim": "^3.9.1",
|
"@tsparticles/slim": "^3.9.1",
|
||||||
"@uiw/react-codemirror": "^4.25.1",
|
"@uiw/react-codemirror": "^4.25.1",
|
||||||
"@vscode/sudo-prompt": "^9.3.2",
|
"@vscode/sudo-prompt": "^9.3.2",
|
||||||
|
"@zumer/snapdom": "^3.0.0",
|
||||||
"antd": "^5.27.1",
|
"antd": "^5.27.1",
|
||||||
"antd-style": "^3.7.1",
|
"antd-style": "^3.7.1",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
@ -57,7 +58,6 @@
|
|||||||
"echarts-for-react": "^3.0.6",
|
"echarts-for-react": "^3.0.6",
|
||||||
"electron-store": "^11.0.2",
|
"electron-store": "^11.0.2",
|
||||||
"gcode-preview": "3.0.0-alpha.6",
|
"gcode-preview": "3.0.0-alpha.6",
|
||||||
"html2canvas-pro": "^2.4.3",
|
|
||||||
"json-schema-traverse": "^1.0.0",
|
"json-schema-traverse": "^1.0.0",
|
||||||
"keycloak-js": "^26.2.0",
|
"keycloak-js": "^26.2.0",
|
||||||
"lodash": "^4.17.23",
|
"lodash": "^4.17.23",
|
||||||
@ -951,6 +951,8 @@
|
|||||||
|
|
||||||
"@zeit/schemas": ["@zeit/schemas@2.36.0", "", {}, "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg=="],
|
"@zeit/schemas": ["@zeit/schemas@2.36.0", "", {}, "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg=="],
|
||||||
|
|
||||||
|
"@zumer/snapdom": ["@zumer/snapdom@3.0.0", "", {}, "sha512-O+gqKYz+7545JNgG4Hzdy9/i+7/H38bDpF6tUOCJBqAUTm9npbaE3ZxuevFBcRi6cBLpvMqVYIjtywAZIP6OFQ=="],
|
||||||
|
|
||||||
"abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="],
|
"abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="],
|
||||||
|
|
||||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "3.0.2", "negotiator": "1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "3.0.2", "negotiator": "1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||||
@ -1039,8 +1041,6 @@
|
|||||||
|
|
||||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
"base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="],
|
|
||||||
|
|
||||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||||
|
|
||||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.11.4", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-s4+sLr9mZ/CyqeRritFeYV/Zx73OAtmaHn6kkBS1XRoJn1hrg3xIDUcpicAEX68tkcIN0iBCgti31C8zxtkhsQ=="],
|
"baseline-browser-mapping": ["baseline-browser-mapping@2.11.4", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-s4+sLr9mZ/CyqeRritFeYV/Zx73OAtmaHn6kkBS1XRoJn1hrg3xIDUcpicAEX68tkcIN0iBCgti31C8zxtkhsQ=="],
|
||||||
@ -1201,8 +1201,6 @@
|
|||||||
|
|
||||||
"css-color-keywords": ["css-color-keywords@1.0.0", "", {}, "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg=="],
|
"css-color-keywords": ["css-color-keywords@1.0.0", "", {}, "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg=="],
|
||||||
|
|
||||||
"css-line-break": ["css-line-break@2.1.0", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w=="],
|
|
||||||
|
|
||||||
"css-mediaquery": ["css-mediaquery@0.1.2", "", {}, "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q=="],
|
"css-mediaquery": ["css-mediaquery@0.1.2", "", {}, "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q=="],
|
||||||
|
|
||||||
"css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "1.0.0", "css-what": "6.2.2", "domhandler": "5.0.3", "domutils": "3.2.2", "nth-check": "2.1.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
|
"css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "1.0.0", "css-what": "6.2.2", "domhandler": "5.0.3", "domutils": "3.2.2", "nth-check": "2.1.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
|
||||||
@ -1579,8 +1577,6 @@
|
|||||||
|
|
||||||
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
|
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
|
||||||
|
|
||||||
"html2canvas-pro": ["html2canvas-pro@2.4.3", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-ov+EmxDc2abD2uKYpZdFWHkQkl3pu2fH0N98khT1Ojfqr3DJOuGyFIFTSfSyaWxddK9jSJu8sM7aUATxM26mDg=="],
|
|
||||||
|
|
||||||
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
|
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
|
||||||
|
|
||||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.2", "toidentifier": "1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.2", "toidentifier": "1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||||
@ -2527,8 +2523,6 @@
|
|||||||
|
|
||||||
"terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "0.3.11", "acorn": "8.17.0", "commander": "2.20.3", "source-map-support": "0.5.21" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="],
|
"terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "0.3.11", "acorn": "8.17.0", "commander": "2.20.3", "source-map-support": "0.5.21" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="],
|
||||||
|
|
||||||
"text-segmentation": ["text-segmentation@1.0.3", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw=="],
|
|
||||||
|
|
||||||
"text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="],
|
"text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="],
|
||||||
|
|
||||||
"three": ["three@0.179.1", "", {}, "sha512-5y/elSIQbrvKOISxpwXCR4sQqHtGiOI+MKLc3SsBdDXA2hz3Mdp3X59aUp8DyybMa34aeBwbFTpdoLJaUDEWSw=="],
|
"three": ["three@0.179.1", "", {}, "sha512-5y/elSIQbrvKOISxpwXCR4sQqHtGiOI+MKLc3SsBdDXA2hz3Mdp3X59aUp8DyybMa34aeBwbFTpdoLJaUDEWSw=="],
|
||||||
@ -2637,8 +2631,6 @@
|
|||||||
|
|
||||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||||
|
|
||||||
"utrie": ["utrie@1.0.2", "", { "dependencies": { "base64-arraybuffer": "^1.0.2" } }, "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw=="],
|
|
||||||
|
|
||||||
"validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="],
|
"validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="],
|
||||||
|
|
||||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||||
|
|||||||
@ -47,6 +47,7 @@
|
|||||||
"@tsparticles/slim": "^3.9.1",
|
"@tsparticles/slim": "^3.9.1",
|
||||||
"@uiw/react-codemirror": "^4.25.1",
|
"@uiw/react-codemirror": "^4.25.1",
|
||||||
"@vscode/sudo-prompt": "^9.3.2",
|
"@vscode/sudo-prompt": "^9.3.2",
|
||||||
|
"@zumer/snapdom": "^3.0.0",
|
||||||
"antd": "^5.27.1",
|
"antd": "^5.27.1",
|
||||||
"antd-style": "^3.7.1",
|
"antd-style": "^3.7.1",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
@ -61,7 +62,6 @@
|
|||||||
"echarts-for-react": "^3.0.6",
|
"echarts-for-react": "^3.0.6",
|
||||||
"electron-store": "^11.0.2",
|
"electron-store": "^11.0.2",
|
||||||
"gcode-preview": "3.0.0-alpha.6",
|
"gcode-preview": "3.0.0-alpha.6",
|
||||||
"html2canvas-pro": "^2.4.3",
|
|
||||||
"json-schema-traverse": "^1.0.0",
|
"json-schema-traverse": "^1.0.0",
|
||||||
"keycloak-js": "^26.2.0",
|
"keycloak-js": "^26.2.0",
|
||||||
"lodash": "^4.17.23",
|
"lodash": "^4.17.23",
|
||||||
|
|||||||
47
pnpm-lock.yaml
generated
47
pnpm-lock.yaml
generated
@ -130,6 +130,9 @@ importers:
|
|||||||
'@vscode/sudo-prompt':
|
'@vscode/sudo-prompt':
|
||||||
specifier: ^9.3.2
|
specifier: ^9.3.2
|
||||||
version: 9.3.2
|
version: 9.3.2
|
||||||
|
'@zumer/snapdom':
|
||||||
|
specifier: ^3.0.0
|
||||||
|
version: 3.0.0
|
||||||
antd:
|
antd:
|
||||||
specifier: ^5.27.1
|
specifier: ^5.27.1
|
||||||
version: 5.29.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 5.29.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
@ -172,9 +175,6 @@ importers:
|
|||||||
gcode-preview:
|
gcode-preview:
|
||||||
specifier: 3.0.0-alpha.6
|
specifier: 3.0.0-alpha.6
|
||||||
version: 3.0.0-alpha.6
|
version: 3.0.0-alpha.6
|
||||||
html2canvas-pro:
|
|
||||||
specifier: ^2.4.3
|
|
||||||
version: 2.4.3
|
|
||||||
json-schema-traverse:
|
json-schema-traverse:
|
||||||
specifier: ^1.0.0
|
specifier: ^1.0.0
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
@ -2418,6 +2418,9 @@ packages:
|
|||||||
'@zeit/schemas@2.36.0':
|
'@zeit/schemas@2.36.0':
|
||||||
resolution: {integrity: sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==}
|
resolution: {integrity: sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==}
|
||||||
|
|
||||||
|
'@zumer/snapdom@3.0.0':
|
||||||
|
resolution: {integrity: sha512-O+gqKYz+7545JNgG4Hzdy9/i+7/H38bDpF6tUOCJBqAUTm9npbaE3ZxuevFBcRi6cBLpvMqVYIjtywAZIP6OFQ==}
|
||||||
|
|
||||||
abbrev@3.0.1:
|
abbrev@3.0.1:
|
||||||
resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==}
|
resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==}
|
||||||
engines: {node: ^18.17.0 || >=20.5.0}
|
engines: {node: ^18.17.0 || >=20.5.0}
|
||||||
@ -2638,10 +2641,6 @@ packages:
|
|||||||
balanced-match@1.0.2:
|
balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
base64-arraybuffer@1.0.2:
|
|
||||||
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
|
|
||||||
engines: {node: '>= 0.6.0'}
|
|
||||||
|
|
||||||
base64-js@1.5.1:
|
base64-js@1.5.1:
|
||||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||||
|
|
||||||
@ -3017,9 +3016,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==}
|
resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
css-line-break@2.1.0:
|
|
||||||
resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
|
|
||||||
|
|
||||||
css-mediaquery@0.1.2:
|
css-mediaquery@0.1.2:
|
||||||
resolution: {integrity: sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==}
|
resolution: {integrity: sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==}
|
||||||
|
|
||||||
@ -3969,10 +3965,6 @@ packages:
|
|||||||
html-url-attributes@3.0.1:
|
html-url-attributes@3.0.1:
|
||||||
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
|
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
|
||||||
|
|
||||||
html2canvas-pro@2.4.3:
|
|
||||||
resolution: {integrity: sha512-ov+EmxDc2abD2uKYpZdFWHkQkl3pu2fH0N98khT1Ojfqr3DJOuGyFIFTSfSyaWxddK9jSJu8sM7aUATxM26mDg==}
|
|
||||||
engines: {node: '>=16.0.0'}
|
|
||||||
|
|
||||||
http-cache-semantics@4.2.0:
|
http-cache-semantics@4.2.0:
|
||||||
resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
|
resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
|
||||||
|
|
||||||
@ -6145,9 +6137,6 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
text-segmentation@1.0.3:
|
|
||||||
resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
|
|
||||||
|
|
||||||
text-table@0.2.0:
|
text-table@0.2.0:
|
||||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||||
|
|
||||||
@ -6380,9 +6369,6 @@ packages:
|
|||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
utrie@1.0.2:
|
|
||||||
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
|
|
||||||
|
|
||||||
validate-npm-package-license@3.0.4:
|
validate-npm-package-license@3.0.4:
|
||||||
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
|
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
|
||||||
|
|
||||||
@ -9014,6 +9000,8 @@ snapshots:
|
|||||||
|
|
||||||
'@zeit/schemas@2.36.0': {}
|
'@zeit/schemas@2.36.0': {}
|
||||||
|
|
||||||
|
'@zumer/snapdom@3.0.0': {}
|
||||||
|
|
||||||
abbrev@3.0.1: {}
|
abbrev@3.0.1: {}
|
||||||
|
|
||||||
accepts@2.0.0:
|
accepts@2.0.0:
|
||||||
@ -9337,8 +9325,6 @@ snapshots:
|
|||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
|
|
||||||
base64-arraybuffer@1.0.2: {}
|
|
||||||
|
|
||||||
base64-js@1.5.1: {}
|
base64-js@1.5.1: {}
|
||||||
|
|
||||||
baseline-browser-mapping@2.11.4: {}
|
baseline-browser-mapping@2.11.4: {}
|
||||||
@ -9754,10 +9740,6 @@ snapshots:
|
|||||||
|
|
||||||
css-color-keywords@1.0.0: {}
|
css-color-keywords@1.0.0: {}
|
||||||
|
|
||||||
css-line-break@2.1.0:
|
|
||||||
dependencies:
|
|
||||||
utrie: 1.0.2
|
|
||||||
|
|
||||||
css-mediaquery@0.1.2: {}
|
css-mediaquery@0.1.2: {}
|
||||||
|
|
||||||
css-select@5.2.2:
|
css-select@5.2.2:
|
||||||
@ -11092,11 +11074,6 @@ snapshots:
|
|||||||
|
|
||||||
html-url-attributes@3.0.1: {}
|
html-url-attributes@3.0.1: {}
|
||||||
|
|
||||||
html2canvas-pro@2.4.3:
|
|
||||||
dependencies:
|
|
||||||
css-line-break: 2.1.0
|
|
||||||
text-segmentation: 1.0.3
|
|
||||||
|
|
||||||
http-cache-semantics@4.2.0: {}
|
http-cache-semantics@4.2.0: {}
|
||||||
|
|
||||||
http-errors@2.0.1:
|
http-errors@2.0.1:
|
||||||
@ -13839,10 +13816,6 @@ snapshots:
|
|||||||
source-map-support: 0.5.21
|
source-map-support: 0.5.21
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
text-segmentation@1.0.3:
|
|
||||||
dependencies:
|
|
||||||
utrie: 1.0.2
|
|
||||||
|
|
||||||
text-table@0.2.0: {}
|
text-table@0.2.0: {}
|
||||||
|
|
||||||
three@0.165.0: {}
|
three@0.165.0: {}
|
||||||
@ -14096,10 +14069,6 @@ snapshots:
|
|||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
utrie@1.0.2:
|
|
||||||
dependencies:
|
|
||||||
base64-arraybuffer: 1.0.2
|
|
||||||
|
|
||||||
validate-npm-package-license@3.0.4:
|
validate-npm-package-license@3.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
spdx-correct: 3.2.0
|
spdx-correct: 3.2.0
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { useLayoutEffect, useRef } from 'react'
|
import { memo, useLayoutEffect, useMemo, useRef } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { Outlet, Routes, UNSAFE_LocationContext, useLocation } from 'react-router-dom'
|
import { Outlet, Routes, UNSAFE_LocationContext, useLocation } from 'react-router-dom'
|
||||||
import { TableStateProvider } from '../context/TableStateContext'
|
import { TableStateProvider } from '../context/TableStateContext'
|
||||||
@ -40,16 +41,65 @@ const CachedTabRouteLayout = () => (
|
|||||||
</TableStateProvider>
|
</TableStateProvider>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const arePanePropsEqual = (prev, next) =>
|
||||||
|
prev.tabId === next.tabId &&
|
||||||
|
prev.isActive === next.isActive &&
|
||||||
|
prev.registerTabPane === next.registerTabPane &&
|
||||||
|
prev.loc === next.loc
|
||||||
|
|
||||||
|
const DashboardTabPane = memo(function DashboardTabPane({
|
||||||
|
tabId,
|
||||||
|
isActive,
|
||||||
|
loc,
|
||||||
|
registerTabPane
|
||||||
|
}) {
|
||||||
|
const tabRoutes = useMemo(
|
||||||
|
() => wrapDashboardChildRoutes(<CachedTabRouteLayout />),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
const locationContextValue = useMemo(
|
||||||
|
() => ({ location: loc, navigationType: 'POP' }),
|
||||||
|
[loc]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={(element) => registerTabPane(tabId, element)}
|
||||||
|
data-tab-id={tabId}
|
||||||
|
className={classNames('dashboard-tab-pane', {
|
||||||
|
'dashboard-tab-pane-active': isActive,
|
||||||
|
'dashboard-tab-pane-inactive': !isActive
|
||||||
|
})}
|
||||||
|
aria-hidden={!isActive}
|
||||||
|
inert={!isActive}
|
||||||
|
>
|
||||||
|
<NavigationTabActiveContext.Provider value={isActive}>
|
||||||
|
<UNSAFE_LocationContext.Provider value={locationContextValue}>
|
||||||
|
<Routes location={loc}>{tabRoutes}</Routes>
|
||||||
|
</UNSAFE_LocationContext.Provider>
|
||||||
|
</NavigationTabActiveContext.Provider>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}, arePanePropsEqual)
|
||||||
|
|
||||||
|
DashboardTabPane.displayName = 'DashboardTabPane'
|
||||||
|
|
||||||
|
DashboardTabPane.propTypes = {
|
||||||
|
tabId: PropTypes.string.isRequired,
|
||||||
|
isActive: PropTypes.bool,
|
||||||
|
loc: PropTypes.shape({
|
||||||
|
pathname: PropTypes.string,
|
||||||
|
search: PropTypes.string,
|
||||||
|
hash: PropTypes.string,
|
||||||
|
key: PropTypes.string,
|
||||||
|
state: PropTypes.any
|
||||||
|
}).isRequired,
|
||||||
|
registerTabPane: PropTypes.func.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
const DashboardTabPanes = () => {
|
const DashboardTabPanes = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const {
|
const { tabs, activeTabId, isElectron, registerTabPane } = useNavigationTabs()
|
||||||
tabs,
|
|
||||||
activeTabId,
|
|
||||||
isElectron,
|
|
||||||
registerTabPane,
|
|
||||||
previewCaptureLayout,
|
|
||||||
reportPreviewCaptureLayout
|
|
||||||
} = useNavigationTabs()
|
|
||||||
const frozenRef = useRef(new Map())
|
const frozenRef = useRef(new Map())
|
||||||
const prevActiveIdRef = useRef(activeTabId)
|
const prevActiveIdRef = useRef(activeTabId)
|
||||||
const lastLocationRef = useRef(location)
|
const lastLocationRef = useRef(location)
|
||||||
@ -80,35 +130,6 @@ const DashboardTabPanes = () => {
|
|||||||
window.dispatchEvent(new Event('resize'))
|
window.dispatchEvent(new Event('resize'))
|
||||||
}, [activeTabId, isElectron])
|
}, [activeTabId, isElectron])
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (!previewCaptureLayout?.tabId) return undefined
|
|
||||||
|
|
||||||
const pane =
|
|
||||||
document.querySelector(
|
|
||||||
`.dashboard-tab-pane[data-tab-id="${CSS.escape(previewCaptureLayout.tabId)}"]`
|
|
||||||
)
|
|
||||||
if (!pane) return undefined
|
|
||||||
|
|
||||||
void pane.offsetHeight
|
|
||||||
window.dispatchEvent(new Event('resize'))
|
|
||||||
|
|
||||||
let cancelled = false
|
|
||||||
let secondFrame = 0
|
|
||||||
const firstFrame = window.requestAnimationFrame(() => {
|
|
||||||
secondFrame = window.requestAnimationFrame(() => {
|
|
||||||
if (cancelled) return
|
|
||||||
void pane.offsetHeight
|
|
||||||
reportPreviewCaptureLayout(previewCaptureLayout.tabId, pane)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true
|
|
||||||
window.cancelAnimationFrame(firstFrame)
|
|
||||||
if (secondFrame) window.cancelAnimationFrame(secondFrame)
|
|
||||||
}
|
|
||||||
}, [previewCaptureLayout, reportPreviewCaptureLayout])
|
|
||||||
|
|
||||||
prevActiveIdRef.current = activeTabId
|
prevActiveIdRef.current = activeTabId
|
||||||
lastLocationRef.current = location
|
lastLocationRef.current = location
|
||||||
|
|
||||||
@ -116,58 +137,32 @@ const DashboardTabPanes = () => {
|
|||||||
return <Outlet />
|
return <Outlet />
|
||||||
}
|
}
|
||||||
|
|
||||||
const capturingTabId = previewCaptureLayout?.tabId
|
const stackedTabs = activeTab
|
||||||
const capturingIndex = capturingTabId
|
? [...tabs.filter((tab) => tab.id !== activeTabId), activeTab]
|
||||||
? tabs.findIndex((tab) => tab.id === capturingTabId)
|
: tabs
|
||||||
: -1
|
|
||||||
const renderTabs =
|
|
||||||
capturingIndex >= 0 && capturingIndex < tabs.length - 1
|
|
||||||
? [...tabs.filter((tab) => tab.id !== capturingTabId), tabs[capturingIndex]]
|
|
||||||
: tabs
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='dashboard-tab-panes'>
|
<div className='dashboard-tab-panes'>
|
||||||
{renderTabs.map((tab) => {
|
{stackedTabs.map((tab) => {
|
||||||
const isActive = tab.id === activeTabId
|
const isActive = tab.id === activeTabId
|
||||||
const isCapturing =
|
|
||||||
!isActive && previewCaptureLayout?.tabId === tab.id
|
|
||||||
const loc =
|
const loc =
|
||||||
isActive && !pendingRestoreRef.current
|
isActive && !pendingRestoreRef.current
|
||||||
? location
|
? location
|
||||||
: frozenRef.current.get(tab.id) ||
|
: frozenRef.current.get(tab.id) ||
|
||||||
entryToLocation(getTabCurrentEntry(tab), location, tab.id)
|
entryToLocation(getTabCurrentEntry(tab), location, tab.id)
|
||||||
|
|
||||||
|
if (!isActive && !frozenRef.current.has(tab.id)) {
|
||||||
|
frozenRef.current.set(tab.id, loc)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<DashboardTabPane
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
ref={(element) => registerTabPane(tab.id, element)}
|
tabId={tab.id}
|
||||||
data-tab-id={tab.id}
|
isActive={isActive}
|
||||||
className={classNames('dashboard-tab-pane', {
|
loc={loc}
|
||||||
'dashboard-tab-pane-active': isActive,
|
registerTabPane={registerTabPane}
|
||||||
'dashboard-tab-pane-inactive': !isActive,
|
/>
|
||||||
'dashboard-tab-pane-capturing': isCapturing
|
|
||||||
})}
|
|
||||||
style={
|
|
||||||
isCapturing
|
|
||||||
? {
|
|
||||||
'--dashboard-tab-capture-width': `${previewCaptureLayout.width}px`,
|
|
||||||
'--dashboard-tab-capture-height': `${previewCaptureLayout.height}px`
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
aria-hidden={!isActive}
|
|
||||||
inert={!isActive && !isCapturing}
|
|
||||||
>
|
|
||||||
<NavigationTabActiveContext.Provider value={isActive}>
|
|
||||||
<UNSAFE_LocationContext.Provider
|
|
||||||
value={{ location: loc, navigationType: 'POP' }}
|
|
||||||
>
|
|
||||||
<Routes location={loc}>
|
|
||||||
{wrapDashboardChildRoutes(<CachedTabRouteLayout />)}
|
|
||||||
</Routes>
|
|
||||||
</UNSAFE_LocationContext.Provider>
|
|
||||||
</NavigationTabActiveContext.Provider>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,28 +1,73 @@
|
|||||||
|
import { useLayoutEffect, useRef, useState } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
const DEFAULT_HEIGHT = 180
|
const DashboardTabPreview = ({ src, loading = false }) => {
|
||||||
|
const lastSrcRef = useRef(src || null)
|
||||||
|
const [overlaySrc, setOverlaySrc] = useState(src || null)
|
||||||
|
const [revealing, setRevealing] = useState(false)
|
||||||
|
|
||||||
const DashboardTabPreview = ({ src, height = DEFAULT_HEIGHT }) => {
|
useLayoutEffect(() => {
|
||||||
const previewHeight = typeof height === 'number' ? `${height}px` : height
|
if (loading) {
|
||||||
|
const locked = lastSrcRef.current || src || null
|
||||||
|
lastSrcRef.current = locked
|
||||||
|
setOverlaySrc(locked)
|
||||||
|
setRevealing(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!src) return null
|
if (src) {
|
||||||
|
lastSrcRef.current = src
|
||||||
|
}
|
||||||
|
|
||||||
|
setRevealing(true)
|
||||||
|
}, [loading, src])
|
||||||
|
|
||||||
|
const handleOverlayTransitionEnd = (event) => {
|
||||||
|
if (event.propertyName !== 'opacity' || !revealing) return
|
||||||
|
setOverlaySrc(null)
|
||||||
|
setRevealing(false)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className='dashboard-tab-preview'>
|
||||||
className='dashboard-tab-preview'
|
<div className='dashboard-tab-preview-stack'>
|
||||||
style={{
|
{src ? (
|
||||||
height: previewHeight,
|
<img
|
||||||
'--dashboard-tab-preview-height': previewHeight
|
src={src}
|
||||||
}}
|
alt=''
|
||||||
>
|
className='dashboard-tab-preview-svg dashboard-tab-preview-svg-current'
|
||||||
<img src={src} alt='' className='dashboard-tab-preview-image' />
|
/>
|
||||||
|
) : null}
|
||||||
|
{overlaySrc ? (
|
||||||
|
<img
|
||||||
|
src={overlaySrc}
|
||||||
|
alt=''
|
||||||
|
className={classNames(
|
||||||
|
'dashboard-tab-preview-svg',
|
||||||
|
'dashboard-tab-preview-svg-previous',
|
||||||
|
{ 'is-revealing': revealing }
|
||||||
|
)}
|
||||||
|
onTransitionEnd={handleOverlayTransitionEnd}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{loading ? (
|
||||||
|
<div className='dashboard-tab-preview-loading'>
|
||||||
|
<LoadingOutlined
|
||||||
|
spin
|
||||||
|
className='dashboard-tab-preview-loading-icon'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
DashboardTabPreview.propTypes = {
|
DashboardTabPreview.propTypes = {
|
||||||
src: PropTypes.string,
|
src: PropTypes.string,
|
||||||
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
|
loading: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DashboardTabPreview
|
export default DashboardTabPreview
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import PlusIcon from '../../Icons/PlusIcon'
|
|||||||
import XMarkIcon from '../../Icons/XMarkIcon'
|
import XMarkIcon from '../../Icons/XMarkIcon'
|
||||||
import HomeIcon from '../../Icons/HomeIcon'
|
import HomeIcon from '../../Icons/HomeIcon'
|
||||||
import { getModelByName } from '../../../database/ObjectModels'
|
import { getModelByName } from '../../../database/ObjectModels'
|
||||||
import { useNavigationTabs } from '../context/NavigationTabsContext'
|
import { useNavigationTabs, useTabPreview } from '../context/NavigationTabsContext'
|
||||||
import { getDesktopWindowId } from '../../../electrobun-bridge.js'
|
import { getDesktopWindowId } from '../../../electrobun-bridge.js'
|
||||||
import { hasExternalTabDrag, writeTabDragData } from './tabDrag'
|
import { hasExternalTabDrag, writeTabDragData } from './tabDrag'
|
||||||
|
|
||||||
@ -80,70 +80,31 @@ const DashboardTabItem = ({
|
|||||||
onClose
|
onClose
|
||||||
}) => {
|
}) => {
|
||||||
const { captureTabPreview } = useNavigationTabs()
|
const { captureTabPreview } = useNavigationTabs()
|
||||||
|
const { src: previewSrc, capturing: previewCapturing } = useTabPreview(tab.id)
|
||||||
const [previewOpen, setPreviewOpen] = useState(false)
|
const [previewOpen, setPreviewOpen] = useState(false)
|
||||||
const [previewSrc, setPreviewSrc] = useState(null)
|
|
||||||
const previewHoverRef = useRef(false)
|
|
||||||
const previewOpenRef = useRef(false)
|
|
||||||
const previewRequestRef = useRef(0)
|
|
||||||
const previewTargetRef = useRef(null)
|
const previewTargetRef = useRef(null)
|
||||||
const model = tab.modelName ? getModelByName(tab.modelName) : null
|
const model = tab.modelName ? getModelByName(tab.modelName) : null
|
||||||
const Icon = model?.icon || HomeIcon
|
const Icon = model?.icon || HomeIcon
|
||||||
|
|
||||||
const closePreview = useCallback(() => {
|
|
||||||
previewHoverRef.current = false
|
|
||||||
previewOpenRef.current = false
|
|
||||||
previewRequestRef.current += 1
|
|
||||||
setPreviewOpen(false)
|
|
||||||
setPreviewSrc(null)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const startPreviewCapture = useCallback(() => {
|
|
||||||
if (isSelected || isDragging) return
|
|
||||||
|
|
||||||
previewHoverRef.current = true
|
|
||||||
const requestId = previewRequestRef.current + 1
|
|
||||||
previewRequestRef.current = requestId
|
|
||||||
previewOpenRef.current = false
|
|
||||||
setPreviewOpen(false)
|
|
||||||
setPreviewSrc(null)
|
|
||||||
|
|
||||||
void captureTabPreview(tab.id).then((dataUrl) => {
|
|
||||||
if (
|
|
||||||
!previewHoverRef.current ||
|
|
||||||
previewRequestRef.current !== requestId ||
|
|
||||||
!dataUrl
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
previewTargetRef.current &&
|
|
||||||
!previewTargetRef.current.matches(':hover')
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setPreviewSrc(dataUrl)
|
|
||||||
previewOpenRef.current = true
|
|
||||||
setPreviewOpen(true)
|
|
||||||
})
|
|
||||||
}, [captureTabPreview, isDragging, isSelected, tab.id])
|
|
||||||
|
|
||||||
const handlePreviewOpenChange = useCallback(
|
const handlePreviewOpenChange = useCallback(
|
||||||
(nextOpen) => {
|
(nextOpen) => {
|
||||||
if (!nextOpen) closePreview()
|
if (isSelected || isDragging) {
|
||||||
|
setPreviewOpen(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setPreviewOpen(nextOpen)
|
||||||
|
if (nextOpen) {
|
||||||
|
void captureTabPreview(tab.id)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[closePreview]
|
[captureTabPreview, isDragging, isSelected, tab.id]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handlePreviewMouseLeave = useCallback(() => {
|
|
||||||
if (previewOpenRef.current) return
|
|
||||||
closePreview()
|
|
||||||
}, [closePreview])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSelected || isDragging) {
|
if (isSelected || isDragging) {
|
||||||
closePreview()
|
setPreviewOpen(false)
|
||||||
}
|
}
|
||||||
}, [closePreview, isDragging, isSelected])
|
}, [isDragging, isSelected])
|
||||||
|
|
||||||
const tabButton = (
|
const tabButton = (
|
||||||
<button
|
<button
|
||||||
@ -169,8 +130,6 @@ const DashboardTabItem = ({
|
|||||||
onKeyDown={(event) => onKeyDown(event, tabIndex)}
|
onKeyDown={(event) => onKeyDown(event, tabIndex)}
|
||||||
onDragOver={(event) => onDragOver(event, tab)}
|
onDragOver={(event) => onDragOver(event, tab)}
|
||||||
onDrop={(event) => onDrop(event, tab)}
|
onDrop={(event) => onDrop(event, tab)}
|
||||||
onMouseEnter={startPreviewCapture}
|
|
||||||
onMouseLeave={handlePreviewMouseLeave}
|
|
||||||
>
|
>
|
||||||
<DashboardTabChrome />
|
<DashboardTabChrome />
|
||||||
<span className='dashboard-tab-item-body'>
|
<span className='dashboard-tab-item-body'>
|
||||||
@ -196,7 +155,9 @@ const DashboardTabItem = ({
|
|||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
open={isDragging ? false : previewOpen}
|
open={isDragging ? false : previewOpen}
|
||||||
onOpenChange={handlePreviewOpenChange}
|
onOpenChange={handlePreviewOpenChange}
|
||||||
content={previewSrc ? <DashboardTabPreview src={previewSrc} /> : null}
|
content={
|
||||||
|
<DashboardTabPreview src={previewSrc} loading={previewCapturing} />
|
||||||
|
}
|
||||||
classNames={{ root: 'dashboard-tab-preview-popover' }}
|
classNames={{ root: 'dashboard-tab-preview-popover' }}
|
||||||
styles={{ body: { padding: 0, overflow: 'hidden' } }}
|
styles={{ body: { padding: 0, overflow: 'hidden' } }}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -14,6 +14,10 @@ import { getDesktopWindowId } from '../../../electrobun-bridge.js'
|
|||||||
import { captureTabPaneImage } from './tabPreviewCapture'
|
import { captureTabPaneImage } from './tabPreviewCapture'
|
||||||
|
|
||||||
const NavigationTabsContext = createContext()
|
const NavigationTabsContext = createContext()
|
||||||
|
const NavigationTabPreviewsContext = createContext({
|
||||||
|
tabPreviews: {},
|
||||||
|
tabPreviewCapturing: {}
|
||||||
|
})
|
||||||
|
|
||||||
// eslint-disable-next-line react-refresh/only-export-components
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export const NavigationTabActiveContext = createContext(true)
|
export const NavigationTabActiveContext = createContext(true)
|
||||||
@ -75,14 +79,15 @@ export const NavigationTabsProvider = ({ children }) => {
|
|||||||
const [tabs, setTabs] = useState([])
|
const [tabs, setTabs] = useState([])
|
||||||
const [activeTabId, setActiveTabId] = useState(null)
|
const [activeTabId, setActiveTabId] = useState(null)
|
||||||
const [hydrated, setHydrated] = useState(!isElectron)
|
const [hydrated, setHydrated] = useState(!isElectron)
|
||||||
const [previewCaptureLayout, setPreviewCaptureLayout] = useState(null)
|
const [tabPreviews, setTabPreviews] = useState({})
|
||||||
|
const [tabPreviewCapturing, setTabPreviewCapturing] = useState({})
|
||||||
const tabsRef = useRef(tabs)
|
const tabsRef = useRef(tabs)
|
||||||
const activeTabIdRef = useRef(activeTabId)
|
const activeTabIdRef = useRef(activeTabId)
|
||||||
|
const previousActiveTabIdRef = useRef(null)
|
||||||
const isRestoringRef = useRef(false)
|
const isRestoringRef = useRef(false)
|
||||||
const tabPaneElsRef = useRef(new Map())
|
const tabPaneElsRef = useRef(new Map())
|
||||||
const captureQueueRef = useRef(Promise.resolve())
|
const captureQueueRef = useRef(Promise.resolve())
|
||||||
const captureInFlightRef = useRef(new Map())
|
const captureInFlightRef = useRef(new Map())
|
||||||
const previewCaptureWaiterRef = useRef(null)
|
|
||||||
tabsRef.current = tabs
|
tabsRef.current = tabs
|
||||||
activeTabIdRef.current = activeTabId
|
activeTabIdRef.current = activeTabId
|
||||||
|
|
||||||
@ -142,6 +147,18 @@ export const NavigationTabsProvider = ({ children }) => {
|
|||||||
: remaining.find((tab) => tab.id === activeTabIdRef.current)
|
: remaining.find((tab) => tab.id === activeTabIdRef.current)
|
||||||
|
|
||||||
setTabs(remaining)
|
setTabs(remaining)
|
||||||
|
setTabPreviews((currentPreviews) => {
|
||||||
|
if (!(tabId in currentPreviews)) return currentPreviews
|
||||||
|
const nextPreviews = { ...currentPreviews }
|
||||||
|
delete nextPreviews[tabId]
|
||||||
|
return nextPreviews
|
||||||
|
})
|
||||||
|
setTabPreviewCapturing((currentCapturing) => {
|
||||||
|
if (!(tabId in currentCapturing)) return currentCapturing
|
||||||
|
const nextCapturing = { ...currentCapturing }
|
||||||
|
delete nextCapturing[tabId]
|
||||||
|
return nextCapturing
|
||||||
|
})
|
||||||
if (nextActive) {
|
if (nextActive) {
|
||||||
setActiveTabId(nextActive.id)
|
setActiveTabId(nextActive.id)
|
||||||
const nextEntry = getTabCurrentEntry(nextActive)
|
const nextEntry = getTabCurrentEntry(nextActive)
|
||||||
@ -256,19 +273,18 @@ export const NavigationTabsProvider = ({ children }) => {
|
|||||||
tabPaneElsRef.current.set(tabId, element)
|
tabPaneElsRef.current.set(tabId, element)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const reportPreviewCaptureLayout = useCallback((tabId, pane) => {
|
const captureTabPreview = useCallback((tabId) => {
|
||||||
const waiter = previewCaptureWaiterRef.current
|
if (!tabId || tabId === activeTabIdRef.current) {
|
||||||
if (!waiter || waiter.tabId !== tabId) return
|
return Promise.resolve(null)
|
||||||
previewCaptureWaiterRef.current = null
|
}
|
||||||
waiter.resolve(pane)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const captureTabPreview = useCallback((tabId, { height = 180, width = 320 } = {}) => {
|
|
||||||
if (!tabId) return Promise.resolve(null)
|
|
||||||
|
|
||||||
const inFlight = captureInFlightRef.current.get(tabId)
|
const inFlight = captureInFlightRef.current.get(tabId)
|
||||||
if (inFlight) return inFlight
|
if (inFlight) return inFlight
|
||||||
|
|
||||||
|
setTabPreviewCapturing((current) =>
|
||||||
|
current[tabId] ? current : { ...current, [tabId]: true }
|
||||||
|
)
|
||||||
|
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
if (tabId === activeTabIdRef.current) {
|
if (tabId === activeTabIdRef.current) {
|
||||||
return null
|
return null
|
||||||
@ -281,52 +297,19 @@ export const NavigationTabsProvider = ({ children }) => {
|
|||||||
)
|
)
|
||||||
if (!pane) return null
|
if (!pane) return null
|
||||||
|
|
||||||
const activePane =
|
|
||||||
tabPaneElsRef.current.get(activeTabIdRef.current) ||
|
|
||||||
document.querySelector('.dashboard-tab-pane-active')
|
|
||||||
const host = pane.parentElement
|
|
||||||
const layoutWidth = Math.round(
|
|
||||||
activePane?.clientWidth || host?.clientWidth || pane.clientWidth || 0
|
|
||||||
)
|
|
||||||
const layoutHeight = Math.round(
|
|
||||||
activePane?.clientHeight || host?.clientHeight || pane.clientHeight || 0
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const layoutReady = new Promise((resolve) => {
|
const dataUrl = await captureTabPaneImage(pane)
|
||||||
previewCaptureWaiterRef.current = { tabId, resolve }
|
if (dataUrl) {
|
||||||
setPreviewCaptureLayout({
|
setTabPreviews((current) =>
|
||||||
tabId,
|
current[tabId] === dataUrl
|
||||||
width: layoutWidth,
|
? current
|
||||||
height: layoutHeight
|
: { ...current, [tabId]: dataUrl }
|
||||||
})
|
)
|
||||||
})
|
}
|
||||||
const laidOutPane = await Promise.race([
|
|
||||||
layoutReady,
|
|
||||||
new Promise((resolve) => {
|
|
||||||
window.setTimeout(() => resolve(pane), 500)
|
|
||||||
})
|
|
||||||
])
|
|
||||||
const capturePane = laidOutPane || pane
|
|
||||||
const dataUrl = await captureTabPaneImage(capturePane, {
|
|
||||||
height,
|
|
||||||
width,
|
|
||||||
layoutWidth,
|
|
||||||
layoutHeight
|
|
||||||
})
|
|
||||||
if (!dataUrl) return null
|
|
||||||
|
|
||||||
return dataUrl
|
return dataUrl
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to capture tab preview', error)
|
console.error('Failed to capture tab preview', error)
|
||||||
return null
|
return null
|
||||||
} finally {
|
|
||||||
if (previewCaptureWaiterRef.current?.tabId === tabId) {
|
|
||||||
previewCaptureWaiterRef.current = null
|
|
||||||
}
|
|
||||||
setPreviewCaptureLayout((current) =>
|
|
||||||
current?.tabId === tabId ? null : current
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,10 +323,26 @@ export const NavigationTabsProvider = ({ children }) => {
|
|||||||
if (captureInFlightRef.current.get(tabId) === next) {
|
if (captureInFlightRef.current.get(tabId) === next) {
|
||||||
captureInFlightRef.current.delete(tabId)
|
captureInFlightRef.current.delete(tabId)
|
||||||
}
|
}
|
||||||
|
setTabPreviewCapturing((current) => {
|
||||||
|
if (!current[tabId]) return current
|
||||||
|
const nextCapturing = { ...current }
|
||||||
|
delete nextCapturing[tabId]
|
||||||
|
return nextCapturing
|
||||||
|
})
|
||||||
})
|
})
|
||||||
return next
|
return next
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const previousId = previousActiveTabIdRef.current
|
||||||
|
previousActiveTabIdRef.current = activeTabId
|
||||||
|
if (!hydrated || !previousId || !activeTabId || previousId === activeTabId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!tabsRef.current.some((tab) => tab.id === previousId)) return
|
||||||
|
void captureTabPreview(previousId)
|
||||||
|
}, [activeTabId, captureTabPreview, hydrated])
|
||||||
|
|
||||||
const setTabPage = useCallback(({ title, modelName } = {}) => {
|
const setTabPage = useCallback(({ title, modelName } = {}) => {
|
||||||
const activeId = activeTabIdRef.current
|
const activeId = activeTabIdRef.current
|
||||||
if (!activeId) return
|
if (!activeId) return
|
||||||
@ -558,9 +557,7 @@ export const NavigationTabsProvider = ({ children }) => {
|
|||||||
canGoForward,
|
canGoForward,
|
||||||
createNewWindow,
|
createNewWindow,
|
||||||
registerTabPane,
|
registerTabPane,
|
||||||
captureTabPreview,
|
captureTabPreview
|
||||||
previewCaptureLayout,
|
|
||||||
reportPreviewCaptureLayout
|
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
activeTab,
|
activeTab,
|
||||||
@ -569,8 +566,6 @@ export const NavigationTabsProvider = ({ children }) => {
|
|||||||
canGoBack,
|
canGoBack,
|
||||||
canGoForward,
|
canGoForward,
|
||||||
captureTabPreview,
|
captureTabPreview,
|
||||||
previewCaptureLayout,
|
|
||||||
reportPreviewCaptureLayout,
|
|
||||||
closeTab,
|
closeTab,
|
||||||
createNewWindow,
|
createNewWindow,
|
||||||
goBack,
|
goBack,
|
||||||
@ -589,9 +584,19 @@ export const NavigationTabsProvider = ({ children }) => {
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const previewsValue = useMemo(
|
||||||
|
() => ({
|
||||||
|
tabPreviews,
|
||||||
|
tabPreviewCapturing
|
||||||
|
}),
|
||||||
|
[tabPreviewCapturing, tabPreviews]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavigationTabsContext.Provider value={value}>
|
<NavigationTabsContext.Provider value={value}>
|
||||||
{children}
|
<NavigationTabPreviewsContext.Provider value={previewsValue}>
|
||||||
|
{children}
|
||||||
|
</NavigationTabPreviewsContext.Provider>
|
||||||
</NavigationTabsContext.Provider>
|
</NavigationTabsContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -607,6 +612,17 @@ export const useNavigationTabs = () => {
|
|||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
|
export const useTabPreview = (tabId) => {
|
||||||
|
const { tabPreviews, tabPreviewCapturing } = useContext(
|
||||||
|
NavigationTabPreviewsContext
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
src: tabId ? tabPreviews[tabId] || null : null,
|
||||||
|
capturing: Boolean(tabId && tabPreviewCapturing[tabId])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react-refresh/only-export-components
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export const useNavigationTabPage = ({ title, modelName } = {}) => {
|
export const useNavigationTabPage = ({ title, modelName } = {}) => {
|
||||||
const { setTabPage, isElectron } = useNavigationTabs()
|
const { setTabPage, isElectron } = useNavigationTabs()
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
import html2canvas from 'html2canvas-pro'
|
import { snapdom } from '@zumer/snapdom'
|
||||||
|
|
||||||
const JPEG_QUALITY = 0.82
|
|
||||||
const PREVIEW_WIDTH = 320
|
|
||||||
const PREVIEW_RESOLUTION_SCALE = 2
|
|
||||||
const LAYOUT_TIMEOUT_MS = 450
|
|
||||||
const FONT_TIMEOUT_MS = 180
|
const FONT_TIMEOUT_MS = 180
|
||||||
|
const CAPTURE_CLASS = 'dashboard-tab-pane-capturing'
|
||||||
|
|
||||||
let previewWorker = null
|
const PREVIEW_EXCLUDE = [
|
||||||
let previewRequestId = 0
|
'.ant-popover',
|
||||||
const previewRequests = new Map()
|
'.dashboard-tab-preview-popover',
|
||||||
|
'.dashboard-tab-preview'
|
||||||
|
]
|
||||||
|
|
||||||
const waitForPaint = () =>
|
const waitForPaint = () =>
|
||||||
new Promise((resolve) => {
|
new Promise((resolve) => {
|
||||||
@ -67,162 +66,15 @@ const getPreviewBackgroundColor = (pane) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isBlankCanvas = (canvas) => {
|
|
||||||
if (!canvas || canvas.width < 8 || canvas.height < 8) return true
|
|
||||||
const ctx = canvas.getContext('2d', { willReadFrequently: true })
|
|
||||||
if (!ctx) return true
|
|
||||||
|
|
||||||
const sample = (x, y, width, height) => {
|
|
||||||
const { data } = ctx.getImageData(x, y, width, height)
|
|
||||||
for (let i = 3; i < data.length; i += 4) {
|
|
||||||
if (data[i] > 0 && (data[i - 3] > 2 || data[i - 2] > 2 || data[i - 1] > 2)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const sampleWidth = Math.min(canvas.width, 24)
|
|
||||||
const sampleHeight = Math.min(canvas.height, 24)
|
|
||||||
const midX = Math.max(0, Math.floor((canvas.width - sampleWidth) / 2))
|
|
||||||
const midY = Math.max(0, Math.floor((canvas.height - sampleHeight) / 2))
|
|
||||||
const maxX = Math.max(0, canvas.width - sampleWidth)
|
|
||||||
const maxY = Math.max(0, canvas.height - sampleHeight)
|
|
||||||
|
|
||||||
return (
|
|
||||||
sample(0, 0, sampleWidth, sampleHeight) &&
|
|
||||||
sample(midX, midY, sampleWidth, sampleHeight) &&
|
|
||||||
sample(maxX, 0, sampleWidth, sampleHeight) &&
|
|
||||||
sample(0, maxY, sampleWidth, sampleHeight)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const encodeJpegOnMainThread = (canvas, { targetWidth, targetHeight, quality }) => {
|
|
||||||
const output = document.createElement('canvas')
|
|
||||||
output.width = targetWidth
|
|
||||||
output.height = targetHeight
|
|
||||||
const ctx = output.getContext('2d')
|
|
||||||
if (!ctx) return canvas.toDataURL('image/jpeg', quality)
|
|
||||||
ctx.imageSmoothingEnabled = true
|
|
||||||
if ('imageSmoothingQuality' in ctx) {
|
|
||||||
ctx.imageSmoothingQuality = 'medium'
|
|
||||||
}
|
|
||||||
ctx.drawImage(canvas, 0, 0, targetWidth, targetHeight)
|
|
||||||
return output.toDataURL('image/jpeg', quality)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPreviewWorker = () => {
|
|
||||||
if (previewWorker) return previewWorker
|
|
||||||
if (typeof Worker === 'undefined') return null
|
|
||||||
|
|
||||||
try {
|
|
||||||
previewWorker = new Worker(new URL('./tabPreviewWorker.js', import.meta.url), {
|
|
||||||
type: 'module'
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to start tab preview worker', error)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
previewWorker.onmessage = (event) => {
|
|
||||||
const { id, dataUrl, error } = event.data || {}
|
|
||||||
const request = previewRequests.get(id)
|
|
||||||
if (!request) return
|
|
||||||
previewRequests.delete(id)
|
|
||||||
if (error) request.reject(new Error(error))
|
|
||||||
else request.resolve(dataUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
previewWorker.onerror = (error) => {
|
|
||||||
console.error('Tab preview worker error', error)
|
|
||||||
for (const request of previewRequests.values()) {
|
|
||||||
request.reject(error)
|
|
||||||
}
|
|
||||||
previewRequests.clear()
|
|
||||||
previewWorker?.terminate()
|
|
||||||
previewWorker = null
|
|
||||||
}
|
|
||||||
|
|
||||||
return previewWorker
|
|
||||||
}
|
|
||||||
|
|
||||||
const createPreviewBitmap = async (canvas, targetWidth, targetHeight) => {
|
|
||||||
if (typeof createImageBitmap !== 'function') return null
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await createImageBitmap(canvas, {
|
|
||||||
resizeWidth: targetWidth,
|
|
||||||
resizeHeight: targetHeight,
|
|
||||||
resizeQuality: 'medium'
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
return createImageBitmap(canvas)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const encodeTabPreviewInWorker = async (canvas, { targetWidth, targetHeight, quality }) => {
|
|
||||||
const worker = getPreviewWorker()
|
|
||||||
if (!worker || typeof OffscreenCanvas === 'undefined') {
|
|
||||||
return encodeJpegOnMainThread(canvas, { targetWidth, targetHeight, quality })
|
|
||||||
}
|
|
||||||
|
|
||||||
const bitmap = await createPreviewBitmap(canvas, targetWidth, targetHeight)
|
|
||||||
if (!bitmap) {
|
|
||||||
return encodeJpegOnMainThread(canvas, { targetWidth, targetHeight, quality })
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = (previewRequestId += 1)
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
previewRequests.set(id, { resolve, reject })
|
|
||||||
worker.postMessage(
|
|
||||||
{ id, bitmap, targetWidth, targetHeight, quality },
|
|
||||||
[bitmap]
|
|
||||||
)
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to encode tab preview in worker', error)
|
|
||||||
return encodeJpegOnMainThread(canvas, { targetWidth, targetHeight, quality })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const waitForFonts = async () => {
|
const waitForFonts = async () => {
|
||||||
if (!document.fonts?.ready) return
|
if (!document.fonts?.ready) return
|
||||||
await waitWithTimeout(document.fonts.ready, FONT_TIMEOUT_MS)
|
await waitWithTimeout(document.fonts.ready, FONT_TIMEOUT_MS)
|
||||||
}
|
}
|
||||||
|
|
||||||
const waitForPaneSize = async (pane, width, height) => {
|
export const waitForTabPreviewLayout = async (pane) => {
|
||||||
const matches = () =>
|
|
||||||
pane.clientWidth >= Math.max(8, width - 2) &&
|
|
||||||
pane.clientHeight >= Math.max(8, height - 2)
|
|
||||||
|
|
||||||
if (matches()) return true
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
let settled = false
|
|
||||||
const finish = (ready) => {
|
|
||||||
if (settled) return
|
|
||||||
settled = true
|
|
||||||
observer.disconnect()
|
|
||||||
window.clearTimeout(timeoutId)
|
|
||||||
resolve(ready)
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = new ResizeObserver(() => {
|
|
||||||
if (matches()) finish(true)
|
|
||||||
})
|
|
||||||
observer.observe(pane)
|
|
||||||
const timeoutId = window.setTimeout(() => finish(matches()), LAYOUT_TIMEOUT_MS)
|
|
||||||
if (matches()) finish(true)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const waitForTabPreviewLayout = async (pane, width, height) => {
|
|
||||||
if (!pane) return false
|
if (!pane) return false
|
||||||
|
|
||||||
void pane.offsetHeight
|
void pane.offsetHeight
|
||||||
await waitForPaneSize(pane, width, height)
|
|
||||||
await waitForFonts()
|
await waitForFonts()
|
||||||
await waitForPaint()
|
await waitForPaint()
|
||||||
void pane.offsetHeight
|
void pane.offsetHeight
|
||||||
@ -231,140 +83,74 @@ export const waitForTabPreviewLayout = async (pane, width, height) => {
|
|||||||
return pane.clientWidth >= 8 && pane.clientHeight >= 8
|
return pane.clientWidth >= 8 && pane.clientHeight >= 8
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderTabPaneCanvas = async (
|
const preparePaneForCapture = (pane) => {
|
||||||
pane,
|
const previousInert = pane.inert
|
||||||
{
|
pane.classList.add(CAPTURE_CLASS)
|
||||||
scale,
|
pane.inert = false
|
||||||
backgroundColor,
|
void pane.offsetHeight
|
||||||
foreignObjectRendering,
|
|
||||||
layoutWidth,
|
|
||||||
layoutHeight,
|
|
||||||
captureWidth,
|
|
||||||
captureHeight
|
|
||||||
}
|
|
||||||
) =>
|
|
||||||
html2canvas(pane, {
|
|
||||||
backgroundColor,
|
|
||||||
scale,
|
|
||||||
logging: false,
|
|
||||||
useCORS: true,
|
|
||||||
imageTimeout: 1500,
|
|
||||||
foreignObjectRendering,
|
|
||||||
width: captureWidth,
|
|
||||||
height: captureHeight,
|
|
||||||
windowWidth: layoutWidth,
|
|
||||||
windowHeight: layoutHeight,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
scrollX: 0,
|
|
||||||
scrollY: 0,
|
|
||||||
onclone: (_document, element) => {
|
|
||||||
const parent = element.parentElement
|
|
||||||
if (parent) {
|
|
||||||
for (const sibling of parent.children) {
|
|
||||||
if (sibling === element) continue
|
|
||||||
sibling.style.setProperty('visibility', 'hidden', 'important')
|
|
||||||
sibling.style.setProperty('opacity', '0', 'important')
|
|
||||||
sibling.style.setProperty('z-index', '0', 'important')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
element.classList.remove(
|
|
||||||
'dashboard-tab-pane-inactive',
|
|
||||||
'dashboard-tab-pane-capturing'
|
|
||||||
)
|
|
||||||
element.classList.add('dashboard-tab-pane-active')
|
|
||||||
element.style.position = 'relative'
|
|
||||||
element.style.left = '0'
|
|
||||||
element.style.top = '0'
|
|
||||||
element.style.right = 'auto'
|
|
||||||
element.style.bottom = 'auto'
|
|
||||||
element.style.zIndex = '2'
|
|
||||||
element.style.width = `${layoutWidth}px`
|
|
||||||
element.style.height = `${layoutHeight}px`
|
|
||||||
element.style.minWidth = `${layoutWidth}px`
|
|
||||||
element.style.minHeight = `${layoutHeight}px`
|
|
||||||
element.style.maxWidth = `${layoutWidth}px`
|
|
||||||
element.style.maxHeight = `${layoutHeight}px`
|
|
||||||
element.style.backgroundColor = backgroundColor
|
|
||||||
element.style.opacity = '1'
|
|
||||||
element.style.visibility = 'visible'
|
|
||||||
element.style.transform = 'none'
|
|
||||||
element.style.pointerEvents = 'auto'
|
|
||||||
element.style.overflow = 'hidden'
|
|
||||||
},
|
|
||||||
ignoreElements: (element) =>
|
|
||||||
element.classList?.contains('ant-popover') ||
|
|
||||||
element.classList?.contains('dashboard-tab-preview-popover') ||
|
|
||||||
element.classList?.contains('dashboard-tab-preview')
|
|
||||||
})
|
|
||||||
|
|
||||||
export const captureTabPaneImage = async (
|
return () => {
|
||||||
pane,
|
pane.classList.remove(CAPTURE_CLASS)
|
||||||
{
|
pane.inert = previousInert
|
||||||
height = 180,
|
}
|
||||||
width = PREVIEW_WIDTH,
|
}
|
||||||
layoutWidth,
|
|
||||||
layoutHeight
|
const tabPreviewPlugin = {
|
||||||
} = {}
|
name: 'tab-preview-layout',
|
||||||
) => {
|
afterClone(ctx) {
|
||||||
|
const element = ctx.clone
|
||||||
|
if (!(element instanceof HTMLElement)) return
|
||||||
|
|
||||||
|
element.classList.remove(
|
||||||
|
'dashboard-tab-pane-inactive',
|
||||||
|
CAPTURE_CLASS
|
||||||
|
)
|
||||||
|
element.classList.add('dashboard-tab-pane-active')
|
||||||
|
|
||||||
|
const styles = element.style
|
||||||
|
styles.position = 'relative'
|
||||||
|
styles.inset = 'auto'
|
||||||
|
styles.left = '0'
|
||||||
|
styles.top = '0'
|
||||||
|
styles.right = 'auto'
|
||||||
|
styles.bottom = 'auto'
|
||||||
|
styles.zIndex = '2'
|
||||||
|
styles.opacity = '1'
|
||||||
|
styles.visibility = 'visible'
|
||||||
|
styles.translate = 'none'
|
||||||
|
styles.transform = 'none'
|
||||||
|
styles.pointerEvents = 'none'
|
||||||
|
styles.overflow = 'hidden'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const captureTabPaneImage = async (pane) => {
|
||||||
if (!pane) return null
|
if (!pane) return null
|
||||||
|
|
||||||
|
const restorePane = preparePaneForCapture(pane)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sourceWidth = Math.max(
|
const ready = await waitForTabPreviewLayout(pane)
|
||||||
8,
|
if (!ready) return null
|
||||||
Math.round(layoutWidth || pane.clientWidth || 0)
|
|
||||||
)
|
|
||||||
const sourceHeight = Math.max(
|
|
||||||
8,
|
|
||||||
Math.round(layoutHeight || pane.clientHeight || 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
const ready = await waitForTabPreviewLayout(pane, sourceWidth, sourceHeight)
|
|
||||||
const paneWidth = pane.clientWidth || sourceWidth
|
|
||||||
const paneHeight = pane.clientHeight || sourceHeight
|
|
||||||
if (!ready && (paneWidth < 8 || paneHeight < 8)) return null
|
|
||||||
|
|
||||||
const displayHeight = Math.max(1, Math.round(height))
|
|
||||||
const displayWidth = Math.max(1, Math.round(width || PREVIEW_WIDTH))
|
|
||||||
const targetHeight = displayHeight * PREVIEW_RESOLUTION_SCALE
|
|
||||||
const targetWidth = displayWidth * PREVIEW_RESOLUTION_SCALE
|
|
||||||
const captureWidth = paneWidth
|
|
||||||
const captureHeight = Math.min(
|
|
||||||
paneHeight,
|
|
||||||
Math.max(1, Math.round(paneWidth * (displayHeight / displayWidth)))
|
|
||||||
)
|
|
||||||
const scale = Math.min(
|
|
||||||
PREVIEW_RESOLUTION_SCALE,
|
|
||||||
Math.max(0.28, targetWidth / captureWidth)
|
|
||||||
)
|
|
||||||
const backgroundColor = getPreviewBackgroundColor(pane)
|
const backgroundColor = getPreviewBackgroundColor(pane)
|
||||||
|
const jpg = await snapdom.toJpg(pane, {
|
||||||
|
backgroundColor,
|
||||||
|
scale: 1,
|
||||||
|
embedFonts: 'auto',
|
||||||
|
exclude: PREVIEW_EXCLUDE,
|
||||||
|
excludeMode: 'remove',
|
||||||
|
outerTransforms: false,
|
||||||
|
placeholders: false,
|
||||||
|
invalidate: true,
|
||||||
|
plugins: [tabPreviewPlugin]
|
||||||
|
})
|
||||||
|
|
||||||
const attempts = [true, false]
|
return jpg?.src || null
|
||||||
for (const foreignObjectRendering of attempts) {
|
|
||||||
try {
|
|
||||||
const canvas = await renderTabPaneCanvas(pane, {
|
|
||||||
scale,
|
|
||||||
backgroundColor,
|
|
||||||
foreignObjectRendering,
|
|
||||||
layoutWidth: paneWidth,
|
|
||||||
layoutHeight: paneHeight,
|
|
||||||
captureWidth,
|
|
||||||
captureHeight
|
|
||||||
})
|
|
||||||
if (isBlankCanvas(canvas)) continue
|
|
||||||
return await encodeTabPreviewInWorker(canvas, {
|
|
||||||
targetWidth,
|
|
||||||
targetHeight,
|
|
||||||
quality: JPEG_QUALITY
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to capture tab preview', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to capture tab preview', error)
|
console.error('Failed to capture tab preview', error)
|
||||||
return null
|
return null
|
||||||
|
} finally {
|
||||||
|
restorePane()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,38 +0,0 @@
|
|||||||
const blobToDataUrl = (blob) =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = () => resolve(reader.result)
|
|
||||||
reader.onerror = () => reject(reader.error)
|
|
||||||
reader.readAsDataURL(blob)
|
|
||||||
})
|
|
||||||
|
|
||||||
self.onmessage = async (event) => {
|
|
||||||
const { id, bitmap, targetWidth, targetHeight, quality } = event.data
|
|
||||||
|
|
||||||
try {
|
|
||||||
const canvas = new OffscreenCanvas(targetWidth, targetHeight)
|
|
||||||
const ctx = canvas.getContext('2d')
|
|
||||||
if (!ctx) {
|
|
||||||
throw new Error('OffscreenCanvas 2d context unavailable')
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.imageSmoothingEnabled = bitmap.width !== targetWidth || bitmap.height !== targetHeight
|
|
||||||
if ('imageSmoothingQuality' in ctx) {
|
|
||||||
ctx.imageSmoothingQuality = 'medium'
|
|
||||||
}
|
|
||||||
ctx.drawImage(bitmap, 0, 0, targetWidth, targetHeight)
|
|
||||||
bitmap.close?.()
|
|
||||||
|
|
||||||
const blob = await canvas.convertToBlob({
|
|
||||||
type: 'image/jpeg',
|
|
||||||
quality
|
|
||||||
})
|
|
||||||
const dataUrl = await blobToDataUrl(blob)
|
|
||||||
self.postMessage({ id, dataUrl })
|
|
||||||
} catch (error) {
|
|
||||||
self.postMessage({
|
|
||||||
id,
|
|
||||||
error: error?.message || String(error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user