From f6a10e998f118d3f528054ae022473c4668bd067 Mon Sep 17 00:00:00 2001 From: Dinoosauro <80783030+Dinoosauro@users.noreply.github.com> Date: Wed, 29 May 2024 17:25:48 +0200 Subject: [PATCH] Bug fixes & code improvements (#11) - React "refs" are widely used - Fixed bugs in keyboard shortcuts - Fixed a bug in the thumbnail viewer --- index.html | 2 +- public/pdfpointer-updatecode | 2 +- src/App.tsx | 8 +- src/Components/CustomDisplay.tsx | 16 +- src/Components/DropdownItem.tsx | 6 +- src/Components/ExportDialog.tsx | 50 +++--- src/Components/GetThumbnail.tsx | 37 +++-- src/Components/PdfUI.tsx | 228 +++++++++++++------------- src/Components/Toolbar.tsx | 5 +- src/SettingTabs/KeyboardShortcuts.tsx | 19 ++- src/index.css | 1 + 11 files changed, 196 insertions(+), 178 deletions(-) diff --git a/index.html b/index.html index fec05fd..4c69236 100644 --- a/index.html +++ b/index.html @@ -15,7 +15,7 @@ }; registerServiceWorker(); } - let appVersion = "2.1.0"; + let appVersion = "2.1.1"; fetch("./pdfpointer-updatecode", { cache: "no-store" }).then((res) => res.text().then((text) => { if (text.replace("\n", "") !== appVersion) if (confirm(`There's a new version of pdf-pointer. Do you want to update? [${appVersion} --> ${text.replace("\n", "")}]`)) { caches.delete("pdfpointer-cache"); location.reload(true); } }).catch((e) => { console.error(e) })).catch((e) => console.error(e)); // Check if the application code is the same as the current application version and, if not, ask the user to update diff --git a/public/pdfpointer-updatecode b/public/pdfpointer-updatecode index 50aea0e..7c32728 100644 --- a/public/pdfpointer-updatecode +++ b/public/pdfpointer-updatecode @@ -1 +1 @@ -2.1.0 \ No newline at end of file +2.1.1 \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 9c0c208..fcf5b65 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,8 +23,8 @@ declare global { heic2any: (e: any) => Promise } } -let installationPrompt: any; export default function App() { + let installationPrompt = useRef(); let [CurrentState, UpdateState] = useState({}); let cardContainer = useRef(null); useEffect(() => { @@ -40,7 +40,7 @@ export default function App() { } window.addEventListener('beforeinstallprompt', (event) => { // Capture the request to install the PWA so that it can be displayed when the button is clicked event.preventDefault(); - installationPrompt = event; + installationPrompt.current = event; }); }, []) async function getNewState(file: File) { @@ -106,8 +106,8 @@ export default function App() { {Lang("Install PDFPointer as an app for offline use and better integration with the OS.")}



diff --git a/src/Components/CustomDisplay.tsx b/src/Components/CustomDisplay.tsx index 9c21fbe..b7d8e28 100644 --- a/src/Components/CustomDisplay.tsx +++ b/src/Components/CustomDisplay.tsx @@ -7,8 +7,6 @@ import AlertManager from "../Scripts/AlertManager"; interface Props { category: "Color" | "Theme" // "color" for the custom color; "theme" for the custom theme } -let typedName = ""; // Custom color name -let typedColor = ""; // Custom color value interface SaveFilePicker { id?: string, suggestedName?: string, @@ -28,6 +26,14 @@ declare global { * @returns the CustomDisplay ReactNode */ export default function CustomDisplay({ category }: Props) { + /** + * Custom color name + */ + let typedName = useRef(""); + /** + * Custom color value + */ + let typedColor = useRef(""); // Get the current theme values so that the color list can be updated const themeValue = [{ ref: "background", desc: Lang("Background color"), value: getComputedStyle(document.body).getPropertyValue("--background") }, { ref: "text", desc: Lang("Text color"), value: getComputedStyle(document.body).getPropertyValue("--text") }, @@ -85,11 +91,11 @@ export default function CustomDisplay({ category }: Props) { return <>
{category === "Color" ? <> - typedColor = e.currentTarget.value}> - typedName = e.currentTarget.value} type="text" placeholder={Lang("Write the color name")}> + typedColor.current = e.currentTarget.value}> + typedName.current = e.currentTarget.value} type="text" placeholder={Lang("Write the color name")}> diff --git a/src/Components/DropdownItem.tsx b/src/Components/DropdownItem.tsx index 2bc9ced..5091ef1 100644 --- a/src/Components/DropdownItem.tsx +++ b/src/Components/DropdownItem.tsx @@ -11,7 +11,6 @@ interface Props { backdropColor?: boolean, defaultValueRef?: keyof DrawingStoredOptions } -let clientX = 0; /** * * @param children the content that'll be always shown, @@ -28,6 +27,7 @@ export default function DropdownItem({ children, content, title, disableAutoDisa /** * Hide the dropdown content with an opacity transition */ + let clientX = useRef(0); async function disappear() { if (ref) { if (ref.current) ref.current.style.opacity = "0"; @@ -49,10 +49,10 @@ export default function DropdownItem({ children, content, title, disableAutoDisa return <> { // Show (or hide) the dropdown item (the button is clicked) - clientX = e.clientX; + clientX.current = e.clientX; UpdateDropdown(!ShowDropdown) }}>{children} - {ShowDropdown &&
window.innerWidth ? `${window.innerWidth - clientX - 10}px` : undefined }} className="dropdownOpen opacity" ref={ref}>

{title}

{ + {ShowDropdown &&
window.innerWidth ? `${window.innerWidth - clientX.current - 10}px` : undefined }} className="dropdownOpen opacity" ref={ref}>

{title}

{ if (!disableAutoDisappear) await disappear(); }}>
diff --git a/src/Components/ExportDialog.tsx b/src/Components/ExportDialog.tsx index bc7aa9a..1bb08db 100644 --- a/src/Components/ExportDialog.tsx +++ b/src/Components/ExportDialog.tsx @@ -3,19 +3,6 @@ import Lang from "../Scripts/LanguageTranslations"; import ImageExport from "../Scripts/Export"; import * as PDFJS from "pdfjs-dist"; -/** - * The custom values for the PDF exportation as image - */ -let exportValue = { - img: "png", - pages: "", - annotations: true, - scale: 2, - zip: false, - quality: 0.85, - filter: "" -} - interface Props { pdfObj?: PDFJS.PDFDocumentProxy, imgObj?: HTMLImageElement @@ -28,9 +15,20 @@ interface Props { */ export default function ExportDialog({ pdfObj, imgObj }: Props) { let exportButton = useRef(null); - + /** + * The custom values for the PDF exportation as image + */ + let exportValue = useRef({ + img: "png", + pages: "", + annotations: true, + scale: 2, + zip: false, + quality: 0.85, + filter: "" + }) return
-


{document.createElement("canvas").getContext("2d")?.filter !== undefined && <>
- { + { let getFilterCanvas = Array.from(document.querySelectorAll("canvas")).filter(e => e.style.filter !== ""); - if ((e.target as HTMLInputElement).checked && getFilterCanvas.length !== 0) { exportValue.filter = getFilterCanvas[0].style.filter; } else exportValue.filter = "" + if ((e.target as HTMLInputElement).checked && getFilterCanvas.length !== 0) { exportValue.current.filter = getFilterCanvas[0].style.filter; } else exportValue.current.filter = "" }}>


}
- exportValue.zip = (e.target as HTMLInputElement).checked}>




+ exportValue.current.zip = (e.target as HTMLInputElement).checked}>




} \ No newline at end of file diff --git a/src/Components/GetThumbnail.tsx b/src/Components/GetThumbnail.tsx index bba6b48..fb24134 100644 --- a/src/Components/GetThumbnail.tsx +++ b/src/Components/GetThumbnail.tsx @@ -2,10 +2,11 @@ import * as PDFJS from "pdfjs-dist"; import { useEffect, useRef, useState } from "react"; import ThumbnailContainer from "./ThumbnailContainer"; import { DynamicImg } from "./DynamicImg"; +import { PdfUIState } from "../Interfaces/CustomOptions"; +import RerenderButtons from "../Scripts/RerenderButtons"; interface Props { PDFObj: PDFJS.PDFDocumentProxy, // The PDF Object from PDF.JS library - pageListener: (e: number) => void, // The event that'll be called for changing PDF page - closeEvent: () => void + PDFState: React.Dispatch> } interface CanvasContainer { dom: HTMLCanvasElement | null, @@ -13,31 +14,31 @@ interface CanvasContainer { viewport?: { height: number, width: number }, } -let canvasContainer: CanvasContainer[] = []; /** * Render all the PDF pages for the thumbnail * @param PDFObj the PDF.JS object - * @param pageListener the function to call to move to a specific page - * @param closeEvent the event to call to close the thumbnail + * @param PDFState the callback to update the PDF UI * @returns the main thumbnail ReactNode */ -export default function Thumbnail({ PDFObj, pageListener, closeEvent }: Props) { +export default function Thumbnail({ PDFObj, PDFState }: Props) { let [pages, updatePages] = useState({ current: 0, // The next thumbnail to render nextSuggested: true // If it's time to render more pages (–> if the user has scrolled a lot of the div) }); + let canvasContainer = useRef([]); + console.log(PDFState); useEffect(() => { if (!pages.nextSuggested) return; // Nothing more to render (async () => { updatePages(prevState => { return { ...prevState, nextSuggested: false } }); // Immediately stop re-renders when moving the page for (let i = pages.current; i < (PDFObj.numPages > pages.current + 5 ? pages.current + 5 : PDFObj.numPages); i++) { // i is the number of the page (- 1, since pages start with 1). The next 5 pages, or the remaining ones, will be re-rendered - if (canvasContainer.findIndex(e => e.page === i) !== -1) continue; // The page has already been rendered. Don't re-render it. - canvasContainer.push({ dom: null, page: i }); // Create a placeholder in the canvasContainer array, so that it's certain that the page will be rendered only one time + if (canvasContainer.current.findIndex(e => e.page === i) !== -1) continue; // The page has already been rendered. Don't re-render it. + canvasContainer.current.push({ dom: null, page: i }); // Create a placeholder in the canvasContainer array, so that it's certain that the page will be rendered only one time // Create a canvas with the PDF image const page = await PDFObj.getPage(i + 1); const viewport = page.getViewport({ scale: 0.5 }); // @ts-ignore - canvasContainer.find(e => e.page === i).viewport = viewport; + canvasContainer.current.find(e => e.page === i).viewport = viewport; const outputScale = window.devicePixelRatio || 1; const canvas = document.createElement("canvas"); canvas.classList.add("simplePointer"); // Cursor: pointer event when hovered @@ -45,8 +46,9 @@ export default function Thumbnail({ PDFObj, pageListener, closeEvent }: Props) { canvas.height = Math.floor(viewport.height * outputScale); canvas.style.width = `${document.documentElement.offsetWidth * 25 / 100}px`; canvas.style.height = `${viewport.height * (document.documentElement.offsetWidth * 25 / 100) / viewport.width}px`; - canvas.onclick = () => { // Move to the clicked page - pageListener(i); + canvas.onclick = () => { // Move to the clicked page, and close the thumbnail viewer + RerenderButtons.update("thumbnail", false); + PDFState(prevState => { return { ...prevState, page: i + 1 } }); } let ctx = canvas.getContext("2d"); if (ctx !== null) { @@ -57,7 +59,7 @@ export default function Thumbnail({ PDFObj, pageListener, closeEvent }: Props) { }); await render.promise; // @ts-ignore - canvasContainer.find(e => e.page === i).dom = canvas; // Update the value of the canvasContainer with the linked canvas; + canvasContainer.current.find(e => e.page === i).dom = canvas; // Update the value of the canvasContainer with the linked canvas; updatePages({ current: i + 1, nextSuggested: false }); // Remember the next page to render } } @@ -65,7 +67,7 @@ export default function Thumbnail({ PDFObj, pageListener, closeEvent }: Props) { }, [pages.nextSuggested]) useEffect(() => { window.addEventListener("resize", () => { // When resizing the window, also these canvas must be resized. The width is always 25vw, while the height is calculated with a proportion. - for (let item of canvasContainer) { + for (let item of canvasContainer.current) { if (item.dom && item.viewport) { item.dom.style.width = `${document.documentElement.offsetWidth * 25 / 100}px`; item.dom.style.height = `${item.viewport.height * (document.documentElement.offsetWidth * 25 / 100) / item.viewport.width}px`; @@ -79,9 +81,12 @@ export default function Thumbnail({ PDFObj, pageListener, closeEvent }: Props) { let percentage = Math.round((container.scrollTop / (container.scrollHeight - container.offsetHeight)) * 100); // Get the percentage of scroll if (percentage > 80) updatePages(prevState => { return { ...prevState, nextSuggested: true } }) }}> -
+
console.log("A")} style={{ position: "sticky", top: "15px", left: "25px", padding: "10px", backgroundColor: "var(--firststruct)", borderRadius: "8px", width: "24px", height: "24px" }} className="simplePointer" onClick={() => { + RerenderButtons.update("thumbnail", false); + PDFState(prevState => { return { ...prevState, showThumbnail: 2 } }) + }}>
- {canvasContainer.filter(e => e.dom !== null).map(e => )} -
+ {canvasContainer.current.filter(e => e.dom !== null).map(e => )} +
} \ No newline at end of file diff --git a/src/Components/PdfUI.tsx b/src/Components/PdfUI.tsx index 90bdfa3..b926348 100644 --- a/src/Components/PdfUI.tsx +++ b/src/Components/PdfUI.tsx @@ -29,65 +29,6 @@ interface MouseMove { clientX: number, clientY: number } -/** - * If true, a PDF operation is in progress, and therefore further requests must be dropped - */ -let isCanvasWorking = false; -/** - * The initial zIndex for the canvas - */ -let zIndex = 2; -/** - * A string that'll contain details about the PDF page and zoom. This will be compared so that the app will know if it's time to re-render the canvas or not. - */ -let currentPdfShow = ""; -let userDrawingOptionsManager: DrawingStoredOptions = { - timer: 15000, - size: 5, - cursorColor: getComputedStyle(document.body).getPropertyValue("--accent"), - cursorSize: 40, - penColor: getComputedStyle(document.body).getPropertyValue("--accent"), - textSize: 25, - textFont: "Work Sans", - textLineSpace: 1.2, - textStrikeLineWidth: 4, - penOpacity: 1 -} -let tempUserDrawingOptions = { - isBold: false, - isItalic: false, - isUnderlined: false, - isStriked: false, - negativeFilter: 0, - hueInversionFilter: 0, - sepiaFilter: 0, - grayscaleFilter: 0 -} -/** - * The keyboard items that the user has pressed, but not released - */ -let userBtnPressed: string[] = []; -try { // Update the persistent drawing options - let parse = JSON.parse(localStorage.getItem("PDFPointer-UserAnnotationSettings") ?? ""); - // @ts-ignore - for (let key in parse) if (userDrawingOptionsManager[key] !== undefined && typeof userDrawingOptionsManager[key] === typeof parse[key]) userDrawingOptionsManager[key] = parse[key]; // Keep only the key that are of the same type -} catch (ex) { - console.warn({ - type: "FirstUser", - desc: "No previous drawing options found", - gravity: -1, - ex: ex - }) -} -let customModes = { - isEraserEnabled: false, - isPenEnabled: false, - isTextEnabled: false, - isTextInCreation: false -} -function getTextAttributes() { // A function that will return the styles of the text tool, so that they must not be added each time. - return { color: userDrawingOptionsManager.penColor, size: userDrawingOptionsManager.textSize, font: userDrawingOptionsManager.textFont, style: { lineSpacing: userDrawingOptionsManager.textLineSpace, ...tempUserDrawingOptions, lineHeight: userDrawingOptionsManager.textStrikeLineWidth } } -} /** * The PDF main UI * @param pdfObj the PDF.JS Object @@ -104,8 +45,74 @@ export default function PDF({ pdfObj, imgObj }: Props) { */ let canvasRef = useRef({ mainCanvas: null, centerDiv: null, hoverCanvas: null, toolbar: null, circleCanvas: null, thumbnailDiv: null }); let [pageSettings, updatePage] = useState({ page: 1, scale: 1, showThumbnail: 0, isFullscreenChange: document.fullscreenElement, langUpdate: 0, requestedTabPart: "hello" }); + let customModes = useRef({ + isEraserEnabled: false, + isPenEnabled: false, + isTextEnabled: false, + isTextInCreation: false + }); + /** + * If true, a PDF operation is in progress, and therefore further requests must be dropped + */ + let isCanvasWorking = useRef(false); + /** + * The initial zIndex for the canvas + */ + let zIndex = useRef(2); + /** + * A string that'll contain details about the PDF page and zoom. This will be compared so that the app will know if it's time to re-render the canvas or not. + */ + let currentPdfShow = useRef(""); + let userDrawingOptionsManager = useRef((() => { + let obj: DrawingStoredOptions = { + timer: 15000, + size: 5, + cursorColor: getComputedStyle(document.body).getPropertyValue("--accent"), + cursorSize: 40, + penColor: getComputedStyle(document.body).getPropertyValue("--accent"), + textSize: 25, + textFont: "Work Sans", + textLineSpace: 1.2, + textStrikeLineWidth: 4, + penOpacity: 1 + + } + try { + let parse = JSON.parse(localStorage.getItem("PDFPointer-UserAnnotationSettings") ?? ""); // Update the persistent drawing options + // @ts-ignore + for (let key in parse) if (obj[key] !== undefined && typeof obj[key] === typeof parse[key]) obj[key] = parse[key]; // Keep only the key that are of the same type + } catch (ex) { + console.warn({ + type: "FirstUser", + desc: "No previous drawing options found", + gravity: -1, + ex: ex + }) + } + + return obj; + })()) + let tempUserDrawingOptions = useRef({ + isBold: false, + isItalic: false, + isUnderlined: false, + isStriked: false, + negativeFilter: 0, + hueInversionFilter: 0, + sepiaFilter: 0, + grayscaleFilter: 0 + + }) + /** + * The keyboard items that the user has pressed, but not released + */ + let userBtnPressed = useRef([]); + function getTextAttributes() { // A function that will return the styles of the text tool, so that they must not be added each time. + return { color: userDrawingOptionsManager.current.penColor, size: userDrawingOptionsManager.current.textSize, font: userDrawingOptionsManager.current.textFont, style: { lineSpacing: userDrawingOptionsManager.current.textLineSpace, ...tempUserDrawingOptions.current, lineHeight: userDrawingOptionsManager.current.textStrikeLineWidth } } + } + useEffect(() => { - if (isCanvasWorking || currentPdfShow === `${pageSettings.page}-${pageSettings.scale}-${pageSettings.isFullscreenChange}`) return; + if (isCanvasWorking.current || currentPdfShow.current === `${pageSettings.page}-${pageSettings.scale}-${pageSettings.isFullscreenChange}`) return; // Create a spinner for loading info const div = document.createElement("div"); createRoot(div).render(
window.innerHeight ? "30vh" : "30vw", height: window.innerWidth > window.innerHeight ? "30vh" : "30vw" }}>
) @@ -157,7 +164,7 @@ export default function PDF({ pdfObj, imgObj }: Props) { div.remove(); return; } - isCanvasWorking = true; // Avoid multiple PDF operations + isCanvasWorking.current = true; // Avoid multiple PDF operations // Render the PDF const pdfPage = await pdfObj.getPage(pageSettings.page); const viewport = pdfPage.getViewport({ scale: getScale(pdfPage.getViewport({ scale: 1 }).height) }); // If it's in fullscreen mode, the 100% zoom must fill the window height @@ -171,8 +178,8 @@ export default function PDF({ pdfObj, imgObj }: Props) { transform: outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : undefined }); await render.promise; - currentPdfShow = `${pageSettings.page}-${pageSettings.scale}-${pageSettings.isFullscreenChange}`; - isCanvasWorking = false; + currentPdfShow.current = `${pageSettings.page}-${pageSettings.scale}-${pageSettings.isFullscreenChange}`; + isCanvasWorking.current = false; } } div.remove(); @@ -208,7 +215,7 @@ export default function PDF({ pdfObj, imgObj }: Props) { }) function stopCanvasEditing() { // Stop mouse annotations, and start the erase timer mouseDown = false; - if (currentCanvas !== undefined && !customModes.isEraserEnabled && !customModes.isTextEnabled) Annotations.end({ canvas: currentCanvas, disappear: userDrawingOptionsManager.timer }) + if (currentCanvas !== undefined && !customModes.current.isEraserEnabled && !customModes.current.isTextEnabled) Annotations.end({ canvas: currentCanvas, disappear: userDrawingOptionsManager.current.timer }) } useEffect((() => { let getContainer = document.querySelector(".thumbnailContainer"); @@ -221,20 +228,16 @@ export default function PDF({ pdfObj, imgObj }: Props) { * Create a new SVG element (that was previously a canvas. I know, lots of things here needs to be renamed.) */ function createCanvas() { - if (isCanvasWorking || customModes.isEraserEnabled) return; - if (canvasRef.current.mainCanvas !== null) currentCanvas = Annotations.adapt(Annotations.create({ zIndex: zIndex, page: pageSettings.page }), canvasRef.current.mainCanvas) as HTMLOrSVGImageElement; // Create a new SVG element, and make it the same width/height as of the current canvas - zIndex++; + if (isCanvasWorking.current || customModes.current.isEraserEnabled) return; + if (canvasRef.current.mainCanvas !== null) currentCanvas = Annotations.adapt(Annotations.create({ zIndex: zIndex.current, page: pageSettings.page }), canvasRef.current.mainCanvas) as HTMLOrSVGImageElement; // Create a new SVG element, and make it the same width/height as of the current canvas + zIndex.current++; if (canvasRef.current.centerDiv !== null && currentCanvas !== undefined) canvasRef.current.centerDiv.append(currentCanvas); } /** * Send to the Annotation script a request to change the style of the text (that'll be automatically fetched) */ function triggerTextChange() { - if (customModes.isTextInCreation && currentCanvas) Annotations.write({ canvas: currentCanvas, ...getTextAttributes() }); - } - if (pageSettings.showThumbnail !== 0 && !document.fullscreenElement) { // Fixes for WebKit: append the container to the body, and then, when it must be deleted, append it to its previous place (the thumbnailDiv) - let container = document.querySelector(".thumbnailContainer"); - if (container && canvasRef.current.thumbnailDiv) pageSettings.showThumbnail !== 2 ? document.body.append(container) : canvasRef.current.thumbnailDiv.append(container); + if (customModes.current.isTextInCreation && currentCanvas) Annotations.write({ canvas: currentCanvas, ...getTextAttributes() }); } /** * The event triggered when the mouse is moved, so that it can be called also for touch events @@ -245,10 +248,10 @@ export default function PDF({ pdfObj, imgObj }: Props) { mouseDown = [(e.clientX - (currentCanvas ?? target).getBoundingClientRect().left) * (window.devicePixelRatio || 1), (e.clientY - (currentCanvas ?? target).getBoundingClientRect().top) * (window.devicePixelRatio || 1)]; // Calculate the relative X and Y positions let targetDown = [(e.clientX - target.getBoundingClientRect().left) * (window.devicePixelRatio || 1), (e.clientY - target.getBoundingClientRect().top) * (window.devicePixelRatio || 1)]; // Used for the eraser if (canvasRef.current.circleCanvas) { // Move the circle div - canvasRef.current.circleCanvas.style.top = `${e.clientY - (userDrawingOptionsManager.cursorSize / 2)}px`; - canvasRef.current.circleCanvas.style.left = `${e.clientX - (userDrawingOptionsManager.cursorSize / 2)}px`; + canvasRef.current.circleCanvas.style.top = `${e.clientY - (userDrawingOptionsManager.current.cursorSize / 2)}px`; + canvasRef.current.circleCanvas.style.left = `${e.clientX - (userDrawingOptionsManager.current.cursorSize / 2)}px`; } - if (customModes.isEraserEnabled && mouseDown) { // Look for items to delete + if (customModes.current.isEraserEnabled && mouseDown) { // Look for items to delete for (let svg of document.querySelectorAll(".hoverCanvas:not([data-noresize])")) { // Basically, the data-x and data-y attributes are calculated by [real canvas width or height / 10]. In this way, it does not require real precision if (svg.querySelector(`[data-x='${Math.floor(targetDown[0] / (window.devicePixelRatio || 1) / 10 * (parseInt(svg.getAttribute("width") ?? "1") / parseInt(canvasRef.current.mainCanvas?.style.width.replace("px", "") ?? "1")))}']`) !== null && svg.querySelector(`[data-y='${Math.floor(targetDown[1] / (window.devicePixelRatio || 1) / 10 * (parseInt(svg.getAttribute("width") ?? "1") / parseInt(canvasRef.current.mainCanvas?.style.width.replace("px", "") ?? "1")))}']`) !== null) { @@ -261,14 +264,14 @@ export default function PDF({ pdfObj, imgObj }: Props) { } return; } - if (!mouseDown || currentCanvas === undefined || !customModes.isPenEnabled || customModes.isTextEnabled) return; // No appropriate operations are being done - Annotations.move({ canvas: currentCanvas, move: mouseDown, size: userDrawingOptionsManager.size, color: userDrawingOptionsManager.penColor, opacity: userDrawingOptionsManager.penOpacity }); + if (!mouseDown || currentCanvas === undefined || !customModes.current.isPenEnabled || customModes.current.isTextEnabled) return; // No appropriate operations are being done + Annotations.move({ canvas: currentCanvas, move: mouseDown, size: userDrawingOptionsManager.current.size, color: userDrawingOptionsManager.current.penColor, opacity: userDrawingOptionsManager.current.penOpacity }); } function onMouseDown() { mouseDown = true; - if (!customModes.isTextEnabled) createCanvas(); else if (!customModes.isTextInCreation) { // If another text is not being created, create a new one - customModes.isTextInCreation = true; + if (!customModes.current.isTextEnabled) createCanvas(); else if (!customModes.current.isTextInCreation) { // If another text is not being created, create a new one + customModes.current.isTextInCreation = true; AlertManager.simpleDelete().then(() => { // Delete other alerts createCanvas(); // Render the AlertTextDom @@ -277,14 +280,14 @@ export default function PDF({ pdfObj, imgObj }: Props) { createRoot(createText).render( { if (currentCanvas) { Annotations.write({ canvas: currentCanvas, final: true, ...getTextAttributes() }); - if (currentCanvas) Annotations.end({ canvas: currentCanvas, disappear: userDrawingOptionsManager.timer }); - customModes.isTextInCreation = false; + if (currentCanvas) Annotations.end({ canvas: currentCanvas, disappear: userDrawingOptionsManager.current.timer }); + customModes.current.isTextInCreation = false; } AlertManager.simpleDelete(); }} remove={() => { if (currentCanvas) Annotations.end({ canvas: currentCanvas, disappear: 1 }); AlertManager.simpleDelete(); - customModes.isTextInCreation = false; + customModes.current.isTextInCreation = false; }} update={(e) => { if (currentCanvas) Annotations.write({ canvas: currentCanvas, text: e, ...getTextAttributes() }) }}>); setTimeout(() => createText.style.opacity = "1", 25); @@ -294,15 +297,16 @@ export default function PDF({ pdfObj, imgObj }: Props) { } useEffect(() => { window.onkeyup = (e) => { - userBtnPressed.indexOf(e.key.toLowerCase()) !== -1 && userBtnPressed.splice(userBtnPressed.indexOf(e.key.toLowerCase()), 1); + while (userBtnPressed.current.indexOf(e.key.toLowerCase()) !== -1) userBtnPressed.current.splice(userBtnPressed.current.indexOf(e.key.toLowerCase()), 1); // If the user keeps pressed a key for too long, multiple "onkeydown" events will be triggered. In this way, when the user releases the key, every instance of that key is deleted. } window.onkeydown = (e) => { // Add keyboard shortcuts + e.preventDefault(); if (document.activeElement?.tagName.toLowerCase() === "textarea" || document.activeElement?.tagName.toLowerCase() === "input" || document.activeElement?.tagName.toLowerCase() === "select" || document.activeElement?.getAttribute("data-nokeyboard") === "a") return; // Avoid processing keyboard shortcuts if the user is writing something const KeyPreference = JSON.parse(localStorage.getItem("PDFPointer-KeyboardPreferences") ?? "{}") as KeyPreference; - userBtnPressed.push(e.key.toLowerCase()); + userBtnPressed.current.push(e.key.toLowerCase()); lookFunction: for (const tempAction in KeyPreference) { const action = tempAction as keyof KeyPreference; - for (let item of (KeyPreference[action] ?? [])) if (userBtnPressed.indexOf(item) === -1) continue lookFunction; + for (let item of (KeyPreference[action] ?? [])) if (userBtnPressed.current.indexOf(item) === -1) continue lookFunction; switch (action) { case "fullscreen": { CircularButtonsFunctions.fullscreen(); @@ -313,11 +317,11 @@ export default function PDF({ pdfObj, imgObj }: Props) { return; } case "stop": { // Stop everything is being done. Useful if this """flawless""" logic fails [and I wouldn't be surprised if it does] - customModes.isEraserEnabled = false; - customModes.isPenEnabled = false; - customModes.isTextEnabled = false; + customModes.current.isEraserEnabled = false; + customModes.current.isPenEnabled = false; + customModes.current.isTextEnabled = false; updatePage(prevState => { return { ...prevState, requestedTabPart: `hello,${Date.now()}` } }); // Update the requestedTabPart so that the Toolbar tab will be set to the default one. A date is added to force the refresh. - for (let item of ["text", "pen", "erase"]) RerenderButtons.update(item, customModes[`is${item === "text" ? "Text" : item === "pen" ? "Pen" : "Eraser"}Enabled`]); // Update all the circular buttons that have a shortcut + for (let item of ["text", "pen", "erase"]) RerenderButtons.update(item, customModes.current[`is${item === "text" ? "Text" : item === "pen" ? "Pen" : "Eraser"}Enabled`]); // Update all the circular buttons that have a shortcut return; } case "export": { @@ -334,16 +338,16 @@ export default function PDF({ pdfObj, imgObj }: Props) { let currentThumbnail = 0; currentThumbnail = action === "thumbnail" ? (prevState.showThumbnail === 1 ? 2 : 1) : prevState.showThumbnail; setTimeout(() => action === "thumbnail" && RerenderButtons.update("thumbnail", currentThumbnail === 1), 150); // Avoid errors for rendering two states at the same time - return { ...prevState, page: action === "prevpage" && prevState.page !== 1 ? prevState.page -= 1 : action === "nextpage" && (pdfObj?.numPages ?? -1) > prevState.page ? prevState.page += 1 : prevState.page, scale: action === "zoomin" ? prevState.scale *= 1.2 : action === "zoomout" ? prevState.scale /= 1.2 : prevState.scale, requestedTabPart: action === "pointer" ? `circle,${Date.now()}` : action === "pen" || action === "text" ? `${action},${Date.now()}` : prevState.requestedTabPart, showThumbnail: currentThumbnail } + return { ...prevState, page: action === "prevpage" && prevState.page !== 1 ? prevState.page -= 1 : action === "nextpage" && (pdfObj?.numPages ?? -1) > prevState.page ? prevState.page += 1 : prevState.page, scale: action === "zoomin" ? prevState.scale *= 1.2 : action === "zoomout" ? prevState.scale /= 1.2 : prevState.scale, requestedTabPart: prevState.requestedTabPart === "hello" ? action === "pointer" ? `circle,${Date.now()}` : action === "pen" || action === "text" ? `${action},${Date.now()}` : prevState.requestedTabPart : "hello", showThumbnail: currentThumbnail } }); if (action === "text" || action === "pointer" || action === "pen" || action === "erase") { - if ((action === "text" || action === "pointer")) customModes.isPenEnabled = false; // Disable pen mode for pointer and text editing - if (action === "pen" || action === "pointer") customModes.isTextEnabled = false; // Disable text prompt for pointer and pen editing - if (action === "pen") customModes.isPenEnabled = !customModes.isPenEnabled; // Toggle pen editing - if (action === "text") customModes.isTextEnabled = !customModes.isTextEnabled; // Toggle text editing - customModes.isEraserEnabled = action === "erase" ? !customModes.isEraserEnabled : false; // Toggle eraser if that action is selected, otherwise disable it - RerenderButtons.update("erase", customModes.isEraserEnabled); // Update the eraser button, since for sure it has been modified - (action === "pen" || action === "text") && RerenderButtons.update(action, customModes[`is${action === "text" ? "Text" : "Pen"}Enabled`]); // And update also the other button that might have been modified + if ((action === "text" || action === "pointer")) customModes.current.isPenEnabled = false; // Disable pen mode for pointer and text editing + if (action === "pen" || action === "pointer") customModes.current.isTextEnabled = false; // Disable text prompt for pointer and pen editing + if (action === "pen") customModes.current.isPenEnabled = !customModes.current.isPenEnabled; // Toggle pen editing + if (action === "text") customModes.current.isTextEnabled = !customModes.current.isTextEnabled; // Toggle text editing + customModes.current.isEraserEnabled = action === "erase" ? !customModes.current.isEraserEnabled : false; // Toggle eraser if that action is selected, otherwise disable it + RerenderButtons.update("erase", customModes.current.isEraserEnabled); // Update the eraser button, since for sure it has been modified + (action === "pen" || action === "text") && RerenderButtons.update(action, customModes.current[`is${action === "text" ? "Text" : "Pen"}Enabled`]); // And update also the other button that might have been modified } break; } @@ -353,7 +357,7 @@ export default function PDF({ pdfObj, imgObj }: Props) { { /** The following map includes all the events that will update user values. - @var "isUtils" means that the "customModes" object will updated; + @var "isUtils" means that the "customModes.current" object will updated; @var "isTemp" means that the "tempUserDrawingOptions" will be updated. "trigger" @var "ref" is the property to update @var "triggerText" asks for the re-render of the current text @@ -382,12 +386,12 @@ export default function PDF({ pdfObj, imgObj }: Props) { ]) switch (e.interface) { case "CustomSelectTimer": // Update the timer length, by converting it in seconds - userDrawingOptionsManager.timer = (isNaN(+e.value) ? 150 : +e.value) * 1000; + userDrawingOptionsManager.current.timer = (isNaN(+e.value) ? 150 : +e.value) * 1000; break; case "ChangedTextStatus": // The user has entered or exited in the text mode. Update the values of some properties to their default ones to avoid UI issues - customModes.isTextEnabled = !customModes.isTextEnabled; + customModes.current.isTextEnabled = !customModes.current.isTextEnabled; // @ts-ignore - for (let item of ["isBold", "isItalic", "isUnderlined", "isStriked"]) tempUserDrawingOptions[item] = false; + for (let item of ["isBold", "isItalic", "isUnderlined", "isStriked"]) tempUserDrawingOptions.current[item] = false; triggerTextChange(); break; default: @@ -405,15 +409,15 @@ export default function PDF({ pdfObj, imgObj }: Props) { } if (updateItem) { // @ts-ignore | Update each object with the correct value, and save them in their appropriate storage - updateItem.isUtils ? customModes[updateItem.ref] = getValue({ source: customModes[updateItem.ref], value: e.value }) : updateItem.isTemp ? tempUserDrawingOptions[updateItem.ref] = getValue({ source: tempUserDrawingOptions[updateItem.ref], value: e.value }) : userDrawingOptionsManager[updateItem.ref] = getValue({ source: userDrawingOptionsManager[updateItem.ref], value: e.value }); + updateItem.isUtils ? customModes.current[updateItem.ref] = getValue({ source: customModes.current[updateItem.ref], value: e.value }) : updateItem.isTemp ? tempUserDrawingOptions.current[updateItem.ref] = getValue({ source: tempUserDrawingOptions.current[updateItem.ref], value: e.value }) : userDrawingOptionsManager.current[updateItem.ref] = getValue({ source: userDrawingOptionsManager.current[updateItem.ref], value: e.value }); updateItem.triggerText && triggerTextChange(); - localStorage.setItem("PDFPointer-UserAnnotationSettings", JSON.stringify(userDrawingOptionsManager)); - sessionStorage.setItem("PDFPointer-UserTempSettings", JSON.stringify(tempUserDrawingOptions)) // Note that this is only used to recover the PDF filters value. Temp values are never restored. - if (updateItem.filter && canvasRef.current.mainCanvas) canvasRef.current.mainCanvas.style.filter = `invert(${tempUserDrawingOptions.negativeFilter}) hue-rotate(${tempUserDrawingOptions.hueInversionFilter}deg) sepia(${tempUserDrawingOptions.sepiaFilter}) grayscale(${tempUserDrawingOptions.grayscaleFilter})`; // Update the canvas filters + localStorage.setItem("PDFPointer-UserAnnotationSettings", JSON.stringify(userDrawingOptionsManager.current)); + sessionStorage.setItem("PDFPointer-UserTempSettings", JSON.stringify(tempUserDrawingOptions.current)) // Note that this is only used to recover the PDF filters value. Temp values are never restored. + if (updateItem.filter && canvasRef.current.mainCanvas) canvasRef.current.mainCanvas.style.filter = `invert(${tempUserDrawingOptions.current.negativeFilter}) hue-rotate(${tempUserDrawingOptions.current.hueInversionFilter}deg) sepia(${tempUserDrawingOptions.current.sepiaFilter}) grayscale(${tempUserDrawingOptions.current.grayscaleFilter})`; // Update the canvas filters } break; } - localStorage.setItem("PDFPointer-UserAnnotationSettings", JSON.stringify(userDrawingOptionsManager)); + localStorage.setItem("PDFPointer-UserAnnotationSettings", JSON.stringify(userDrawingOptionsManager.current)); }}>

@@ -422,8 +426,8 @@ export default function PDF({ pdfObj, imgObj }: Props) {
(canvasRef.current.circleCanvas = el)}>
(canvasRef.current.hoverCanvas = el)} data-noresize onMouseDown={(e) => onMouseDown()} onTouchStart={() => { // Disable (or enable) standard touch actions so that, when the user is using a pen/eraser, the canvas won't move - if (canvasRef.current.centerDiv?.parentElement) canvasRef.current.centerDiv.parentElement.style.touchAction = customModes.isPenEnabled || customModes.isEraserEnabled ? "none" : ""; - if (canvasRef.current.centerDiv) canvasRef.current.centerDiv.style.touchAction = customModes.isPenEnabled || customModes.isEraserEnabled ? "none" : ""; + if (canvasRef.current.centerDiv?.parentElement) canvasRef.current.centerDiv.parentElement.style.touchAction = customModes.current.isPenEnabled || customModes.current.isEraserEnabled ? "none" : ""; + if (canvasRef.current.centerDiv) canvasRef.current.centerDiv.style.touchAction = customModes.current.isPenEnabled || customModes.current.isEraserEnabled ? "none" : ""; onMouseDown() } } onMouseMove={(e) => mouseMove({ @@ -439,7 +443,7 @@ export default function PDF({ pdfObj, imgObj }: Props) { }} onClick={(e) => { let target = e.target as HTMLCanvasElement; - if (customModes.isTextEnabled && currentCanvas) { // Update the position of the text + if (customModes.current.isTextEnabled && currentCanvas) { // Update the position of the text mouseDown = [(e.clientX - (currentCanvas ?? target).getBoundingClientRect().left) * (window.devicePixelRatio || 1), (e.clientY - (currentCanvas ?? target).getBoundingClientRect().top) * (window.devicePixelRatio || 1)]; Annotations.write({ canvas: currentCanvas, position: mouseDown, ...getTextAttributes() }); } @@ -447,14 +451,14 @@ export default function PDF({ pdfObj, imgObj }: Props) { onMouseUp={stopCanvasEditing} onTouchEnd={stopCanvasEditing} onMouseLeave={() => { stopCanvasEditing(); if (canvasRef.current.circleCanvas) canvasRef.current.circleCanvas.style.display = "none" }} onMouseEnter={() => { if (canvasRef.current.circleCanvas) { // Update properties of the pointer circlee canvasRef.current.circleCanvas.style.display = "block"; - canvasRef.current.circleCanvas.style.backgroundColor = userDrawingOptionsManager.cursorColor; - for (let item of ["width", "height"]) canvasRef.current.circleCanvas.style[item as "width" | "height"] = `${userDrawingOptionsManager.cursorSize}px`; + canvasRef.current.circleCanvas.style.backgroundColor = userDrawingOptionsManager.current.cursorColor; + for (let item of ["width", "height"]) canvasRef.current.circleCanvas.style[item as "width" | "height"] = `${userDrawingOptionsManager.current.cursorSize}px`; } }}>
(canvasRef.current.thumbnailDiv = el)}> - {pageSettings.showThumbnail !== 0 && pdfObj && { RerenderButtons.update("thumbnail", false); updatePage(prevState => { return { ...prevState, showThumbnail: 2 } }) }} PDFObj={pdfObj} pageListener={(e) => { updatePage(prevState => { return { ...prevState, page: e + 1 } }) }}>} + {pageSettings.showThumbnail !== 0 && pdfObj && }
} \ No newline at end of file diff --git a/src/Components/Toolbar.tsx b/src/Components/Toolbar.tsx index 34c39cc..5f4b50c 100644 --- a/src/Components/Toolbar.tsx +++ b/src/Components/Toolbar.tsx @@ -41,14 +41,13 @@ declare global { * @param requestedTab the ID of the Card that will be showj * @returns */ -let stateReflect = "hello" export default function Toolbar({ pageSettings, updatePage, settingsCallback, pdfObj, requestedTab, imgObj }: Props) { let [CardShown, UpdateState] = useState("hello"); - stateReflect = CardShown; + let stateReflect = useRef(CardShown); useEffect(() => { if (!requestedTab) return; const newTab = requestedTab.substring(0, requestedTab.indexOf(",")); - UpdateState(stateReflect === newTab ? "hello" : newTab); + UpdateState(stateReflect.current === newTab ? "hello" : newTab); }, [requestedTab]) const usefulBtn = { // NOTE: Always add the key attribute. Otherwise React, when exiting from the custom Card mode, will trigger the animation on the first element, causing UI issues. pen: { if (settingsCallback) settingsCallback({ interface: "ChangedPenStatus", value: "" }); requestedTab = undefined; UpdateState(CardShown === "pen" ? "hello" : "pen"); }}>, diff --git a/src/SettingTabs/KeyboardShortcuts.tsx b/src/SettingTabs/KeyboardShortcuts.tsx index dacd114..ae78320 100644 --- a/src/SettingTabs/KeyboardShortcuts.tsx +++ b/src/SettingTabs/KeyboardShortcuts.tsx @@ -1,9 +1,8 @@ import Lang from "../Scripts/LanguageTranslations"; import { KeyPreference } from "../Interfaces/CustomOptions"; import Card from "../Components/Card"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; -let keyPressed: string[] = []; /** * Change keyboard shortcuts inside PDFPointer * @returns the KeyboardShortcuts ReactNode tab @@ -11,6 +10,11 @@ let keyPressed: string[] = []; export default function KeyboardShortcuts() { const KeyPreference = JSON.parse(localStorage.getItem("PDFPointer-KeyboardPreferences") ?? "{}") as KeyPreference; const [state, updateState] = useState("zoomin"); + let keyPressed: string[] = []; + let keyList = useRef(null); + function updateValues() { + if (keyList.current) keyList.current.textContent = keyPressed.join(" — "); + }; return <>

{Lang("Keyboard shortcuts:")}

{Lang("Change, or delete the keyboard shortcuts to useful elements. Click on the dashed surface, and press the key for this action")}



@@ -31,14 +35,15 @@ export default function KeyboardShortcuts() {



-
{ +
{ + e.preventDefault(); keyPressed.push(e.key.toLowerCase()); - (e.target as HTMLInputElement).textContent = keyPressed.join(" — "); + updateValues(); localStorage.setItem("PDFPointer-KeyboardPreferences", JSON.stringify({ ...JSON.parse(localStorage.getItem("PDFPointer-KeyboardPreferences") ?? "{}"), [state]: keyPressed })); - }} onInput={(e) => e.preventDefault()} onKeyUp={(e) => { - keyPressed.indexOf(e.key.toLowerCase()) !== -1 && keyPressed.splice(keyPressed.indexOf(e.key.toLowerCase()), 1); - }}>{KeyPreference[state]}
diff --git a/src/index.css b/src/index.css index b815879..ea531c6 100644 --- a/src/index.css +++ b/src/index.css @@ -163,6 +163,7 @@ h3 { border-radius: 16px; width: 30vw; z-index: 999992; + -webkit-backdrop-filter: blur(16px) brightness(50%); backdrop-filter: blur(16px) brightness(50%); }