mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-26 17:52:59 +02:00
fixes
This commit is contained in:
parent
f9542a9257
commit
3d2607f72a
77
frontend/package-lock.json
generated
77
frontend/package-lock.json
generated
@ -12,7 +12,9 @@
|
|||||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||||
"@embedpdf/core": "^1.3.0",
|
"@embedpdf/core": "^1.3.0",
|
||||||
"@embedpdf/engines": "^1.2.1",
|
"@embedpdf/engines": "^1.2.1",
|
||||||
|
"@embedpdf/plugin-annotation": "^1.3.0",
|
||||||
"@embedpdf/plugin-export": "^1.3.0",
|
"@embedpdf/plugin-export": "^1.3.0",
|
||||||
|
"@embedpdf/plugin-history": "^1.3.0",
|
||||||
"@embedpdf/plugin-interaction-manager": "^1.3.0",
|
"@embedpdf/plugin-interaction-manager": "^1.3.0",
|
||||||
"@embedpdf/plugin-loader": "^1.3.0",
|
"@embedpdf/plugin-loader": "^1.3.0",
|
||||||
"@embedpdf/plugin-pan": "^1.3.0",
|
"@embedpdf/plugin-pan": "^1.3.0",
|
||||||
@ -532,6 +534,26 @@
|
|||||||
"integrity": "sha512-rSBFYjxwQ58L/HcqR0l5Vv4G5t+CCOKlFYrDReTZYNN7fhzKPUWbXUn4ARahZWCNmF8svHumV2P4ArakJJviuw==",
|
"integrity": "sha512-rSBFYjxwQ58L/HcqR0l5Vv4G5t+CCOKlFYrDReTZYNN7fhzKPUWbXUn4ARahZWCNmF8svHumV2P4ArakJJviuw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@embedpdf/plugin-annotation": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-W9N8kQebnOT5ci7pp4RRPXK2ZAMvQbdd4Qkt4vXAsL9QIKqprAMrvo0GKzUAqMaYUk9WhVHgc5zwpeSP3PVUHg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@embedpdf/models": "1.3.0",
|
||||||
|
"@embedpdf/utils": "1.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@embedpdf/core": "1.3.0",
|
||||||
|
"@embedpdf/plugin-history": "1.3.0",
|
||||||
|
"@embedpdf/plugin-interaction-manager": "1.3.0",
|
||||||
|
"@embedpdf/plugin-selection": "1.3.0",
|
||||||
|
"preact": "^10.26.4",
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0",
|
||||||
|
"vue": ">=3.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@embedpdf/plugin-export": {
|
"node_modules/@embedpdf/plugin-export": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-1.3.0.tgz",
|
||||||
@ -548,6 +570,22 @@
|
|||||||
"vue": ">=3.2.0"
|
"vue": ">=3.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@embedpdf/plugin-history": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-HiNig94e6jE4h3BTL8Yi1fLLtYPY50N7vrkHSImqDmUTIcNHbQVbBYVCPpFJxN5NtuuaQqN9p1Mr7DwOKX8lkw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@embedpdf/models": "1.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@embedpdf/core": "1.3.0",
|
||||||
|
"preact": "^10.26.4",
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0",
|
||||||
|
"vue": ">=3.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@embedpdf/plugin-interaction-manager": {
|
"node_modules/@embedpdf/plugin-interaction-manager": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.3.0.tgz",
|
||||||
@ -770,6 +808,18 @@
|
|||||||
"vue": ">=3.2.0"
|
"vue": ">=3.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@embedpdf/utils": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@embedpdf/utils/-/utils-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-KEgdR85vd2CNKoSBoE5h4+e1n7MqEuIq3jZwD9MXAVKpHMaAIuD+S1khD8m4XLnbQXn32A9cO6Z6fmH0ndZ7+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"preact": "^10.26.4",
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0",
|
||||||
|
"vue": ">=3.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@emotion/babel-plugin": {
|
"node_modules/@emotion/babel-plugin": {
|
||||||
"version": "11.13.5",
|
"version": "11.13.5",
|
||||||
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
|
||||||
@ -5830,10 +5880,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/estree-walker": {
|
"node_modules/estree-walker": {
|
||||||
"version": "2.0.2",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||||
"license": "MIT"
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "^1.0.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esutils": {
|
"node_modules/esutils": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
@ -10948,21 +11002,6 @@
|
|||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite/node_modules/fsevents": {
|
|
||||||
"version": "2.3.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
|
||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vite/node_modules/picomatch": {
|
"node_modules/vite/node_modules/picomatch": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
|
@ -8,7 +8,9 @@
|
|||||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||||
"@embedpdf/core": "^1.3.0",
|
"@embedpdf/core": "^1.3.0",
|
||||||
"@embedpdf/engines": "^1.2.1",
|
"@embedpdf/engines": "^1.2.1",
|
||||||
|
"@embedpdf/plugin-annotation": "^1.3.0",
|
||||||
"@embedpdf/plugin-export": "^1.3.0",
|
"@embedpdf/plugin-export": "^1.3.0",
|
||||||
|
"@embedpdf/plugin-history": "^1.3.0",
|
||||||
"@embedpdf/plugin-interaction-manager": "^1.3.0",
|
"@embedpdf/plugin-interaction-manager": "^1.3.0",
|
||||||
"@embedpdf/plugin-loader": "^1.3.0",
|
"@embedpdf/plugin-loader": "^1.3.0",
|
||||||
"@embedpdf/plugin-pan": "^1.3.0",
|
"@embedpdf/plugin-pan": "^1.3.0",
|
||||||
|
@ -15,7 +15,6 @@ import "./index.css";
|
|||||||
import { RightRailProvider } from "./contexts/RightRailContext";
|
import { RightRailProvider } from "./contexts/RightRailContext";
|
||||||
import { ViewerProvider } from "./contexts/ViewerContext";
|
import { ViewerProvider } from "./contexts/ViewerContext";
|
||||||
import { SignatureProvider } from "./contexts/SignatureContext";
|
import { SignatureProvider } from "./contexts/SignatureContext";
|
||||||
import { ViewerProvider } from "./contexts/ViewerContext";
|
|
||||||
|
|
||||||
// Import file ID debugging helpers (development only)
|
// Import file ID debugging helpers (development only)
|
||||||
import "./utils/fileIdSafety";
|
import "./utils/fileIdSafety";
|
||||||
|
@ -196,201 +196,3 @@ export function ThumbnailSidebar({ visible, onToggle: _onToggle }: ThumbnailSide
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { Box, ScrollArea } from '@mantine/core';
|
|
||||||
import { useViewer } from '../../contexts/ViewerContext';
|
|
||||||
|
|
||||||
interface ThumbnailSidebarProps {
|
|
||||||
visible: boolean;
|
|
||||||
onToggle: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ThumbnailSidebar({ visible, onToggle: _onToggle }: ThumbnailSidebarProps) {
|
|
||||||
const { getScrollState, scrollActions, getThumbnailAPI } = useViewer();
|
|
||||||
const [thumbnails, setThumbnails] = useState<{ [key: number]: string }>({});
|
|
||||||
|
|
||||||
const scrollState = getScrollState();
|
|
||||||
const thumbnailAPI = getThumbnailAPI();
|
|
||||||
|
|
||||||
// Clear thumbnails when sidebar closes and revoke blob URLs to prevent memory leaks
|
|
||||||
useEffect(() => {
|
|
||||||
if (!visible) {
|
|
||||||
Object.values(thumbnails).forEach((thumbUrl) => {
|
|
||||||
// Only revoke if it's a blob URL (not 'error')
|
|
||||||
if (typeof thumbUrl === 'string' && thumbUrl.startsWith('blob:')) {
|
|
||||||
URL.revokeObjectURL(thumbUrl);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setThumbnails({});
|
|
||||||
}
|
|
||||||
}, [visible, thumbnails]);
|
|
||||||
|
|
||||||
// Generate thumbnails when sidebar becomes visible
|
|
||||||
useEffect(() => {
|
|
||||||
if (!visible || scrollState.totalPages === 0) return;
|
|
||||||
if (!thumbnailAPI) return;
|
|
||||||
|
|
||||||
const generateThumbnails = async () => {
|
|
||||||
for (let pageIndex = 0; pageIndex < scrollState.totalPages; pageIndex++) {
|
|
||||||
if (thumbnails[pageIndex]) continue; // Skip if already generated
|
|
||||||
|
|
||||||
try {
|
|
||||||
const thumbTask = thumbnailAPI.renderThumb(pageIndex, 1.0);
|
|
||||||
|
|
||||||
// Convert Task to Promise and handle properly
|
|
||||||
thumbTask.toPromise().then((thumbBlob: Blob) => {
|
|
||||||
const thumbUrl = URL.createObjectURL(thumbBlob);
|
|
||||||
|
|
||||||
setThumbnails(prev => ({
|
|
||||||
...prev,
|
|
||||||
[pageIndex]: thumbUrl
|
|
||||||
}));
|
|
||||||
}).catch((error: any) => {
|
|
||||||
console.error('Failed to generate thumbnail for page', pageIndex + 1, error);
|
|
||||||
setThumbnails(prev => ({
|
|
||||||
...prev,
|
|
||||||
[pageIndex]: 'error'
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to generate thumbnail for page', pageIndex + 1, error);
|
|
||||||
setThumbnails(prev => ({
|
|
||||||
...prev,
|
|
||||||
[pageIndex]: 'error'
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
generateThumbnails();
|
|
||||||
}, [visible, scrollState.totalPages, thumbnailAPI]);
|
|
||||||
|
|
||||||
const handlePageClick = (pageIndex: number) => {
|
|
||||||
const pageNumber = pageIndex + 1; // Convert to 1-based
|
|
||||||
scrollActions.scrollToPage(pageNumber);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* Thumbnail Sidebar */}
|
|
||||||
{visible && (
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
position: 'fixed',
|
|
||||||
right: 0,
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
width: '15rem',
|
|
||||||
backgroundColor: 'var(--bg-surface)',
|
|
||||||
borderLeft: '1px solid var(--border-subtle)',
|
|
||||||
zIndex: 998,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
boxShadow: '-2px 0 8px rgba(0, 0, 0, 0.1)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Thumbnails Container */}
|
|
||||||
<ScrollArea style={{ flex: 1 }}>
|
|
||||||
<Box p="sm">
|
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: '12px'
|
|
||||||
}}>
|
|
||||||
{Array.from({ length: scrollState.totalPages }, (_, pageIndex) => (
|
|
||||||
<Box
|
|
||||||
key={pageIndex}
|
|
||||||
onClick={() => handlePageClick(pageIndex)}
|
|
||||||
style={{
|
|
||||||
cursor: 'pointer',
|
|
||||||
borderRadius: '8px',
|
|
||||||
padding: '8px',
|
|
||||||
backgroundColor: scrollState.currentPage === pageIndex + 1
|
|
||||||
? 'var(--color-primary-100)'
|
|
||||||
: 'transparent',
|
|
||||||
border: scrollState.currentPage === pageIndex + 1
|
|
||||||
? '2px solid var(--color-primary-500)'
|
|
||||||
: '2px solid transparent',
|
|
||||||
transition: 'all 0.2s ease',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '8px'
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
if (scrollState.currentPage !== pageIndex + 1) {
|
|
||||||
e.currentTarget.style.backgroundColor = 'var(--hover-bg)';
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
if (scrollState.currentPage !== pageIndex + 1) {
|
|
||||||
e.currentTarget.style.backgroundColor = 'transparent';
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Thumbnail Image */}
|
|
||||||
{thumbnails[pageIndex] && thumbnails[pageIndex] !== 'error' ? (
|
|
||||||
<img
|
|
||||||
src={thumbnails[pageIndex]}
|
|
||||||
alt={`Page ${pageIndex + 1} thumbnail`}
|
|
||||||
style={{
|
|
||||||
maxWidth: '100%',
|
|
||||||
height: 'auto',
|
|
||||||
borderRadius: '4px',
|
|
||||||
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
|
|
||||||
border: '1px solid var(--border-subtle)'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : thumbnails[pageIndex] === 'error' ? (
|
|
||||||
<div style={{
|
|
||||||
width: '11.5rem',
|
|
||||||
height: '15rem',
|
|
||||||
backgroundColor: 'var(--color-red-50)',
|
|
||||||
border: '1px solid var(--color-red-200)',
|
|
||||||
borderRadius: '4px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
color: 'var(--color-red-500)',
|
|
||||||
fontSize: '12px'
|
|
||||||
}}>
|
|
||||||
Failed
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div style={{
|
|
||||||
width: '11.5rem',
|
|
||||||
height: '15rem',
|
|
||||||
backgroundColor: 'var(--bg-muted)',
|
|
||||||
border: '1px solid var(--border-subtle)',
|
|
||||||
borderRadius: '4px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
color: 'var(--text-muted)',
|
|
||||||
fontSize: '12px'
|
|
||||||
}}>
|
|
||||||
Loading...
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Page Number */}
|
|
||||||
<div style={{
|
|
||||||
fontSize: '12px',
|
|
||||||
fontWeight: 500,
|
|
||||||
color: scrollState.currentPage === pageIndex + 1
|
|
||||||
? 'var(--color-primary-500)'
|
|
||||||
: 'var(--text-muted)'
|
|
||||||
}}>
|
|
||||||
Page {pageIndex + 1}
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
</ScrollArea>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
@ -51,549 +51,6 @@ interface ThumbnailAPIWrapper {
|
|||||||
renderThumb: (pageIndex: number, scale: number) => { toPromise: () => Promise<Blob> };
|
renderThumb: (pageIndex: number, scale: number) => { toPromise: () => Promise<Blob> };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// State interfaces - represent the shape of data from each bridge
|
|
||||||
interface ScrollState {
|
|
||||||
currentPage: number;
|
|
||||||
totalPages: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ZoomState {
|
|
||||||
currentZoom: number;
|
|
||||||
zoomPercent: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PanState {
|
|
||||||
isPanning: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SelectionState {
|
|
||||||
hasSelection: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SpreadState {
|
|
||||||
spreadMode: SpreadMode;
|
|
||||||
isDualPage: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RotationState {
|
|
||||||
rotation: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SearchResult {
|
|
||||||
pageIndex: number;
|
|
||||||
rects: Array<{
|
|
||||||
origin: { x: number; y: number };
|
|
||||||
size: { width: number; height: number };
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SearchState {
|
|
||||||
results: SearchResult[] | null;
|
|
||||||
activeIndex: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bridge registration interface - bridges register with state and API
|
|
||||||
interface BridgeRef<TState = unknown, TApi = unknown> {
|
|
||||||
state: TState;
|
|
||||||
api: TApi;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ViewerContext provides a unified interface to EmbedPDF functionality.
|
|
||||||
*
|
|
||||||
* Architecture:
|
|
||||||
* - Bridges store their own state locally and register with this context
|
|
||||||
* - Context provides read-only access to bridge state via getter functions
|
|
||||||
* - Actions call EmbedPDF APIs directly through bridge references
|
|
||||||
* - No circular dependencies - bridges don't call back into this context
|
|
||||||
*/
|
|
||||||
interface ViewerContextType {
|
|
||||||
// UI state managed by this context
|
|
||||||
isThumbnailSidebarVisible: boolean;
|
|
||||||
toggleThumbnailSidebar: () => void;
|
|
||||||
|
|
||||||
// State getters - read current state from bridges
|
|
||||||
getScrollState: () => ScrollState;
|
|
||||||
getZoomState: () => ZoomState;
|
|
||||||
getPanState: () => PanState;
|
|
||||||
getSelectionState: () => SelectionState;
|
|
||||||
getSpreadState: () => SpreadState;
|
|
||||||
getRotationState: () => RotationState;
|
|
||||||
getSearchState: () => SearchState;
|
|
||||||
getThumbnailAPI: () => ThumbnailAPIWrapper | null;
|
|
||||||
|
|
||||||
// Immediate update callbacks
|
|
||||||
registerImmediateZoomUpdate: (callback: (percent: number) => void) => void;
|
|
||||||
registerImmediateScrollUpdate: (callback: (currentPage: number, totalPages: number) => void) => void;
|
|
||||||
|
|
||||||
// Internal - for bridges to trigger immediate updates
|
|
||||||
triggerImmediateScrollUpdate: (currentPage: number, totalPages: number) => void;
|
|
||||||
triggerImmediateZoomUpdate: (zoomPercent: number) => void;
|
|
||||||
|
|
||||||
// Action handlers - call EmbedPDF APIs directly
|
|
||||||
scrollActions: {
|
|
||||||
scrollToPage: (page: number) => void;
|
|
||||||
scrollToFirstPage: () => void;
|
|
||||||
scrollToPreviousPage: () => void;
|
|
||||||
scrollToNextPage: () => void;
|
|
||||||
scrollToLastPage: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
zoomActions: {
|
|
||||||
zoomIn: () => void;
|
|
||||||
zoomOut: () => void;
|
|
||||||
toggleMarqueeZoom: () => void;
|
|
||||||
requestZoom: (level: number) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
panActions: {
|
|
||||||
enablePan: () => void;
|
|
||||||
disablePan: () => void;
|
|
||||||
togglePan: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
selectionActions: {
|
|
||||||
copyToClipboard: () => void;
|
|
||||||
getSelectedText: () => string;
|
|
||||||
getFormattedSelection: () => unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
spreadActions: {
|
|
||||||
setSpreadMode: (mode: SpreadMode) => void;
|
|
||||||
getSpreadMode: () => SpreadMode | null;
|
|
||||||
toggleSpreadMode: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
rotationActions: {
|
|
||||||
rotateForward: () => void;
|
|
||||||
rotateBackward: () => void;
|
|
||||||
setRotation: (rotation: number) => void;
|
|
||||||
getRotation: () => number;
|
|
||||||
};
|
|
||||||
|
|
||||||
searchActions: {
|
|
||||||
search: (query: string) => Promise<void>;
|
|
||||||
next: () => void;
|
|
||||||
previous: () => void;
|
|
||||||
clear: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Bridge registration - internal use by bridges
|
|
||||||
registerBridge: (type: string, ref: BridgeRef) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ViewerContext = createContext<ViewerContextType | null>(null);
|
|
||||||
|
|
||||||
interface ViewerProviderProps {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
|
||||||
// UI state - only state directly managed by this context
|
|
||||||
const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = useState(false);
|
|
||||||
|
|
||||||
// Bridge registry - bridges register their state and APIs here
|
|
||||||
const bridgeRefs = useRef({
|
|
||||||
scroll: null as BridgeRef<ScrollState, ScrollAPIWrapper> | null,
|
|
||||||
zoom: null as BridgeRef<ZoomState, ZoomAPIWrapper> | null,
|
|
||||||
pan: null as BridgeRef<PanState, PanAPIWrapper> | null,
|
|
||||||
selection: null as BridgeRef<SelectionState, SelectionAPIWrapper> | null,
|
|
||||||
search: null as BridgeRef<SearchState, SearchAPIWrapper> | null,
|
|
||||||
spread: null as BridgeRef<SpreadState, SpreadAPIWrapper> | null,
|
|
||||||
rotation: null as BridgeRef<RotationState, RotationAPIWrapper> | null,
|
|
||||||
thumbnail: null as BridgeRef<unknown, ThumbnailAPIWrapper> | null,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Immediate zoom callback for responsive display updates
|
|
||||||
const immediateZoomUpdateCallback = useRef<((percent: number) => void) | null>(null);
|
|
||||||
|
|
||||||
// Immediate scroll callback for responsive display updates
|
|
||||||
const immediateScrollUpdateCallback = useRef<((currentPage: number, totalPages: number) => void) | null>(null);
|
|
||||||
|
|
||||||
const registerBridge = (type: string, ref: BridgeRef) => {
|
|
||||||
// Type-safe assignment - we know the bridges will provide correct types
|
|
||||||
switch (type) {
|
|
||||||
case 'scroll':
|
|
||||||
bridgeRefs.current.scroll = ref as BridgeRef<ScrollState, ScrollAPIWrapper>;
|
|
||||||
break;
|
|
||||||
case 'zoom':
|
|
||||||
bridgeRefs.current.zoom = ref as BridgeRef<ZoomState, ZoomAPIWrapper>;
|
|
||||||
break;
|
|
||||||
case 'pan':
|
|
||||||
bridgeRefs.current.pan = ref as BridgeRef<PanState, PanAPIWrapper>;
|
|
||||||
break;
|
|
||||||
case 'selection':
|
|
||||||
bridgeRefs.current.selection = ref as BridgeRef<SelectionState, SelectionAPIWrapper>;
|
|
||||||
break;
|
|
||||||
case 'search':
|
|
||||||
bridgeRefs.current.search = ref as BridgeRef<SearchState, SearchAPIWrapper>;
|
|
||||||
break;
|
|
||||||
case 'spread':
|
|
||||||
bridgeRefs.current.spread = ref as BridgeRef<SpreadState, SpreadAPIWrapper>;
|
|
||||||
break;
|
|
||||||
case 'rotation':
|
|
||||||
bridgeRefs.current.rotation = ref as BridgeRef<RotationState, RotationAPIWrapper>;
|
|
||||||
break;
|
|
||||||
case 'thumbnail':
|
|
||||||
bridgeRefs.current.thumbnail = ref as BridgeRef<unknown, ThumbnailAPIWrapper>;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleThumbnailSidebar = () => {
|
|
||||||
setIsThumbnailSidebarVisible(prev => !prev);
|
|
||||||
};
|
|
||||||
|
|
||||||
// State getters - read from bridge refs
|
|
||||||
const getScrollState = (): ScrollState => {
|
|
||||||
return bridgeRefs.current.scroll?.state || { currentPage: 1, totalPages: 0 };
|
|
||||||
};
|
|
||||||
|
|
||||||
const getZoomState = (): ZoomState => {
|
|
||||||
return bridgeRefs.current.zoom?.state || { currentZoom: 1.4, zoomPercent: 140 };
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPanState = (): PanState => {
|
|
||||||
return bridgeRefs.current.pan?.state || { isPanning: false };
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSelectionState = (): SelectionState => {
|
|
||||||
return bridgeRefs.current.selection?.state || { hasSelection: false };
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSpreadState = (): SpreadState => {
|
|
||||||
return bridgeRefs.current.spread?.state || { spreadMode: SpreadMode.None, isDualPage: false };
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRotationState = (): RotationState => {
|
|
||||||
return bridgeRefs.current.rotation?.state || { rotation: 0 };
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSearchState = (): SearchState => {
|
|
||||||
return bridgeRefs.current.search?.state || { results: null, activeIndex: 0 };
|
|
||||||
};
|
|
||||||
|
|
||||||
const getThumbnailAPI = () => {
|
|
||||||
return bridgeRefs.current.thumbnail?.api || null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Action handlers - call APIs directly
|
|
||||||
const scrollActions = {
|
|
||||||
scrollToPage: (page: number) => {
|
|
||||||
const api = bridgeRefs.current.scroll?.api;
|
|
||||||
if (api?.scrollToPage) {
|
|
||||||
api.scrollToPage({ pageNumber: page });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scrollToFirstPage: () => {
|
|
||||||
const api = bridgeRefs.current.scroll?.api;
|
|
||||||
if (api?.scrollToPage) {
|
|
||||||
api.scrollToPage({ pageNumber: 1 });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scrollToPreviousPage: () => {
|
|
||||||
const api = bridgeRefs.current.scroll?.api;
|
|
||||||
if (api?.scrollToPreviousPage) {
|
|
||||||
api.scrollToPreviousPage();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scrollToNextPage: () => {
|
|
||||||
const api = bridgeRefs.current.scroll?.api;
|
|
||||||
if (api?.scrollToNextPage) {
|
|
||||||
api.scrollToNextPage();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scrollToLastPage: () => {
|
|
||||||
const scrollState = getScrollState();
|
|
||||||
const api = bridgeRefs.current.scroll?.api;
|
|
||||||
if (api?.scrollToPage && scrollState.totalPages > 0) {
|
|
||||||
api.scrollToPage({ pageNumber: scrollState.totalPages });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const zoomActions = {
|
|
||||||
zoomIn: () => {
|
|
||||||
const api = bridgeRefs.current.zoom?.api;
|
|
||||||
if (api?.zoomIn) {
|
|
||||||
// Update display immediately if callback is registered
|
|
||||||
if (immediateZoomUpdateCallback.current) {
|
|
||||||
const currentState = getZoomState();
|
|
||||||
const newPercent = Math.min(Math.round(currentState.zoomPercent * 1.2), 300);
|
|
||||||
immediateZoomUpdateCallback.current(newPercent);
|
|
||||||
}
|
|
||||||
api.zoomIn();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
zoomOut: () => {
|
|
||||||
const api = bridgeRefs.current.zoom?.api;
|
|
||||||
if (api?.zoomOut) {
|
|
||||||
// Update display immediately if callback is registered
|
|
||||||
if (immediateZoomUpdateCallback.current) {
|
|
||||||
const currentState = getZoomState();
|
|
||||||
const newPercent = Math.max(Math.round(currentState.zoomPercent / 1.2), 20);
|
|
||||||
immediateZoomUpdateCallback.current(newPercent);
|
|
||||||
}
|
|
||||||
api.zoomOut();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toggleMarqueeZoom: () => {
|
|
||||||
const api = bridgeRefs.current.zoom?.api;
|
|
||||||
if (api?.toggleMarqueeZoom) {
|
|
||||||
api.toggleMarqueeZoom();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
requestZoom: (level: number) => {
|
|
||||||
const api = bridgeRefs.current.zoom?.api;
|
|
||||||
if (api?.requestZoom) {
|
|
||||||
api.requestZoom(level);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const panActions = {
|
|
||||||
enablePan: () => {
|
|
||||||
const api = bridgeRefs.current.pan?.api;
|
|
||||||
if (api?.enable) {
|
|
||||||
api.enable();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
disablePan: () => {
|
|
||||||
const api = bridgeRefs.current.pan?.api;
|
|
||||||
if (api?.disable) {
|
|
||||||
api.disable();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
togglePan: () => {
|
|
||||||
const api = bridgeRefs.current.pan?.api;
|
|
||||||
if (api?.toggle) {
|
|
||||||
api.toggle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectionActions = {
|
|
||||||
copyToClipboard: () => {
|
|
||||||
const api = bridgeRefs.current.selection?.api;
|
|
||||||
if (api?.copyToClipboard) {
|
|
||||||
api.copyToClipboard();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getSelectedText: () => {
|
|
||||||
const api = bridgeRefs.current.selection?.api;
|
|
||||||
if (api?.getSelectedText) {
|
|
||||||
return api.getSelectedText();
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
},
|
|
||||||
getFormattedSelection: () => {
|
|
||||||
const api = bridgeRefs.current.selection?.api;
|
|
||||||
if (api?.getFormattedSelection) {
|
|
||||||
return api.getFormattedSelection();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const spreadActions = {
|
|
||||||
setSpreadMode: (mode: SpreadMode) => {
|
|
||||||
const api = bridgeRefs.current.spread?.api;
|
|
||||||
if (api?.setSpreadMode) {
|
|
||||||
api.setSpreadMode(mode);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getSpreadMode: () => {
|
|
||||||
const api = bridgeRefs.current.spread?.api;
|
|
||||||
if (api?.getSpreadMode) {
|
|
||||||
return api.getSpreadMode();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
toggleSpreadMode: () => {
|
|
||||||
const api = bridgeRefs.current.spread?.api;
|
|
||||||
if (api?.toggleSpreadMode) {
|
|
||||||
api.toggleSpreadMode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const rotationActions = {
|
|
||||||
rotateForward: () => {
|
|
||||||
const api = bridgeRefs.current.rotation?.api;
|
|
||||||
if (api?.rotateForward) {
|
|
||||||
api.rotateForward();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
rotateBackward: () => {
|
|
||||||
const api = bridgeRefs.current.rotation?.api;
|
|
||||||
if (api?.rotateBackward) {
|
|
||||||
api.rotateBackward();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setRotation: (rotation: number) => {
|
|
||||||
const api = bridgeRefs.current.rotation?.api;
|
|
||||||
if (api?.setRotation) {
|
|
||||||
api.setRotation(rotation);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getRotation: () => {
|
|
||||||
const api = bridgeRefs.current.rotation?.api;
|
|
||||||
if (api?.getRotation) {
|
|
||||||
return api.getRotation();
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const searchActions = {
|
|
||||||
search: async (query: string) => {
|
|
||||||
const api = bridgeRefs.current.search?.api;
|
|
||||||
if (api?.search) {
|
|
||||||
return api.search(query);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
next: () => {
|
|
||||||
const api = bridgeRefs.current.search?.api;
|
|
||||||
if (api?.next) {
|
|
||||||
api.next();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
previous: () => {
|
|
||||||
const api = bridgeRefs.current.search?.api;
|
|
||||||
if (api?.previous) {
|
|
||||||
api.previous();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
clear: () => {
|
|
||||||
const api = bridgeRefs.current.search?.api;
|
|
||||||
if (api?.clear) {
|
|
||||||
api.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const registerImmediateZoomUpdate = (callback: (percent: number) => void) => {
|
|
||||||
immediateZoomUpdateCallback.current = callback;
|
|
||||||
};
|
|
||||||
|
|
||||||
const registerImmediateScrollUpdate = (callback: (currentPage: number, totalPages: number) => void) => {
|
|
||||||
immediateScrollUpdateCallback.current = callback;
|
|
||||||
};
|
|
||||||
|
|
||||||
const triggerImmediateScrollUpdate = (currentPage: number, totalPages: number) => {
|
|
||||||
if (immediateScrollUpdateCallback.current) {
|
|
||||||
immediateScrollUpdateCallback.current(currentPage, totalPages);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const triggerImmediateZoomUpdate = (zoomPercent: number) => {
|
|
||||||
if (immediateZoomUpdateCallback.current) {
|
|
||||||
immediateZoomUpdateCallback.current(zoomPercent);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const value: ViewerContextType = {
|
|
||||||
// UI state
|
|
||||||
isThumbnailSidebarVisible,
|
|
||||||
toggleThumbnailSidebar,
|
|
||||||
|
|
||||||
// State getters
|
|
||||||
getScrollState,
|
|
||||||
getZoomState,
|
|
||||||
getPanState,
|
|
||||||
getSelectionState,
|
|
||||||
getSpreadState,
|
|
||||||
getRotationState,
|
|
||||||
getSearchState,
|
|
||||||
getThumbnailAPI,
|
|
||||||
|
|
||||||
// Immediate updates
|
|
||||||
registerImmediateZoomUpdate,
|
|
||||||
registerImmediateScrollUpdate,
|
|
||||||
triggerImmediateScrollUpdate,
|
|
||||||
triggerImmediateZoomUpdate,
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
scrollActions,
|
|
||||||
zoomActions,
|
|
||||||
panActions,
|
|
||||||
selectionActions,
|
|
||||||
spreadActions,
|
|
||||||
rotationActions,
|
|
||||||
searchActions,
|
|
||||||
|
|
||||||
// Bridge registration
|
|
||||||
registerBridge,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ViewerContext.Provider value={value}>
|
|
||||||
{children}
|
|
||||||
</ViewerContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useViewer = (): ViewerContextType => {
|
|
||||||
const context = useContext(ViewerContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useViewer must be used within a ViewerProvider');
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
import React, { createContext, useContext, useState, ReactNode, useRef } from 'react';
|
|
||||||
import { SpreadMode } from '@embedpdf/plugin-spread/react';
|
|
||||||
|
|
||||||
// Bridge API interfaces - these match what the bridges provide
|
|
||||||
interface ScrollAPIWrapper {
|
|
||||||
scrollToPage: (params: { pageNumber: number }) => void;
|
|
||||||
scrollToPreviousPage: () => void;
|
|
||||||
scrollToNextPage: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ZoomAPIWrapper {
|
|
||||||
zoomIn: () => void;
|
|
||||||
zoomOut: () => void;
|
|
||||||
toggleMarqueeZoom: () => void;
|
|
||||||
requestZoom: (level: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PanAPIWrapper {
|
|
||||||
enable: () => void;
|
|
||||||
disable: () => void;
|
|
||||||
toggle: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SelectionAPIWrapper {
|
|
||||||
copyToClipboard: () => void;
|
|
||||||
getSelectedText: () => string | any;
|
|
||||||
getFormattedSelection: () => any;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SpreadAPIWrapper {
|
|
||||||
setSpreadMode: (mode: SpreadMode) => void;
|
|
||||||
getSpreadMode: () => SpreadMode | null;
|
|
||||||
toggleSpreadMode: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RotationAPIWrapper {
|
|
||||||
rotateForward: () => void;
|
|
||||||
rotateBackward: () => void;
|
|
||||||
setRotation: (rotation: number) => void;
|
|
||||||
getRotation: () => number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SearchAPIWrapper {
|
|
||||||
search: (query: string) => Promise<any>;
|
|
||||||
clear: () => void;
|
|
||||||
next: () => void;
|
|
||||||
previous: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ThumbnailAPIWrapper {
|
|
||||||
renderThumb: (pageIndex: number, scale: number) => { toPromise: () => Promise<Blob> };
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExportAPIWrapper {
|
interface ExportAPIWrapper {
|
||||||
download: () => void;
|
download: () => void;
|
||||||
saveAsCopy: () => { toPromise: () => Promise<ArrayBuffer> };
|
saveAsCopy: () => { toPromise: () => Promise<ArrayBuffer> };
|
||||||
|
@ -49,6 +49,8 @@ import { flattenOperationConfig } from "../hooks/tools/flatten/useFlattenOperati
|
|||||||
import { redactOperationConfig } from "../hooks/tools/redact/useRedactOperation";
|
import { redactOperationConfig } from "../hooks/tools/redact/useRedactOperation";
|
||||||
import { rotateOperationConfig } from "../hooks/tools/rotate/useRotateOperation";
|
import { rotateOperationConfig } from "../hooks/tools/rotate/useRotateOperation";
|
||||||
import { changeMetadataOperationConfig } from "../hooks/tools/changeMetadata/useChangeMetadataOperation";
|
import { changeMetadataOperationConfig } from "../hooks/tools/changeMetadata/useChangeMetadataOperation";
|
||||||
|
import { signOperationConfig } from "../hooks/tools/sign/useSignOperation";
|
||||||
|
import { cropOperationConfig } from "../hooks/tools/crop/useCropOperation";
|
||||||
import CompressSettings from "../components/tools/compress/CompressSettings";
|
import CompressSettings from "../components/tools/compress/CompressSettings";
|
||||||
import SplitSettings from "../components/tools/split/SplitSettings";
|
import SplitSettings from "../components/tools/split/SplitSettings";
|
||||||
import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings";
|
import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings";
|
||||||
@ -72,6 +74,8 @@ import MergeSettings from '../components/tools/merge/MergeSettings';
|
|||||||
import { adjustPageScaleOperationConfig } from "../hooks/tools/adjustPageScale/useAdjustPageScaleOperation";
|
import { adjustPageScaleOperationConfig } from "../hooks/tools/adjustPageScale/useAdjustPageScaleOperation";
|
||||||
import AdjustPageScaleSettings from "../components/tools/adjustPageScale/AdjustPageScaleSettings";
|
import AdjustPageScaleSettings from "../components/tools/adjustPageScale/AdjustPageScaleSettings";
|
||||||
import ChangeMetadataSingleStep from "../components/tools/changeMetadata/ChangeMetadataSingleStep";
|
import ChangeMetadataSingleStep from "../components/tools/changeMetadata/ChangeMetadataSingleStep";
|
||||||
|
import SignSettings from "../components/tools/sign/SignSettings";
|
||||||
|
import CropSettings from "../components/tools/crop/CropSettings";
|
||||||
|
|
||||||
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
|
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user