I'm tried to convert angular app to vue 3, but i get some problems. There are:
- black window while i call startScan() but android says that camera in using and all permissions allowed
- function scan() from google is working and scanning
I'll mean that problems with modal window and its inits or problems with css while i convert ionic to tailwindcss
here original github: https://github.com/robingenz/capacitor-mlkit-plugin-demo/tree/main/src/app/modules/barcode-scanning
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import {
BarcodeFormat,
BarcodeScanner,
LensFacing,
} from '@capacitor-mlkit/barcode-scanning';
import type {
Barcode,
} from '@capacitor-mlkit/barcode-scanning';
import { FilePicker } from '@capawesome/capacitor-file-picker';
import BarcodeScanningModal from './BarcodeScanningModal.vue';
const GH_URL = 'https://github.com/capawesome-team/capacitor-barcode-scanning';
const formats = ref<BarcodeFormat[]>([]);
const lensFacing = ref<LensFacing>(LensFacing.Back);
const googleBarcodeScannerModuleInstallState = ref(0);
const googleBarcodeScannerModuleInstallProgress = ref<number | undefined>(0);
const barcodes = ref<Barcode[]>([]);
const isSupported = ref(false);
const isPermissionGranted = ref(false);
const showModal = ref(false);
const openOnGithub = () => {
window.open(GH_URL, '_blank');
};
const startScan = async () => {
try {
const status = await BarcodeScanner.checkPermissions();
if (status.camera !== 'granted') {
await BarcodeScanner.requestPermissions();
}
showModal.value = true;
} catch (error) {
console.error('Scan failed:', error);
}
};
const handleModalClose = (barcode?: Barcode) => {
showModal.value = false;
if (barcode) {
barcodes.value = [barcode];
}
};
const readBarcodeFromImage = async () => {
const { files } = await FilePicker.pickImages({ limit: 1 });
const path = files[0]?.path;
if (!path) return;
const { barcodes: scannedBarcodes } = await BarcodeScanner.readBarcodesFromImage({
path,
formats: formats.value,
});
barcodes.value = scannedBarcodes;
};
const scan = async () => {
const { barcodes: scannedBarcodes } = await BarcodeScanner.scan({
formats: formats.value,
});
barcodes.value = scannedBarcodes;
};
const openSettings = async () => {
await BarcodeScanner.openSettings();
};
const installGoogleBarcodeScannerModule = async () => {
await BarcodeScanner.installGoogleBarcodeScannerModule();
};
const requestPermissions = async () => {
await BarcodeScanner.requestPermissions();
};
onMounted(() => {
BarcodeScanner.isSupported().then((result) => {
isSupported.value = result.supported;
});
BarcodeScanner.checkPermissions().then((result) => {
isPermissionGranted.value = result.camera === 'granted';
});
BarcodeScanner.removeAllListeners().then(() => {
BarcodeScanner.addListener(
'googleBarcodeScannerModuleInstallProgress',
(event) => {
console.log('googleBarcodeScannerModuleInstallProgress', event);
const { state, progress } = event;
googleBarcodeScannerModuleInstallState.value = state;
googleBarcodeScannerModuleInstallProgress.value = progress ;
}
);
});
});
</script>
<template>
<div class="min-h-screen bg-gray-100">
<header class="bg-white shadow">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8 flex items-center">
<router-link to="/home" class="mr-4">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-gray-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 19l-7-7m0 0l7-7m-7 7h18"
/>
</svg>
</router-link>
<h1 class="text-2xl font-bold text-gray-900">Barcode Scanning</h1>
</div>
</header>
<main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="bg-white shadow rounded-lg overflow-hidden mb-6">
<div class="p-6">
<h2 class="text-lg font-medium text-gray-900 mb-2">About</h2>
<p class="text-gray-600">
⚡️ Capacitor plugin for scanning barcodes and QR codes.
</p>
<div class="flex justify-end mt-4">
<button
@click="openOnGithub"
class="text-indigo-600 hover:text-indigo-800 font-medium"
>
GitHub
</button>
</div>
</div>
</div>
<div class="bg-white shadow rounded-lg overflow-hidden">
<div class="p-6">
<h2 class="text-lg font-medium text-gray-900 mb-4">Demo</h2>
<form class="space-y-4">
<div class="grid grid-cols-1 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Formats
</label>
<select
v-model="formats"
multiple
class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md border"
>
<option :value="BarcodeFormat.Aztec">Aztec</option>
<option :value="BarcodeFormat.Codabar">Codabar</option>
<option :value="BarcodeFormat.Code39">Code39</option>
<option :value="BarcodeFormat.Code93">Code93</option>
<option :value="BarcodeFormat.Code128">Code128</option>
<option :value="BarcodeFormat.DataMatrix">DataMatrix</option>
<option :value="BarcodeFormat.Ean8">Ean8</option>
<option :value="BarcodeFormat.Ean13">Ean13</option>
<option :value="BarcodeFormat.Itf">Itf</option>
<option :value="BarcodeFormat.Pdf417">Pdf417</option>
<option :value="BarcodeFormat.QrCode">QrCode</option>
<option :value="BarcodeFormat.UpcA">UpcA</option>
<option :value="BarcodeFormat.UpcE">UpcE</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Lens Facing
</label>
<select
v-model="lensFacing"
class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md border"
>
<option :value="LensFacing.Front">Front</option>
<option :value="LensFacing.Back">Back</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Google Barcode Scanner Module Install State
</label>
<input
v-model="googleBarcodeScannerModuleInstallState"
type="number"
readonly
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-gray-100"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Google Barcode Scanner Module Install Progress
</label>
<input
v-model="googleBarcodeScannerModuleInstallProgress"
type="number"
readonly
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-gray-100"
/>
</div>
</div>
<div class="space-y-2">
<button
@click.prevent="startScan"
:disabled="!isSupported"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
Start Scan
</button>
<button
@click.prevent="readBarcodeFromImage"
:disabled="!isSupported"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
Read Barcode From Image
</button>
<button
@click.prevent="scan"
:disabled="!isSupported"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
Scan
</button>
<button
@click.prevent="openSettings"
:disabled="!isSupported"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
Open Settings
</button>
<button
@click.prevent="installGoogleBarcodeScannerModule"
:disabled="!isSupported"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
Install Google Barcode Scanner Module
</button>
<button
@click.prevent="requestPermissions"
:disabled="isPermissionGranted"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
Request Permissions
</button>
</div>
</form>
</div>
</div>
<div v-for="barcode in barcodes" :key="barcode.rawValue" class="mt-6">
<div class="bg-white shadow rounded-lg overflow-hidden">
<div class="p-6 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Bytes
</label>
<input
type="text"
readonly
:value="barcode.bytes?.toString() || ''"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-gray-100"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Corner Points
</label>
<input
type="text"
readonly
:value="barcode.cornerPoints?.toString() || ''"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-gray-100"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Display Value
</label>
<input
type="text"
readonly
:value="barcode.displayValue"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-gray-100"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Format
</label>
<input
type="text"
readonly
:value="barcode.format"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-gray-100"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Raw Value
</label>
<input
type="text"
readonly
:value="barcode.rawValue"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-gray-100"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Value Type
</label>
<input
type="text"
readonly
:value="barcode.valueType"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-gray-100"
/>
</div>
</div>
</div>
</div>
</main>
</div>
<BarcodeScanningModal
v-if="showModal"
:formats="formats"
:lensFacing="lensFacing"
@close="handleModalClose"
/>
</template>
ModalWindow.vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import {
BarcodeFormat,
BarcodeScanner,
LensFacing,
} from '@capacitor-mlkit/barcode-scanning';
import type {
Barcode,
StartScanOptions,
} from '@capacitor-mlkit/barcode-scanning';
import { Capacitor } from '@capacitor/core';
const props = defineProps<{
formats: BarcodeFormat[];
lensFacing: LensFacing;
}>();
const emit = defineEmits<{
(e: 'close', barcode?: Barcode): void;
}>();
const squareElement = ref<HTMLDivElement | null>(null);
const videoElement = ref<HTMLVideoElement | null>(null);
const isTorchAvailable = ref(false);
const isWeb = Capacitor.getPlatform() === 'web';
const minZoomRatio = ref<number>();
const maxZoomRatio = ref<number>();
const isCameraActive = ref(false);
const errorMessage = ref<string>('');
const closeModal = (barcode?: Barcode) => {
emit('close', barcode);
};
const setZoomRatio = (event: Event) => {
const target = event.target as HTMLInputElement;
if (!target.value) return;
BarcodeScanner.setZoomRatio({
zoomRatio: parseInt(target.value, 10),
});
};
const toggleTorch = async () => {
await BarcodeScanner.toggleTorch();
};
const startScan = async () => {
try {
if (isWeb && !videoElement.value) {
throw new Error('Video element is not available');
}
document.body.classList.add('barcode-scanning-active');
isCameraActive.value = true;
errorMessage.value = '';
const options: StartScanOptions = {
formats: props.formats,
lensFacing: props.lensFacing,
videoElement: isWeb ? videoElement.value || undefined : undefined,
};
const squareElementBoundingClientRect =
squareElement.value?.getBoundingClientRect();
const scaledRect = squareElementBoundingClientRect
? {
left: squareElementBoundingClientRect.left * window.devicePixelRatio,
right: squareElementBoundingClientRect.right * window.devicePixelRatio,
top: squareElementBoundingClientRect.top * window.devicePixelRatio,
bottom: squareElementBoundingClientRect.bottom * window.devicePixelRatio,
width: squareElementBoundingClientRect.width * window.devicePixelRatio,
height: squareElementBoundingClientRect.height * window.devicePixelRatio,
}
: undefined;
const detectionCornerPoints = scaledRect
? [
[scaledRect.left, scaledRect.top],
[scaledRect.left + scaledRect.width, scaledRect.top],
[scaledRect.left + scaledRect.width, scaledRect.top + scaledRect.height],
[scaledRect.left, scaledRect.top + scaledRect.height],
]
: undefined;
const listener = await BarcodeScanner.addListener(
'barcodesScanned',
async (event) => {
const firstBarcode = event.barcodes[0];
if (!firstBarcode) return;
const cornerPoints = firstBarcode.cornerPoints;
if (
detectionCornerPoints &&
cornerPoints &&
Capacitor.getPlatform() !== 'web'
) {
if (
detectionCornerPoints[0][0] > cornerPoints[0][0] ||
detectionCornerPoints[0][1] > cornerPoints[0][1] ||
detectionCornerPoints[1][0] < cornerPoints[1][0] ||
detectionCornerPoints[1][1] > cornerPoints[1][1] ||
detectionCornerPoints[2][0] < cornerPoints[2][0] ||
detectionCornerPoints[2][1] < cornerPoints[2][1] ||
detectionCornerPoints[3][0] > cornerPoints[3][0] ||
detectionCornerPoints[3][1] < cornerPoints[3][1]
) {
return;
}
}
await listener.remove();
closeModal(firstBarcode);
}
);
await BarcodeScanner.startScan(options);
if (Capacitor.getPlatform() !== 'web') {
BarcodeScanner.getMinZoomRatio().then((result) => {
minZoomRatio.value = result.zoomRatio;
});
BarcodeScanner.getMaxZoomRatio().then((result) => {
maxZoomRatio.value = result.zoomRatio;
});
}
} catch (error) {
console.error('Failed to start scan:', error);
errorMessage.value = 'Не удалось запустить камеру';
isCameraActive.value = false;
}
};
const stopScan = async () => {
try {
document.body.classList.remove('barcode-scanning-active');
await BarcodeScanner.stopScan();
isCameraActive.value = false;
} catch (error) {
console.error('Failed to stop scan:', error);
}
};
const restartCamera = async () => {
await stopScan();
setTimeout(async () => {
await startScan();
}, 100);
};
onMounted(async () => {
try {
const isSupported = await BarcodeScanner.isSupported();
if (!isSupported.supported) {
errorMessage.value = 'Сканирование штрих-кодов не поддерживается на этом устройстве';
return;
}
await startScan();
const torchResult = await BarcodeScanner.isTorchAvailable();
isTorchAvailable.value = torchResult.available;
} catch (error) {
console.error('Failed to initialize scanner:', error);
errorMessage.value = 'Ошибка инициализации сканера';
}
});
onUnmounted(() => {
stopScan();
});
</script>
<template>
<div class="fixed inset-0 z-50 overflow-hidden barcode-scanning-modal">
<div class="absolute inset-0 bg-black bg-opacity-30"></div>
<div class="fixed inset-0 flex items-center justify-center">
<div class="relative w-full h-full">
<!-- Video element for web -->
<video
v-if="isWeb"
ref="videoElement"
autoplay
class="absolute inset-0 w-full h-full object-cover"
></video>
<!-- Scanning square overlay -->
<div
ref="squareElement"
class="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 w-48 h-48 rounded-xl border-4 border-white shadow-lg"
></div>
<!-- Error message -->
<div
v-if="errorMessage"
class="absolute top-20 left-1/2 transform -translate-x-1/2 bg-red-500 text-white p-4 rounded-lg text-center"
>
{{ errorMessage }}
</div>
<!-- Restart camera button -->
<button
v-if="!isCameraActive"
@click="restartCamera"
class="absolute left-4 bottom-4 p-3 bg-blue-500 text-white rounded-full shadow-lg hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
</button>
<!-- Zoom controls -->
<div
class="absolute left-1/2 bottom-4 transform -translate-x-1/2 w-1/2"
>
<input
type="range"
:min="minZoomRatio"
:max="maxZoomRatio"
:disabled="minZoomRatio === undefined || maxZoomRatio === undefined"
@input="setZoomRatio"
class="w-full"
/>
</div>
<!-- Torch button -->
<button
v-if="isTorchAvailable"
@click="toggleTorch"
class="absolute right-4 bottom-4 p-3 bg-white rounded-full shadow-lg hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-gray-700"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
/>
</svg>
</button>
<!-- Close button -->
<button
@click="closeModal()"
class="absolute top-4 right-4 p-2 bg-white rounded-full shadow-lg hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-gray-700"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
</div>
</template>
<style scoped>
.barcode-scanning-modal {
}
video {
width: 100%;
height: 100%;
object-fit: cover;
}
body.barcode-scanning-active {
background-color: transparent !important;
visibility: visible !important;
}
</style>