mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-26 17:52:59 +02:00
Merge branch 'V2' into eslint_20250926
This commit is contained in:
commit
05a79b3285
@ -6,6 +6,11 @@ http {
|
|||||||
include /etc/nginx/mime.types;
|
include /etc/nginx/mime.types;
|
||||||
default_type application/octet-stream;
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
# Add .mjs MIME type mapping
|
||||||
|
types {
|
||||||
|
text/javascript mjs;
|
||||||
|
}
|
||||||
|
|
||||||
# Gzip compression
|
# Gzip compression
|
||||||
gzip on;
|
gzip on;
|
||||||
gzip_vary on;
|
gzip_vary on;
|
||||||
@ -90,6 +95,14 @@ http {
|
|||||||
proxy_set_header X-Forwarded-Port $server_port;
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Serve .mjs files with correct MIME type (must come before general static assets)
|
||||||
|
location ~* \.mjs$ {
|
||||||
|
try_files $uri =404;
|
||||||
|
add_header Content-Type "text/javascript; charset=utf-8" always;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
# Cache static assets
|
# Cache static assets
|
||||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
expires 1y;
|
expires 1y;
|
||||||
|
17
frontend/package-lock.json
generated
17
frontend/package-lock.json
generated
@ -25,6 +25,7 @@
|
|||||||
"@embedpdf/plugin-tiling": "^1.3.1",
|
"@embedpdf/plugin-tiling": "^1.3.1",
|
||||||
"@embedpdf/plugin-viewport": "^1.3.1",
|
"@embedpdf/plugin-viewport": "^1.3.1",
|
||||||
"@embedpdf/plugin-zoom": "^1.3.1",
|
"@embedpdf/plugin-zoom": "^1.3.1",
|
||||||
|
"@embedpdf/plugin-export": "^1.3.1",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@iconify/react": "^6.0.2",
|
"@iconify/react": "^6.0.2",
|
||||||
@ -537,6 +538,22 @@
|
|||||||
"integrity": "sha512-qYGSS5ntz6DSY9Cxw/aigvHqGB+AKJLEcymNTZOL0GdlBzZpL++dOIYNEYHO2Tm/lOQVpE7I0e+Xh2TvD8O1zQ==",
|
"integrity": "sha512-qYGSS5ntz6DSY9Cxw/aigvHqGB+AKJLEcymNTZOL0GdlBzZpL++dOIYNEYHO2Tm/lOQVpE7I0e+Xh2TvD8O1zQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@embedpdf/plugin-export": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-R6VItLmXmXbb0/4AsH1YGUZd0c64K/8kxQd0XAvgUJwcL7Z4s8bLsqRx4sVQqwVllaPEJbAdFn1CC/ymkGB+fg==",
|
||||||
|
"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.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.3.1.tgz",
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
"@embedpdf/plugin-tiling": "^1.3.1",
|
"@embedpdf/plugin-tiling": "^1.3.1",
|
||||||
"@embedpdf/plugin-viewport": "^1.3.1",
|
"@embedpdf/plugin-viewport": "^1.3.1",
|
||||||
"@embedpdf/plugin-zoom": "^1.3.1",
|
"@embedpdf/plugin-zoom": "^1.3.1",
|
||||||
|
"@embedpdf/plugin-export": "^1.3.1",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@iconify/react": "^6.0.2",
|
"@iconify/react": "^6.0.2",
|
||||||
|
@ -66,6 +66,9 @@ export default function RightRail() {
|
|||||||
|
|
||||||
const { totalItems, selectedCount } = getSelectionState();
|
const { totalItems, selectedCount } = getSelectionState();
|
||||||
|
|
||||||
|
// Get export state for viewer mode
|
||||||
|
const exportState = viewerContext?.getExportState?.();
|
||||||
|
|
||||||
const handleSelectAll = useCallback(() => {
|
const handleSelectAll = useCallback(() => {
|
||||||
if (currentView === 'fileEditor' || currentView === 'viewer') {
|
if (currentView === 'fileEditor' || currentView === 'viewer') {
|
||||||
// Select all file IDs
|
// Select all file IDs
|
||||||
@ -96,7 +99,10 @@ export default function RightRail() {
|
|||||||
}, [currentView, setSelectedFiles, pageEditorFunctions]);
|
}, [currentView, setSelectedFiles, pageEditorFunctions]);
|
||||||
|
|
||||||
const handleExportAll = useCallback(() => {
|
const handleExportAll = useCallback(() => {
|
||||||
if (currentView === 'fileEditor' || currentView === 'viewer') {
|
if (currentView === 'viewer') {
|
||||||
|
// Use EmbedPDF export functionality for viewer mode
|
||||||
|
viewerContext?.exportActions?.download();
|
||||||
|
} else if (currentView === 'fileEditor') {
|
||||||
// Download selected files (or all if none selected)
|
// Download selected files (or all if none selected)
|
||||||
const filesToDownload = selectedFiles.length > 0 ? selectedFiles : activeFiles;
|
const filesToDownload = selectedFiles.length > 0 ? selectedFiles : activeFiles;
|
||||||
|
|
||||||
@ -113,7 +119,7 @@ export default function RightRail() {
|
|||||||
// Export all pages (not just selected)
|
// Export all pages (not just selected)
|
||||||
pageEditorFunctions?.onExportAll?.();
|
pageEditorFunctions?.onExportAll?.();
|
||||||
}
|
}
|
||||||
}, [currentView, activeFiles, selectedFiles, pageEditorFunctions]);
|
}, [currentView, activeFiles, selectedFiles, pageEditorFunctions, viewerContext]);
|
||||||
|
|
||||||
const handleCloseSelected = useCallback(() => {
|
const handleCloseSelected = useCallback(() => {
|
||||||
if (currentView !== 'fileEditor') return;
|
if (currentView !== 'fileEditor') return;
|
||||||
@ -445,7 +451,9 @@ export default function RightRail() {
|
|||||||
radius="md"
|
radius="md"
|
||||||
className="right-rail-icon"
|
className="right-rail-icon"
|
||||||
onClick={handleExportAll}
|
onClick={handleExportAll}
|
||||||
disabled={currentView === 'viewer' || totalItems === 0}
|
disabled={
|
||||||
|
currentView === 'viewer' ? !exportState?.canExport : totalItems === 0
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="download" width="1.5rem" height="1.5rem" />
|
<LocalIcon icon="download" width="1.5rem" height="1.5rem" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
25
frontend/src/components/viewer/ExportAPIBridge.tsx
Normal file
25
frontend/src/components/viewer/ExportAPIBridge.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useExportCapability } from '@embedpdf/plugin-export/react';
|
||||||
|
import { useViewer } from '../../contexts/ViewerContext';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that runs inside EmbedPDF context and provides export functionality
|
||||||
|
*/
|
||||||
|
export function ExportAPIBridge() {
|
||||||
|
const { provides: exportApi } = useExportCapability();
|
||||||
|
const { registerBridge } = useViewer();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (exportApi) {
|
||||||
|
// Register this bridge with ViewerContext
|
||||||
|
registerBridge('export', {
|
||||||
|
state: {
|
||||||
|
canExport: true,
|
||||||
|
},
|
||||||
|
api: exportApi
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [exportApi, registerBridge]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
@ -17,6 +17,7 @@ import { SpreadPluginPackage, SpreadMode } from '@embedpdf/plugin-spread/react';
|
|||||||
import { SearchPluginPackage } from '@embedpdf/plugin-search/react';
|
import { SearchPluginPackage } from '@embedpdf/plugin-search/react';
|
||||||
import { ThumbnailPluginPackage } from '@embedpdf/plugin-thumbnail/react';
|
import { ThumbnailPluginPackage } from '@embedpdf/plugin-thumbnail/react';
|
||||||
import { RotatePluginPackage, Rotate } from '@embedpdf/plugin-rotate/react';
|
import { RotatePluginPackage, Rotate } from '@embedpdf/plugin-rotate/react';
|
||||||
|
import { ExportPluginPackage } from '@embedpdf/plugin-export/react';
|
||||||
import { Rotation } from '@embedpdf/models';
|
import { Rotation } from '@embedpdf/models';
|
||||||
import { CustomSearchLayer } from './CustomSearchLayer';
|
import { CustomSearchLayer } from './CustomSearchLayer';
|
||||||
import { ZoomAPIBridge } from './ZoomAPIBridge';
|
import { ZoomAPIBridge } from './ZoomAPIBridge';
|
||||||
@ -29,6 +30,7 @@ import { SpreadAPIBridge } from './SpreadAPIBridge';
|
|||||||
import { SearchAPIBridge } from './SearchAPIBridge';
|
import { SearchAPIBridge } from './SearchAPIBridge';
|
||||||
import { ThumbnailAPIBridge } from './ThumbnailAPIBridge';
|
import { ThumbnailAPIBridge } from './ThumbnailAPIBridge';
|
||||||
import { RotateAPIBridge } from './RotateAPIBridge';
|
import { RotateAPIBridge } from './RotateAPIBridge';
|
||||||
|
import { ExportAPIBridge } from './ExportAPIBridge';
|
||||||
|
|
||||||
interface LocalEmbedPDFProps {
|
interface LocalEmbedPDFProps {
|
||||||
file?: File | Blob;
|
file?: File | Blob;
|
||||||
@ -112,6 +114,11 @@ export function LocalEmbedPDF({ file, url }: LocalEmbedPDFProps) {
|
|||||||
createPluginRegistration(RotatePluginPackage, {
|
createPluginRegistration(RotatePluginPackage, {
|
||||||
defaultRotation: Rotation.Degree0, // Start with no rotation
|
defaultRotation: Rotation.Degree0, // Start with no rotation
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// Register export plugin for downloading PDFs
|
||||||
|
createPluginRegistration(ExportPluginPackage, {
|
||||||
|
defaultFileName: 'document.pdf',
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
}, [pdfUrl]);
|
}, [pdfUrl]);
|
||||||
|
|
||||||
@ -170,6 +177,7 @@ export function LocalEmbedPDF({ file, url }: LocalEmbedPDFProps) {
|
|||||||
<SearchAPIBridge />
|
<SearchAPIBridge />
|
||||||
<ThumbnailAPIBridge />
|
<ThumbnailAPIBridge />
|
||||||
<RotateAPIBridge />
|
<RotateAPIBridge />
|
||||||
|
<ExportAPIBridge />
|
||||||
<GlobalPointerProvider>
|
<GlobalPointerProvider>
|
||||||
<Viewport
|
<Viewport
|
||||||
style={{
|
style={{
|
||||||
|
@ -51,6 +51,11 @@ interface ThumbnailAPIWrapper {
|
|||||||
renderThumb: (pageIndex: number, scale: number) => { toPromise: () => Promise<Blob> };
|
renderThumb: (pageIndex: number, scale: number) => { toPromise: () => Promise<Blob> };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ExportAPIWrapper {
|
||||||
|
download: () => void;
|
||||||
|
saveAsCopy: () => { toPromise: () => Promise<ArrayBuffer> };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// State interfaces - represent the shape of data from each bridge
|
// State interfaces - represent the shape of data from each bridge
|
||||||
interface ScrollState {
|
interface ScrollState {
|
||||||
@ -93,6 +98,10 @@ interface SearchState {
|
|||||||
activeIndex: number;
|
activeIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ExportState {
|
||||||
|
canExport: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// Bridge registration interface - bridges register with state and API
|
// Bridge registration interface - bridges register with state and API
|
||||||
interface BridgeRef<TState = unknown, TApi = unknown> {
|
interface BridgeRef<TState = unknown, TApi = unknown> {
|
||||||
state: TState;
|
state: TState;
|
||||||
@ -122,6 +131,7 @@ interface ViewerContextType {
|
|||||||
getRotationState: () => RotationState;
|
getRotationState: () => RotationState;
|
||||||
getSearchState: () => SearchState;
|
getSearchState: () => SearchState;
|
||||||
getThumbnailAPI: () => ThumbnailAPIWrapper | null;
|
getThumbnailAPI: () => ThumbnailAPIWrapper | null;
|
||||||
|
getExportState: () => ExportState;
|
||||||
|
|
||||||
// Immediate update callbacks
|
// Immediate update callbacks
|
||||||
registerImmediateZoomUpdate: (callback: (percent: number) => void) => void;
|
registerImmediateZoomUpdate: (callback: (percent: number) => void) => void;
|
||||||
@ -179,7 +189,12 @@ interface ViewerContextType {
|
|||||||
clear: () => void;
|
clear: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Bridge registration - internal use by bridges
|
exportActions: {
|
||||||
|
download: () => void;
|
||||||
|
saveAsCopy: () => Promise<ArrayBuffer | null>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bridge registration - internal use by bridges
|
||||||
registerBridge: (type: string, ref: BridgeRef) => void;
|
registerBridge: (type: string, ref: BridgeRef) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,6 +218,7 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
|||||||
spread: null as BridgeRef<SpreadState, SpreadAPIWrapper> | null,
|
spread: null as BridgeRef<SpreadState, SpreadAPIWrapper> | null,
|
||||||
rotation: null as BridgeRef<RotationState, RotationAPIWrapper> | null,
|
rotation: null as BridgeRef<RotationState, RotationAPIWrapper> | null,
|
||||||
thumbnail: null as BridgeRef<unknown, ThumbnailAPIWrapper> | null,
|
thumbnail: null as BridgeRef<unknown, ThumbnailAPIWrapper> | null,
|
||||||
|
export: null as BridgeRef<ExportState, ExportAPIWrapper> | null,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Immediate zoom callback for responsive display updates
|
// Immediate zoom callback for responsive display updates
|
||||||
@ -238,6 +254,9 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
|||||||
case 'thumbnail':
|
case 'thumbnail':
|
||||||
bridgeRefs.current.thumbnail = ref as BridgeRef<unknown, ThumbnailAPIWrapper>;
|
bridgeRefs.current.thumbnail = ref as BridgeRef<unknown, ThumbnailAPIWrapper>;
|
||||||
break;
|
break;
|
||||||
|
case 'export':
|
||||||
|
bridgeRefs.current.export = ref as BridgeRef<ExportState, ExportAPIWrapper>;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -278,6 +297,10 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
|||||||
return bridgeRefs.current.thumbnail?.api || null;
|
return bridgeRefs.current.thumbnail?.api || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getExportState = (): ExportState => {
|
||||||
|
return bridgeRefs.current.export?.state || { canExport: false };
|
||||||
|
};
|
||||||
|
|
||||||
// Action handlers - call APIs directly
|
// Action handlers - call APIs directly
|
||||||
const scrollActions = {
|
const scrollActions = {
|
||||||
scrollToPage: (page: number) => {
|
scrollToPage: (page: number) => {
|
||||||
@ -473,6 +496,28 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const exportActions = {
|
||||||
|
download: () => {
|
||||||
|
const api = bridgeRefs.current.export?.api;
|
||||||
|
if (api?.download) {
|
||||||
|
api.download();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
saveAsCopy: async () => {
|
||||||
|
const api = bridgeRefs.current.export?.api;
|
||||||
|
if (api?.saveAsCopy) {
|
||||||
|
try {
|
||||||
|
const result = api.saveAsCopy();
|
||||||
|
return await result.toPromise();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save PDF copy:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const registerImmediateZoomUpdate = (callback: (percent: number) => void) => {
|
const registerImmediateZoomUpdate = (callback: (percent: number) => void) => {
|
||||||
immediateZoomUpdateCallback.current = callback;
|
immediateZoomUpdateCallback.current = callback;
|
||||||
};
|
};
|
||||||
@ -507,6 +552,7 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
|||||||
getRotationState,
|
getRotationState,
|
||||||
getSearchState,
|
getSearchState,
|
||||||
getThumbnailAPI,
|
getThumbnailAPI,
|
||||||
|
getExportState,
|
||||||
|
|
||||||
// Immediate updates
|
// Immediate updates
|
||||||
registerImmediateZoomUpdate,
|
registerImmediateZoomUpdate,
|
||||||
@ -522,6 +568,7 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
|||||||
spreadActions,
|
spreadActions,
|
||||||
rotationActions,
|
rotationActions,
|
||||||
searchActions,
|
searchActions,
|
||||||
|
exportActions,
|
||||||
|
|
||||||
// Bridge registration
|
// Bridge registration
|
||||||
registerBridge,
|
registerBridge,
|
||||||
|
Loading…
Reference in New Issue
Block a user