From 3cab6166a327fdce24a55526435c6d4c8409c091 Mon Sep 17 00:00:00 2001 From: Donkie Date: Sat, 15 Jul 2023 14:38:40 +0200 Subject: [PATCH 01/16] Client: Created general useSavedState function --- client/src/pages/spools/list.tsx | 7 +++++-- client/src/utils/saveload.ts | 14 +++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/client/src/pages/spools/list.tsx b/client/src/pages/spools/list.tsx index 36c9c46..0918c30 100644 --- a/client/src/pages/spools/list.tsx +++ b/client/src/pages/spools/list.tsx @@ -20,7 +20,7 @@ import { ISpool } from "./model"; import { TableState, useInitialTableState, - useShowArchive, + useSavedState, useStoreInitialState, } from "../../utils/saveload"; import { @@ -55,7 +55,10 @@ export const SpoolList: React.FC = () => { const initialState = useInitialTableState("spoolList"); // State for the switch to show archived spools - const [showArchived, setShowArchived] = useShowArchive("spoolList"); + const [showArchived, setShowArchived] = useSavedState( + "spoolList-showArchived", + false + ); // Fetch data from the API const { diff --git a/client/src/utils/saveload.ts b/client/src/utils/saveload.ts index a20aaa0..6677e7e 100644 --- a/client/src/utils/saveload.ts +++ b/client/src/utils/saveload.ts @@ -60,17 +60,17 @@ export function useStoreInitialState(tableId: string, state: TableState) { }, [tableId, state.showColumns]); } -export function useShowArchive(tableId: string) { - const [showArchive, setShowArchive] = React.useState(() => { - const savedShowArchive = isLocalStorageAvailable ? localStorage.getItem(`${tableId}-showArchive`) : null; - return savedShowArchive ? JSON.parse(savedShowArchive) : false; +export function useSavedState(id: string, defaultValue: T) { + const [state, setState] = React.useState(() => { + const savedState = isLocalStorageAvailable ? localStorage.getItem(`savedStates-${id}`) : null; + return savedState ? JSON.parse(savedState) : defaultValue; }); React.useEffect(() => { if (isLocalStorageAvailable) { - localStorage.setItem(`${tableId}-showArchive`, JSON.stringify(showArchive)); + localStorage.setItem(`savedStates-${id}`, JSON.stringify(state)); } - }, [tableId, showArchive]); + }, [id, state]); - return [showArchive, setShowArchive] as const; + return [state, setState] as const; } From 06e53d05dc23494d4f0cfd506c01cde13dadedd4 Mon Sep 17 00:00:00 2001 From: Donkie Date: Sat, 15 Jul 2023 14:39:20 +0200 Subject: [PATCH 02/16] Client: Created QR code printing dialog component --- .../src/components/qrCodePrintingDialog.tsx | 359 ++++++++++++++++++ 1 file changed, 359 insertions(+) create mode 100644 client/src/components/qrCodePrintingDialog.tsx diff --git a/client/src/components/qrCodePrintingDialog.tsx b/client/src/components/qrCodePrintingDialog.tsx new file mode 100644 index 0000000..061bc0a --- /dev/null +++ b/client/src/components/qrCodePrintingDialog.tsx @@ -0,0 +1,359 @@ +import React, { useRef } from "react"; +import { + Modal, + Slider, + Button, + Select, + Row, + Col, + QRCode, + Form, + Divider, + Switch, +} from "antd"; +import ReactToPrint from "react-to-print"; +import { useSavedState } from "../utils/saveload"; + +interface QRCodeData { + id: number; + content: string; +} + +interface QRCodePrintingDialogProps { + visible: boolean; + onCancel: () => void; +} + +interface PaperDimensions { + width: number; + height: number; +} + +const paperDimensions: { [key: string]: PaperDimensions } = { + A3: { + width: 297, + height: 420, + }, + A4: { + width: 210, + height: 297, + }, + A5: { + width: 148, + height: 210, + }, + Letter: { + width: 216, + height: 279, + }, + Legal: { + width: 216, + height: 356, + }, + Tabloid: { + width: 279, + height: 432, + }, +}; + +const QRCodePrintingDialog: React.FC = ({ + visible, + onCancel, +}) => { + const [marginLeft, setMarginLeft] = useSavedState("print-marginLeft", 10); + const [marginTop, setMarginTop] = useSavedState("print-marginTop", 10); + const [marginRight, setMarginRight] = useSavedState("print-marginRight", 10); + const [marginBottom, setMarginBottom] = useSavedState( + "print-marginBottom", + 10 + ); + const [codesPerRow, setCodesPerRow] = useSavedState("print-codesPerRow", 3); + const [paperSize, setPaperSize] = useSavedState("print-paperSize", "A4"); + const [rowHeight, setRowHeight] = useSavedState("print-rowHeight", 70); + const [showContent, setShowContent] = useSavedState( + "print-showContent", + true + ); + const [previewScale, setPreviewScale] = useSavedState( + "print-previewScale", + 0.6 + ); + const [showBorder, setShowBorder] = useSavedState("print-showBorder", true); + const [textSize, setTextSize] = useSavedState("print-textSize", 7); + + const qrCodeData: QRCodeData[] = Array.from({ length: 10 }).map( + (_, index) => ({ + id: index, + content: `QR Code ${index + 1}`, + }) + ); + + const paperWidth = paperDimensions[paperSize].width; + const paperHeight = paperDimensions[paperSize].height; + + const printRef = useRef(null); + + const codesPerPage = + codesPerRow * + Math.floor((paperHeight - marginTop - marginBottom) / rowHeight); + const pageBlocks = []; + for (let i = 0; i < qrCodeData.length; i += 1) { + if (i % codesPerPage === 0) { + pageBlocks.push(qrCodeData.slice(i, i + codesPerPage)); + } + } + const rowBlocks = pageBlocks.map((pageBlock, idx) => ( +
+ + {pageBlock.map((qrCode) => ( +
+ + {showContent && ( +
+ {qrCode.content} +
+ )} +
+ ))} +
+
+ )); + + return ( + } + content={() => printRef.current} + />, + ]} + width={1000} // Set the modal width to accommodate the preview + > + + +
+
+ + {rowBlocks} +
+
+ + +
+ + { + setMarginLeft(value); + }} + /> + + + { + setMarginTop(value); + }} + /> + + + { + setMarginRight(value); + }} + /> + + + { + setMarginBottom(value); + }} + /> + + + { + setCodesPerRow(value); + }} + /> + + + + + + { + setRowHeight(value); + }} + /> + + + { + setTextSize(value); + }} + /> + + + setShowContent(checked)} + /> + + + setShowBorder(checked)} + /> + + + + { + setPreviewScale(value); + }} + /> + + + +
+
+ ); +}; + +export default QRCodePrintingDialog; From 884b380b8c13d0f3cc03795f9dfe6c48a7019059 Mon Sep 17 00:00:00 2001 From: Donkie Date: Sat, 15 Jul 2023 14:40:14 +0200 Subject: [PATCH 03/16] Client: Moved QR code printing dialog --- client/src/components/{ => printing}/qrCodePrintingDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename client/src/components/{ => printing}/qrCodePrintingDialog.tsx (99%) diff --git a/client/src/components/qrCodePrintingDialog.tsx b/client/src/components/printing/qrCodePrintingDialog.tsx similarity index 99% rename from client/src/components/qrCodePrintingDialog.tsx rename to client/src/components/printing/qrCodePrintingDialog.tsx index 061bc0a..5175a07 100644 --- a/client/src/components/qrCodePrintingDialog.tsx +++ b/client/src/components/printing/qrCodePrintingDialog.tsx @@ -12,7 +12,7 @@ import { Switch, } from "antd"; import ReactToPrint from "react-to-print"; -import { useSavedState } from "../utils/saveload"; +import { useSavedState } from "../../utils/saveload"; interface QRCodeData { id: number; From e6e865a75aa3a58bc1e49d1e382aa952ab71023f Mon Sep 17 00:00:00 2001 From: Donkie Date: Sat, 15 Jul 2023 15:23:35 +0200 Subject: [PATCH 04/16] Client: Added missing react-to-print dependency --- client/package-lock.json | 10 ++++++++++ client/package.json | 1 + 2 files changed, 11 insertions(+) diff --git a/client/package-lock.json b/client/package-lock.json index d6af05b..f12f1ff 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -26,6 +26,7 @@ "react-dom": "^18.2.0", "react-i18next": "^13.0.1", "react-router-dom": "^6.14.1", + "react-to-print": "^2.14.13", "vite-plugin-svgr": "^3.2.0" }, "devDependencies": { @@ -8487,6 +8488,15 @@ "react-dom": "^16.3.0 || ^17.0.0" } }, + "node_modules/react-to-print": { + "version": "2.14.13", + "resolved": "https://registry.npmjs.org/react-to-print/-/react-to-print-2.14.13.tgz", + "integrity": "sha512-PqUGgTRZvkyBzWgaZHVBECWPX2nGEc3HOUy6WNXof6HT4yTFI92wtIkqQtr4jfvHbadqjwTgzgh6vgN8KXlWWw==", + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-virtual": { "version": "2.10.4", "resolved": "https://registry.npmjs.org/react-virtual/-/react-virtual-2.10.4.tgz", diff --git a/client/package.json b/client/package.json index 8ffdbb0..6b8800e 100644 --- a/client/package.json +++ b/client/package.json @@ -25,6 +25,7 @@ "react-dom": "^18.2.0", "react-i18next": "^13.0.1", "react-router-dom": "^6.14.1", + "react-to-print": "^2.14.13", "vite-plugin-svgr": "^3.2.0" }, "devDependencies": { From 78cccfa2aae06904a4a3e34326709848eabd0e02 Mon Sep 17 00:00:00 2001 From: Donkie Date: Sat, 15 Jul 2023 15:24:06 +0200 Subject: [PATCH 05/16] Client: Abstracted out the printing dialog --- .../components/printing/printingDialog.tsx | 305 +++++++++++++ .../printing/qrCodePrintingDialog.tsx | 404 ++++-------------- 2 files changed, 381 insertions(+), 328 deletions(-) create mode 100644 client/src/components/printing/printingDialog.tsx diff --git a/client/src/components/printing/printingDialog.tsx b/client/src/components/printing/printingDialog.tsx new file mode 100644 index 0000000..45e2c54 --- /dev/null +++ b/client/src/components/printing/printingDialog.tsx @@ -0,0 +1,305 @@ +import React, { useRef } from "react"; +import { + Modal, + Slider, + Button, + Select, + Row, + Col, + Form, + Divider, + Switch, +} from "antd"; +import ReactToPrint from "react-to-print"; +import { useSavedState } from "../../utils/saveload"; + +interface PrintingDialogProps { + items: JSX.Element[]; + style?: string; + extraSettings?: JSX.Element; + visible: boolean; + onCancel: () => void; + title?: string; +} + +interface PaperDimensions { + width: number; + height: number; +} + +const paperDimensions: { [key: string]: PaperDimensions } = { + A3: { + width: 297, + height: 420, + }, + A4: { + width: 210, + height: 297, + }, + A5: { + width: 148, + height: 210, + }, + Letter: { + width: 216, + height: 279, + }, + Legal: { + width: 216, + height: 356, + }, + Tabloid: { + width: 279, + height: 432, + }, +}; + +const PrintingDialog: React.FC = ({ + items, + style, + extraSettings, + visible, + onCancel, + title, +}) => { + const [marginLeft, setMarginLeft] = useSavedState("print-marginLeft", 10); + const [marginTop, setMarginTop] = useSavedState("print-marginTop", 10); + const [marginRight, setMarginRight] = useSavedState("print-marginRight", 10); + const [marginBottom, setMarginBottom] = useSavedState( + "print-marginBottom", + 10 + ); + const [itemsPerRow, setItemsPerRow] = useSavedState("print-itemsPerRow", 3); + const [paperSize, setPaperSize] = useSavedState("print-paperSize", "A4"); + const [rowHeight, setRowHeight] = useSavedState("print-rowHeight", 70); + const [previewScale, setPreviewScale] = useSavedState( + "print-previewScale", + 0.6 + ); + const [showBorder, setShowBorder] = useSavedState("print-showBorder", true); + + const paperWidth = paperDimensions[paperSize].width; + const paperHeight = paperDimensions[paperSize].height; + + const printRef = useRef(null); + + const itemsPerPage = + itemsPerRow * + Math.floor((paperHeight - marginTop - marginBottom) / rowHeight); + const pageBlocks = []; + for (let i = 0; i < items.length; i += 1) { + if (i % itemsPerPage === 0) { + pageBlocks.push(items.slice(i, i + itemsPerPage)); + } + } + const rowBlocks = pageBlocks.map((pageBlock, idx) => ( +
+ + {pageBlock.map((item, index) => ( +
+ {item} +
+ ))} +
+
+ )); + + return ( + } + content={() => printRef.current} + />, + ]} + width={1000} // Set the modal width to accommodate the preview + > + + +
+
+ + {rowBlocks} +
+
+ + +
+ + { + setMarginLeft(value); + }} + /> + + + { + setMarginTop(value); + }} + /> + + + { + setMarginRight(value); + }} + /> + + + { + setMarginBottom(value); + }} + /> + + + { + setItemsPerRow(value); + }} + /> + + + + + + { + setRowHeight(value); + }} + /> + + + setShowBorder(checked)} + /> + + {extraSettings && } + {extraSettings} + + + { + setPreviewScale(value); + }} + /> + + + +
+
+ ); +}; + +export default PrintingDialog; diff --git a/client/src/components/printing/qrCodePrintingDialog.tsx b/client/src/components/printing/qrCodePrintingDialog.tsx index 5175a07..13d6aac 100644 --- a/client/src/components/printing/qrCodePrintingDialog.tsx +++ b/client/src/components/printing/qrCodePrintingDialog.tsx @@ -1,358 +1,106 @@ -import React, { useRef } from "react"; -import { - Modal, - Slider, - Button, - Select, - Row, - Col, - QRCode, - Form, - Divider, - Switch, -} from "antd"; -import ReactToPrint from "react-to-print"; +import { Form, QRCode, Slider, Switch } from "antd"; import { useSavedState } from "../../utils/saveload"; +import PrintingDialog from "./printingDialog"; interface QRCodeData { - id: number; - content: string; + value: string; + label?: string; + errorLevel?: "L" | "M" | "Q" | "H"; } interface QRCodePrintingDialogProps { visible: boolean; - onCancel: () => void; + items: QRCodeData[]; } -interface PaperDimensions { - width: number; - height: number; -} - -const paperDimensions: { [key: string]: PaperDimensions } = { - A3: { - width: 297, - height: 420, - }, - A4: { - width: 210, - height: 297, - }, - A5: { - width: 148, - height: 210, - }, - Letter: { - width: 216, - height: 279, - }, - Legal: { - width: 216, - height: 356, - }, - Tabloid: { - width: 279, - height: 432, - }, -}; - const QRCodePrintingDialog: React.FC = ({ visible, - onCancel, + items, }) => { - const [marginLeft, setMarginLeft] = useSavedState("print-marginLeft", 10); - const [marginTop, setMarginTop] = useSavedState("print-marginTop", 10); - const [marginRight, setMarginRight] = useSavedState("print-marginRight", 10); - const [marginBottom, setMarginBottom] = useSavedState( - "print-marginBottom", - 10 - ); - const [codesPerRow, setCodesPerRow] = useSavedState("print-codesPerRow", 3); - const [paperSize, setPaperSize] = useSavedState("print-paperSize", "A4"); - const [rowHeight, setRowHeight] = useSavedState("print-rowHeight", 70); const [showContent, setShowContent] = useSavedState( "print-showContent", true ); - const [previewScale, setPreviewScale] = useSavedState( - "print-previewScale", - 0.6 - ); - const [showBorder, setShowBorder] = useSavedState("print-showBorder", true); - const [textSize, setTextSize] = useSavedState("print-textSize", 7); - - const qrCodeData: QRCodeData[] = Array.from({ length: 10 }).map( - (_, index) => ({ - id: index, - content: `QR Code ${index + 1}`, - }) + const [textSize, setTextSize] = useSavedState("print-textSize", 5); + const [showSpoolmanIcon, setShowSpoolmanIcon] = useSavedState( + "print-showSpoolmanIcon", + true ); - const paperWidth = paperDimensions[paperSize].width; - const paperHeight = paperDimensions[paperSize].height; - - const printRef = useRef(null); - - const codesPerPage = - codesPerRow * - Math.floor((paperHeight - marginTop - marginBottom) / rowHeight); - const pageBlocks = []; - for (let i = 0; i < qrCodeData.length; i += 1) { - if (i % codesPerPage === 0) { - pageBlocks.push(qrCodeData.slice(i, i + codesPerPage)); - } - } - const rowBlocks = pageBlocks.map((pageBlock, idx) => ( -
- - {pageBlock.map((qrCode) => ( + const elements = items.map((item) => { + return ( + <> + + {showContent && (
- - {showContent && ( -
- {qrCode.content} -
- )} + {item.label ?? item.value}
- ))} -
-
- )); + )} + + ); + }); return ( - } - content={() => printRef.current} - />, - ]} - width={1000} // Set the modal width to accommodate the preview - > - - -
-
+ + setShowContent(checked)} + /> + + + { + setTextSize(value); }} - > - - {rowBlocks} -
-
- - -
- - { - setMarginLeft(value); - }} - /> - - - { - setMarginTop(value); - }} - /> - - - { - setMarginRight(value); - }} - /> - - - { - setMarginBottom(value); - }} - /> - - - { - setCodesPerRow(value); - }} - /> - - - - - - { - setRowHeight(value); - }} - /> - - - { - setTextSize(value); - }} - /> - - - setShowContent(checked)} - /> - - - setShowBorder(checked)} - /> - - - - { - setPreviewScale(value); - }} - /> - - - -
-
+ .print-page svg { + display: block; + height: 100%; + width: auto; + } + `} + onCancel={() => true} + /> ); }; From 42373baa03e95f2bfee4e0cf1201412fcc4297e3 Mon Sep 17 00:00:00 2001 From: Donkie Date: Sun, 30 Jul 2023 09:51:49 +0200 Subject: [PATCH 06/16] Client: Moved form units to tooltip --- .../src/components/printing/printingDialog.tsx | 17 +++++++++++------ .../printing/qrCodePrintingDialog.tsx | 3 ++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/client/src/components/printing/printingDialog.tsx b/client/src/components/printing/printingDialog.tsx index 45e2c54..a0df3af 100644 --- a/client/src/components/printing/printingDialog.tsx +++ b/client/src/components/printing/printingDialog.tsx @@ -203,47 +203,51 @@ const PrintingDialog: React.FC = ({ labelCol={{ span: 14 }} wrapperCol={{ span: 10 }} > - + `${value} mm` }} value={marginLeft} onChange={(value) => { setMarginLeft(value); }} /> - + `${value} mm` }} value={marginTop} onChange={(value) => { setMarginTop(value); }} /> - + `${value} mm` }} value={marginRight} onChange={(value) => { setMarginRight(value); }} /> - + `${value} mm` }} value={marginBottom} onChange={(value) => { setMarginBottom(value); }} /> - + = ({ ))} - + `${value} mm` }} value={rowHeight} onChange={(value) => { setRowHeight(value); diff --git a/client/src/components/printing/qrCodePrintingDialog.tsx b/client/src/components/printing/qrCodePrintingDialog.tsx index 13d6aac..0b72416 100644 --- a/client/src/components/printing/qrCodePrintingDialog.tsx +++ b/client/src/components/printing/qrCodePrintingDialog.tsx @@ -63,9 +63,10 @@ const QRCodePrintingDialog: React.FC = ({ onChange={(checked) => setShowContent(checked)} /> - + `${value} mm` }} min={3} max={15} value={textSize} From 475e64ff4842f35c7933ee1fc68c725233cce9f9 Mon Sep 17 00:00:00 2001 From: Donkie Date: Sun, 30 Jul 2023 09:54:33 +0200 Subject: [PATCH 07/16] Client: Added a spool selection dialog --- .../printing/qrCodePrintingDialog.tsx | 4 +- .../src/components/selectAndPrintDialog.tsx | 51 +++++ client/src/components/spoolSelectModal.tsx | 206 ++++++++++++++++++ client/src/pages/spools/list.tsx | 2 + 4 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 client/src/components/selectAndPrintDialog.tsx create mode 100644 client/src/components/spoolSelectModal.tsx diff --git a/client/src/components/printing/qrCodePrintingDialog.tsx b/client/src/components/printing/qrCodePrintingDialog.tsx index 0b72416..fbc886e 100644 --- a/client/src/components/printing/qrCodePrintingDialog.tsx +++ b/client/src/components/printing/qrCodePrintingDialog.tsx @@ -11,11 +11,13 @@ interface QRCodeData { interface QRCodePrintingDialogProps { visible: boolean; items: QRCodeData[]; + onCancel: () => void; } const QRCodePrintingDialog: React.FC = ({ visible, items, + onCancel, }) => { const [showContent, setShowContent] = useSavedState( "print-showContent", @@ -100,7 +102,7 @@ const QRCodePrintingDialog: React.FC = ({ width: auto; } `} - onCancel={() => true} + onCancel={onCancel} /> ); }; diff --git a/client/src/components/selectAndPrintDialog.tsx b/client/src/components/selectAndPrintDialog.tsx new file mode 100644 index 0000000..6a812e0 --- /dev/null +++ b/client/src/components/selectAndPrintDialog.tsx @@ -0,0 +1,51 @@ +// React FC that combines the functionality of first selecting what spools to print QR code for, and then opens up the QR code printing dialog. + +import React from "react"; +import SpoolSelectModal from "./spoolSelectModal"; +import QRCodePrintingDialog from "./printing/qrCodePrintingDialog"; +import { Button } from "antd"; +import { ISpool } from "../pages/spools/model"; +import { PrinterOutlined } from "@ant-design/icons"; + +const SelectAndPrint: React.FC = () => { + const [step, setStep] = React.useState(0); + const [selectedSpools, setSelectedSpools] = React.useState([]); + return ( + <> + + { + setStep(0); + }} + onContinue={(spools) => { + setSelectedSpools(spools); + setStep(2); + }} + /> + { + setStep(1); + }} + items={selectedSpools.map((spool) => ({ + value: `S:${spool.id}`, + // value: `https://spoolman.lan/spool/show/${spool.id}`, + // label: `hello`, + errorLevel: "H", + }))} + /> + + ); +}; + +export default SelectAndPrint; diff --git a/client/src/components/spoolSelectModal.tsx b/client/src/components/spoolSelectModal.tsx new file mode 100644 index 0000000..8d158a5 --- /dev/null +++ b/client/src/components/spoolSelectModal.tsx @@ -0,0 +1,206 @@ +import React, { useState } from "react"; +import { Modal, Table, Checkbox, Space, Row, Col } from "antd"; +import { ISpool } from "../pages/spools/model"; +import { FilteredColumn, SortedColumn, SpoolIconColumn } from "./column"; +import { TableState } from "../utils/saveload"; +import { useTable } from "@refinedev/antd"; +import { genericSorter, typeSorters } from "../utils/sorting"; +import { genericFilterer, typeFilters } from "../utils/filtering"; + +interface Props { + visible: boolean; + description?: string; + onCancel: () => void; + onContinue: (selectedSpools: ISpool[]) => void; +} + +interface ISpoolCollapsed extends ISpool { + filament_name: string; + material?: string; +} + +const SpoolSelectModal: React.FC = ({ + visible, + description, + onCancel, + onContinue, +}) => { + const [selectedItems, setSelectedItems] = useState([]); + const [showArchived, setShowArchived] = useState(false); + + const { tableProps, sorters, filters, current, pageSize } = useTable({ + meta: { + queryParams: { + ["allow_archived"]: showArchived, + }, + }, + syncWithLocation: false, + pagination: { + mode: "off", + current: 1, + pageSize: 10, + }, + sorters: { + mode: "off", + }, + filters: { + mode: "off", + }, + }); + + // Store state in local storage + const tableState: TableState = { + sorters, + filters, + pagination: { current, pageSize }, + }; + + // Collapse the dataSource to a mutable list and add a filament_name field + const dataSource: ISpoolCollapsed[] = React.useMemo( + () => + (tableProps.dataSource ?? []).map((element) => { + let filament_name: string; + if (element.filament.vendor && "name" in element.filament.vendor) { + filament_name = `${element.filament.vendor.name} - ${element.filament.name}`; + } else { + filament_name = + element.filament.name ?? element.filament.id.toString(); + } + return { + ...element, + filament_name, + material: element.filament.material, + }; + }), + [tableProps.dataSource] + ); + + // Type the sorters and filters + const typedSorters = typeSorters(sorters); + const typedFilters = typeFilters(filters); + + // Filter and sort the dataSource + const filteredDataSource = React.useMemo(() => { + const filtered = dataSource.filter(genericFilterer(typedFilters)); + filtered.sort(genericSorter(typedSorters)); + return filtered; + }, [dataSource, typedFilters, typedSorters]); + + // Function to add/remove all filtered items from selected items + const selectUnselectFiltered = (select: boolean) => { + setSelectedItems((prevSelected) => { + const filtered = filteredDataSource + .map((spool) => spool.id) + .filter((spool) => !prevSelected.includes(spool)); + return select ? [...prevSelected, ...filtered] : filtered; + }); + }; + + // Handler for selecting/unselecting individual items + const handleSelectItem = (item: number) => { + setSelectedItems((prevSelected) => + prevSelected.includes(item) + ? prevSelected.filter((selected) => selected !== item) + : [...prevSelected, item] + ); + }; + + // State for the select/unselect all checkbox + const isAllFilteredSelected = filteredDataSource.every((spool) => + selectedItems.includes(spool.id) + ); + + return ( + { + onContinue( + dataSource.filter((spool) => selectedItems.includes(spool.id)) + ); + }} + width={600} + > + + {description &&
{description}
} + + ( + handleSelectItem(item.id)} + /> + )} + /> + {SortedColumn({ + id: "id", + i18ncat: "spool", + dataSource, + tableState, + })} + {SpoolIconColumn({ + id: "filament_name", + i18ncat: "spool", + color: (record: ISpoolCollapsed) => record.filament.color_hex, + dataSource, + tableState, + })} + {FilteredColumn({ + id: "material", + i18ncat: "spool", + dataSource, + tableState, + })} +
+ + + { + selectUnselectFiltered(e.target.checked); + }} + > + Select/Unselect All + + + +
+ {selectedItems.length} spool + {selectedItems.length === 1 ? "" : "s"} selected +
+ + + { + setShowArchived(e.target.checked); + if (!e.target.checked) { + // Remove archived spools from selected items + setSelectedItems((prevSelected) => + prevSelected.filter( + (selected) => + dataSource.find((spool) => spool.id === selected) + ?.archived !== true + ) + ); + } + }} + > + Show Archived + + +
+
+
+ ); +}; + +export default SpoolSelectModal; diff --git a/client/src/pages/spools/list.tsx b/client/src/pages/spools/list.tsx index 0918c30..9cddaa7 100644 --- a/client/src/pages/spools/list.tsx +++ b/client/src/pages/spools/list.tsx @@ -37,6 +37,7 @@ import { SpoolIconColumn, } from "../../components/column"; import { setSpoolArchived } from "./functions"; +import SelectAndPrint from "../../components/selectAndPrintDialog"; dayjs.extend(utc); @@ -191,6 +192,7 @@ export const SpoolList: React.FC = () => { ( <> + } + trigger={() => ( + + )} content={() => printRef.current} />, ]} @@ -203,7 +208,7 @@ const PrintingDialog: React.FC = ({ labelCol={{ span: 14 }} wrapperCol={{ span: 10 }} > - + = ({ }} /> - + = ({ }} /> - + = ({ }} /> - + = ({ }} /> - + = ({ }} /> - + - + = ({ }} /> - + setShowBorder(checked)} @@ -289,7 +294,7 @@ const PrintingDialog: React.FC = ({ {extraSettings && } {extraSettings} - + = ({ items, onCancel, }) => { + const t = useTranslate(); + const [showContent, setShowContent] = useSavedState( "print-showContent", true @@ -55,17 +58,17 @@ const QRCodePrintingDialog: React.FC = ({ return ( - + setShowContent(checked)} /> - + `${value} mm` }} @@ -78,7 +81,7 @@ const QRCodePrintingDialog: React.FC = ({ }} /> - + setShowSpoolmanIcon(checked)} diff --git a/client/src/components/selectAndPrintDialog.tsx b/client/src/components/selectAndPrintDialog.tsx index 6a812e0..455e915 100644 --- a/client/src/components/selectAndPrintDialog.tsx +++ b/client/src/components/selectAndPrintDialog.tsx @@ -6,8 +6,11 @@ import QRCodePrintingDialog from "./printing/qrCodePrintingDialog"; import { Button } from "antd"; import { ISpool } from "../pages/spools/model"; import { PrinterOutlined } from "@ant-design/icons"; +import { useTranslate } from "@refinedev/core"; const SelectAndPrint: React.FC = () => { + const t = useTranslate(); + const [step, setStep] = React.useState(0); const [selectedSpools, setSelectedSpools] = React.useState([]); return ( @@ -19,11 +22,11 @@ const SelectAndPrint: React.FC = () => { setStep(1); }} > - Print QR Codes + {t("printing.qrcode.button")} { setStep(0); }} diff --git a/client/src/components/spoolSelectModal.tsx b/client/src/components/spoolSelectModal.tsx index 8d158a5..71faed9 100644 --- a/client/src/components/spoolSelectModal.tsx +++ b/client/src/components/spoolSelectModal.tsx @@ -1,11 +1,12 @@ import React, { useState } from "react"; -import { Modal, Table, Checkbox, Space, Row, Col } from "antd"; +import { Modal, Table, Checkbox, Space, Row, Col, message } from "antd"; import { ISpool } from "../pages/spools/model"; import { FilteredColumn, SortedColumn, SpoolIconColumn } from "./column"; import { TableState } from "../utils/saveload"; import { useTable } from "@refinedev/antd"; import { genericSorter, typeSorters } from "../utils/sorting"; import { genericFilterer, typeFilters } from "../utils/filtering"; +import { t } from "i18next"; interface Props { visible: boolean; @@ -27,6 +28,7 @@ const SpoolSelectModal: React.FC = ({ }) => { const [selectedItems, setSelectedItems] = useState([]); const [showArchived, setShowArchived] = useState(false); + const [messageApi, contextHolder] = message.useMessage(); const { tableProps, sorters, filters, current, pageSize } = useTable({ meta: { @@ -112,16 +114,24 @@ const SpoolSelectModal: React.FC = ({ return ( { + if (selectedItems.length === 0) { + messageApi.open({ + type: "error", + content: t("printing.spoolSelect.noSpoolsSelected"), + }); + return; + } onContinue( dataSource.filter((spool) => selectedItems.includes(spool.id)) ); }} width={600} > + {contextHolder} {description &&
{description}
} = ({ selectUnselectFiltered(e.target.checked); }} > - Select/Unselect All + {t("printing.spoolSelect.selectAll")}
- {selectedItems.length} spool - {selectedItems.length === 1 ? "" : "s"} selected + {t("printing.spoolSelect.selectedTotal", { + count: selectedItems.length, + })}
@@ -194,7 +205,7 @@ const SpoolSelectModal: React.FC = ({ } }} > - Show Archived + {t("printing.spoolSelect.showArchived")} From 437213c7d42efb9e7be4c20680abe289f11fc372 Mon Sep 17 00:00:00 2001 From: Donkie Date: Wed, 2 Aug 2023 21:00:36 +0200 Subject: [PATCH 09/16] Client: QR print updates * Added grid borders * Replaced row height with instead choosing number of rows * Added support for negative margins * New QR code content format * Added number inputs to all sliders --- client/public/locales/en/common.json | 10 +- client/public/locales/sv/common.json | 10 +- .../components/printing/printingDialog.tsx | 410 ++++++++++++------ .../printing/qrCodePrintingDialog.tsx | 41 +- .../src/components/selectAndPrintDialog.tsx | 5 +- 5 files changed, 335 insertions(+), 141 deletions(-) diff --git a/client/public/locales/en/common.json b/client/public/locales/en/common.json index 21abefa..b057d51 100644 --- a/client/public/locales/en/common.json +++ b/client/public/locales/en/common.json @@ -56,16 +56,22 @@ "printing": { "generic": { "title": "Printing", + "description": "Tune in the settings below to get the desired print layout. Keep in mind that printers and your OS may apply it's own margins and scaling, so you may need to adjust the settings accordingly. Test it on a piece of regular paper before printing on the actual labels.", "print": "Print", "columns": "Columns", + "rows": "Rows", "paperSize": "Paper Size", - "rowHeight": "Row Height", "showBorder": "Show Border", "previewScale": "Preview Scale", "marginLeft": "Left Margin", "marginRight": "Right Margin", "marginTop": "Top Margin", - "marginBottom": "Bottom Margin" + "marginBottom": "Bottom Margin", + "borders": { + "none": "None", + "border": "Border", + "grid": "Grid" + } }, "qrcode": { "button": "Print QR Codes", diff --git a/client/public/locales/sv/common.json b/client/public/locales/sv/common.json index 37a25fb..70c7c5d 100644 --- a/client/public/locales/sv/common.json +++ b/client/public/locales/sv/common.json @@ -56,16 +56,22 @@ "printing": { "generic": { "title": "Utskrift", + "description": "Justera inställningarna nedan för att få önskat utskriftslayout. Kom ihåg att skrivare och ditt operativsystem kan tillämpa sina egna marginaler och skalning, så du kan behöva justera inställningarna därefter. Testa på ett vanligt papper innan du skriver ut på de faktiska etiketterna.", "print": "Skriv ut", "columns": "Kolumner", + "rows": "Rader", "paperSize": "Papperstyp", - "rowHeight": "Radhöjd", "showBorder": "Visa ram", "previewScale": "Skala förhandsgranskning", "marginLeft": "Marginal - vänster", "marginRight": "Marginal - höger", "marginTop": "Marginal - topp", - "marginBottom": "Marginal - botten" + "marginBottom": "Marginal - botten", + "borders": { + "none": "Ingen", + "border": "Ram", + "grid": "Rutnät" + } }, "qrcode": { "button": "Skriv ut QR-koder", diff --git a/client/src/components/printing/printingDialog.tsx b/client/src/components/printing/printingDialog.tsx index 2778d34..fd3eee4 100644 --- a/client/src/components/printing/printingDialog.tsx +++ b/client/src/components/printing/printingDialog.tsx @@ -8,7 +8,9 @@ import { Col, Form, Divider, - Switch, + RadioChangeEvent, + Radio, + InputNumber, } from "antd"; import ReactToPrint from "react-to-print"; import { useSavedState } from "../../utils/saveload"; @@ -73,72 +75,112 @@ const PrintingDialog: React.FC = ({ 10 ); const [itemsPerRow, setItemsPerRow] = useSavedState("print-itemsPerRow", 3); + const [rowsPerPage, setRowsPerPage] = useSavedState("print-rowsPerPage", 8); const [paperSize, setPaperSize] = useSavedState("print-paperSize", "A4"); - const [rowHeight, setRowHeight] = useSavedState("print-rowHeight", 70); const [previewScale, setPreviewScale] = useSavedState( "print-previewScale", 0.6 ); - const [showBorder, setShowBorder] = useSavedState("print-showBorder", true); + const [borderShowMode, setBorderShowMode] = useSavedState< + "none" | "border" | "grid" + >("print-borderShowMode", "grid"); const paperWidth = paperDimensions[paperSize].width; const paperHeight = paperDimensions[paperSize].height; const printRef = useRef(null); - const itemsPerPage = - itemsPerRow * - Math.floor((paperHeight - marginTop - marginBottom) / rowHeight); - const pageBlocks = []; - for (let i = 0; i < items.length; i += 1) { - if (i % itemsPerPage === 0) { - pageBlocks.push(items.slice(i, i + itemsPerPage)); - } + const calculatedRowHeight = + (paperHeight - marginTop - marginBottom) / rowsPerPage; + + const rowsOfItems = []; + for (let row_idx = 0; row_idx <= items.length / itemsPerRow; row_idx += 1) { + rowsOfItems.push( + items.slice(row_idx * itemsPerRow, (row_idx + 1) * itemsPerRow) + ); } - const rowBlocks = pageBlocks.map((pageBlock, idx) => ( -
- { + return ( +
+ {row.map((item, index) => ( + + ))} + + ); + }); + + return ( +
- {pageBlock.map((item, index) => ( -
+
+
+ {item} +
+
- {item} - - ))} - - - )); + {itemRows} +
+ + + ); + }); return ( = ({ content={() => printRef.current} />, ]} - width={1000} // Set the modal width to accommodate the preview + width={1200} // Set the modal width to accommodate the preview > - + {t("printing.generic.description")} +
= ({ margin-top: 10mm; } } + + .print-page td { + padding: 0; + } ${style ?? ""} `} - {rowBlocks} + {pages}
- +
- `${value} mm` }} - value={marginLeft} - onChange={(value) => { - setMarginLeft(value); - }} - /> + + + `${value} mm` }} + value={marginLeft} + onChange={(value) => { + setMarginLeft(value); + }} + /> + + + { + setMarginLeft(value ?? 0); + }} + /> + + - `${value} mm` }} - value={marginTop} - onChange={(value) => { - setMarginTop(value); - }} - /> + + + `${value} mm` }} + value={marginTop} + onChange={(value) => { + setMarginTop(value); + }} + /> + + + { + setMarginTop(value ?? 0); + }} + /> + + - `${value} mm` }} - value={marginRight} - onChange={(value) => { - setMarginRight(value); - }} - /> + + + `${value} mm` }} + value={marginRight} + onChange={(value) => { + setMarginRight(value); + }} + /> + + + { + setMarginRight(value ?? 0); + }} + /> + + - `${value} mm` }} - value={marginBottom} - onChange={(value) => { - setMarginBottom(value); - }} - /> + + + `${value} mm` }} + value={marginBottom} + onChange={(value) => { + setMarginBottom(value); + }} + /> + + + { + setMarginBottom(value ?? 0); + }} + /> + + - { - setItemsPerRow(value); - }} - /> + + + { + setItemsPerRow(value); + }} + /> + + + { + setItemsPerRow(value ?? 1); + }} + /> + + + + + + + { + setRowsPerPage(value); + }} + /> + + + { + setRowsPerPage(value ?? 1); + }} + /> + + - - `${value} mm` }} - value={rowHeight} - onChange={(value) => { - setRowHeight(value); - }} - /> - - setShowBorder(checked)} + { + setBorderShowMode(e.target.value); + }} + value={borderShowMode} + optionType="button" + buttonStyle="solid" /> {extraSettings && } {extraSettings} - { - setPreviewScale(value); - }} - /> + + + { + setPreviewScale(value); + }} + /> + + + { + setPreviewScale(value ?? 0.1); + }} + /> + + diff --git a/client/src/components/printing/qrCodePrintingDialog.tsx b/client/src/components/printing/qrCodePrintingDialog.tsx index 6e0dc2f..9ad30b2 100644 --- a/client/src/components/printing/qrCodePrintingDialog.tsx +++ b/client/src/components/printing/qrCodePrintingDialog.tsx @@ -1,4 +1,4 @@ -import { Form, QRCode, Slider, Switch } from "antd"; +import { Col, Form, InputNumber, QRCode, Row, Slider, Switch } from "antd"; import { useSavedState } from "../../utils/saveload"; import PrintingDialog from "./printingDialog"; import { useTranslate } from "@refinedev/core"; @@ -69,17 +69,34 @@ const QRCodePrintingDialog: React.FC = ({ />
- `${value} mm` }} - min={3} - max={15} - value={textSize} - step={0.1} - onChange={(value) => { - setTextSize(value); - }} - /> + + + `${value} mm` }} + min={3} + max={15} + value={textSize} + step={0.1} + onChange={(value) => { + setTextSize(value); + }} + /> + + + { + setTextSize(value ?? 5); + }} + /> + + { setStep(1); }} items={selectedSpools.map((spool) => ({ - value: `S:${spool.id}`, - // value: `https://spoolman.lan/spool/show/${spool.id}`, - // label: `hello`, + value: `web+spoolman:s-${spool.id}`, + label: `s-${spool.id}`, errorLevel: "H", }))} /> From d7ccd2425747951fa8ee9865543e38796ca391dd Mon Sep 17 00:00:00 2001 From: Donkie Date: Wed, 2 Aug 2023 21:55:46 +0200 Subject: [PATCH 10/16] Client: Add ability to block rows/columns in print Also fixed some issues with drawing the border --- client/public/locales/en/common.json | 4 + client/public/locales/sv/common.json | 4 + .../components/printing/printingDialog.tsx | 189 ++++++++++++++++-- 3 files changed, 182 insertions(+), 15 deletions(-) diff --git a/client/public/locales/en/common.json b/client/public/locales/en/common.json index b057d51..1e475ba 100644 --- a/client/public/locales/en/common.json +++ b/client/public/locales/en/common.json @@ -67,6 +67,10 @@ "marginRight": "Right Margin", "marginTop": "Top Margin", "marginBottom": "Bottom Margin", + "blockRowsTop": "Block Rows Top", + "blockRowsBottom": "Block Rows Bottom", + "blockColumnsLeft": "Block Columns Left", + "blockColumnsRight": "Block Columns Right", "borders": { "none": "None", "border": "Border", diff --git a/client/public/locales/sv/common.json b/client/public/locales/sv/common.json index 70c7c5d..28ee75d 100644 --- a/client/public/locales/sv/common.json +++ b/client/public/locales/sv/common.json @@ -67,6 +67,10 @@ "marginRight": "Marginal - höger", "marginTop": "Marginal - topp", "marginBottom": "Marginal - botten", + "blockRowsTop": "Blockera rader - topp", + "blockRowsBottom": "Blockera rader - botten", + "blockColumnsLeft": "Blockera kolumner - vänster", + "blockColumnsRight": "Blockera kolumner - höger", "borders": { "none": "Ingen", "border": "Ram", diff --git a/client/src/components/printing/printingDialog.tsx b/client/src/components/printing/printingDialog.tsx index fd3eee4..b95ed87 100644 --- a/client/src/components/printing/printingDialog.tsx +++ b/client/src/components/printing/printingDialog.tsx @@ -74,8 +74,24 @@ const PrintingDialog: React.FC = ({ "print-marginBottom", 10 ); - const [itemsPerRow, setItemsPerRow] = useSavedState("print-itemsPerRow", 3); - const [rowsPerPage, setRowsPerPage] = useSavedState("print-rowsPerPage", 8); + const [blockRowsTop, setBlockRowsTop] = useSavedState( + "print-blockRowsTop", + 0 + ); + const [blockRowsBottom, setBlockRowsBottom] = useSavedState( + "print-blockRowsBottom", + 0 + ); + const [blockColumnsLeft, setBlockColumnsLeft] = useSavedState( + "print-blockColumnsLeft", + 0 + ); + const [blockColumnsRight, setBlockColumnsRight] = useSavedState( + "print-blockColumnsRight", + 0 + ); + const [paperColumns, setPaperColumns] = useSavedState("print-itemsPerRow", 3); + const [paperRows, setPaperRows] = useSavedState("print-rowsPerPage", 8); const [paperSize, setPaperSize] = useSavedState("print-paperSize", "A4"); const [previewScale, setPreviewScale] = useSavedState( "print-previewScale", @@ -91,7 +107,10 @@ const PrintingDialog: React.FC = ({ const printRef = useRef(null); const calculatedRowHeight = - (paperHeight - marginTop - marginBottom) / rowsPerPage; + (paperHeight - marginTop - marginBottom) / paperRows; + + const itemsPerRow = paperColumns - blockColumnsLeft - blockColumnsRight; + const rowsPerPage = paperRows - blockRowsTop - blockRowsBottom; const rowsOfItems = []; for (let row_idx = 0; row_idx <= items.length / itemsPerRow; row_idx += 1) { @@ -112,9 +131,30 @@ const PrintingDialog: React.FC = ({ } const pages = pageBlocks.map(function (rows, idx) { + const itemRowsBlockTop = [...Array(blockRowsTop).keys()].map((row) => ( + + +
+ + + )); const itemRows = rows.map((row, idx) => { + const itemColumnsBlockLeft = [...Array(blockColumnsLeft).keys()].map( + (col) => ( + +
+ + ) + ); return ( + {itemColumnsBlockLeft} {row.map((item, index) => (
= ({ justifyContent: "center", alignItems: "center", width: `${ - (paperWidth - marginLeft - marginRight) / itemsPerRow + (paperWidth - marginLeft - marginRight) / paperColumns }mm`, height: `${calculatedRowHeight}mm`, flexDirection: "column", @@ -153,6 +193,9 @@ const PrintingDialog: React.FC = ({
= ({ > + {itemRowsBlockTop} {itemRows}
@@ -252,6 +293,7 @@ const PrintingDialog: React.FC = ({
@@ -369,9 +411,11 @@ const PrintingDialog: React.FC = ({ { - setItemsPerRow(value); + if (value - blockColumnsLeft - blockColumnsRight < 1) + return; + setPaperColumns(value); }} /> @@ -379,9 +423,14 @@ const PrintingDialog: React.FC = ({ { - setItemsPerRow(value ?? 1); + if ( + (value ?? 1) - blockColumnsLeft - blockColumnsRight < + 1 + ) + return; + setPaperColumns(value ?? 1); }} /> @@ -393,9 +442,10 @@ const PrintingDialog: React.FC = ({ { - setRowsPerPage(value); + if (value - blockRowsTop - blockRowsBottom < 1) return; + setPaperRows(value); }} /> @@ -403,9 +453,11 @@ const PrintingDialog: React.FC = ({ { - setRowsPerPage(value ?? 1); + if ((value ?? 1) - blockRowsTop - blockRowsBottom < 1) + return; + setPaperRows(value ?? 1); }} /> @@ -441,6 +493,113 @@ const PrintingDialog: React.FC = ({ buttonStyle="solid" /> + + + + { + if (paperRows - value - blockRowsBottom < 1) return; + setBlockRowsTop(value); + }} + /> + + + { + if (paperRows - (value ?? 0) - blockRowsBottom < 1) + return; + setBlockRowsTop(value ?? 0); + }} + /> + + + + + + + { + if (paperRows - value - blockRowsTop < 1) return; + setBlockRowsBottom(value); + }} + /> + + + { + if (paperRows - (value ?? 0) - blockRowsTop < 1) return; + setBlockRowsBottom(value ?? 0); + }} + /> + + + + + + + { + if (paperColumns - value - blockColumnsRight < 1) return; + setBlockColumnsLeft(value); + }} + /> + + + { + if (paperColumns - (value ?? 0) - blockColumnsRight < 1) + return; + setBlockColumnsLeft(value ?? 0); + }} + /> + + + + + + + { + if (paperColumns - value - blockColumnsLeft < 1) return; + setBlockColumnsRight(value); + }} + /> + + + { + if (paperColumns - (value ?? 0) - blockColumnsLeft < 1) + return; + setBlockColumnsRight(value ?? 0); + }} + /> + + + {extraSettings && } {extraSettings} From 3eda3580f60fec182569be9d261eba54f134a2ba Mon Sep 17 00:00:00 2001 From: Donkie Date: Thu, 3 Aug 2023 21:51:49 +0200 Subject: [PATCH 11/16] Client: Added simple qr code scanner --- client/package.json | 1 + client/src/components/header/index.tsx | 2 + client/src/components/qrCodeScanner.tsx | 114 ++++++++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 client/src/components/qrCodeScanner.tsx diff --git a/client/package.json b/client/package.json index 6b8800e..a3e6c72 100644 --- a/client/package.json +++ b/client/package.json @@ -21,6 +21,7 @@ "i18next": "^23.2.8", "i18next-browser-languagedetector": "^7.1.0", "i18next-xhr-backend": "^3.2.2", + "qr-scanner": "^1.4.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^13.0.1", diff --git a/client/src/components/header/index.tsx b/client/src/components/header/index.tsx index e57c734..eacdd25 100644 --- a/client/src/components/header/index.tsx +++ b/client/src/components/header/index.tsx @@ -15,6 +15,7 @@ import { ColorModeContext } from "../../contexts/color-mode"; import "/node_modules/flag-icons/css/flag-icons.min.css"; import { languages } from "../../i18n"; +import QRCodeScannerModal from "../qrCodeScanner"; const { useToken } = theme; @@ -84,6 +85,7 @@ export const Header: React.FC = ({ onChange={() => setMode(mode === "light" ? "dark" : "light")} defaultChecked={mode === "dark"} /> + ); diff --git a/client/src/components/qrCodeScanner.tsx b/client/src/components/qrCodeScanner.tsx new file mode 100644 index 0000000..74f0265 --- /dev/null +++ b/client/src/components/qrCodeScanner.tsx @@ -0,0 +1,114 @@ +import React, { useEffect, useRef, useState } from "react"; +import { Button, FloatButton, Modal, Space } from "antd"; +import QrScanner from "qr-scanner"; // Make sure to import from the correct path +import { useNavigate } from "react-router-dom"; +import { CameraOutlined } from "@ant-design/icons"; + +const QRCodeScannerModal: React.FC = () => { + const [visible, setVisible] = useState(false); + const videoRef = useRef(null); + const scanner = useRef(null); + const navigate = useNavigate(); + const [hasCamera, setHasCamera] = useState(false); + + useEffect(() => { + const onScan = (result: string) => { + // Check for the spoolman ID format + const match = result.match(/^web+spoolman:s-(?[0-9]+)$/); + if (match && match.groups) { + navigate(`/spool/show/${match.groups.id}`); + } + }; + + const startCamera = async () => { + console.log("Starting camera"); + if (!(await QrScanner.hasCamera())) { + console.log("No camera found on this device"); + setHasCamera(false); + return; + } else { + console.log("Camera found on this device"); + setHasCamera(true); + if (videoRef.current) { + scanner.current = new QrScanner( + videoRef.current, + (result) => { + console.log("QR Code detected:", result); + onScan(result.data); + }, + { + preferredCamera: "environment", + } + ); + console.log("Starting scanner"); + try { + await scanner.current.start(); + } catch (e) { + setHasCamera(false); + console.log(e); + } + } else { + console.log("No video ref"); + } + } + }; + if (visible) { + startCamera(); + } + + return () => { + console.log("Destroying scanner"); + scanner.current?.destroy(); + scanner.current = null; + }; + }, [visible, navigate]); + + // Draw a centered div if no camera was found + + return ( + <> + setVisible(true)} + icon={} + shape="circle" + /> + setVisible(false)} + footer={null} + title="QR Code Scanner" + > + +

Scan a Spoolman QR code to view details about the spool.

+
+
+ + ); +}; + +export default QRCodeScannerModal; From edd75e86be41d75b404556202d0c38b152cc3b99 Mon Sep 17 00:00:00 2001 From: Donkie Date: Thu, 3 Aug 2023 22:26:17 +0200 Subject: [PATCH 12/16] Client: Switched to react-qr-scanner --- client/package-lock.json | 327 +++++++++++++++++++++++- client/package.json | 5 +- client/public/locales/en/common.json | 9 + client/src/components/qrCodeScanner.tsx | 125 ++++----- client/vite.config.ts | 4 +- 5 files changed, 388 insertions(+), 82 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index f12f1ff..83c8f79 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -17,6 +17,7 @@ "@refinedev/simple-rest": "^4.5.0", "@tanstack/react-query": "^4.29.19", "@types/loadable__component": "^5.13.4", + "@yudiel/react-qr-scanner": "^1.1.10", "antd": "^5.6.4", "flag-icons": "^6.7.0", "i18next": "^23.2.8", @@ -41,7 +42,8 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.1", "typescript": "^5.1.6", - "vite": "^4.4.1" + "vite": "^4.4.1", + "vite-plugin-mkcert": "^1.16.0" }, "engines": { "node": "16.x" @@ -1554,6 +1556,195 @@ "node": ">= 8" } }, + "node_modules/@octokit/auth-token": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.4.tgz", + "integrity": "sha512-TWFX7cZF2LXoCvdmJWY7XVPi74aSY0+FfBZNSXEXFkMpjcqsQwDSYVv5FhRFaI0V1ECnwbz4j59T/G+rXNWaIQ==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/core": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.2.4.tgz", + "integrity": "sha512-rYKilwgzQ7/imScn3M9/pFfUf4I1AZEH3KhyJmtPdE2zfaXAn2mFfUy4FbKewzc2We5y/LlKLj36fWJLKC2SIQ==", + "dev": true, + "dependencies": { + "@octokit/auth-token": "^3.0.0", + "@octokit/graphql": "^5.0.0", + "@octokit/request": "^6.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^9.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/endpoint": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.6.tgz", + "integrity": "sha512-5L4fseVRUsDFGR00tMWD/Trdeeihn999rTMGRMC1G/Ldi1uWlWJzI98H4Iak5DB/RVvQuyMYKqSK/R6mbSOQyg==", + "dev": true, + "dependencies": { + "@octokit/types": "^9.0.0", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/endpoint/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@octokit/graphql": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.6.tgz", + "integrity": "sha512-Fxyxdy/JH0MnIB5h+UQ3yCoh1FG4kWXfFKkpWqjZHw/p+Kc8Y44Hu/kCgNBT6nU1shNumEchmW/sUO1JuQnPcw==", + "dev": true, + "dependencies": { + "@octokit/request": "^6.0.0", + "@octokit/types": "^9.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.0.0.tgz", + "integrity": "sha512-V8GImKs3TeQRxRtXFpG2wl19V7444NIOTDF24AWuIbmNaNYOQMWRbjcGDXV5B+0n887fgDcuMNOmlul+k+oJtw==", + "dev": true + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-6.1.2.tgz", + "integrity": "sha512-qhrmtQeHU/IivxucOV1bbI/xZyC/iOBhclokv7Sut5vnejAIAEXVcGQeRpQlU39E0WwK9lNvJHphHri/DB6lbQ==", + "dev": true, + "dependencies": { + "@octokit/tsconfig": "^1.0.2", + "@octokit/types": "^9.2.3" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "@octokit/core": ">=4" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", + "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", + "dev": true, + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-7.2.3.tgz", + "integrity": "sha512-I5Gml6kTAkzVlN7KCtjOM+Ruwe/rQppp0QU372K1GP7kNOYEKe8Xn5BW4sE62JAHdwpq95OQK/qGNyKQMUzVgA==", + "dev": true, + "dependencies": { + "@octokit/types": "^10.0.0" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-10.0.0.tgz", + "integrity": "sha512-Vm8IddVmhCgU1fxC1eyinpwqzXPEYu0NrYzD3YZjlGjyftdLBTeqNblRC0jmJmgxbJIsQlyogVeGnrNaaMVzIg==", + "dev": true, + "dependencies": { + "@octokit/openapi-types": "^18.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.8.tgz", + "integrity": "sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw==", + "dev": true, + "dependencies": { + "@octokit/endpoint": "^7.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^9.0.0", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/request-error": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz", + "integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==", + "dev": true, + "dependencies": { + "@octokit/types": "^9.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/request/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@octokit/rest": { + "version": "19.0.13", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-19.0.13.tgz", + "integrity": "sha512-/EzVox5V9gYGdbAI+ovYj3nXQT1TtTHRT+0eZPcuC05UFSWO3mdO9UY1C0i2eLF9Un1ONJkAk+IEtYGAC+TahA==", + "dev": true, + "dependencies": { + "@octokit/core": "^4.2.1", + "@octokit/plugin-paginate-rest": "^6.1.2", + "@octokit/plugin-request-log": "^1.0.4", + "@octokit/plugin-rest-endpoint-methods": "^7.1.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/tsconfig": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@octokit/tsconfig/-/tsconfig-1.0.2.tgz", + "integrity": "sha512-I0vDR0rdtP8p2lGMzvsJzbhdOWy405HcGovrspJ8RRibHnyRgggUSNO5AIox5LmqiwmatHKYsvj6VGFHkqS7lA==", + "dev": true + }, + "node_modules/@octokit/types": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz", + "integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==", + "dev": true, + "dependencies": { + "@octokit/openapi-types": "^18.0.0" + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", @@ -2539,6 +2730,38 @@ "vite": "^4.2.0" } }, + "node_modules/@yudiel/react-qr-scanner": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@yudiel/react-qr-scanner/-/react-qr-scanner-1.1.10.tgz", + "integrity": "sha512-+YIHbnocynkkGgyMK8MMkn1NBnJRwJntdm4shQ+linm4NdGdSAavLYxJYwnk2PixZyVu/GZ3Uc2aM0XWlBq+Zw==", + "dependencies": { + "@zxing/library": "^0.20.0" + }, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + } + }, + "node_modules/@zxing/library": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.20.0.tgz", + "integrity": "sha512-6Ev6rcqVjMakZFIDvbUf0dtpPGeZMTfyxYg4HkVWioWeN7cRcnUWT3bU6sdohc82O1nPXcjq6WiGfXX2Pnit6A==", + "dependencies": { + "ts-custom-error": "^3.2.1" + }, + "engines": { + "node": ">= 10.4.0" + }, + "optionalDependencies": { + "@zxing/text-encoding": "~0.9.0" + } + }, + "node_modules/@zxing/text-encoding": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", + "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", + "optional": true + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -2831,6 +3054,12 @@ "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", @@ -2946,6 +3175,12 @@ } ] }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "dev": true + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -3523,6 +3758,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/comma-separated-tokens": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", @@ -3791,6 +4038,15 @@ "node": ">=0.10.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -3800,6 +4056,12 @@ "node": ">= 0.8" } }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -4927,6 +5189,20 @@ "node": ">=0.10.0" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -7641,6 +7917,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -9791,6 +10073,14 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-custom-error": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz", + "integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tslib": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", @@ -10004,6 +10294,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", + "dev": true + }, "node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -10255,6 +10551,35 @@ } } }, + "node_modules/vite-plugin-mkcert": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/vite-plugin-mkcert/-/vite-plugin-mkcert-1.16.0.tgz", + "integrity": "sha512-5r+g8SB9wZzLNUFekGwZo3e0P6QlS6rbxK5p9z/itxNAimsYohgjK/YfVPVxM9EuglP9hjridq0lUejo9v1nVg==", + "dev": true, + "dependencies": { + "@octokit/rest": "^19.0.5", + "axios": "^1.2.2", + "debug": "^4.3.4", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=v16.7.0" + }, + "peerDependencies": { + "vite": ">=3" + } + }, + "node_modules/vite-plugin-mkcert/node_modules/axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/vite-plugin-svgr": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-3.2.0.tgz", diff --git a/client/package.json b/client/package.json index a3e6c72..38995e1 100644 --- a/client/package.json +++ b/client/package.json @@ -16,12 +16,12 @@ "@refinedev/simple-rest": "^4.5.0", "@tanstack/react-query": "^4.29.19", "@types/loadable__component": "^5.13.4", + "@yudiel/react-qr-scanner": "^1.1.10", "antd": "^5.6.4", "flag-icons": "^6.7.0", "i18next": "^23.2.8", "i18next-browser-languagedetector": "^7.1.0", "i18next-xhr-backend": "^3.2.2", - "qr-scanner": "^1.4.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^13.0.1", @@ -41,7 +41,8 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.1", "typescript": "^5.1.6", - "vite": "^4.4.1" + "vite": "^4.4.1", + "vite-plugin-mkcert": "^1.16.0" }, "scripts": { "dev": "refine dev", diff --git a/client/public/locales/en/common.json b/client/public/locales/en/common.json index 1e475ba..8c86887 100644 --- a/client/public/locales/en/common.json +++ b/client/public/locales/en/common.json @@ -94,6 +94,15 @@ "selectedTotal_other": "{{count}} spools selected" } }, + "scanner": { + "error": { + "notAllowed": "You have not allowed access to the camera.", + "insecureContext": "The page is not served over HTTPS.", + "streamApiNotSupported": "The browser does not support the MediaStream API.", + "notReadable": "The camera is not readable.", + "unknown": "An unknown error occurred. ({{error}})" + } + }, "spool": { "spool": "Spools", "fields": { diff --git a/client/src/components/qrCodeScanner.tsx b/client/src/components/qrCodeScanner.tsx index 74f0265..32d8a3a 100644 --- a/client/src/components/qrCodeScanner.tsx +++ b/client/src/components/qrCodeScanner.tsx @@ -1,69 +1,30 @@ -import React, { useEffect, useRef, useState } from "react"; -import { Button, FloatButton, Modal, Space } from "antd"; -import QrScanner from "qr-scanner"; // Make sure to import from the correct path +import React, { useEffect, useState } from "react"; +import { FloatButton, Modal, Space } from "antd"; +import { QrScanner } from "@yudiel/react-qr-scanner"; import { useNavigate } from "react-router-dom"; import { CameraOutlined } from "@ant-design/icons"; +import { useTranslate } from "@refinedev/core"; const QRCodeScannerModal: React.FC = () => { const [visible, setVisible] = useState(false); - const videoRef = useRef(null); - const scanner = useRef(null); + const [lastError, setLastError] = useState(null); + const t = useTranslate(); const navigate = useNavigate(); - const [hasCamera, setHasCamera] = useState(false); + + const onScan = (result: string) => { + // Check for the spoolman ID format + const match = result.match(/^web\+spoolman:s-(?[0-9]+)$/); + if (match && match.groups) { + setVisible(false); + navigate(`/spool/show/${match.groups.id}`); + } + }; useEffect(() => { - const onScan = (result: string) => { - // Check for the spoolman ID format - const match = result.match(/^web+spoolman:s-(?[0-9]+)$/); - if (match && match.groups) { - navigate(`/spool/show/${match.groups.id}`); - } - }; - - const startCamera = async () => { - console.log("Starting camera"); - if (!(await QrScanner.hasCamera())) { - console.log("No camera found on this device"); - setHasCamera(false); - return; - } else { - console.log("Camera found on this device"); - setHasCamera(true); - if (videoRef.current) { - scanner.current = new QrScanner( - videoRef.current, - (result) => { - console.log("QR Code detected:", result); - onScan(result.data); - }, - { - preferredCamera: "environment", - } - ); - console.log("Starting scanner"); - try { - await scanner.current.start(); - } catch (e) { - setHasCamera(false); - console.log(e); - } - } else { - console.log("No video ref"); - } - } - }; if (visible) { - startCamera(); + setLastError(null); } - - return () => { - console.log("Destroying scanner"); - scanner.current?.destroy(); - scanner.current = null; - }; - }, [visible, navigate]); - - // Draw a centered div if no camera was found + }, [visible]); return ( <> @@ -81,30 +42,38 @@ const QRCodeScannerModal: React.FC = () => { >

Scan a Spoolman QR code to view details about the spool.

-
diff --git a/client/vite.config.ts b/client/vite.config.ts index 6335a4d..e315b23 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,8 +1,10 @@ import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; import svgr from "vite-plugin-svgr"; +import mkcert from 'vite-plugin-mkcert' export default defineConfig({ - plugins: [react(), svgr()], + server: { https: true }, + plugins: [react(), svgr(), mkcert()], }); From b95443228cb8a06dbb1efdb63a7ee663a09051a2 Mon Sep 17 00:00:00 2001 From: Donkie Date: Thu, 3 Aug 2023 22:27:20 +0200 Subject: [PATCH 13/16] Client: Removed unsetting lasterror --- client/src/components/qrCodeScanner.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/client/src/components/qrCodeScanner.tsx b/client/src/components/qrCodeScanner.tsx index 32d8a3a..0b4bf87 100644 --- a/client/src/components/qrCodeScanner.tsx +++ b/client/src/components/qrCodeScanner.tsx @@ -20,12 +20,6 @@ const QRCodeScannerModal: React.FC = () => { } }; - useEffect(() => { - if (visible) { - setLastError(null); - } - }, [visible]); - return ( <> Date: Thu, 3 Aug 2023 22:31:09 +0200 Subject: [PATCH 14/16] Client: Scanner translations update --- client/public/locales/en/common.json | 2 ++ client/public/locales/sv/common.json | 11 +++++++++++ client/src/components/qrCodeScanner.tsx | 6 +++--- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/client/public/locales/en/common.json b/client/public/locales/en/common.json index 8c86887..0bceaad 100644 --- a/client/public/locales/en/common.json +++ b/client/public/locales/en/common.json @@ -95,6 +95,8 @@ } }, "scanner": { + "title": "QR Code Scanner", + "description": "Scan a Spoolman QR code to view details about the spool.", "error": { "notAllowed": "You have not allowed access to the camera.", "insecureContext": "The page is not served over HTTPS.", diff --git a/client/public/locales/sv/common.json b/client/public/locales/sv/common.json index 28ee75d..e9de4a4 100644 --- a/client/public/locales/sv/common.json +++ b/client/public/locales/sv/common.json @@ -94,6 +94,17 @@ "selectedTotal_other": "{{count}} spolar valda" } }, + "scanner": { + "title": "Skanna QR-kod", + "description": "Skanna en QR-kod för att visa information om en spole.", + "error": { + "notAllowed": "Du har inte tillåtit åtkomst till kameran.", + "insecureContext": "Sidan måste använda sig av HTTPS.", + "streamApiNotSupported": "Webbläsaren stödjer inte MediaStream API.", + "notReadable": "Kameran är inte läsbar.", + "unknown": "Ett okänt fel inträffade. ({{error}})" + } + }, "spool": { "spool": "Spolar", "fields": { diff --git a/client/src/components/qrCodeScanner.tsx b/client/src/components/qrCodeScanner.tsx index 0b4bf87..aa80e80 100644 --- a/client/src/components/qrCodeScanner.tsx +++ b/client/src/components/qrCodeScanner.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { FloatButton, Modal, Space } from "antd"; import { QrScanner } from "@yudiel/react-qr-scanner"; import { useNavigate } from "react-router-dom"; @@ -32,10 +32,10 @@ const QRCodeScannerModal: React.FC = () => { open={visible} onCancel={() => setVisible(false)} footer={null} - title="QR Code Scanner" + title={t("scanner.title")} > -

Scan a Spoolman QR code to view details about the spool.

+

{t("scanner.description")}

Date: Tue, 8 Aug 2023 21:31:45 +0200 Subject: [PATCH 15/16] Client: Improved scanner error if not HTTPS --- client/src/components/qrCodeScanner.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/src/components/qrCodeScanner.tsx b/client/src/components/qrCodeScanner.tsx index aa80e80..cd09006 100644 --- a/client/src/components/qrCodeScanner.tsx +++ b/client/src/components/qrCodeScanner.tsx @@ -55,9 +55,14 @@ const QRCodeScannerModal: React.FC = () => { } onDecode={onScan} onError={(error: Error) => { + console.error(error); if (error.name === "NotAllowedError") { setLastError(t("scanner.error.notAllowed")); - } else if (error.name === "InsecureContextError") { + } else if ( + error.name === "InsecureContextError" || + (location.protocol !== "https:" && + navigator.mediaDevices === undefined) + ) { setLastError(t("scanner.error.insecureContext")); } else if (error.name === "StreamApiNotSupportedError") { setLastError(t("scanner.error.streamApiNotSupported")); From c6de1c403a1d49e872b45f69e9f130281659f7fa Mon Sep 17 00:00:00 2001 From: Donkie Date: Tue, 8 Aug 2023 21:32:07 +0200 Subject: [PATCH 16/16] Improved dockerfile client builder --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index d6f46a3..7878be9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,9 +2,9 @@ FROM node:16-alpine as client-builder COPY ./client /client WORKDIR /client -RUN npm install +RUN npm ci -RUN echo "VITE_APIURL=/api/v1" > .env.production +RUN rm -f .env && echo "VITE_APIURL=/api/v1" > .env.production RUN npm run build FROM python:3.11-alpine as python-builder