mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-24 04:26:14 +00:00
Compare commits
No commits in common. "1709ca904925e2c6f672fcbeeabe806eb48d79f6" and "423617db52203f4de20c20188aae317b83fa851f" have entirely different histories.
1709ca9049
...
423617db52
@ -7,16 +7,16 @@ import { useRightRail } from '../../contexts/RightRailContext';
|
|||||||
import { useFileState, useFileSelection, useFileManagement } from '../../contexts/FileContext';
|
import { useFileState, useFileSelection, useFileManagement } from '../../contexts/FileContext';
|
||||||
import { useNavigationState } from '../../contexts/NavigationContext';
|
import { useNavigationState } from '../../contexts/NavigationContext';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { usePanState } from '../../hooks/usePanState';
|
||||||
|
|
||||||
import LanguageSelector from '../shared/LanguageSelector';
|
import LanguageSelector from '../shared/LanguageSelector';
|
||||||
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
|
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
|
||||||
import { Tooltip } from '../shared/Tooltip';
|
import { Tooltip } from '../shared/Tooltip';
|
||||||
import BulkSelectionPanel from '../pageEditor/BulkSelectionPanel';
|
import BulkSelectionPanel from '../pageEditor/BulkSelectionPanel';
|
||||||
import { SearchInterface } from '../viewer/SearchInterface';
|
|
||||||
|
|
||||||
export default function RightRail() {
|
export default function RightRail() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isPanning, setIsPanning] = useState(false);
|
const isPanning = usePanState();
|
||||||
const { toggleTheme } = useRainbowThemeContext();
|
const { toggleTheme } = useRainbowThemeContext();
|
||||||
const { buttons, actions } = useRightRail();
|
const { buttons, actions } = useRightRail();
|
||||||
const topButtons = useMemo(() => buttons.filter(b => (b.section || 'top') === 'top' && (b.visible ?? true)), [buttons]);
|
const topButtons = useMemo(() => buttons.filter(b => (b.section || 'top') === 'top' && (b.visible ?? true)), [buttons]);
|
||||||
@ -215,29 +215,15 @@ export default function RightRail() {
|
|||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1rem' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1rem' }}>
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<Tooltip content={t('rightRail.search', 'Search PDF')} position="left" offset={12} arrow>
|
<Tooltip content={t('rightRail.search', 'Search PDF')} position="left" offset={12} arrow>
|
||||||
<Popover position="left" withArrow shadow="md" offset={8}>
|
|
||||||
<Popover.Target>
|
|
||||||
<div style={{ display: 'inline-flex' }}>
|
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
radius="md"
|
radius="md"
|
||||||
className="right-rail-icon"
|
className="right-rail-icon"
|
||||||
|
onClick={() => (window as any).togglePdfSearch?.()}
|
||||||
disabled={currentView !== 'viewer'}
|
disabled={currentView !== 'viewer'}
|
||||||
aria-label={typeof t === 'function' ? t('rightRail.search', 'Search PDF') : 'Search PDF'}
|
|
||||||
>
|
>
|
||||||
<LocalIcon icon="search" width="1.5rem" height="1.5rem" />
|
<LocalIcon icon="search" width="1.5rem" height="1.5rem" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</div>
|
|
||||||
</Popover.Target>
|
|
||||||
<Popover.Dropdown>
|
|
||||||
<div style={{ minWidth: '20rem' }}>
|
|
||||||
<SearchInterface
|
|
||||||
visible={true}
|
|
||||||
onClose={() => {}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Popover.Dropdown>
|
|
||||||
</Popover>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
|
||||||
@ -248,10 +234,7 @@ export default function RightRail() {
|
|||||||
color={isPanning ? "blue" : undefined}
|
color={isPanning ? "blue" : undefined}
|
||||||
radius="md"
|
radius="md"
|
||||||
className="right-rail-icon"
|
className="right-rail-icon"
|
||||||
onClick={() => {
|
onClick={() => (window as any).embedPdfPan?.togglePan()}
|
||||||
(window as any).embedPdfPan?.togglePan();
|
|
||||||
setIsPanning(!isPanning);
|
|
||||||
}}
|
|
||||||
disabled={currentView !== 'viewer'}
|
disabled={currentView !== 'viewer'}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="pan-tool-rounded" width="1.5rem" height="1.5rem" />
|
<LocalIcon icon="pan-tool-rounded" width="1.5rem" height="1.5rem" />
|
||||||
@ -343,7 +326,7 @@ export default function RightRail() {
|
|||||||
</div>
|
</div>
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
<Popover.Dropdown>
|
<Popover.Dropdown>
|
||||||
<div style={{ minWidth: '17.5rem' }}>
|
<div style={{ minWidth: 280 }}>
|
||||||
<BulkSelectionPanel
|
<BulkSelectionPanel
|
||||||
csvInput={csvInput}
|
csvInput={csvInput}
|
||||||
setCsvInput={setCsvInput}
|
setCsvInput={setCsvInput}
|
||||||
|
@ -8,6 +8,7 @@ import { useFileState } from "../../contexts/FileContext";
|
|||||||
import { useFileWithUrl } from "../../hooks/useFileWithUrl";
|
import { useFileWithUrl } from "../../hooks/useFileWithUrl";
|
||||||
import { LocalEmbedPDF } from './LocalEmbedPDF';
|
import { LocalEmbedPDF } from './LocalEmbedPDF';
|
||||||
import { PdfViewerToolbar } from './PdfViewerToolbar';
|
import { PdfViewerToolbar } from './PdfViewerToolbar';
|
||||||
|
import { SearchInterface } from './SearchInterface';
|
||||||
import { ThumbnailSidebar } from './ThumbnailSidebar';
|
import { ThumbnailSidebar } from './ThumbnailSidebar';
|
||||||
|
|
||||||
export interface EmbedPdfViewerProps {
|
export interface EmbedPdfViewerProps {
|
||||||
@ -28,6 +29,7 @@ const EmbedPdfViewer = ({
|
|||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
const viewerRef = React.useRef<HTMLDivElement>(null);
|
const viewerRef = React.useRef<HTMLDivElement>(null);
|
||||||
const [isViewerHovered, setIsViewerHovered] = React.useState(false);
|
const [isViewerHovered, setIsViewerHovered] = React.useState(false);
|
||||||
|
const [isSearchVisible, setIsSearchVisible] = React.useState(false);
|
||||||
const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = React.useState(false);
|
const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = React.useState(false);
|
||||||
|
|
||||||
// Get current file from FileContext
|
// Get current file from FileContext
|
||||||
@ -120,11 +122,16 @@ const EmbedPdfViewer = ({
|
|||||||
|
|
||||||
// Expose toggle functions globally for right rail buttons
|
// Expose toggle functions globally for right rail buttons
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
(window as any).togglePdfSearch = () => {
|
||||||
|
setIsSearchVisible(prev => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
(window as any).toggleThumbnailSidebar = () => {
|
(window as any).toggleThumbnailSidebar = () => {
|
||||||
setIsThumbnailSidebarVisible(prev => !prev);
|
setIsThumbnailSidebarVisible(prev => !prev);
|
||||||
};
|
};
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
delete (window as any).togglePdfSearch;
|
||||||
delete (window as any).toggleThumbnailSidebar;
|
delete (window as any).toggleThumbnailSidebar;
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
@ -220,6 +227,11 @@ const EmbedPdfViewer = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Search Interface Overlay */}
|
||||||
|
<SearchInterface
|
||||||
|
visible={isSearchVisible}
|
||||||
|
onClose={() => setIsSearchVisible(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Thumbnail Sidebar */}
|
{/* Thumbnail Sidebar */}
|
||||||
<ThumbnailSidebar
|
<ThumbnailSidebar
|
||||||
|
@ -17,13 +17,13 @@ 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 { CustomSearchLayer } from './CustomSearchLayer';
|
import { CustomSearchLayer } from './CustomSearchLayer';
|
||||||
import { ZoomAPIBridge } from './ZoomAPIBridge';
|
import { ZoomControlsExporter } from './ZoomControlsExporter';
|
||||||
import { ScrollAPIBridge } from './ScrollAPIBridge';
|
import { ScrollControlsExporter } from './ScrollControlsExporter';
|
||||||
import { SelectionAPIBridge } from './SelectionAPIBridge';
|
import { SelectionControlsExporter } from './SelectionControlsExporter';
|
||||||
import { PanAPIBridge } from './PanAPIBridge';
|
import { PanControlsExporter } from './PanControlsExporter';
|
||||||
import { SpreadAPIBridge } from './SpreadAPIBridge';
|
import { SpreadControlsExporter } from './SpreadControlsExporter';
|
||||||
import { SearchAPIBridge } from './SearchAPIBridge';
|
import { SearchControlsExporter } from './SearchControlsExporter';
|
||||||
import { ThumbnailAPIBridge } from './ThumbnailAPIBridge';
|
import { ThumbnailControlsExporter } from './ThumbnailControlsExporter';
|
||||||
|
|
||||||
interface LocalEmbedPDFProps {
|
interface LocalEmbedPDFProps {
|
||||||
file?: File | Blob;
|
file?: File | Blob;
|
||||||
@ -180,13 +180,13 @@ export function LocalEmbedPDF({ file, url, colorScheme }: LocalEmbedPDFProps) {
|
|||||||
minWidth: 0
|
minWidth: 0
|
||||||
}}>
|
}}>
|
||||||
<EmbedPDF engine={engine} plugins={plugins}>
|
<EmbedPDF engine={engine} plugins={plugins}>
|
||||||
<ZoomAPIBridge />
|
<ZoomControlsExporter />
|
||||||
<ScrollAPIBridge />
|
<ScrollControlsExporter />
|
||||||
<SelectionAPIBridge />
|
<SelectionControlsExporter />
|
||||||
<PanAPIBridge />
|
<PanControlsExporter />
|
||||||
<SpreadAPIBridge />
|
<SpreadControlsExporter />
|
||||||
<SearchAPIBridge />
|
<SearchControlsExporter />
|
||||||
<ThumbnailAPIBridge />
|
<ThumbnailControlsExporter />
|
||||||
<GlobalPointerProvider>
|
<GlobalPointerProvider>
|
||||||
<Viewport
|
<Viewport
|
||||||
style={{
|
style={{
|
||||||
|
@ -2,9 +2,9 @@ import { useEffect, useState } from 'react';
|
|||||||
import { usePan } from '@embedpdf/plugin-pan/react';
|
import { usePan } from '@embedpdf/plugin-pan/react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that runs inside EmbedPDF context and bridges pan controls to global window
|
* Component that runs inside EmbedPDF context and exports pan controls globally
|
||||||
*/
|
*/
|
||||||
export function PanAPIBridge() {
|
export function PanControlsExporter() {
|
||||||
const { provides: pan, isPanning } = usePan();
|
const { provides: pan, isPanning } = usePan();
|
||||||
const [panStateListeners, setPanStateListeners] = useState<Array<(isPanning: boolean) => void>>([]);
|
const [panStateListeners, setPanStateListeners] = useState<Array<(isPanning: boolean) => void>>([]);
|
||||||
|
|
||||||
@ -21,10 +21,11 @@ export function PanAPIBridge() {
|
|||||||
pan.disablePan();
|
pan.disablePan();
|
||||||
},
|
},
|
||||||
togglePan: () => {
|
togglePan: () => {
|
||||||
|
console.log('EmbedPDF: Toggling pan mode, current isPanning:', isPanning);
|
||||||
pan.togglePan();
|
pan.togglePan();
|
||||||
},
|
},
|
||||||
makePanDefault: () => pan.makePanDefault(),
|
makePanDefault: () => pan.makePanDefault(),
|
||||||
get isPanning() { return isPanning; }, // Use getter to always return current value
|
isPanning: isPanning,
|
||||||
// Subscribe to pan state changes for reactive UI
|
// Subscribe to pan state changes for reactive UI
|
||||||
onPanStateChange: (callback: (isPanning: boolean) => void) => {
|
onPanStateChange: (callback: (isPanning: boolean) => void) => {
|
||||||
setPanStateListeners(prev => [...prev, callback]);
|
setPanStateListeners(prev => [...prev, callback]);
|
||||||
@ -35,6 +36,11 @@ export function PanAPIBridge() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('EmbedPDF pan controls exported to window.embedPdfPan', {
|
||||||
|
isPanning,
|
||||||
|
panAPI: pan,
|
||||||
|
availableMethods: Object.keys(pan)
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.warn('EmbedPDF pan API not available yet');
|
console.warn('EmbedPDF pan API not available yet');
|
||||||
}
|
}
|
||||||
@ -45,5 +51,5 @@ export function PanAPIBridge() {
|
|||||||
panStateListeners.forEach(callback => callback(isPanning));
|
panStateListeners.forEach(callback => callback(isPanning));
|
||||||
}, [isPanning, panStateListeners]);
|
}, [isPanning, panStateListeners]);
|
||||||
|
|
||||||
return null;
|
return null; // This component doesn't render anything
|
||||||
}
|
}
|
@ -35,7 +35,6 @@ export function PdfViewerToolbar({
|
|||||||
const [dynamicZoom, setDynamicZoom] = useState(currentZoom);
|
const [dynamicZoom, setDynamicZoom] = useState(currentZoom);
|
||||||
const [dynamicPage, setDynamicPage] = useState(currentPage);
|
const [dynamicPage, setDynamicPage] = useState(currentPage);
|
||||||
const [dynamicTotalPages, setDynamicTotalPages] = useState(totalPages);
|
const [dynamicTotalPages, setDynamicTotalPages] = useState(totalPages);
|
||||||
const [isPanning, setIsPanning] = useState(false);
|
|
||||||
|
|
||||||
// Update zoom and scroll state from EmbedPDF APIs
|
// Update zoom and scroll state from EmbedPDF APIs
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -54,12 +53,6 @@ export function PdfViewerToolbar({
|
|||||||
setDynamicTotalPages(totalPagesNum);
|
setDynamicTotalPages(totalPagesNum);
|
||||||
setPageInput(currentPageNum);
|
setPageInput(currentPageNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update pan mode state
|
|
||||||
if ((window as any).embedPdfPan) {
|
|
||||||
const panState = (window as any).embedPdfPan.isPanning || false;
|
|
||||||
setIsPanning(panState);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update state immediately
|
// Update state immediately
|
||||||
@ -140,7 +133,7 @@ export function PdfViewerToolbar({
|
|||||||
borderBottomRightRadius: 0,
|
borderBottomRightRadius: 0,
|
||||||
boxShadow: "0 -2px 8px rgba(0,0,0,0.04)",
|
boxShadow: "0 -2px 8px rgba(0,0,0,0.04)",
|
||||||
pointerEvents: "auto",
|
pointerEvents: "auto",
|
||||||
minWidth: '26.5rem',
|
minWidth: 420,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* First Page Button */}
|
{/* First Page Button */}
|
||||||
@ -152,7 +145,7 @@ export function PdfViewerToolbar({
|
|||||||
radius="xl"
|
radius="xl"
|
||||||
onClick={handleFirstPage}
|
onClick={handleFirstPage}
|
||||||
disabled={dynamicPage === 1}
|
disabled={dynamicPage === 1}
|
||||||
style={{ minWidth: '2.5rem' }}
|
style={{ minWidth: 36 }}
|
||||||
title={t("viewer.firstPage", "First Page")}
|
title={t("viewer.firstPage", "First Page")}
|
||||||
>
|
>
|
||||||
<FirstPageIcon fontSize="small" />
|
<FirstPageIcon fontSize="small" />
|
||||||
@ -167,7 +160,7 @@ export function PdfViewerToolbar({
|
|||||||
radius="xl"
|
radius="xl"
|
||||||
onClick={handlePreviousPage}
|
onClick={handlePreviousPage}
|
||||||
disabled={dynamicPage === 1}
|
disabled={dynamicPage === 1}
|
||||||
style={{ minWidth: '2.5rem' }}
|
style={{ minWidth: 36 }}
|
||||||
title={t("viewer.previousPage", "Previous Page")}
|
title={t("viewer.previousPage", "Previous Page")}
|
||||||
>
|
>
|
||||||
<ArrowBackIosIcon fontSize="small" />
|
<ArrowBackIosIcon fontSize="small" />
|
||||||
@ -204,7 +197,7 @@ export function PdfViewerToolbar({
|
|||||||
radius="xl"
|
radius="xl"
|
||||||
onClick={handleNextPage}
|
onClick={handleNextPage}
|
||||||
disabled={dynamicPage === dynamicTotalPages}
|
disabled={dynamicPage === dynamicTotalPages}
|
||||||
style={{ minWidth: '2.5rem' }}
|
style={{ minWidth: 36 }}
|
||||||
title={t("viewer.nextPage", "Next Page")}
|
title={t("viewer.nextPage", "Next Page")}
|
||||||
>
|
>
|
||||||
<ArrowForwardIosIcon fontSize="small" />
|
<ArrowForwardIosIcon fontSize="small" />
|
||||||
@ -219,7 +212,7 @@ export function PdfViewerToolbar({
|
|||||||
radius="xl"
|
radius="xl"
|
||||||
onClick={handleLastPage}
|
onClick={handleLastPage}
|
||||||
disabled={dynamicPage === dynamicTotalPages}
|
disabled={dynamicPage === dynamicTotalPages}
|
||||||
style={{ minWidth: '2.5rem' }}
|
style={{ minWidth: 36 }}
|
||||||
title={t("viewer.lastPage", "Last Page")}
|
title={t("viewer.lastPage", "Last Page")}
|
||||||
>
|
>
|
||||||
<LastPageIcon fontSize="small" />
|
<LastPageIcon fontSize="small" />
|
||||||
@ -232,7 +225,7 @@ export function PdfViewerToolbar({
|
|||||||
size="md"
|
size="md"
|
||||||
radius="xl"
|
radius="xl"
|
||||||
onClick={onDualPageToggle}
|
onClick={onDualPageToggle}
|
||||||
style={{ minWidth: '2.5rem' }}
|
style={{ minWidth: 36 }}
|
||||||
title={dualPage ? t("viewer.singlePageView", "Single Page View") : t("viewer.dualPageView", "Dual Page View")}
|
title={dualPage ? t("viewer.singlePageView", "Single Page View") : t("viewer.dualPageView", "Dual Page View")}
|
||||||
>
|
>
|
||||||
{dualPage ? <DescriptionIcon fontSize="small" /> : <ViewWeekIcon fontSize="small" />}
|
{dualPage ? <DescriptionIcon fontSize="small" /> : <ViewWeekIcon fontSize="small" />}
|
||||||
@ -246,12 +239,12 @@ export function PdfViewerToolbar({
|
|||||||
size="md"
|
size="md"
|
||||||
radius="xl"
|
radius="xl"
|
||||||
onClick={handleZoomOut}
|
onClick={handleZoomOut}
|
||||||
style={{ minWidth: '2rem', padding: 0 }}
|
style={{ minWidth: 32, padding: 0 }}
|
||||||
title={t("viewer.zoomOut", "Zoom out")}
|
title={t("viewer.zoomOut", "Zoom out")}
|
||||||
>
|
>
|
||||||
−
|
−
|
||||||
</Button>
|
</Button>
|
||||||
<span style={{ minWidth: '2.5rem', textAlign: "center" }}>
|
<span style={{ minWidth: 40, textAlign: "center" }}>
|
||||||
{dynamicZoom}%
|
{dynamicZoom}%
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
@ -260,7 +253,7 @@ export function PdfViewerToolbar({
|
|||||||
size="md"
|
size="md"
|
||||||
radius="xl"
|
radius="xl"
|
||||||
onClick={handleZoomIn}
|
onClick={handleZoomIn}
|
||||||
style={{ minWidth: '2rem', padding: 0 }}
|
style={{ minWidth: 32, padding: 0 }}
|
||||||
title={t("viewer.zoomIn", "Zoom in")}
|
title={t("viewer.zoomIn", "Zoom in")}
|
||||||
>
|
>
|
||||||
+
|
+
|
||||||
|
@ -4,7 +4,7 @@ import { useScroll } from '@embedpdf/plugin-scroll/react';
|
|||||||
/**
|
/**
|
||||||
* Component that runs inside EmbedPDF context and exports scroll controls globally
|
* Component that runs inside EmbedPDF context and exports scroll controls globally
|
||||||
*/
|
*/
|
||||||
export function ScrollAPIBridge() {
|
export function ScrollControlsExporter() {
|
||||||
const { provides: scroll, state: scrollState } = useScroll();
|
const { provides: scroll, state: scrollState } = useScroll();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -23,5 +23,5 @@ export function ScrollAPIBridge() {
|
|||||||
}
|
}
|
||||||
}, [scroll, scrollState]);
|
}, [scroll, scrollState]);
|
||||||
|
|
||||||
return null;
|
return null; // This component doesn't render anything
|
||||||
}
|
}
|
@ -2,9 +2,9 @@ import { useEffect } from 'react';
|
|||||||
import { useSearch } from '@embedpdf/plugin-search/react';
|
import { useSearch } from '@embedpdf/plugin-search/react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that runs inside EmbedPDF context and bridges search controls to global window
|
* Component that runs inside EmbedPDF context and exports search controls globally
|
||||||
*/
|
*/
|
||||||
export function SearchAPIBridge() {
|
export function SearchControlsExporter() {
|
||||||
const { provides: search, state } = useSearch();
|
const { provides: search, state } = useSearch();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -48,5 +48,5 @@ export function SearchAPIBridge() {
|
|||||||
}
|
}
|
||||||
}, [search, state]);
|
}, [search, state]);
|
||||||
|
|
||||||
return null;
|
return null; // This component doesn't render anything
|
||||||
}
|
}
|
@ -11,7 +11,6 @@ interface SearchInterfaceProps {
|
|||||||
export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [jumpToValue, setJumpToValue] = useState('');
|
|
||||||
const [resultInfo, setResultInfo] = useState<{
|
const [resultInfo, setResultInfo] = useState<{
|
||||||
currentIndex: number;
|
currentIndex: number;
|
||||||
totalResults: number;
|
totalResults: number;
|
||||||
@ -55,11 +54,7 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
|||||||
}, [visible]);
|
}, [visible]);
|
||||||
|
|
||||||
const handleSearch = async (query: string) => {
|
const handleSearch = async (query: string) => {
|
||||||
if (!query.trim()) {
|
if (!query.trim()) return;
|
||||||
// If query is empty, clear the search
|
|
||||||
handleClearSearch();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchAPI = (window as any).embedPdfSearch;
|
const searchAPI = (window as any).embedPdfSearch;
|
||||||
if (searchAPI) {
|
if (searchAPI) {
|
||||||
@ -100,61 +95,47 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
|||||||
const searchAPI = (window as any).embedPdfSearch;
|
const searchAPI = (window as any).embedPdfSearch;
|
||||||
if (searchAPI) {
|
if (searchAPI) {
|
||||||
searchAPI.clearSearch();
|
searchAPI.clearSearch();
|
||||||
// Also try to explicitly clear highlights if available
|
|
||||||
if (searchAPI.searchAPI && searchAPI.searchAPI.clearHighlights) {
|
|
||||||
searchAPI.searchAPI.clearHighlights();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
setResultInfo(null);
|
setResultInfo(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sync search query with API state on mount
|
|
||||||
useEffect(() => {
|
|
||||||
const searchAPI = (window as any).embedPdfSearch;
|
|
||||||
if (searchAPI && searchAPI.state && searchAPI.state.query) {
|
|
||||||
setSearchQuery(searchAPI.state.query);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleJumpToResult = (index: number) => {
|
|
||||||
const searchAPI = (window as any).embedPdfSearch;
|
|
||||||
if (searchAPI && resultInfo && index >= 1 && index <= resultInfo.totalResults) {
|
|
||||||
// Convert 1-based user input to 0-based API index
|
|
||||||
searchAPI.goToResult(index - 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleJumpToSubmit = () => {
|
|
||||||
const index = parseInt(jumpToValue);
|
|
||||||
if (index && resultInfo && index >= 1 && index <= resultInfo.totalResults) {
|
|
||||||
handleJumpToResult(index);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleJumpToKeyDown = (e: React.KeyboardEvent) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
handleJumpToSubmit();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
handleClearSearch();
|
handleClearSearch();
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!visible) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
padding: '0px'
|
position: 'fixed',
|
||||||
|
top: '20px',
|
||||||
|
right: '20px',
|
||||||
|
zIndex: 1000,
|
||||||
|
backgroundColor: 'var(--mantine-color-body)',
|
||||||
|
border: '1px solid var(--mantine-color-gray-3)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '16px',
|
||||||
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||||
|
minWidth: '320px',
|
||||||
|
maxWidth: '400px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header with close button */}
|
||||||
<Group mb="md">
|
<Group justify="space-between" mb="md">
|
||||||
<Text size="sm" fw={600}>
|
<Text size="sm" fw={600}>
|
||||||
{t('search.title', 'Search PDF')}
|
{t('search.title', 'Search PDF')}
|
||||||
</Text>
|
</Text>
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleClose}
|
||||||
|
aria-label="Close search"
|
||||||
|
>
|
||||||
|
<LocalIcon icon="close" width="1rem" height="1rem" />
|
||||||
|
</ActionIcon>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{/* Search input */}
|
{/* Search input */}
|
||||||
@ -162,14 +143,7 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder={t('search.placeholder', 'Enter search term...')}
|
placeholder={t('search.placeholder', 'Enter search term...')}
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => {
|
onChange={(e) => setSearchQuery(e.currentTarget.value)}
|
||||||
const newValue = e.currentTarget.value;
|
|
||||||
setSearchQuery(newValue);
|
|
||||||
// If user clears the input, clear the search highlights
|
|
||||||
if (!newValue.trim()) {
|
|
||||||
handleClearSearch();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
rightSection={
|
rightSection={
|
||||||
@ -188,29 +162,15 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
|||||||
{/* Results info and navigation */}
|
{/* Results info and navigation */}
|
||||||
{resultInfo && (
|
{resultInfo && (
|
||||||
<Group justify="space-between" align="center">
|
<Group justify="space-between" align="center">
|
||||||
{resultInfo.totalResults === 0 ? (
|
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
{t('search.noResults', 'No results found')}
|
{resultInfo.totalResults === 0
|
||||||
|
? t('search.noResults', 'No results found')
|
||||||
|
: t('search.resultCount', '{{current}} of {{total}}', {
|
||||||
|
current: resultInfo.currentIndex,
|
||||||
|
total: resultInfo.totalResults
|
||||||
|
})
|
||||||
|
}
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
|
||||||
<Group gap="xs" align="center">
|
|
||||||
<TextInput
|
|
||||||
size="xs"
|
|
||||||
value={jumpToValue}
|
|
||||||
onChange={(e) => setJumpToValue(e.currentTarget.value)}
|
|
||||||
onKeyDown={handleJumpToKeyDown}
|
|
||||||
onBlur={handleJumpToSubmit}
|
|
||||||
placeholder={resultInfo.currentIndex.toString()}
|
|
||||||
style={{ width: '3rem' }}
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max={resultInfo.totalResults}
|
|
||||||
/>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
of {resultInfo.totalResults}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{resultInfo.totalResults > 0 && (
|
{resultInfo.totalResults > 0 && (
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
|
@ -4,7 +4,7 @@ import { useSelectionCapability, SelectionRangeX } from '@embedpdf/plugin-select
|
|||||||
/**
|
/**
|
||||||
* Component that runs inside EmbedPDF context and exports selection controls globally
|
* Component that runs inside EmbedPDF context and exports selection controls globally
|
||||||
*/
|
*/
|
||||||
export function SelectionAPIBridge() {
|
export function SelectionControlsExporter() {
|
||||||
const { provides: selection } = useSelectionCapability();
|
const { provides: selection } = useSelectionCapability();
|
||||||
const [hasSelection, setHasSelection] = useState(false);
|
const [hasSelection, setHasSelection] = useState(false);
|
||||||
|
|
||||||
@ -47,5 +47,5 @@ export function SelectionAPIBridge() {
|
|||||||
}
|
}
|
||||||
}, [selection, hasSelection]);
|
}, [selection, hasSelection]);
|
||||||
|
|
||||||
return null;
|
return null; // This component doesn't render anything
|
||||||
}
|
}
|
@ -4,7 +4,7 @@ import { useSpread, SpreadMode } from '@embedpdf/plugin-spread/react';
|
|||||||
/**
|
/**
|
||||||
* Component that runs inside EmbedPDF context and exports spread controls globally
|
* Component that runs inside EmbedPDF context and exports spread controls globally
|
||||||
*/
|
*/
|
||||||
export function SpreadAPIBridge() {
|
export function SpreadControlsExporter() {
|
||||||
const { provides: spread, spreadMode } = useSpread();
|
const { provides: spread, spreadMode } = useSpread();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -38,5 +38,5 @@ export function SpreadAPIBridge() {
|
|||||||
}
|
}
|
||||||
}, [spread, spreadMode]);
|
}, [spread, spreadMode]);
|
||||||
|
|
||||||
return null;
|
return null; // This component doesn't render anything
|
||||||
}
|
}
|
@ -4,11 +4,11 @@ import { useThumbnailCapability } from '@embedpdf/plugin-thumbnail/react';
|
|||||||
/**
|
/**
|
||||||
* Component that runs inside EmbedPDF context and exports thumbnail controls globally
|
* Component that runs inside EmbedPDF context and exports thumbnail controls globally
|
||||||
*/
|
*/
|
||||||
export function ThumbnailAPIBridge() {
|
export function ThumbnailControlsExporter() {
|
||||||
const { provides: thumbnail } = useThumbnailCapability();
|
const { provides: thumbnail } = useThumbnailCapability();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('📄 ThumbnailAPIBridge useEffect:', { thumbnail: !!thumbnail });
|
console.log('📄 ThumbnailControlsExporter useEffect:', { thumbnail: !!thumbnail });
|
||||||
if (thumbnail) {
|
if (thumbnail) {
|
||||||
console.log('📄 Exporting thumbnail controls to window:', {
|
console.log('📄 Exporting thumbnail controls to window:', {
|
||||||
availableMethods: Object.keys(thumbnail),
|
availableMethods: Object.keys(thumbnail),
|
||||||
@ -22,5 +22,5 @@ export function ThumbnailAPIBridge() {
|
|||||||
}
|
}
|
||||||
}, [thumbnail]);
|
}, [thumbnail]);
|
||||||
|
|
||||||
return null;
|
return null; // This component doesn't render anything
|
||||||
}
|
}
|
@ -111,7 +111,7 @@ export function ThumbnailSidebar({ visible, onToggle, colorScheme }: ThumbnailSi
|
|||||||
right: 0,
|
right: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
width: '15rem',
|
width: '240px',
|
||||||
backgroundColor: actualColorScheme === 'dark' ? '#1a1b1e' : '#f8f9fa',
|
backgroundColor: actualColorScheme === 'dark' ? '#1a1b1e' : '#f8f9fa',
|
||||||
borderLeft: `1px solid ${actualColorScheme === 'dark' ? '#373A40' : '#e9ecef'}`,
|
borderLeft: `1px solid ${actualColorScheme === 'dark' ? '#373A40' : '#e9ecef'}`,
|
||||||
zIndex: 998,
|
zIndex: 998,
|
||||||
@ -174,8 +174,8 @@ export function ThumbnailSidebar({ visible, onToggle, colorScheme }: ThumbnailSi
|
|||||||
/>
|
/>
|
||||||
) : thumbnails[pageIndex] === 'error' ? (
|
) : thumbnails[pageIndex] === 'error' ? (
|
||||||
<div style={{
|
<div style={{
|
||||||
width: '11.5rem',
|
width: '180px',
|
||||||
height: '15rem',
|
height: '240px',
|
||||||
backgroundColor: actualColorScheme === 'dark' ? '#2d1b1b' : '#ffebee',
|
backgroundColor: actualColorScheme === 'dark' ? '#2d1b1b' : '#ffebee',
|
||||||
border: `1px solid ${actualColorScheme === 'dark' ? '#5d3737' : '#ffcdd2'}`,
|
border: `1px solid ${actualColorScheme === 'dark' ? '#5d3737' : '#ffcdd2'}`,
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
@ -189,8 +189,8 @@ export function ThumbnailSidebar({ visible, onToggle, colorScheme }: ThumbnailSi
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{
|
<div style={{
|
||||||
width: '11.5rem',
|
width: '180px',
|
||||||
height: '15rem',
|
height: '240px',
|
||||||
backgroundColor: actualColorScheme === 'dark' ? '#25262b' : '#f8f9fa',
|
backgroundColor: actualColorScheme === 'dark' ? '#25262b' : '#f8f9fa',
|
||||||
border: `1px solid ${actualColorScheme === 'dark' ? '#373A40' : '#e9ecef'}`,
|
border: `1px solid ${actualColorScheme === 'dark' ? '#373A40' : '#e9ecef'}`,
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
|
@ -4,7 +4,7 @@ import { useZoom } from '@embedpdf/plugin-zoom/react';
|
|||||||
/**
|
/**
|
||||||
* Component that runs inside EmbedPDF context and exports zoom controls globally
|
* Component that runs inside EmbedPDF context and exports zoom controls globally
|
||||||
*/
|
*/
|
||||||
export function ZoomAPIBridge() {
|
export function ZoomControlsExporter() {
|
||||||
const { provides: zoom, state: zoomState } = useZoom();
|
const { provides: zoom, state: zoomState } = useZoom();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -22,5 +22,5 @@ export function ZoomAPIBridge() {
|
|||||||
}
|
}
|
||||||
}, [zoom, zoomState]);
|
}, [zoom, zoomState]);
|
||||||
|
|
||||||
return null;
|
return null; // This component doesn't render anything
|
||||||
}
|
}
|
24
frontend/src/hooks/usePanState.ts
Normal file
24
frontend/src/hooks/usePanState.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to track EmbedPDF pan state for reactive UI components
|
||||||
|
*/
|
||||||
|
export function usePanState() {
|
||||||
|
const [isPanning, setIsPanning] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Subscribe to pan state changes
|
||||||
|
const unsubscribe = (window as any).embedPdfPan?.onPanStateChange?.((newIsPanning: boolean) => {
|
||||||
|
setIsPanning(newIsPanning);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get initial state
|
||||||
|
if ((window as any).embedPdfPan?.isPanning !== undefined) {
|
||||||
|
setIsPanning((window as any).embedPdfPan.isPanning);
|
||||||
|
}
|
||||||
|
|
||||||
|
return unsubscribe || (() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return isPanning;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user