From d21681131787a6e673c06a4e4efeb20c2b03ae70 Mon Sep 17 00:00:00 2001 From: Reece Date: Tue, 27 May 2025 19:22:26 +0100 Subject: [PATCH] - Viewer overhaul -Dark mode toggle -URL params improvements -app.js set up fix - UI clean up --- frontend/src/App.js | 29 +- frontend/src/components/FileManager.tsx | 18 +- frontend/src/components/ToolPicker.tsx | 1 - frontend/src/components/Viewer.tsx | 367 +++++++++++++++++++-- frontend/src/index.js | 1 + frontend/src/pages/HomePage.tsx | 406 ++++++++++++++---------- frontend/src/tools/Compress.tsx | 7 +- frontend/src/tools/Merge.tsx | 23 +- frontend/src/tools/Split.tsx | 236 +++++++------- 9 files changed, 740 insertions(+), 348 deletions(-) diff --git a/frontend/src/App.js b/frontend/src/App.js index 19f2719f3..9a98599e2 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,31 +1,6 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import { BrowserRouter, Routes, Route } from 'react-router-dom'; -import { ColorSchemeScript, MantineProvider } from '@mantine/core'; import './index.css'; import HomePage from './pages/HomePage'; -import SplitPdfPanel from './tools/Split'; -import reportWebVitals from './reportWebVitals'; -export default function App() { - return ( - - } /> - } /> - - ); +export default function App({ colorScheme, toggleColorScheme }) { + return ; } - -const root = ReactDOM.createRoot(document.getElementById('root')); -root.render( - - - - - - - - -); - -reportWebVitals(); diff --git a/frontend/src/components/FileManager.tsx b/frontend/src/components/FileManager.tsx index f4a05e704..c3cdc1775 100644 --- a/frontend/src/components/FileManager.tsx +++ b/frontend/src/components/FileManager.tsx @@ -1,7 +1,8 @@ import React, { useState, useEffect } from "react"; -import { Card, Group, Text, Stack, Image, Badge, Button, Box, Flex } from "@mantine/core"; +import { Card, Group, Text, Stack, Image, Badge, Button, Box, Flex, ThemeIcon } from "@mantine/core"; import { Dropzone, MIME_TYPES } from "@mantine/dropzone"; import { GlobalWorkerOptions, getDocument } from "pdfjs-dist"; +import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf"; GlobalWorkerOptions.workerSrc = (import.meta as any).env?.PUBLIC_URL @@ -93,7 +94,15 @@ function FileCard({ file, onRemove, onDoubleClick }: FileCardProps) { {thumb ? ( PDF thumbnail ) : ( - PDF + + + )} @@ -145,21 +154,22 @@ const FileManager: React.FC = ({ }; return ( -
+
diff --git a/frontend/src/components/ToolPicker.tsx b/frontend/src/components/ToolPicker.tsx index 5ece14018..dfc7573c1 100644 --- a/frontend/src/components/ToolPicker.tsx +++ b/frontend/src/components/ToolPicker.tsx @@ -27,7 +27,6 @@ const ToolPicker: React.FC = ({ selectedToolKey, onSelect, tool void; + sidebarsVisible: boolean; + setSidebarsVisible: (v: boolean) => void; } -const Viewer: React.FC = ({ pdfFile, setPdfFile }) => { +const Viewer: React.FC = ({ + pdfFile, + setPdfFile, + sidebarsVisible, + setSidebarsVisible, +}) => { + const theme = useMantineTheme(); const [numPages, setNumPages] = useState(0); const [pageImages, setPageImages] = useState([]); const [loading, setLoading] = useState(false); + const [currentPage, setCurrentPage] = useState(null); + const [dualPage, setDualPage] = useState(false); + const [zoom, setZoom] = useState(1); // 1 = 100% + const pageRefs = useRef<(HTMLImageElement | null)[]>([]); + const scrollAreaRef = useRef(null); + const userInitiatedRef = useRef(false); + const suppressScrollRef = useRef(false); + + + // Listen for hash changes and update currentPage + useEffect(() => { + function handleHashChange() { + if (window.location.hash.startsWith("#page=")) { + const page = parseInt(window.location.hash.replace("#page=", ""), 10); + if (!isNaN(page) && page >= 1 && page <= numPages) { + setCurrentPage(page); + } + } + userInitiatedRef.current = false; + } + window.addEventListener("hashchange", handleHashChange); + handleHashChange(); // Run on mount + return () => window.removeEventListener("hashchange", handleHashChange); + }, [numPages]); + + // Scroll to the current page when it changes + useEffect(() => { + if (currentPage && pageRefs.current[currentPage - 1]) { + suppressScrollRef.current = true; + const el = pageRefs.current[currentPage - 1]; + el?.scrollIntoView({ behavior: "smooth", block: "center" }); + + // Try to use scrollend if supported + const viewport = scrollAreaRef.current; + let timeout: NodeJS.Timeout | null = null; + let scrollEndHandler: (() => void) | null = null; + + if (viewport && "onscrollend" in viewport) { + scrollEndHandler = () => { + suppressScrollRef.current = false; + viewport.removeEventListener("scrollend", scrollEndHandler!); + }; + viewport.addEventListener("scrollend", scrollEndHandler); + } else { + // Fallback for non-Chromium browsers + timeout = setTimeout(() => { + suppressScrollRef.current = false; + }, 1000); + } + + return () => { + if (viewport && scrollEndHandler) { + viewport.removeEventListener("scrollend", scrollEndHandler); + } + if (timeout) clearTimeout(timeout); + }; + } + }, [currentPage, pageImages]); + + // Detect visible page on scroll and update hash + const handleScroll = () => { + if (suppressScrollRef.current) return; + const scrollArea = scrollAreaRef.current; + if (!scrollArea || !pageRefs.current.length) return; + + const areaRect = scrollArea.getBoundingClientRect(); + let closestIdx = 0; + let minDist = Infinity; + + pageRefs.current.forEach((img, idx) => { + if (img) { + const imgRect = img.getBoundingClientRect(); + const dist = Math.abs(imgRect.top - areaRect.top); + if (dist < minDist) { + minDist = dist; + closestIdx = idx; + } + } + }); + + if (currentPage !== closestIdx + 1) { + setCurrentPage(closestIdx + 1); + if (window.location.hash !== `#page=${closestIdx + 1}`) { + window.location.hash = `#page=${closestIdx + 1}`; + } + } + }; useEffect(() => { let cancelled = false; @@ -49,12 +152,31 @@ const Viewer: React.FC = ({ pdfFile, setPdfFile }) => { return () => { cancelled = true; }; }, [pdfFile]); + useEffect(() => { + const viewport = scrollAreaRef.current; + if (!viewport) return; + const handler = () => { + handleScroll(); + }; + viewport.addEventListener("scroll", handler); + return () => viewport.removeEventListener("scroll", handler); + }, [pageImages]); + return ( - + {!pdfFile ? (
- No PDF loaded. Click to upload a PDF. + No PDF loaded. Click to upload a PDF.
) : ( - - + + {pageImages.length === 0 && ( No pages to display. )} - {pageImages.map((img, idx) => ( - {`Page ( + + { pageRefs.current[i * 2] = el; }} + src={pageImages[i * 2]} + alt={`Page ${i * 2 + 1}`} + style={{ + width: `${100 * zoom}%`, + maxWidth: 700 * zoom, + boxShadow: "0 2px 8px rgba(0,0,0,0.08)", + borderRadius: 8, + marginTop: i === 0 ? theme.spacing.xl : 0, // <-- add gap to first row + }} + /> + {pageImages[i * 2 + 1] && ( + { pageRefs.current[i * 2 + 1] = el; }} + src={pageImages[i * 2 + 1]} + alt={`Page ${i * 2 + 2}`} + style={{ + width: `${100 * zoom}%`, + maxWidth: 700 * zoom, + boxShadow: "0 2px 8px rgba(0,0,0,0.08)", + borderRadius: 8, + marginTop: i === 0 ? theme.spacing.xl : 0, // <-- add gap to first row + }} + /> + )} + + )) + : pageImages.map((img, idx) => ( + { pageRefs.current[idx] = el; }} + src={img} + alt={`Page ${idx + 1}`} + style={{ + width: `${100 * zoom}%`, + maxWidth: 700 * zoom, + boxShadow: "0 2px 8px rgba(0,0,0,0.08)", + borderRadius: 8, + marginTop: idx === 0 ? theme.spacing.xl : 0, // <-- add gap to first page + }} + /> + ))} + + {/* Navigation bar overlays the scroll area */} +
+ + + + { + const page = Number(value); + if (!isNaN(page) && page >= 1 && page <= numPages) { + window.location.hash = `#page=${page}`; + } + }} + min={1} + max={numPages} + hideControls + styles={{ + input: { width: 48, textAlign: "center", fontWeight: 500, fontSize: 16}, }} /> - ))} - + + / {numPages} + + + + + + + + {Math.round(zoom * 100)}% + + + +
)} - {pdfFile && ( - - - - )} +
); }; diff --git a/frontend/src/index.js b/frontend/src/index.js index 9915027bf..0735f589f 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -7,6 +7,7 @@ import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; + const root = ReactDOM.createRoot(document.getElementById('root')); // Finds the root DOM element root.render( diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index baebfbc46..a7487086b 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -6,7 +6,7 @@ import ZoomInMapIcon from "@mui/icons-material/ZoomInMap"; import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile"; import VisibilityIcon from "@mui/icons-material/Visibility"; import EditNoteIcon from "@mui/icons-material/EditNote"; -import { Group, SegmentedControl, Paper, Center, Box } from "@mantine/core"; +import { Group, SegmentedControl, Paper, Center, Box, Button, useMantineTheme, useMantineColorScheme } from "@mantine/core"; import ToolPicker from "../components/ToolPicker"; import FileManager from "../components/FileManager"; @@ -15,6 +15,8 @@ import CompressPdfPanel from "../tools/Compress"; import MergePdfPanel from "../tools/Merge"; import PageEditor from "../components/PageEditor"; import Viewer from "../components/Viewer"; +import DarkModeIcon from '@mui/icons-material/DarkMode'; +import LightModeIcon from '@mui/icons-material/LightMode'; type ToolRegistryEntry = { icon: React.ReactNode; @@ -60,8 +62,98 @@ const VIEW_OPTIONS = [ }, ]; +// Utility to extract params for a tool from searchParams +function getToolParams(toolKey: string, searchParams: URLSearchParams) { + switch (toolKey) { + case "split": + return { + mode: searchParams.get("splitMode") || "byPages", + pages: searchParams.get("pages") || "", + hDiv: searchParams.get("hDiv") || "0", + vDiv: searchParams.get("vDiv") || "1", + merge: searchParams.get("merge") === "true", + splitType: searchParams.get("splitType") || "size", + splitValue: searchParams.get("splitValue") || "", + bookmarkLevel: searchParams.get("bookmarkLevel") || "0", + includeMetadata: searchParams.get("includeMetadata") === "true", + allowDuplicates: searchParams.get("allowDuplicates") === "true", + }; + case "compress": + return { + level: searchParams.get("compressLevel") || "medium", + keepQuality: searchParams.get("keepQuality") === "true", + }; + case "merge": + return { + order: searchParams.get("mergeOrder") || "default", + removeDuplicates: searchParams.get("removeDuplicates") === "true", + }; + // Add more tools here as needed + default: + return {}; + } +} + +// Utility to update params for a tool +function updateToolParams(toolKey: string, searchParams: URLSearchParams, setSearchParams: any, newParams: any) { + const params = new URLSearchParams(searchParams); + + // Clear tool-specific params + if (toolKey === "split") { + [ + "splitMode", "pages", "hDiv", "vDiv", "merge", + "splitType", "splitValue", "bookmarkLevel", "includeMetadata", "allowDuplicates" + ].forEach((k) => params.delete(k)); + // Set new split params + const merged = { ...getToolParams("split", searchParams), ...newParams }; + params.set("splitMode", merged.mode); + if (merged.mode === "byPages") params.set("pages", merged.pages); + else if (merged.mode === "bySections") { + params.set("hDiv", merged.hDiv); + params.set("vDiv", merged.vDiv); + params.set("merge", String(merged.merge)); + } else if (merged.mode === "bySizeOrCount") { + params.set("splitType", merged.splitType); + params.set("splitValue", merged.splitValue); + } else if (merged.mode === "byChapters") { + params.set("bookmarkLevel", merged.bookmarkLevel); + params.set("includeMetadata", String(merged.includeMetadata)); + params.set("allowDuplicates", String(merged.allowDuplicates)); + } + } else if (toolKey === "compress") { + ["compressLevel", "keepQuality"].forEach((k) => params.delete(k)); + const merged = { ...getToolParams("compress", searchParams), ...newParams }; + params.set("compressLevel", merged.level); + params.set("keepQuality", String(merged.keepQuality)); + } else if (toolKey === "merge") { + ["mergeOrder", "removeDuplicates"].forEach((k) => params.delete(k)); + const merged = { ...getToolParams("merge", searchParams), ...newParams }; + params.set("mergeOrder", merged.order); + params.set("removeDuplicates", String(merged.removeDuplicates)); + } + // Add more tools as needed + + setSearchParams(params, { replace: true }); +} + +// List of all tool-specific params +const TOOL_PARAMS = { + split: [ + "splitMode", "pages", "hDiv", "vDiv", "merge", + "splitType", "splitValue", "bookmarkLevel", "includeMetadata", "allowDuplicates" + ], + compress: [ + "compressLevel", "keepQuality" + ], + merge: [ + "mergeOrder", "removeDuplicates" + ] + // Add more tools as needed +}; + export default function HomePage() { const [searchParams, setSearchParams] = useSearchParams(); + const theme = useMantineTheme(); // Core app state const [selectedToolKey, setSelectedToolKey] = useState(searchParams.get("tool") || "split"); @@ -69,28 +161,37 @@ export default function HomePage() { const [pdfFile, setPdfFile] = useState(null); const [files, setFiles] = useState([]); const [downloadUrl, setDownloadUrl] = useState(null); + const [sidebarsVisible, setSidebarsVisible] = useState(true); - // Tool-specific parameters - const [splitParams, setSplitParams] = useState({ - mode: searchParams.get("splitMode") || "byPages", - pages: searchParams.get("pages") || "", - hDiv: searchParams.get("hDiv") || "0", - vDiv: searchParams.get("vDiv") || "1", - merge: searchParams.get("merge") === "true", - splitType: searchParams.get("splitType") || "size", - splitValue: searchParams.get("splitValue") || "", - bookmarkLevel: searchParams.get("bookmarkLevel") || "0", - includeMetadata: searchParams.get("includeMetadata") === "true", - allowDuplicates: searchParams.get("allowDuplicates") === "true", - }); + const toolParams = getToolParams(selectedToolKey, searchParams); + + const updateParams = (newParams: any) => + updateToolParams(selectedToolKey, searchParams, setSearchParams, newParams); // Update URL when core state changes useEffect(() => { const params = new URLSearchParams(searchParams); - params.set("tool", selectedToolKey); - params.set("view", currentView); - setSearchParams(params, { replace: true }); - }, [selectedToolKey, currentView, setSearchParams]); + + // Remove all tool-specific params except for the current tool + Object.entries(TOOL_PARAMS).forEach(([tool, keys]) => { + if (tool !== selectedToolKey) { + keys.forEach((k) => params.delete(k)); + } + }); + + // Collect all params except 'view' + const entries = Array.from(params.entries()).filter(([key]) => key !== "view"); + + // Rebuild params with 'view' first + const newParams = new URLSearchParams(); + newParams.set("view", currentView); + newParams.set("tool", selectedToolKey); + entries.forEach(([key, value]) => { + if (key !== "tool") newParams.set(key, value); + }); + + setSearchParams(newParams, { replace: true }); + }, [selectedToolKey, currentView, setSearchParams, searchParams]); // Handle tool selection const handleToolSelect = useCallback( @@ -101,51 +202,6 @@ export default function HomePage() { [toolRegistry] ); - // Handle split parameter updates - const updateSplitParams = useCallback((newParams: Partial) => { - setSplitParams(prev => { - const updated = { ...prev, ...newParams }; - - // Update URL with split params - const params = new URLSearchParams(searchParams); - - // Clear old parameters when mode changes - if (newParams.mode && newParams.mode !== prev.mode) { - params.delete("pages"); - params.delete("hDiv"); - params.delete("vDiv"); - params.delete("merge"); - params.delete("splitType"); - params.delete("splitValue"); - params.delete("bookmarkLevel"); - params.delete("includeMetadata"); - params.delete("allowDuplicates"); - } - - // Set the mode - params.set("splitMode", updated.mode); - - // Set mode-specific parameters - if (updated.mode === "byPages" && updated.pages) { - params.set("pages", updated.pages); - } else if (updated.mode === "bySections") { - params.set("hDiv", updated.hDiv); - params.set("vDiv", updated.vDiv); - params.set("merge", String(updated.merge)); - } else if (updated.mode === "bySizeOrCount") { - params.set("splitType", updated.splitType); - if (updated.splitValue) params.set("splitValue", updated.splitValue); - } else if (updated.mode === "byChapters") { - params.set("bookmarkLevel", updated.bookmarkLevel); - params.set("includeMetadata", String(updated.includeMetadata)); - params.set("allowDuplicates", String(updated.allowDuplicates)); - } - - setSearchParams(params, { replace: true }); - return updated; - }); - }, [searchParams, setSearchParams]); - const selectedTool = toolRegistry[selectedToolKey]; // Tool component rendering @@ -154,130 +210,144 @@ export default function HomePage() { return
Tool not found
; } - // Pass appropriate props based on tool type - if (selectedToolKey === "split") { - return React.createElement(selectedTool.component, { - file: pdfFile, - setPdfFile, - downloadUrl, - setDownloadUrl, - // Tool-specific params and update function - params: splitParams, - updateParams: updateSplitParams - }); - } - - // For other tools, pass standard props + // Pass only the necessary props return React.createElement(selectedTool.component, { - file: pdfFile, - setPdfFile, files, - setFiles, - downloadUrl, setDownloadUrl, + params: toolParams, + updateParams, }); }; - return ( {/* Left: Tool Picker */} - + {sidebarsVisible && ( + + )} {/* Middle: Main View (Viewer, Editor, Manager) */} -
- - - -
- - {(currentView === "viewer" || currentView === "pageEditor") && !pdfFile ? ( - - ) : currentView === "viewer" ? ( - - ) : currentView === "pageEditor" ? ( - - ) : ( - - )} - + {/* Overlayed View Switcher */} +
+
+ +
+
+ {/* Main content area with matching Paper */} + + + {(currentView === "viewer" || currentView === "pageEditor") && !pdfFile ? ( + + ) : currentView === "viewer" ? ( + + ) : currentView === "pageEditor" ? ( + + ) : ( + + )} + +
{/* Right: Tool Interaction */} - + {selectedTool && selectedTool.component && ( + <> + {renderTool()} + + )} + + )} +
); } diff --git a/frontend/src/tools/Compress.tsx b/frontend/src/tools/Compress.tsx index 2701bb64a..c3897cb7a 100644 --- a/frontend/src/tools/Compress.tsx +++ b/frontend/src/tools/Compress.tsx @@ -1,4 +1,5 @@ import React, { useState } from "react"; +import { useSearchParams } from "react-router-dom"; import { Stack, Slider, Group, Text, Button, Checkbox, TextInput, Paper } from "@mantine/core"; export interface CompressProps { @@ -12,6 +13,9 @@ const CompressPdfPanel: React.FC = ({ setDownloadUrl, setLoading, }) => { + const [searchParams] = useSearchParams(); + + const [selected, setSelected] = useState(files.map(() => false)); const [compressionLevel, setCompressionLevel] = useState(5); const [grayscale, setGrayscale] = useState(false); @@ -56,8 +60,8 @@ const CompressPdfPanel: React.FC = ({ } }; + return ( - Select files to compress: @@ -118,7 +122,6 @@ const CompressPdfPanel: React.FC = ({ Compress Selected PDF{selected.filter(Boolean).length > 1 ? "s" : ""} - ); }; diff --git a/frontend/src/tools/Merge.tsx b/frontend/src/tools/Merge.tsx index 9cc969a0f..d0da12e47 100644 --- a/frontend/src/tools/Merge.tsx +++ b/frontend/src/tools/Merge.tsx @@ -1,12 +1,24 @@ import React, { useState, useEffect } from "react"; import { Paper, Button, Checkbox, Stack, Text, Group, Loader, Alert } from "@mantine/core"; +import { useSearchParams } from "react-router-dom"; export interface MergePdfPanelProps { files: File[]; setDownloadUrl: (url: string) => void; + params: { + order: string; + removeDuplicates: boolean; + }; + updateParams: (newParams: Partial) => void; } -const MergePdfPanel: React.FC = ({ files, setDownloadUrl }) => { +const MergePdfPanel: React.FC = ({ + files, + setDownloadUrl, + params, + updateParams, +}) => { + const [searchParams] = useSearchParams(); const [selectedFiles, setSelectedFiles] = useState([]); const [downloadUrl, setLocalDownloadUrl] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -59,8 +71,9 @@ const MergePdfPanel: React.FC = ({ files, setDownloadUrl }) const selectedCount = selectedFiles.filter(Boolean).length; + const { order, removeDuplicates } = params; + return ( - Merge PDFs @@ -104,8 +117,12 @@ const MergePdfPanel: React.FC = ({ files, setDownloadUrl }) Download Merged PDF )} + updateParams({ removeDuplicates: !removeDuplicates })} + /> - ); }; diff --git a/frontend/src/tools/Split.tsx b/frontend/src/tools/Split.tsx index 2e4d3a7c9..abac174c5 100644 --- a/frontend/src/tools/Split.tsx +++ b/frontend/src/tools/Split.tsx @@ -7,7 +7,9 @@ import { Checkbox, Notification, Stack, + Paper, } from "@mantine/core"; +import { useSearchParams } from "react-router-dom"; import DownloadIcon from "@mui/icons-material/Download"; export interface SplitPdfPanelProps { @@ -26,7 +28,7 @@ export interface SplitPdfPanelProps { includeMetadata: boolean; allowDuplicates: boolean; }; - updateParams: (newParams: Partial) => void; + updateParams: (newParams: Partial) => void; } const SplitPdfPanel: React.FC = ({ @@ -36,16 +38,26 @@ const SplitPdfPanel: React.FC = ({ params, updateParams, }) => { + const [searchParams] = useSearchParams(); + const [status, setStatus] = useState(""); const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const { - mode, pages, hDiv, vDiv, merge, - splitType, splitValue, bookmarkLevel, - includeMetadata, allowDuplicates + mode, + pages, + hDiv, + vDiv, + merge, + splitType, + splitValue, + bookmarkLevel, + includeMetadata, + allowDuplicates, } = params; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!file) { @@ -109,123 +121,123 @@ const SplitPdfPanel: React.FC = ({ }; return ( -
- - v && updateParams({ mode: v })} + data={[ + { value: "byPages", label: "Split by Pages (e.g. 1,3,5-10)" }, + { value: "bySections", label: "Split by Grid Sections" }, + { value: "bySizeOrCount", label: "Split by Size or Count" }, + { value: "byChapters", label: "Split by Chapters" }, + ]} /> - )} - {mode === "bySections" && ( - + {mode === "byPages" && ( updateParams({ hDiv: e.target.value })} + label="Pages" + placeholder="e.g. 1,3,5-10" + value={pages} + onChange={(e) => updateParams({ pages: e.target.value })} /> - updateParams({ vDiv: e.target.value })} - /> - updateParams({ merge: e.currentTarget.checked })} - /> - - )} + )} - {mode === "bySizeOrCount" && ( - - v && updateParams({ splitType: v })} + data={[ + { value: "size", label: "By Size" }, + { value: "pages", label: "By Page Count" }, + { value: "docs", label: "By Document Count" }, + ]} + /> + updateParams({ splitValue: e.target.value })} + /> + + )} - + {mode === "byChapters" && ( + + updateParams({ bookmarkLevel: e.target.value })} + /> + updateParams({ includeMetadata: e.currentTarget.checked })} + /> + updateParams({ allowDuplicates: e.currentTarget.checked })} + /> + + )} - {status &&

{status}

} - - {errorMessage && ( - setErrorMessage(null)}> - {errorMessage} - - )} - - {status === "Download ready." && downloadUrl && ( - - )} -
-
+ + {status &&

{status}

} + + {errorMessage && ( + setErrorMessage(null)}> + {errorMessage} + + )} + + {status === "Download ready." && downloadUrl && ( + + )} + + ); };