diff --git a/package-lock.json b/package-lock.json index 58c2a95..4a87af7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "unplugin-vue-markdown": "^0.28.0", "uuid": "^9.0.0", "vue": "^3.4.29", + "vue-qrcode-reader": "^5.7.1", "vue-router": "^4.3.3" }, "devDependencies": { @@ -3080,6 +3081,18 @@ "@types/underscore": "*" } }, + "node_modules/@types/dom-webcodecs": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.14.tgz", + "integrity": "sha512-ba9aF0qARLLQpLihONIRbj8VvAdUxO+5jIxlscVcDAQTcJmq5qVr781+ino5qbQUJUmO21cLP2eLeXYWzao5Vg==", + "license": "MIT" + }, + "node_modules/@types/emscripten": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.40.0.tgz", + "integrity": "sha512-MD2JJ25S4tnjnhjWyalMS6K6p0h+zQV6+Ylm+aGbiS8tSn/aHLSGNzBgduj6FB4zH0ax2GRMGYi/8G1uOxhXWA==", + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "9.6.0", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.0.tgz", @@ -4109,6 +4122,16 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/barcode-detector": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/barcode-detector/-/barcode-detector-2.2.2.tgz", + "integrity": "sha512-JcSekql+EV93evfzF9zBr+Y6aRfkR+QFvgyzbwQ0dbymZXoAI9+WgT7H1E429f+3RKNncHz2CW98VQtaaKpmfQ==", + "license": "MIT", + "dependencies": { + "@types/dom-webcodecs": "^0.1.11", + "zxing-wasm": "1.1.3" + } + }, "node_modules/bare-events": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz", @@ -8696,6 +8719,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sdp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz", + "integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==", + "license": "MIT" + }, "node_modules/section-matter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", @@ -10167,6 +10196,19 @@ "eslint": ">=6.0.0" } }, + "node_modules/vue-qrcode-reader": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/vue-qrcode-reader/-/vue-qrcode-reader-5.7.1.tgz", + "integrity": "sha512-7QBu3PqaPJHxobiDLqgcrp6wsjdTk9GJWhRCd4CgQYi93gBw/sIXNNWtbjeKz8d3QYj13n9dyPvcPMUcGOsBHw==", + "license": "MIT", + "dependencies": { + "barcode-detector": "2.2.2", + "webrtc-adapter": "8.2.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/vue-router": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.3.tgz", @@ -10210,6 +10252,19 @@ "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "license": "MIT" }, + "node_modules/webrtc-adapter": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-8.2.3.tgz", + "integrity": "sha512-gnmRz++suzmvxtp3ehQts6s2JtAGPuDPjA1F3a9ckNpG1kYdYuHWYpazoAnL9FS5/B21tKlhkorbdCXat0+4xQ==", + "license": "BSD-3-Clause", + "dependencies": { + "sdp": "^3.2.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">=3.10.0" + } + }, "node_modules/whatwg-url": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", @@ -10841,6 +10896,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zxing-wasm": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/zxing-wasm/-/zxing-wasm-1.1.3.tgz", + "integrity": "sha512-MYm9k/5YVs4ZOTIFwlRjfFKD0crhefgbnt1+6TEpmKUDFp3E2uwqGSKwQOd2hOIsta/7Usq4hnpNRYTLoljnfA==", + "license": "MIT", + "dependencies": { + "@types/emscripten": "^1.39.10" + } } } } diff --git a/package.json b/package.json index bb37882..32aab8c 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "unplugin-vue-markdown": "^0.28.0", "uuid": "^9.0.0", "vue": "^3.4.29", + "vue-qrcode-reader": "^5.7.1", "vue-router": "^4.3.3" }, "devDependencies": { diff --git a/src/components/CodeDetector.vue b/src/components/CodeDetector.vue new file mode 100644 index 0000000..1832f1a --- /dev/null +++ b/src/components/CodeDetector.vue @@ -0,0 +1,73 @@ + + + + + + + {{ c.label }} + + + weiter scannen + bestätigen + + + + + + + diff --git a/src/components/Modal.vue b/src/components/Modal.vue index fac5fcf..2b296bd 100644 --- a/src/components/Modal.vue +++ b/src/components/Modal.vue @@ -9,6 +9,7 @@ @@ -23,7 +24,7 @@ import { useModalStore } from "@/stores/modal"; - - diff --git a/src/helpers/codeScanner.ts b/src/helpers/codeScanner.ts new file mode 100644 index 0000000..bf3c026 --- /dev/null +++ b/src/helpers/codeScanner.ts @@ -0,0 +1,143 @@ +import type { BarcodeFormat, DetectedBarcode } from "barcode-detector/pure"; + +/*** select camera ***/ +export interface Camera { + label: string; + constraints: { + deviceId?: string; + facingMode: string; + }; +} +export const defaultConstraintOptions: Array = [ + { label: "rear camera", constraints: { facingMode: "environment" } }, + { label: "front camera", constraints: { facingMode: "user" } }, +]; + +export async function getAvailableCameras(): Promise> { + // NOTE: on iOS we can't invoke `enumerateDevices` before the user has given + // camera access permission. `QrcodeStream` internally takes care of + // requesting the permissions. The `camera-on` event should guarantee that this + // has happened. + const devices = await navigator.mediaDevices.enumerateDevices(); + const videoDevices = devices.filter(({ kind }) => kind === "videoinput"); + + return [ + ...defaultConstraintOptions, + ...videoDevices.map(({ deviceId, label }) => ({ + label: `${label} (ID: ${deviceId})`, + constraints: { deviceId, facingMode: "custom" }, + })), + ]; +} + +/*** track functons ***/ +export function paintOutline(detectedCodes: DetectedBarcode[], ctx: CanvasRenderingContext2D) { + for (const detectedCode of detectedCodes) { + const [firstPoint, ...otherPoints] = detectedCode.cornerPoints; + + ctx.strokeStyle = "red"; + + ctx.beginPath(); + ctx.moveTo(firstPoint.x, firstPoint.y); + for (const { x, y } of otherPoints) { + ctx.lineTo(x, y); + } + ctx.lineTo(firstPoint.x, firstPoint.y); + ctx.closePath(); + ctx.stroke(); + } +} +export function paintBoundingBox(detectedCodes: DetectedBarcode[], ctx: CanvasRenderingContext2D) { + for (const detectedCode of detectedCodes) { + const { + boundingBox: { x, y, width, height }, + } = detectedCode; + + ctx.lineWidth = 2; + ctx.strokeStyle = "#007bff"; + ctx.strokeRect(x, y, width, height); + } +} +export function paintCenterText(detectedCodes: DetectedBarcode[], ctx: CanvasRenderingContext2D) { + for (const detectedCode of detectedCodes) { + const { boundingBox, rawValue } = detectedCode; + + const centerX = boundingBox.x + boundingBox.width / 2; + const centerY = boundingBox.y + boundingBox.height / 2; + + const fontSize = Math.max(12, (50 * boundingBox.width) / ctx.canvas.width); + + ctx.font = `bold ${fontSize}px sans-serif`; + ctx.textAlign = "center"; + + ctx.lineWidth = 3; + ctx.strokeStyle = "#35495e"; + ctx.strokeText(detectedCode.rawValue, centerX, centerY); + + ctx.fillStyle = "#5cb984"; + ctx.fillText(rawValue, centerX, centerY); + } +} +export const trackFunctionOptions = [ + { text: "nothing (default)", value: undefined }, + { text: "outline", value: paintOutline }, + { text: "centered text", value: paintCenterText }, + { text: "bounding box", value: paintBoundingBox }, + { + text: "mixed", + value: (detectedCodes: DetectedBarcode[], ctx: CanvasRenderingContext2D) => { + paintOutline(detectedCodes, ctx); + paintCenterText(detectedCodes, ctx); + }, + }, +]; + +/*** barcode formats ***/ +export const barcodeFormats: Array = [ + "aztec", + "code_128", + "code_39", + "code_93", + "codabar", + "databar", + "databar_expanded", + "data_matrix", + "dx_film_edge", + "ean_13", + "ean_8", + "itf", + "maxi_code", + "micro_qr_code", + "pdf417", + "qr_code", + "rm_qr_code", + "upc_a", + "upc_e", + "linear_codes", + "matrix_codes", +]; + +/*** error handling ***/ +export function handleScannerError(err: Error) { + let error = `[${err.name}]: `; + + if (err.name === "NotAllowedError") { + error += "you need to grant camera access permission"; + } else if (err.name === "NotFoundError") { + error += "no camera on this device"; + } else if (err.name === "NotSupportedError") { + error += "secure context required (HTTPS, localhost)"; + } else if (err.name === "NotReadableError") { + error += "is the camera already in use?"; + } else if (err.name === "OverconstrainedError") { + error += "installed cameras are not suitable"; + } else if (err.name === "StreamApiNotSupportedError") { + error += "Stream API is not supported in this browser"; + } else if (err.name === "InsecureContextError") { + error += "Camera access is only permitted in secure context. Use HTTPS or localhost rather than HTTP."; + } else { + error += err.message; + } + + return error; +} diff --git a/src/stores/modal.ts b/src/stores/modal.ts index cdc4c49..a537848 100644 --- a/src/stores/modal.ts +++ b/src/stores/modal.ts @@ -4,19 +4,22 @@ export const useModalStore = defineStore("modal", { state: () => { return { show: false, - component_ref: null as any, - data: null as any, + component_ref: undefined as any, + data: undefined as any, + callback: undefined as undefined | Function, }; }, actions: { - openModal(component_ref: any, data?: any) { + openModal(component_ref: any, data?: any, callback?: Function) { this.component_ref = component_ref; this.data = data; + this.callback = callback; this.show = true; }, closeModal() { - this.component_ref = null; - this.data = null; + this.component_ref = undefined; + this.data = undefined; + this.callback = undefined; this.show = false; }, }, diff --git a/src/views/admin/unit/equipment/Equipment.vue b/src/views/admin/unit/equipment/Equipment.vue index 4348010..a3e6cc8 100644 --- a/src/views/admin/unit/equipment/Equipment.vue +++ b/src/views/admin/unit/equipment/Equipment.vue @@ -8,10 +8,11 @@ fetchEquipments(offset, count, search)" @search="(search) => fetchEquipments(0, maxEntriesPerPage, search, true)" > diff --git a/src/views/admin/unit/vehicle/Vehicle.vue b/src/views/admin/unit/vehicle/Vehicle.vue index 6b16546..538e2db 100644 --- a/src/views/admin/unit/vehicle/Vehicle.vue +++ b/src/views/admin/unit/vehicle/Vehicle.vue @@ -8,10 +8,11 @@ fetchVehicles(offset, count, search)" @search="(search) => fetchVehicles(0, maxEntriesPerPage, search, true)" >