feat(qr-scanner): remove frame, add shape detection overlay, improve fullscreen desktop layout
This commit is contained in:
@@ -83,12 +83,12 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.app-body {
|
.app-body {
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
overflow-y: auto;
|
overflow: visible;
|
||||||
height: 100%;
|
height: auto;
|
||||||
padding-bottom: 2rem;
|
padding-bottom: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ onMounted(() => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.app-header {
|
.app-header {
|
||||||
/* Remove hardcoded colors and use theme variables */
|
/* Remove hardcoded colors and use theme variables */
|
||||||
background: var(--glass-bg);
|
background: var(--header-bg);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
/* box-shadow handled by glass-panel class */
|
/* box-shadow handled by glass-panel class */
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch } from 'vue'
|
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
|
||||||
import { QrcodeStream } from 'vue-qrcode-reader'
|
import { QrcodeStream } from 'vue-qrcode-reader'
|
||||||
import { SwitchCamera, Trash2, Copy, Download, X } from 'lucide-vue-next'
|
import { SwitchCamera, Trash2, Copy, Download, X } from 'lucide-vue-next'
|
||||||
|
|
||||||
@@ -8,6 +8,21 @@ const facingMode = ref('environment')
|
|||||||
const scannedCodes = ref([])
|
const scannedCodes = ref([])
|
||||||
const hasMultipleCameras = ref(false)
|
const hasMultipleCameras = ref(false)
|
||||||
const isFullscreen = ref(false)
|
const isFullscreen = ref(false)
|
||||||
|
const videoAspect = ref(1)
|
||||||
|
const updateVideoAspect = () => {
|
||||||
|
const videoEl = document.querySelector('.camera-wrapper video')
|
||||||
|
if (videoEl && videoEl.videoWidth && videoEl.videoHeight) {
|
||||||
|
videoAspect.value = videoEl.videoWidth / videoEl.videoHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const desktopFullscreenStyle = computed(() => {
|
||||||
|
if (!isFullscreen.value) return {}
|
||||||
|
const isDesktop = window.innerWidth >= 768
|
||||||
|
if (!isDesktop) return {}
|
||||||
|
const halfHeight = Math.floor(window.innerHeight * 0.5)
|
||||||
|
const widthPx = Math.min(window.innerWidth, Math.floor(halfHeight * videoAspect.value))
|
||||||
|
return { height: `${halfHeight}px`, width: `${widthPx}px`, margin: '0 auto' }
|
||||||
|
})
|
||||||
|
|
||||||
const processCodes = (codes) => {
|
const processCodes = (codes) => {
|
||||||
codes.forEach(code => {
|
codes.forEach(code => {
|
||||||
@@ -79,8 +94,41 @@ const onDetect = (detectedCodes) => {
|
|||||||
|
|
||||||
const onCameraOn = async (capabilities) => {
|
const onCameraOn = async (capabilities) => {
|
||||||
// Camera is ready
|
// Camera is ready
|
||||||
|
setTimeout(updateVideoAspect, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const paintDetections = (detectedCodes, ctx) => {
|
||||||
|
try {
|
||||||
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
|
||||||
|
const styles = getComputedStyle(document.documentElement)
|
||||||
|
const accent = styles.getPropertyValue('--primary-accent').trim() || '#00f2fe'
|
||||||
|
detectedCodes.forEach(code => {
|
||||||
|
if (code.format && code.format !== 'qr_code') return
|
||||||
|
const points = code.cornerPoints || []
|
||||||
|
if (points.length < 4) return
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(points[0].x, points[0].y)
|
||||||
|
for (let i = 1; i < points.length; i++) {
|
||||||
|
ctx.lineTo(points[i].x, points[i].y)
|
||||||
|
}
|
||||||
|
ctx.closePath()
|
||||||
|
ctx.lineWidth = 3
|
||||||
|
ctx.strokeStyle = accent
|
||||||
|
ctx.shadowColor = accent
|
||||||
|
ctx.shadowBlur = 8
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.shadowBlur = 0
|
||||||
|
ctx.fillStyle = accent
|
||||||
|
points.forEach(p => {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(p.x, p.y, 2.5, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
// ignore drawing errors
|
||||||
|
}
|
||||||
|
}
|
||||||
const onError = (err) => {
|
const onError = (err) => {
|
||||||
if (err.name === 'NotAllowedError') {
|
if (err.name === 'NotAllowedError') {
|
||||||
error.value = 'Camera permission denied'
|
error.value = 'Camera permission denied'
|
||||||
@@ -122,6 +170,11 @@ watch(scannedCodes, (newVal) => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
checkCameras()
|
checkCameras()
|
||||||
loadHistory()
|
loadHistory()
|
||||||
|
window.addEventListener('resize', updateVideoAspect)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', updateVideoAspect)
|
||||||
})
|
})
|
||||||
|
|
||||||
const switchCamera = (event) => {
|
const switchCamera = (event) => {
|
||||||
@@ -195,12 +248,13 @@ const isUrl = (string) => {
|
|||||||
<X size="24" />
|
<X size="24" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="camera-wrapper" :class="{ 'clickable': !isFullscreen }" @click="!isFullscreen && toggleFullscreen()">
|
<div class="camera-wrapper" :class="{ 'clickable': !isFullscreen }" :style="desktopFullscreenStyle" @click="!isFullscreen && toggleFullscreen()">
|
||||||
<QrcodeStream
|
<QrcodeStream
|
||||||
:constraints="{ facingMode }"
|
:constraints="{ facingMode }"
|
||||||
@detect="onDetect"
|
@detect="onDetect"
|
||||||
@error="onError"
|
@error="onError"
|
||||||
@camera-on="onCameraOn"
|
@camera-on="onCameraOn"
|
||||||
|
:track="paintDetections"
|
||||||
>
|
>
|
||||||
<div v-if="error" class="error-overlay">
|
<div v-if="error" class="error-overlay">
|
||||||
<p>{{ error }}</p>
|
<p>{{ error }}</p>
|
||||||
@@ -214,10 +268,6 @@ const isUrl = (string) => {
|
|||||||
>
|
>
|
||||||
<SwitchCamera size="24" />
|
<SwitchCamera size="24" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="scan-overlay">
|
|
||||||
<div class="scan-frame"></div>
|
|
||||||
</div>
|
|
||||||
</QrcodeStream>
|
</QrcodeStream>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -395,25 +445,7 @@ const isUrl = (string) => {
|
|||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scan-overlay {
|
/* Removed legacy scan frame overlay - using shape detection rendering via track instead */
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scan-frame {
|
|
||||||
width: 70%;
|
|
||||||
height: 70%;
|
|
||||||
border: 2px solid rgba(255, 255, 255, 0.5);
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.results-section {
|
.results-section {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -581,4 +613,10 @@ const isUrl = (string) => {
|
|||||||
position: absolute !important;
|
position: absolute !important;
|
||||||
inset: 0 !important;
|
inset: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
:deep(.scanner-content.is-fullscreen .camera-wrapper video) {
|
||||||
|
object-fit: contain !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
--ripple-color: rgba(255, 255, 255, 0.3);
|
--ripple-color: rgba(255, 255, 255, 0.3);
|
||||||
--nav-item-weight: 400;
|
--nav-item-weight: 400;
|
||||||
--list-hover-bg: rgba(255, 255, 255, 0.05);
|
--list-hover-bg: rgba(255, 255, 255, 0.05);
|
||||||
|
--header-bg: rgba(0, 0, 0, 0.6);
|
||||||
|
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
background-color: #242424; /* Fallback */
|
background-color: #242424; /* Fallback */
|
||||||
@@ -86,6 +87,7 @@
|
|||||||
--title-gradient: linear-gradient(45deg, #0ea5e9, #6366f1);
|
--title-gradient: linear-gradient(45deg, #0ea5e9, #6366f1);
|
||||||
--ripple-color: rgba(0, 0, 0, 0.1);
|
--ripple-color: rgba(0, 0, 0, 0.1);
|
||||||
--list-hover-bg: rgba(0, 0, 0, 0.05);
|
--list-hover-bg: rgba(0, 0, 0, 0.05);
|
||||||
|
--header-bg: rgba(255, 255, 255, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|||||||
Reference in New Issue
Block a user