diff --git a/docs/tdev/gallery/tools/qr-scanner/index.mdx b/docs/tdev/gallery/tools/qr-scanner/index.mdx new file mode 100644 index 000000000..cc9f40035 --- /dev/null +++ b/docs/tdev/gallery/tools/qr-scanner/index.mdx @@ -0,0 +1,69 @@ +--- +page_id: e831b10f-8331-4e6f-a6d5-8503cbffec54 +tags: ["tools"] +--- + +import Scanner from '@tdev/qr-scanner'; +import BrowserWindow from '@tdev-components/BrowserWindow'; + +# QR Code Scanner + +QR Codes sind praktisch um Inhalte wie URLs oder anderes schnell zu teilen. Mit dem QR Code Scanner können Sie QR Codes scannen und die darin enthaltenen Informationen anzeigen. + +```mdx +import Scanner from '@tdev/qr-scanner'; + + +``` + + + + + + +## Links direkt öffnen + +```mdx +import Scanner from '@tdev-components/shared/QR-Code/Scanner'; + + +``` + + + + + +## Optionen + +`onScan` +: Callback Funktion, die aufgerufen wird, wenn ein QR Code gescannt wurde. +:::dd +```tsx + console.log(content)} /> +``` +::: +`hideOpenLinkButton` +: Versteckt den Button zum Öffnen des Inhalts als Link. +: `boolean` +: Standard: `false` +`redirect` +: Der Inhalt des QR Codes als URL behandelt und direkt geöffnet. +: `boolean` +: Standard: `false` +:::dd +`inNewTab` +: Der Inhalt des QR Codes wird in einem neuen Tab geöffnet. +: `boolean` +: Standard: `false` +::: +## Installation + +:::info[Code] +- `packages/tdev/qr-scanner` +::: + +:::info[npm] +```bash +yarn add @yudiel/react-qr-scanner +``` +::: \ No newline at end of file diff --git a/packages/tdev/qr-scanner/README.mdx b/packages/tdev/qr-scanner/README.mdx new file mode 100644 index 000000000..072ac5da0 --- /dev/null +++ b/packages/tdev/qr-scanner/README.mdx @@ -0,0 +1,85 @@ +--- +page_id: e831b10f-8331-4e6f-a6d5-8503cbffec54 +tags: ['tools'] +--- + +import { mdiEarth } from '@mdi/js'; + +import QrCode from '@tdev-scanner'; + +import BrowserWindow from '@tdev-components/BrowserWindow'; + +# QR Codes + +QR Codes sind praktisch um Inhalte wie URLs oder anderes schnell zu teilen. + +```mdx +import QrCode from '@tdev-components/shared/QrCode'; + + +``` + + + + + +## Inhalt anzeigen + +```mdx + +``` + + + + + +## Mit Texteingabe + +```mdx + +``` + + + + + +## Mit Icon + +Es können auch MdiIcons im QR Code eingebettet werden. + +:::warning[Grösse und Farbe] Da das Icon als SVG eingebettet wird, muss die Grösse als Zahl (in Pixel) und die Farbe als HTML-Farbe angegeben werden. CSS-Variablen oder -Grössen funktionieren nicht. ::: + +```mdx + + +``` + + + :::flex{justifyContent="space-around"} + + +::br + + + +::: + + + +## Optionen + +`text` : Der Text, der im QR Code codiert wird. : `string` `showText` : Zeigt den Text unter dem QR Code an. : `boolean` : Standard: `false` `isLink` : Der Text wird als Link interpretiert und als ``-Tag dargestellt. `linkText` : Linktext der anstelle des `text`-Feldes angezeigt werden soll. : Standard: die URL im `text` Feld `withInput` : Zeigt ein Input Feld um den Text zu ändern. : `boolean` : Standard: `false` `size` : Die Grösse des QR Codes. : `string` - CSS Grösse, z.B. `20em` : Standard: `15em` (im css gesetzt) `className` : Eine CSS Klasse für den umgebenden `div`-Container. : `string` `icon` : Ein MdiIcon (oder ein SVG-Path), das im QR Code eingebettet wird. : `string` `download` : Es wird ein Download-Button angezeigt und der QR-Code kann als `.png` heruntergeladen werden. : Standard: `false` `iconSize` : Die Grösse des Icons. : `number` - eine Zahl in Pixel (kann nicht als CSS-Grösse angegeben werden!) : Standard: `32` `iconColor` : Die Farbe des Icons. : `string` - eine HTML-Farbe (kann nicht als CSS-Variable angegeben werden!) : Standard: #306cce `qrProps` : Weitere Props für das `QRCode`-Element. : [QRCode.React](https://www.npmjs.com/package/qrcode.react#props) + +## Installation + +:::info[Code] + +- `src/components/shared/QrCode` ::: + +:::info[npm] + +```bash +yarn add qrcode.react +``` + +::: diff --git a/packages/tdev/qr-scanner/index.tsx b/packages/tdev/qr-scanner/index.tsx new file mode 100644 index 000000000..5f0425c7d --- /dev/null +++ b/packages/tdev/qr-scanner/index.tsx @@ -0,0 +1,181 @@ +import React from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.scss'; +import Loader from '@tdev-components/Loader'; +import Button from '@tdev-components/shared/Button'; +import { useClientLib } from '@tdev-hooks/useClientLib'; +import type { default as QrScannerLib } from '@yudiel/react-qr-scanner'; +import Storage from '@tdev-stores/utils/Storage'; +import { mdiCameraFlipOutline } from '@mdi/js'; +import SelectInput from '@tdev-components/shared/SelectInput'; +import CodeBlockWrapper from '@tdev/theme/CodeBlock'; + +interface Props { + redirect?: boolean; + inNewTab?: boolean; + hideOpenLinkButton?: boolean; + onScan?: (qr: string) => void; +} +const Scanner = (props: Props) => { + const [reloadKey, setReloadKey] = React.useState(1); + const deviceId = React.useRef(Storage.get('QrScannerDeviceId')); + const Lib = useClientLib( + () => import('@yudiel/react-qr-scanner'), + '@yudiel/react-qr-scanner' + ); + if (!Lib) { + return ; + } + return ( + { + if (deviceId.current === id && reloadKey > 5) { + setReloadKey(1); + Storage.remove('QrScannerDeviceId'); + } else { + setReloadKey((prev) => prev + 1); + } + deviceId.current = id; + }} + {...props} + /> + ); +}; + +const ScannerComponent = ( + props: { Lib: typeof QrScannerLib; onChangeSrc: (id?: string) => void } & Props +) => { + const { Lib } = props; + const [error, setError] = React.useState(); + const [qr, setQr] = React.useState(''); + const [deviceId, setDeviceId] = React.useState( + Storage.get('QrScannerDeviceId', undefined) + ); + const devices = Lib.useDevices(); + React.useEffect(() => { + if (devices.length > 0 && deviceId) { + if (!devices.find((d) => d.deviceId === deviceId)) { + Storage.remove('QrScannerDeviceId'); + setDeviceId(undefined); + } + } + }, [devices]); + const clearErrorMessage = React.useCallback(() => { + setError(undefined); + }, []); + const deviceIdx = devices.findIndex((d) => d.deviceId === deviceId); + const showFooter = qr || devices.length > 1 || error; + return ( +
+
+ {devices.length > 0 ? ( + { + clearErrorMessage(); + setQr(result[0].rawValue); + if (props.onScan) { + props.onScan(result[0].rawValue); + } + if (props.redirect) { + try { + const url = new URL(result[0].rawValue); + if (props.inNewTab) { + window.open(url.href, '_blank'); + } else { + window.location.href = url.href; + } + } catch (e) { + setError(`Invalide URL: "${result[0].rawValue}"`); + } + } + }} + onError={(err) => { + setTimeout(() => { + props.onChangeSrc(deviceId); + }, 30); + setError('Die Kamera konnte nicht gestartet werden.'); + }} + constraints={{ + deviceId: deviceId + }} + allowMultiple={false} + components={{ + torch: true, + finder: true, + zoom: true, + onOff: true + }} + sound={false} + /> + ) : ( + + )} +
+ {showFooter && ( +
+ {devices.length > 1 && ( + { + Storage.set('QrScannerDeviceId', id); + props.onChangeSrc(id); + }} + options={devices.map((d) => d.deviceId)} + labels={devices.map((d, idx) => d.label || `${d.kind}-${idx}`)} + /> + //
+ + )} + {error && ( + + {error} + + )} +
+ )} + + ); +}; + +export default Scanner; diff --git a/packages/tdev/qr-scanner/package.json b/packages/tdev/qr-scanner/package.json new file mode 100644 index 000000000..95fd81ca3 --- /dev/null +++ b/packages/tdev/qr-scanner/package.json @@ -0,0 +1,19 @@ +{ + "name": "@tdev/qr-scanner", + "version": "1.0.0", + "main": "index.tsx", + "types": "index.tsx", + "dependencies": { + "@yudiel/react-qr-scanner": "^2.3.1" + }, + "devDependencies": { + "vitest": "*", + "@docusaurus/module-type-aliases": "*", + "@docusaurus/core": "*", + "@docusaurus/theme-classic": "*", + "@types/node": "*" + }, + "peerDependencies": { + "@tdev/core": "1.0.0" + } +} diff --git a/packages/tdev/qr-scanner/styles.module.scss b/packages/tdev/qr-scanner/styles.module.scss new file mode 100644 index 000000000..d2e5c72d9 --- /dev/null +++ b/packages/tdev/qr-scanner/styles.module.scss @@ -0,0 +1,14 @@ +.qr { + width: 15em; + .scanner { + padding: 0; + height: 15em; + } +} + +.error { + :global(.theme-code-block-highlighted-line) { + color: var(--ifm-color-danger-darker) !important; + background: transparent; + } +} diff --git a/packages/tdev/qr-scanner/tsconfig.json b/packages/tdev/qr-scanner/tsconfig.json new file mode 100644 index 000000000..ea56794f8 --- /dev/null +++ b/packages/tdev/qr-scanner/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../../tsconfig.json" +} diff --git a/yarn.lock b/yarn.lock index 8f311ebdf..cb9252d0f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2241,7 +2241,7 @@ "@docusaurus/theme-search-algolia" "3.8.1" "@docusaurus/types" "3.8.1" -"@docusaurus/theme-classic@3.8.1", "@docusaurus/theme-classic@^3.8.1": +"@docusaurus/theme-classic@*", "@docusaurus/theme-classic@3.8.1", "@docusaurus/theme-classic@^3.8.1": version "3.8.1" resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-3.8.1.tgz#1e45c66d89ded359225fcd29bf3258d9205765c1" integrity sha512-bqDUCNqXeYypMCsE1VcTXSI1QuO4KXfx8Cvl6rYfY0bhhqN6d2WZlRkyLg/p6pm+DzvanqHOyYlqdPyP0iz+iw== @@ -5453,6 +5453,11 @@ resolved "https://registry.yarnpkg.com/@types/deep-eql/-/deep-eql-4.0.2.tgz#334311971d3a07121e7eb91b684a605e7eea9cbd" integrity sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw== +"@types/emscripten@^1.40.1": + version "1.40.1" + resolved "https://registry.yarnpkg.com/@types/emscripten/-/emscripten-1.40.1.tgz#4c34102d7cd1503979d4e6652082c23fd805805e" + integrity sha512-sr53lnYkQNhjHNN0oJDdUm5564biioI5DuOpycufDVK7D3y+GR3oUswe2rlwY1nPNyusHbrJ9WoTyIHl4/Bpwg== + "@types/eslint-scope@^3.7.7": version "3.7.7" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" @@ -6118,6 +6123,14 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== +"@yudiel/react-qr-scanner@^2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@yudiel/react-qr-scanner/-/react-qr-scanner-2.3.1.tgz#0ab45c438d81d58979ff459518e8e83230fdf3fd" + integrity sha512-fE7217QvMKT/AxAEeKIheFhkKO13PSGHiqJfg4dLK/SGPpelpXNpZZ0Qeph1Cm08/AqMlGhvzQbCmoPeD/VVIg== + dependencies: + barcode-detector "3.0.3" + webrtc-adapter "9.0.3" + accepts@~1.3.4, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -6488,6 +6501,13 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +barcode-detector@3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/barcode-detector/-/barcode-detector-3.0.3.tgz#53d284feaa02b6f7536acfd688fb33aaed111842" + integrity sha512-N07CNbpudOB3oIYm0tvaezCM6zy9HOlYnUCBhX6Q5UGhnqngyBgOf/p/5ZhqHxsf9/QYy5dX95RrblIs0hNaiw== + dependencies: + zxing-wasm "^2.1.2" + base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -13746,6 +13766,11 @@ schema-utils@^4.0.0, schema-utils@^4.0.1, schema-utils@^4.3.0, schema-utils@^4.3 ajv-formats "^2.1.1" ajv-keywords "^5.1.0" +sdp@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/sdp/-/sdp-3.2.1.tgz#a2f79eecd7c5adb90d54e1bc9812775d80f3c06c" + integrity sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw== + section-matter@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167" @@ -14602,6 +14627,11 @@ type-fest@^2.13.0, type-fest@^2.5.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== +type-fest@^4.41.0: + version "4.41.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.41.0.tgz#6ae1c8e5731273c2bf1f58ad39cbae2c91a46c58" + integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA== + type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -15236,6 +15266,13 @@ webpackbar@^6.0.1: std-env "^3.7.0" wrap-ansi "^7.0.0" +webrtc-adapter@9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/webrtc-adapter/-/webrtc-adapter-9.0.3.tgz#b446ed7cd72129d00c652dd7b9a5716d9ffdd87d" + integrity sha512-5fALBcroIl31OeXAdd1YUntxiZl1eHlZZWzNg3U4Fn+J9/cGL3eT80YlrsWGvj2ojuz1rZr2OXkgCzIxAZ7vRQ== + dependencies: + sdp "^3.2.0" + websocket-driver@>=0.5.1, websocket-driver@^0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" @@ -15414,3 +15451,11 @@ zwitch@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== + +zxing-wasm@^2.1.2: + version "2.2.1" + resolved "https://registry.yarnpkg.com/zxing-wasm/-/zxing-wasm-2.2.1.tgz#ba649ea0d13d1b4d5477f57d0faf588254c9fe07" + integrity sha512-njVYnrDCDLVssruDprHGL/NSHm/lH5qsNqHQLNptLSNEf4Zsw9G2Z8A+dN+HFNYmbP8EAlEk3AxV1RiMHdNAQQ== + dependencies: + "@types/emscripten" "^1.40.1" + type-fest "^4.41.0"