Skip to content

Commit cd03cb4

Browse files
committed
feat: a more concise scanner UI
1 parent 17a2f07 commit cd03cb4

File tree

3 files changed

+101
-90
lines changed

3 files changed

+101
-90
lines changed

app/components/Collapsable.vue

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ watchEffect(() => {
1414
</script>
1515

1616
<template>
17-
<div flex="~ col" border="~ gray/25 rounded-lg" divide="y dashed gray/25" of-hidden shadow-sm>
17+
<div flex="~ col" border="~ gray/25 rounded-lg" divide="y dashed gray/25" of-clip shadow-sm>
1818
<button
19-
flex items-center justify-between px2 py1 text-sm
19+
sticky top-0 z-10 flex items-center justify-between px2 py1 text-sm backdrop-blur-xl
2020
@click="isVisible = !isVisible"
2121
>
2222
<span>
2323
<slot name="label">
24-
{{ props.label ?? 'Inspect' }}
24+
{{ props.label ?? 'Collapsable' }}
2525
</slot>
2626
</span> <span op50>{{ isVisible ? '▲' : '▼' }}</span>
2727
</button>

app/components/Scan.vue

+97-86
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,9 @@ onMounted(async () => {
111111
}
112112
},
113113
})
114-
selectedCamera.value && qrScanner.setCamera(selectedCamera.value)
114+
selectedCamera.value && setTimeout(() => {
115+
qrScanner!.setCamera(selectedCamera.value!)
116+
})
115117
qrScanner.setInversionMode('both')
116118
qrScanner.start()
117119
updateCameraStatus()
@@ -184,6 +186,7 @@ const receivedBytes = computed(() => decoderStatus.value.encodedCount * (decoder
184186
const filename = ref<string | undefined>()
185187
const contentType = ref<string | undefined>()
186188
const textContent = ref<string | undefined>()
189+
const dataType = ref<'file' | 'link'>('file')
187190
188191
const bytesFormatted = useKiloBytesNumberFormat(computed(() => (bytes.value / 1024).toFixed(2)))
189192
const receivedBytesFormatted = useKiloBytesNumberFormat(computed(() => (receivedBytes.value / 1024).toFixed(2)))
@@ -295,38 +298,24 @@ async function scanFrame(result: QrScanner.ScanResult) {
295298
contentType.value = meta.contentType
296299
297300
if (contentType.value.startsWith('text/')) {
298-
textContent.value = new TextDecoder().decode(mergedData)
301+
const text = new TextDecoder().decode(mergedData)
299302
303+
textContent.value = text
300304
// auto open if it's a URL
301-
if (/^https?:\/\//.test(textContent.value)) {
302-
window.open(textContent.value, '_blank')
305+
if (/^https?:\/\//.test(text)) {
306+
dataType.value = 'link'
307+
308+
setTimeout(() => {
309+
try {
310+
window.open(text, '_blank')
311+
}
312+
catch (e) {
313+
console.error(e)
314+
}
315+
}, 250)
303316
}
304317
}
305318
}
306-
// console.log({ data })
307-
// if (Array.isArray(data)) {
308-
// if (data[0] !== id.value) {
309-
// chunks.length = 0
310-
// dataUrl.value = undefined
311-
// }
312-
313-
//
314-
315-
// chunks[data[2]] = data
316-
// pluse(data[2])
317-
318-
// if (!length.value)
319-
// return
320-
// if (picked.value.every(i => !!i)) {
321-
// try {
322-
// const merged = merge(picked.value as SliceData[])
323-
// dataUrl.value = URL.createObjectURL(new Blob([merged], { type: 'application/octet-stream' }))
324-
// }
325-
// catch (e) {
326-
// error.value = e
327-
// }
328-
// }
329-
// }
330319
}
331320
332321
useUnsavedChange(() => {
@@ -344,20 +333,85 @@ function now() {
344333
<div items-left flex flex-col gap-4>
345334
<pre v-if="error" overflow-x-auto text-red v-text="error" />
346335

347-
<div w-full flex flex-wrap gap-2>
348-
<button
349-
v-for="item of cameras" :key="item.deviceId" :class="{
350-
'text-blue': selectedCamera === item.deviceId,
351-
}"
352-
px2 py1 text-sm shadow-sm
353-
border="~ gray/25 rounded-lg"
354-
@click="selectedCamera = item.deviceId"
355-
>
356-
{{ item.label }}
357-
</button>
336+
<Collapsable label="Cameras" :default="true">
337+
<div w-full flex flex-wrap gap-2 p2>
338+
<button
339+
v-for="(item, index) of cameras" :key="item.deviceId" :class="{
340+
'text-blue bg-blue/20': selectedCamera === item.deviceId,
341+
}"
342+
px2 py1 text-sm shadow-sm
343+
border="~ gray/25 rounded-lg"
344+
@click="selectedCamera = item.deviceId"
345+
>
346+
<span i-carbon-camera mr-1 inline-block align-text-top />
347+
{{ item.label || `Camera ${index + 1}` }}
348+
</button>
349+
</div>
350+
</Collapsable>
351+
352+
<Collapsable v-if="dataUrl" label="Result" :default="true">
353+
<div flex="~ col gap-2" relative>
354+
<div flex="~ col gap-2" p2>
355+
<div v-if="dataType === 'link'" :src="dataUrl" break-words text-wrap text-blue underline op80 hover:op100>
356+
<a :href="textContent" target="_blank" rel="noopener noreferrer">{{ textContent }}</a>
357+
</div>
358+
<img v-else-if="contentType?.startsWith('image/')" :src="dataUrl">
359+
<video v-else-if="contentType?.startsWith('video/')" controls autoplay muted>
360+
<source :src="dataUrl" :type="contentType">
361+
</video>
362+
<div v-else-if="contentType?.startsWith('text/')" :src="dataUrl" break-words text-wrap>
363+
{{ textContent }}
364+
</div>
365+
</div>
366+
<div sticky bottom-0 p4 shadow backdrop-blur-xl>
367+
<a
368+
v-if="dataType === 'file'"
369+
:href="dataUrl"
370+
:download="filename"
371+
class="block w-full rounded-md bg-white px2 py1 text-center text-sm dark:bg-neutral-8"
372+
border="~ gray/25 hover:gray:10" shadow="~ gray/25"
373+
>
374+
Download as file
375+
</a>
376+
<a
377+
v-else-if="dataType === 'link'"
378+
:href="textContent"
379+
target="_blank"
380+
rel="noopener noreferrer"
381+
class="block w-full rounded-md bg-white px2 py1 text-center text-sm dark:bg-neutral-8"
382+
border="~ gray/25 hover:gray:10" shadow="~ gray/25"
383+
>
384+
Open as link
385+
</a>
386+
</div>
387+
</div>
388+
</Collapsable>
389+
390+
<!-- This is a progress bar that is not accurate but feels comfortable. -->
391+
<div v-if="k" relative h-4 rounded bg-black:75 text-white font-mono shadow>
392+
<div
393+
bg="green-400" border="~ green4 rounded" transition="all ease" absolute inset-y-0 h-full w-full duration-1000
394+
:style="{ maxWidth: `${decodedBlocks === k ? 100 : (Math.min(1, receivedBytes / bytes * 0.66) * 100).toFixed(2)}%` }"
395+
/>
358396
</div>
359397

360-
<Collapsable>
398+
<Camera
399+
:k="k"
400+
:fps="fps"
401+
:bytes="bytes"
402+
:received-bytes="receivedBytes"
403+
:current-bytes="currentBytesFormatted"
404+
:current-valid-bytes-speed="currentValidBytesSpeedFormatted"
405+
:camera-signal-status="cameraSignalStatus"
406+
>
407+
<video
408+
ref="video"
409+
:controls="false"
410+
autoplay muted playsinline h-full w-full rounded-lg
411+
/>
412+
</Camera>
413+
414+
<Collapsable label="Inspect">
361415
<div grid-cols="[150px_1fr]" font="mono!" :class="endTime ? 'text-green-500' : ''" grid gap-x-4 gap-y-2 overflow-x-auto whitespace-nowrap p2 text-sm>
362416
<span text-neutral-500>Filename</span>
363417
<span text-right md:text-left>{{ filename || '<unknown>' }}</span>
@@ -382,7 +436,7 @@ function now() {
382436
</div>
383437
</Collapsable>
384438

385-
<Collapsable v-if="k" :default="k < 500">
439+
<Collapsable v-if="k">
386440
<template #label>
387441
<span>Packets</span>
388442
<span ml-2 text-neutral-400>({{ k }})</span>
@@ -406,51 +460,8 @@ function now() {
406460
</div>
407461
</Collapsable>
408462

409-
<Collapsable v-if="dataUrl" label="Download" :default="true">
410-
<div flex="~ col gap-2" p2>
411-
<img v-if="contentType?.startsWith('image/')" :src="dataUrl">
412-
<video v-else-if="contentType?.startsWith('video/')" controls autoplay muted>
413-
<source :src="dataUrl" :type="contentType">
414-
</video>
415-
<p v-else-if="contentType?.startsWith('text/')" :src="dataUrl">
416-
{{ textContent }}
417-
</p>
418-
<a
419-
:href="dataUrl"
420-
:download="filename"
421-
class="w-max border border-gray:50 rounded-md px2 py1 text-sm hover:bg-gray:10"
422-
>
423-
Download
424-
</a>
425-
</div>
426-
</Collapsable>
427-
428-
<!-- This is a progress bar that is not accurate but feels comfortable. -->
429-
<div v-if="k" relative h-4 rounded bg-black:75 text-white font-mono shadow>
430-
<div
431-
bg="green-400" border="~ green4 rounded" transition="all ease" absolute inset-y-0 h-full w-full duration-1000
432-
:style="{ maxWidth: `${decodedBlocks === k ? 100 : (Math.min(1, receivedBytes / bytes * 0.66) * 100).toFixed(2)}%` }"
433-
/>
434-
</div>
435-
436-
<Camera
437-
:k="k"
438-
:fps="fps"
439-
:bytes="bytes"
440-
:received-bytes="receivedBytes"
441-
:current-bytes="currentBytesFormatted"
442-
:current-valid-bytes-speed="currentValidBytesSpeedFormatted"
443-
:camera-signal-status="cameraSignalStatus"
444-
>
445-
<video
446-
ref="video"
447-
:controls="false"
448-
autoplay muted playsinline h-full w-full rounded-lg
449-
/>
450-
</Camera>
451-
452-
<Collapsable label="Blocks">
453-
<div flex="~ gap-1 wrap" max-w-150 text-xs>
463+
<Collapsable v-if="k" label="Blocks">
464+
<div flex="~ gap-1 wrap" max-w-150 p2 text-xs>
454465
<div v-for="i, idx of decoderStatus.encodedBlocks" :key="idx" border="~ gray/10 rounded" p1>
455466
<template v-for="x, idy of i.indices" :key="x">
456467
<span v-if="idy !== 0" op25>, </span>

app/pages/scan.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const throttledFps = useDebounce(fps, 500)
66

77
<template>
88
<div px-4 pb-8 pt-2 space-y-4>
9+
<Scan :max-scans-per-second="throttledFps" />
910
<div w-full inline-flex flex-row items-center>
1011
<span min-w-40>
1112
<span pr-2 text-zinc-400>Ideal scans</span>
@@ -19,7 +20,6 @@ const throttledFps = useDebounce(fps, 500)
1920
w-full flex-1
2021
/>
2122
</div>
22-
<Scan :max-scans-per-second="throttledFps" />
2323
<!-- <h2 flex items-center gap-4 text-3xl>
2424
Results: {{ results.size }}
2525
<button class="flex items-center gap2 border rounded-md px2 py1 text-base shadow" @click="results = new Set()">

0 commit comments

Comments
 (0)