8 Commits

Author SHA1 Message Date
f3a4c1af05 0.6.9
All checks were successful
Deploy to Production / deploy (push) Successful in 14s
2026-02-28 18:05:14 +00:00
616f615d7c fix: improve front camera detection on macOS by checking video track label 2026-02-28 18:04:28 +00:00
4d572b55ca 0.6.8
All checks were successful
Deploy to Production / deploy (push) Successful in 13s
2026-02-28 17:51:40 +00:00
9822cab93e feat: restore QR code detection overlay in custom QrScanner 2026-02-28 17:50:29 +00:00
9409bd3e21 0.6.7
All checks were successful
Deploy to Production / deploy (push) Successful in 14s
2026-02-28 17:46:21 +00:00
346ded460a chore: remove wasm file from repo and add to gitignore 2026-02-28 17:45:21 +00:00
170539a62f 0.6.6
All checks were successful
Deploy to Production / deploy (push) Successful in 13s
2026-02-28 17:41:06 +00:00
cfc1785863 chore: cleanup unused code in QrScanner and remove vue-qrcode-reader dependency 2026-02-28 17:40:42 +00:00
5 changed files with 131 additions and 17 deletions

1
.gitignore vendored
View File

@@ -11,6 +11,7 @@ node_modules
dist
dist-ssr
*.local
public/wasm
# Editor directories and files
.vscode/*

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "tools-app",
"version": "0.6.5",
"version": "0.6.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "tools-app",
"version": "0.6.5",
"version": "0.6.9",
"hasInstallScript": true,
"dependencies": {
"barcode-detector": "^3.1.0",

View File

@@ -1,7 +1,7 @@
{
"name": "tools-app",
"private": true,
"version": "0.6.5",
"version": "0.6.9",
"type": "module",
"scripts": {
"dev": "vite",

Binary file not shown.

View File

@@ -8,7 +8,7 @@ const scannedCodes = ref([])
const hasMultipleCameras = ref(false)
const isFullscreen = ref(false)
const videoAspect = ref(1)
const isFront = computed(() => facingMode.value === 'user')
const isMirrored = ref(false)
const wrapperRef = ref(null)
const bgCanvas = ref(null)
let bgRafId = null
@@ -19,6 +19,95 @@ let stream = null
let scanRafId = null
let barcodeDetector = null
const overlayCanvas = ref(null)
const paintDetections = (codes) => {
const canvas = overlayCanvas.value
const video = videoRef.value
if (!canvas || !video) return
const ctx = canvas.getContext('2d')
const { width, height } = canvas.getBoundingClientRect()
// Update canvas size if needed (to match CSS size)
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width
canvas.height = height
}
ctx.clearRect(0, 0, width, height)
if (!codes || codes.length === 0) return
const vw = video.videoWidth
const vh = video.videoHeight
if (!vw || !vh) return
// Calculate object-fit: cover scaling
const videoRatio = vw / vh
const canvasRatio = width / height
let drawWidth, drawHeight, startX, startY
if (canvasRatio > videoRatio) {
// Canvas is wider than video (video cropped top/bottom)
drawWidth = width
drawHeight = width / videoRatio
startX = 0
startY = (height - drawHeight) / 2
} else {
// Canvas is taller than video (video cropped left/right)
drawHeight = height
drawWidth = height * videoRatio
startY = 0
startX = (width - drawWidth) / 2
}
const scale = drawWidth / vw
// Canvas is mirrored via CSS if isMirrored is true, so no manual coordinate mirroring needed
// Styles
const styles = getComputedStyle(document.documentElement)
const accent = styles.getPropertyValue('--primary-accent').trim() || '#00f2fe'
ctx.lineWidth = 4
ctx.strokeStyle = accent
ctx.fillStyle = accent
codes.forEach(code => {
const points = code.cornerPoints
if (!points || points.length < 4) return
ctx.beginPath()
const transform = (p) => {
let x = p.x * scale + startX
let y = p.y * scale + startY
return { x, y }
}
const p0 = transform(points[0])
ctx.moveTo(p0.x, p0.y)
for (let i = 1; i < points.length; i++) {
const p = transform(points[i])
ctx.lineTo(p.x, p.y)
}
ctx.closePath()
ctx.stroke()
// Draw corners
points.forEach(p => {
const tp = transform(p)
ctx.beginPath()
ctx.arc(tp.x, tp.y, 4, 0, Math.PI * 2)
ctx.fill()
})
})
}
const updateVideoAspect = () => {
if (videoRef.value && videoRef.value.videoWidth && videoRef.value.videoHeight) {
videoAspect.value = videoRef.value.videoWidth / videoRef.value.videoHeight
@@ -110,6 +199,23 @@ const startScan = async () => {
stream = await navigator.mediaDevices.getUserMedia(constraints)
// Detect actual facing mode to mirror front camera correctly
const videoTrack = stream.getVideoTracks()[0]
if (videoTrack) {
const settings = videoTrack.getSettings()
if (settings.facingMode) {
isMirrored.value = settings.facingMode === 'user'
} else {
// Fallback: check label for desktop cameras or assume requested mode
const label = videoTrack.label ? videoTrack.label.toLowerCase() : ''
if (label.includes('front') || label.includes('facetime') || label.includes('macbook')) {
isMirrored.value = true
} else {
isMirrored.value = facingMode.value === 'user'
}
}
}
if (videoRef.value) {
videoRef.value.srcObject = stream
// Wait for metadata to play
@@ -142,6 +248,7 @@ const detectLoop = async () => {
try {
const codes = await barcodeDetector.detect(videoRef.value)
paintDetections(codes)
if (codes.length > 0) {
onDetect(codes)
}
@@ -228,20 +335,10 @@ const onDetect = (detectedCodes) => {
processCodes(validCodes)
}
const onCameraOn = async () => {} // Removed
const ensureFrontMirror = () => {} // Removed
const startFrontMirrorObserver = () => {} // Removed
const stopFrontMirrorObserver = () => {} // Removed
watch(facingMode, () => {
startScan()
})
const paintDetections = () => {} // Removed, we don't paint detections on video anymore (too complex without overlay canvas)
const onError = (err) => {
if (err.name === 'NotAllowedError') {
error.value = 'Camera permission denied'
@@ -380,7 +477,7 @@ const isUrl = (string) => {
<div
class="camera-wrapper"
:class="{ 'clickable': !isFullscreen, 'is-front': isFront }"
:class="{ 'clickable': !isFullscreen, 'is-mirrored': isMirrored }"
:style="desktopFullscreenStyle"
ref="wrapperRef"
@click="!isFullscreen && toggleFullscreen()"
@@ -388,12 +485,14 @@ const isUrl = (string) => {
<video
ref="videoRef"
class="camera-feed"
:class="{ 'is-front': isFront }"
:class="{ 'is-mirrored': isMirrored }"
autoplay
playsinline
muted
></video>
<canvas ref="overlayCanvas" class="scan-overlay-canvas" :class="{ 'is-mirrored': isMirrored }"></canvas>
<div v-if="error" class="error-overlay">
<p>{{ error }}</p>
<button @click="startScan" class="retry-btn">Retry</button>
@@ -546,7 +645,21 @@ const isUrl = (string) => {
display: block;
}
.camera-feed.is-front {
.camera-feed.is-mirrored {
transform: scaleX(-1);
}
.scan-overlay-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 5;
}
.scan-overlay-canvas.is-mirrored {
transform: scaleX(-1);
}