chore: updates for qr code reader rules, linting, and cleanup

This commit is contained in:
Matt Raymer
2025-05-07 01:57:18 -04:00
parent fdd1ff80ad
commit 74b9caa94f
8 changed files with 592 additions and 1520 deletions

View File

@@ -9,15 +9,33 @@ alwaysApply: true
| Feature | Web (PWA) | Capacitor (Mobile) | Electron (Desktop) | PyWebView (Desktop) |
|---------|-----------|-------------------|-------------------|-------------------|
| QR Code Scanning | vue-qrcode-reader | @capacitor-mlkit/barcode-scanning | Not Implemented | Not Implemented |
| QR Code Scanning | WebInlineQRScanner | @capacitor-mlkit/barcode-scanning | Not Implemented | Not Implemented |
| Deep Linking | URL Parameters | App URL Open Events | Not Implemented | Not Implemented |
| File System | Limited (Browser API) | Capacitor Filesystem | Electron fs | PyWebView Python Bridge |
| Camera Access | MediaDevices API | Capacitor Camera | Not Implemented | Not Implemented |
| Platform Detection | Web APIs | Capacitor.isNativePlatform() | process.env checks | process.env checks |
## 2. Build Configuration Structure
## 2. Project Structure
### 2.1 Entry Points
### 2.1 Core Directories
```
src/
├── components/ # Vue components
├── services/ # Platform services and business logic
├── views/ # Page components
├── router/ # Vue router configuration
├── types/ # TypeScript type definitions
├── utils/ # Utility functions
├── lib/ # Core libraries
├── platforms/ # Platform-specific implementations
├── electron/ # Electron-specific code
├── constants/ # Application constants
├── db/ # Database related code
├── interfaces/ # TypeScript interfaces
└── assets/ # Static assets
```
### 2.2 Entry Points
```
src/
├── main.ts # Base entry
@@ -28,19 +46,35 @@ src/
└── main.web.ts # Web/PWA entry
```
### 2.2 Build Configurations
### 2.3 Build Configurations
```
root/
├── vite.config.common.mts # Shared config
├── vite.config.capacitor.mts # Mobile build
├── vite.config.electron.mts # Electron build
├── vite.config.pywebview.mts # PyWebView build
── vite.config.web.mts # Web/PWA build
── vite.config.web.mts # Web/PWA build
└── vite.config.utils.mts # Build utilities
```
## 3. Platform Service Architecture
## 3. Service Architecture
### 3.1 Service Factory Pattern
### 3.1 Service Organization
```
services/
├── QRScanner/ # QR code scanning service
│ ├── WebInlineQRScanner.ts
│ └── interfaces.ts
├── platforms/ # Platform-specific services
│ ├── WebPlatformService.ts
│ ├── CapacitorPlatformService.ts
│ ├── ElectronPlatformService.ts
│ └── PyWebViewPlatformService.ts
└── factory/ # Service factories
└── PlatformServiceFactory.ts
```
### 3.2 Service Factory Pattern
```typescript
// PlatformServiceFactory.ts
export class PlatformServiceFactory {
@@ -56,40 +90,34 @@ export class PlatformServiceFactory {
}
```
### 3.2 Platform-Specific Implementations
```
services/platforms/
├── WebPlatformService.ts
├── CapacitorPlatformService.ts
├── ElectronPlatformService.ts
└── PyWebViewPlatformService.ts
```
## 4. Feature Implementation Guidelines
### 4.1 QR Code Scanning
1. **Factory Pattern**
1. **Service Interface**
```typescript
export class QRScannerFactory {
static getInstance(): QRScannerService {
if (__IS_MOBILE__ || Capacitor.isNativePlatform()) {
return new CapacitorQRScanner();
} else if (__USE_QR_READER__) {
return new WebDialogQRScanner();
}
throw new Error("No QR scanner implementation available");
}
interface QRScannerService {
checkPermissions(): Promise<boolean>;
requestPermissions(): Promise<boolean>;
isSupported(): Promise<boolean>;
startScan(): Promise<void>;
stopScan(): Promise<void>;
addListener(listener: ScanListener): void;
onStream(callback: (stream: MediaStream | null) => void): void;
cleanup(): Promise<void>;
}
```
2. **Platform-Specific Implementation**
```typescript
// Example for Capacitor
export class CapacitorQRScanner implements QRScannerService {
async startScan(options?: QRScannerOptions): Promise<void> {
// Platform-specific implementation
}
// WebInlineQRScanner.ts
export class WebInlineQRScanner implements QRScannerService {
private scanListener: ScanListener | null = null;
private isScanning = false;
private stream: MediaStream | null = null;
private events = new EventEmitter();
// Implementation of interface methods
}
```
@@ -202,6 +230,8 @@ if (process.env.VITE_PLATFORM === 'capacitor') {
- Use platform-specific directories for unique implementations
- Share common code through service interfaces
- Implement feature detection before using platform capabilities
- Keep platform-specific code isolated in dedicated directories
- Use TypeScript interfaces for cross-platform compatibility
### 8.2 Platform Detection
```typescript
@@ -219,6 +249,7 @@ if (capabilities.hasCamera) {
3. Use factory pattern for instantiation
4. Implement graceful fallbacks
5. Add comprehensive error handling
6. Use dependency injection for better testability
## 9. Dependency Management
@@ -228,7 +259,7 @@ if (capabilities.hasCamera) {
"dependencies": {
"@capacitor/core": "^6.2.0",
"electron": "^33.2.1",
"vue-qrcode-reader": "^5.5.3"
"vue": "^3.4.0"
}
}
```
@@ -253,8 +284,9 @@ async checkPermissions(): Promise<boolean> {
```
### 10.2 Data Storage
- Use platform-appropriate storage mechanisms
- Implement encryption for sensitive data
- Handle permissions appropriately
- Use secure storage mechanisms for sensitive data
- Implement proper encryption for stored data
- Follow platform-specific security guidelines
- Regular security audits and updates
This document should be updated as new features are added or platform-specific implementations change. Regular reviews ensure it remains current with the codebase.

View File

@@ -14,11 +14,11 @@ The QR code scanning functionality follows a platform-agnostic design using a fa
1. **Factory Pattern**
- `QRScannerFactory` - Creates appropriate scanner instance based on platform
- Common interface `QRScannerService` implemented by all scanners
- Platform detection via Vite config flags: `__USE_QR_READER__` and `__IS_MOBILE__`
- Platform detection via Capacitor and build flags
2. **Platform-Specific Implementations**
- `CapacitorQRScanner` - Native mobile implementation
- `WebDialogQRScanner` - Web browser implementation
- `CapacitorQRScanner` - Native mobile implementation using MLKit
- `WebInlineQRScanner` - Web browser implementation using MediaDevices API
- `QRScannerDialog.vue` - Shared UI component
## Mobile Implementation (Capacitor)
@@ -54,13 +54,13 @@ MLKitBarcodeScanner: {
## Web Implementation
### Technology Stack
- Uses `vue-qrcode-reader` library
- Browser's MediaDevices API
- Vue.js dialog component
- Uses browser's MediaDevices API
- Vue.js components for UI
- EventEmitter for stream management
### Key Features
- Browser-based camera access
- Fallback UI for unsupported browsers
- Inline camera preview
- Responsive design
- Cross-browser compatibility
- Progressive enhancement
@@ -97,9 +97,10 @@ MLKitBarcodeScanner: {
### Platform Detection
```typescript
if (__IS_MOBILE__ || Capacitor.isNativePlatform()) {
const isNative = QRScannerFactory.isNativePlatform();
if (isNative) {
// Use native scanner
} else if (__USE_QR_READER__) {
} else {
// Use web scanner
}
```
@@ -112,6 +113,9 @@ await scanner.startScan();
scanner.addListener({
onScan: (result) => {
// Handle scan result
},
onError: (error) => {
// Handle error
}
});
```
@@ -145,4 +149,29 @@ scanner.addListener({
2. Verify permission flows
3. Check error handling
4. Validate cleanup
5. Verify cross-platform behavior
5. Verify cross-platform behavior
## Service Interface
```typescript
interface QRScannerService {
checkPermissions(): Promise<boolean>;
requestPermissions(): Promise<boolean>;
isSupported(): Promise<boolean>;
startScan(options?: QRScannerOptions): Promise<void>;
stopScan(): Promise<void>;
addListener(listener: ScanListener): void;
onStream(callback: (stream: MediaStream | null) => void): void;
cleanup(): Promise<void>;
}
interface ScanListener {
onScan: (result: string) => void;
onError?: (error: Error) => void;
}
interface QRScannerOptions {
camera?: "front" | "back";
showPreview?: boolean;
playSound?: boolean;
}

View File

@@ -9,19 +9,16 @@ alwaysApply: true
```
src/
├── components/
│ └── QRScanner/
│ ├── types.ts
│ ├── factory.ts
│ ├── CapacitorScanner.ts
│ ├── WebDialogScanner.ts
│ └── QRScannerDialog.vue
├── services/
│ └── QRScanner/
│ ├── types.ts
│ ├── QRScannerFactory.ts
│ ├── CapacitorQRScanner.ts
── WebDialogQRScanner.ts
│ ├── types.ts # Core interfaces and types
│ ├── QRScannerFactory.ts # Factory for creating scanner instances
│ ├── CapacitorQRScanner.ts # Mobile implementation using MLKit
── WebInlineQRScanner.ts # Web implementation using MediaDevices API
│ └── interfaces.ts # Additional interfaces
├── components/
│ └── QRScanner/
│ └── QRScannerDialog.vue # Shared UI component
```
## Core Interfaces
@@ -33,13 +30,20 @@ export interface ScanListener {
onError?: (error: Error) => void;
}
export interface QRScannerOptions {
camera?: "front" | "back";
showPreview?: boolean;
playSound?: boolean;
}
export interface QRScannerService {
checkPermissions(): Promise<boolean>;
requestPermissions(): Promise<boolean>;
isSupported(): Promise<boolean>;
startScan(): Promise<void>;
startScan(options?: QRScannerOptions): Promise<void>;
stopScan(): Promise<void>;
addListener(listener: ScanListener): void;
onStream(callback: (stream: MediaStream | null) => void): void;
cleanup(): Promise<void>;
}
```
@@ -48,18 +52,17 @@ export interface QRScannerService {
### Vite Configuration
```typescript
// vite.config.ts
export default defineConfig({
define: {
__USE_QR_READER__: JSON.stringify(!isMobile),
__IS_MOBILE__: JSON.stringify(isMobile),
},
build: {
rollupOptions: {
external: isMobile ? ['vue-qrcode-reader'] : [],
// vite.config.common.mts
export function createBuildConfig(mode: string) {
return {
define: {
'process.env.VITE_PLATFORM': JSON.stringify(mode),
'process.env.VITE_PWA_ENABLED': JSON.stringify(!isNative),
__IS_MOBILE__: JSON.stringify(isCapacitor),
__USE_QR_READER__: JSON.stringify(!isCapacitor)
}
}
});
};
}
```
### Capacitor Configuration
@@ -81,7 +84,7 @@ const config: CapacitorConfig = {
1. **Install Dependencies**
```bash
npm install @capacitor-mlkit/barcode-scanning vue-qrcode-reader
npm install @capacitor-mlkit/barcode-scanning
```
2. **Create Core Types**
@@ -93,20 +96,34 @@ Create the interface files as shown above.
export class QRScannerFactory {
private static instance: QRScannerService | null = null;
static getInstance(): QRScannerService {
if (!this.instance) {
if (__IS_MOBILE__ || Capacitor.isNativePlatform()) {
this.instance = new CapacitorQRScanner();
} else if (__USE_QR_READER__) {
this.instance = new WebDialogQRScanner();
} else {
throw new Error('No QR scanner implementation available');
}
private static isNativePlatform(): boolean {
const capacitorNative = Capacitor.isNativePlatform();
const isMobile = typeof __IS_MOBILE__ !== "undefined" ? __IS_MOBILE__ : capacitorNative;
const platform = Capacitor.getPlatform();
// Always use native scanner on Android/iOS
if (platform === "android" || platform === "ios") {
return true;
}
return this.instance;
// For other platforms, use native if available
return capacitorNative || isMobile;
}
static async cleanup() {
static getInstance(): QRScannerService {
if (!this.instance) {
const isNative = this.isNativePlatform();
if (isNative) {
this.instance = new CapacitorQRScanner();
} else {
this.instance = new WebInlineQRScanner();
}
}
return this.instance!;
}
static async cleanup(): Promise<void> {
if (this.instance) {
await this.instance.cleanup();
this.instance = null;
@@ -122,65 +139,104 @@ export class CapacitorQRScanner implements QRScannerService {
private scanListener: ScanListener | null = null;
private isScanning = false;
private listenerHandles: Array<() => Promise<void>> = [];
private cleanupPromise: Promise<void> | null = null;
async checkPermissions() {
async checkPermissions(): Promise<boolean> {
try {
const { camera } = await BarcodeScanner.checkPermissions();
return camera === 'granted';
return camera === "granted";
} catch (error) {
logger.error('Error checking camera permissions:', error);
logger.error("Error checking camera permissions:", error);
return false;
}
}
async requestPermissions() {
async requestPermissions(): Promise<boolean> {
try {
if (await this.checkPermissions()) {
return true;
}
const { camera } = await BarcodeScanner.requestPermissions();
return camera === 'granted';
return camera === "granted";
} catch (error) {
logger.error('Error requesting camera permissions:', error);
logger.error("Error requesting camera permissions:", error);
return false;
}
}
async isSupported() {
return Capacitor.isNativePlatform();
async isSupported(): Promise<boolean> {
try {
const { supported } = await BarcodeScanner.isSupported();
return supported;
} catch (error) {
logger.error("Error checking scanner support:", error);
return false;
}
}
async startScan() {
async startScan(options?: QRScannerOptions): Promise<void> {
if (this.isScanning) return;
this.isScanning = true;
if (this.cleanupPromise) {
await this.cleanupPromise;
}
try {
await BarcodeScanner.startScan();
if (!(await this.checkPermissions())) {
const granted = await this.requestPermissions();
if (!granted) {
throw new Error("Camera permission denied");
}
}
if (!(await this.isSupported())) {
throw new Error("QR scanning not supported on this device");
}
this.isScanning = true;
const scanOptions: StartScanOptions = {
formats: [BarcodeFormat.QrCode],
lensFacing: options?.camera === "front" ? LensFacing.Front : LensFacing.Back,
};
const handle = await BarcodeScanner.addListener("barcodeScanned", (result) => {
if (this.scanListener && result.barcode?.rawValue) {
this.scanListener.onScan(result.barcode.rawValue);
}
});
this.listenerHandles.push(handle.remove);
await BarcodeScanner.startScan(scanOptions);
} catch (error) {
this.isScanning = false;
await this.cleanup();
this.scanListener?.onError?.(error instanceof Error ? error : new Error(String(error)));
throw error;
}
}
async stopScan() {
async stopScan(): Promise<void> {
if (!this.isScanning) return;
this.isScanning = false;
try {
await BarcodeScanner.stopScan();
} catch (error) {
logger.error('Error stopping scan:', error);
logger.error("Error stopping scan:", error);
throw error;
}
}
addListener(listener: ScanListener) {
addListener(listener: ScanListener): void {
this.scanListener = listener;
const handle = BarcodeScanner.addListener('barcodeScanned', (result) => {
if (this.scanListener) {
this.scanListener.onScan(result.barcode);
}
});
this.listenerHandles.push(handle.remove);
}
async cleanup() {
onStream(callback: (stream: MediaStream | null) => void): void {
// No-op for native scanner
callback(null);
}
async cleanup(): Promise<void> {
await this.stopScan();
for (const handle of this.listenerHandles) {
await handle();
@@ -193,31 +249,40 @@ export class CapacitorQRScanner implements QRScannerService {
5. **Implement Web Scanner**
```typescript
// WebDialogQRScanner.ts
export class WebDialogQRScanner implements QRScannerService {
private dialogInstance: App | null = null;
private dialogComponent: InstanceType<typeof QRScannerDialog> | null = null;
// WebInlineQRScanner.ts
export class WebInlineQRScanner implements QRScannerService {
private scanListener: ScanListener | null = null;
private isScanning = false;
private stream: MediaStream | null = null;
private events = new EventEmitter();
constructor(private options?: QRScannerOptions) {}
async checkPermissions(): Promise<boolean> {
try {
const permissions = await navigator.permissions.query({
name: 'camera' as PermissionName
name: "camera" as PermissionName,
});
return permissions.state === 'granted';
return permissions.state === "granted";
} catch (error) {
logger.error('Error checking camera permissions:', error);
logger.error("Error checking camera permissions:", error);
return false;
}
}
async requestPermissions(): Promise<boolean> {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
const stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: "environment",
width: { ideal: 1280 },
height: { ideal: 720 },
},
});
stream.getTracks().forEach(track => track.stop());
return true;
} catch (error) {
logger.error('Error requesting camera permissions:', error);
logger.error("Error requesting camera permissions:", error);
return false;
}
}
@@ -226,208 +291,150 @@ export class WebDialogQRScanner implements QRScannerService {
return 'mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices;
}
async startScan() {
if (this.dialogInstance) return;
async startScan(): Promise<void> {
if (this.isScanning) return;
const container = document.createElement('div');
document.body.appendChild(container);
try {
this.isScanning = true;
this.stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: "environment",
width: { ideal: 1280 },
height: { ideal: 720 },
},
});
this.events.emit("stream", this.stream);
} catch (error) {
this.isScanning = false;
const wrappedError = error instanceof Error ? error : new Error(String(error));
this.scanListener?.onError?.(wrappedError);
throw wrappedError;
}
}
this.dialogInstance = createApp(QRScannerDialog, {
onScan: (result: string) => {
if (this.scanListener) {
this.scanListener.onScan(result);
}
},
onError: (error: Error) => {
if (this.scanListener?.onError) {
this.scanListener.onError(error);
}
},
onClose: () => {
this.cleanup();
async stopScan(): Promise<void> {
if (!this.isScanning) return;
try {
if (this.stream) {
this.stream.getTracks().forEach(track => track.stop());
this.stream = null;
}
});
this.dialogComponent = this.dialogInstance.mount(container) as InstanceType<typeof QRScannerDialog>;
this.events.emit("stream", null);
} catch (error) {
logger.error("Error stopping scan:", error);
throw error;
} finally {
this.isScanning = false;
}
}
async stopScan() {
await this.cleanup();
}
addListener(listener: ScanListener) {
addListener(listener: ScanListener): void {
this.scanListener = listener;
}
async cleanup() {
if (this.dialogInstance) {
this.dialogInstance.unmount();
this.dialogInstance = null;
this.dialogComponent = null;
onStream(callback: (stream: MediaStream | null) => void): void {
this.events.on("stream", callback);
}
async cleanup(): Promise<void> {
try {
await this.stopScan();
this.events.removeAllListeners();
} catch (error) {
logger.error("Error during cleanup:", error);
}
}
}
```
6. **Create Dialog Component**
```vue
<!-- QRScannerDialog.vue -->
<template>
<div v-if="visible" class="dialog-overlay z-[60]">
<div class="dialog relative">
<div class="dialog-header">
<h2>Scan QR Code</h2>
<button @click="onClose" class="close-button">×</button>
</div>
<div class="dialog-content">
<div v-if="useQRReader">
<qrcode-stream
class="w-full max-w-lg mx-auto"
@detect="onScanDetect"
@error="onScanError"
/>
</div>
<div v-else>
<button @click="startMobileScan" class="scan-button">
Start Camera
</button>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { QrcodeStream } from 'vue-qrcode-reader';
export default defineComponent({
name: 'QRScannerDialog',
components: { QrcodeStream },
props: {
onScan: {
type: Function,
required: true
},
onError: {
type: Function,
required: true
},
onClose: {
type: Function,
required: true
}
},
data() {
return {
visible: true,
useQRReader: __USE_QR_READER__
};
},
methods: {
onScanDetect(promisedResult: Promise<string>) {
promisedResult
.then(result => this.onScan(result))
.catch(error => this.onError(error));
},
onScanError(error: Error) {
this.onError(error);
},
async startMobileScan() {
try {
const scanner = QRScannerFactory.getInstance();
await scanner.startScan();
} catch (error) {
this.onError(error as Error);
}
}
}
});
</script>
<style scoped>
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.dialog {
background: white;
border-radius: 8px;
padding: 20px;
max-width: 90vw;
max-height: 90vh;
overflow: auto;
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.close-button {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
}
.scan-button {
background: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.scan-button:hover {
background: #45a049;
}
</style>
```
## Usage Example
```typescript
// In your component
async function scanQRCode() {
const scanner = QRScannerFactory.getInstance();
if (!(await scanner.checkPermissions())) {
const granted = await scanner.requestPermissions();
if (!granted) {
throw new Error('Camera permission denied');
// Example usage in a Vue component
import { QRScannerFactory } from '@/services/QRScanner/QRScannerFactory';
export default defineComponent({
async mounted() {
const scanner = QRScannerFactory.getInstance();
try {
// Check and request permissions
if (!(await scanner.checkPermissions())) {
const granted = await scanner.requestPermissions();
if (!granted) {
throw new Error('Camera permission denied');
}
}
// Add scan listener
scanner.addListener({
onScan: (result) => {
console.log('QR Code scanned:', result);
},
onError: (error) => {
console.error('Scan error:', error);
}
});
// Start scanning
await scanner.startScan({
camera: 'back',
showPreview: true
});
// Handle stream for preview
scanner.onStream((stream) => {
if (stream) {
// Update video element with stream
this.videoElement.srcObject = stream;
}
});
} catch (error) {
console.error('Failed to start scanner:', error);
}
},
async beforeUnmount() {
// Clean up scanner
await QRScannerFactory.cleanup();
}
scanner.addListener({
onScan: (result) => {
console.log('Scanned:', result);
},
onError: (error) => {
console.error('Scan error:', error);
}
});
await scanner.startScan();
}
// Cleanup when done
onUnmounted(() => {
QRScannerFactory.cleanup();
});
```
## Best Practices
1. **Error Handling**
- Always implement error handlers in scan listeners
- Handle permission denials gracefully
- Provide user feedback for errors
- Clean up resources on errors
2. **Resource Management**
- Always call cleanup when done
- Stop camera streams properly
- Remove event listeners
- Handle component unmounting
3. **Performance**
- Use appropriate camera resolution
- Clean up resources promptly
- Handle platform-specific optimizations
- Monitor memory usage
4. **Security**
- Require HTTPS for web implementation
- Validate scanned data
- Handle permissions properly
- Sanitize user input
5. **Testing**
- Test on multiple devices
- Verify permission flows
- Check error scenarios
- Validate cleanup
- Test cross-platform behavior
## Platform-Specific Notes
### Mobile (Capacitor)