docs: update build pattern conversion plan with consistent naming and mode handling

- Change build:* naming from hyphen to colon (build:web-dev → build:web:dev)
- Add missing build:web:test and build:web:prod scripts
- Update build:electron:dev to include electron startup (build + start)
- Remove hardcoded --mode electron to allow proper mode override
- Add comprehensive mode override behavior documentation
- Fix mode conflicts between hardcoded and passed --mode arguments

The plan now properly supports:
- Development builds with default --mode development
- Testing builds with explicit --mode test override
- Production builds with explicit --mode production override
- Consistent naming across all platforms (web, capacitor, electron)
This commit is contained in:
Matthew Raymer
2025-07-09 13:13:44 +00:00
parent 95b038c717
commit b35c1d693f
11 changed files with 4209 additions and 0 deletions

View File

@@ -0,0 +1,708 @@
<template>
<div class="qr-scanner-component">
<h2>QR Code Scanner</h2>
<!-- Camera controls -->
<div class="camera-controls">
<button @click="startScanning" :disabled="isScanning || !hasCamera">
{{ isScanning ? 'Scanning...' : 'Start Scanning' }}
</button>
<button @click="stopScanning" :disabled="!isScanning">
Stop Scanning
</button>
<button @click="switchCamera" :disabled="!isScanning || cameras.length <= 1">
Switch Camera
</button>
</div>
<!-- Camera status -->
<div class="camera-status">
<div v-if="!hasCamera" class="status-error">
<p>Camera not available</p>
<p class="status-detail">This device doesn't have a camera or camera access is denied.</p>
</div>
<div v-else-if="!isScanning" class="status-info">
<p>Camera ready</p>
<p class="status-detail">Click "Start Scanning" to begin QR code detection.</p>
</div>
<div v-else class="status-scanning">
<p>Scanning for QR codes...</p>
<p class="status-detail">Point camera at a QR code to scan.</p>
</div>
</div>
<!-- Camera view -->
<div v-if="isScanning && hasCamera" class="camera-container">
<video
ref="videoElement"
class="camera-video"
autoplay
playsinline
muted
></video>
<!-- Scanning overlay -->
<div class="scanning-overlay">
<div class="scan-frame"></div>
<div class="scan-line"></div>
</div>
</div>
<!-- Scan results -->
<div v-if="scanResults.length > 0" class="scan-results">
<h3>Scan Results ({{ scanResults.length }})</h3>
<div class="results-list">
<div
v-for="(result, index) in scanResults"
:key="index"
class="result-item"
>
<div class="result-header">
<span class="result-number">#{{ index + 1 }}</span>
<span class="result-time">{{ formatTime(result.timestamp) }}</span>
</div>
<div class="result-content">
<div class="qr-data">
<strong>Data:</strong> {{ result.data }}
</div>
<div class="qr-format">
<strong>Format:</strong> {{ result.format }}
</div>
</div>
<div class="result-actions">
<button @click="copyToClipboard(result.data)" class="copy-btn">
Copy
</button>
<button @click="removeResult(index)" class="remove-btn">
Remove
</button>
</div>
</div>
</div>
<div class="results-actions">
<button @click="clearResults" class="clear-btn">
Clear All Results
</button>
<button @click="exportResults" class="export-btn">
Export Results
</button>
</div>
</div>
<!-- Settings panel -->
<div class="settings-panel">
<h3>Scanner Settings</h3>
<div class="setting-group">
<label>
<input
type="checkbox"
v-model="settings.continuousScanning"
/>
Continuous Scanning
</label>
<p class="setting-description">
Automatically scan multiple QR codes without stopping
</p>
</div>
<div class="setting-group">
<label>
<input
type="checkbox"
v-model="settings.audioFeedback"
/>
Audio Feedback
</label>
<p class="setting-description">
Play sound when QR code is detected
</p>
</div>
<div class="setting-group">
<label>
<input
type="checkbox"
v-model="settings.vibrateOnScan"
/>
Vibration Feedback
</label>
<p class="setting-description">
Vibrate device when QR code is detected
</p>
</div>
<div class="setting-group">
<label>Scan Interval (ms):</label>
<input
type="number"
v-model.number="settings.scanInterval"
min="100"
max="5000"
step="100"
/>
<p class="setting-description">
Time between scans (lower = faster, higher = more accurate)
</p>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Emit } from 'vue-facing-decorator';
interface ScanResult {
data: string;
format: string;
timestamp: Date;
}
interface ScannerSettings {
continuousScanning: boolean;
audioFeedback: boolean;
vibrateOnScan: boolean;
scanInterval: number;
}
/**
* QR Scanner Component
*
* Demonstrates lazy loading for camera-dependent features.
* This component would benefit from lazy loading as it requires
* camera permissions and heavy camera processing libraries.
*
* @author Matthew Raymer
* @version 1.0.0
*/
@Component({
name: 'QRScannerComponent'
})
export default class QRScannerComponent extends Vue {
// Component state
isScanning = false;
hasCamera = false;
cameras: MediaDeviceInfo[] = [];
currentCameraIndex = 0;
// Video element reference
videoElement: HTMLVideoElement | null = null;
// Scan results
scanResults: ScanResult[] = [];
// Scanner settings
settings: ScannerSettings = {
continuousScanning: true,
audioFeedback: true,
vibrateOnScan: true,
scanInterval: 500
};
// Internal state
private stream: MediaStream | null = null;
private scanInterval: number | null = null;
private lastScanTime = 0;
// Lifecycle hooks
async mounted(): Promise<void> {
console.log('[QRScannerComponent] Component mounted');
await this.initializeCamera();
}
beforeUnmount(): void {
this.stopScanning();
console.log('[QRScannerComponent] Component unmounting');
}
// Methods
async initializeCamera(): Promise<void> {
try {
// Check if camera is available
const devices = await navigator.mediaDevices.enumerateDevices();
this.cameras = devices.filter(device => device.kind === 'videoinput');
this.hasCamera = this.cameras.length > 0;
if (this.hasCamera) {
console.log('[QRScannerComponent] Camera available:', this.cameras.length, 'devices');
} else {
console.warn('[QRScannerComponent] No camera devices found');
}
} catch (error) {
console.error('[QRScannerComponent] Camera initialization error:', error);
this.hasCamera = false;
}
}
async startScanning(): Promise<void> {
if (!this.hasCamera || this.isScanning) return;
try {
console.log('[QRScannerComponent] Starting QR scanning...');
// Get camera stream
const constraints = {
video: {
deviceId: this.cameras[this.currentCameraIndex]?.deviceId
}
};
this.stream = await navigator.mediaDevices.getUserMedia(constraints);
// Set up video element
this.videoElement = this.$refs.videoElement as HTMLVideoElement;
if (this.videoElement) {
this.videoElement.srcObject = this.stream;
await this.videoElement.play();
}
this.isScanning = true;
// Start QR code detection
this.startQRDetection();
console.log('[QRScannerComponent] QR scanning started');
} catch (error) {
console.error('[QRScannerComponent] Failed to start scanning:', error);
this.hasCamera = false;
}
}
stopScanning(): void {
if (!this.isScanning) return;
console.log('[QRScannerComponent] Stopping QR scanning...');
// Stop QR detection
this.stopQRDetection();
// Stop camera stream
if (this.stream) {
this.stream.getTracks().forEach(track => track.stop());
this.stream = null;
}
// Clear video element
if (this.videoElement) {
this.videoElement.srcObject = null;
this.videoElement = null;
}
this.isScanning = false;
console.log('[QRScannerComponent] QR scanning stopped');
}
async switchCamera(): Promise<void> {
if (this.cameras.length <= 1) return;
// Stop current scanning
this.stopScanning();
// Switch to next camera
this.currentCameraIndex = (this.currentCameraIndex + 1) % this.cameras.length;
// Restart scanning with new camera
await this.startScanning();
console.log('[QRScannerComponent] Switched to camera:', this.currentCameraIndex);
}
private startQRDetection(): void {
if (!this.settings.continuousScanning) return;
this.scanInterval = window.setInterval(() => {
this.detectQRCode();
}, this.settings.scanInterval);
}
private stopQRDetection(): void {
if (this.scanInterval) {
clearInterval(this.scanInterval);
this.scanInterval = null;
}
}
private async detectQRCode(): Promise<void> {
if (!this.videoElement || !this.isScanning) return;
const now = Date.now();
if (now - this.lastScanTime < this.settings.scanInterval) return;
try {
// Simulate QR code detection
// In a real implementation, you would use a QR code library like jsQR
const detectedQR = await this.simulateQRDetection();
if (detectedQR) {
this.addScanResult(detectedQR);
this.lastScanTime = now;
}
} catch (error) {
console.error('[QRScannerComponent] QR detection error:', error);
}
}
private async simulateQRDetection(): Promise<ScanResult | null> {
// Simulate QR code detection with random chance
if (Math.random() < 0.1) { // 10% chance of detection
const sampleData = [
'https://example.com/qr1',
'WIFI:S:MyNetwork;T:WPA;P:password123;;',
'BEGIN:VCARD\nVERSION:3.0\nFN:John Doe\nTEL:+1234567890\nEND:VCARD',
'otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example'
];
const formats = ['URL', 'WiFi', 'vCard', 'TOTP'];
const randomIndex = Math.floor(Math.random() * sampleData.length);
return {
data: sampleData[randomIndex],
format: formats[randomIndex],
timestamp: new Date()
};
}
return null;
}
private addScanResult(result: ScanResult): void {
// Check for duplicates
const isDuplicate = this.scanResults.some(
existing => existing.data === result.data
);
if (!isDuplicate) {
this.scanResults.unshift(result);
// Provide feedback
this.provideFeedback();
// Emit event
this.$emit('qr-detected', result.data);
console.log('[QRScannerComponent] QR code detected:', result.data);
}
}
private provideFeedback(): void {
// Audio feedback
if (this.settings.audioFeedback) {
this.playBeepSound();
}
// Vibration feedback
if (this.settings.vibrateOnScan && 'vibrate' in navigator) {
navigator.vibrate(100);
}
}
private playBeepSound(): void {
// Create a simple beep sound
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
oscillator.type = 'sine';
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.1);
}
copyToClipboard(text: string): void {
navigator.clipboard.writeText(text).then(() => {
console.log('[QRScannerComponent] Copied to clipboard:', text);
}).catch(error => {
console.error('[QRScannerComponent] Failed to copy:', error);
});
}
removeResult(index: number): void {
this.scanResults.splice(index, 1);
}
clearResults(): void {
this.scanResults = [];
console.log('[QRScannerComponent] Results cleared');
}
exportResults(): void {
const data = JSON.stringify(this.scanResults, null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `qr-scan-results-${new Date().toISOString().split('T')[0]}.json`;
a.click();
URL.revokeObjectURL(url);
console.log('[QRScannerComponent] Results exported');
}
formatTime(date: Date): string {
return date.toLocaleTimeString();
}
// Event emitters
@Emit('qr-detected')
emitQRDetected(data: string): string {
return data;
}
}
</script>
<style scoped>
.qr-scanner-component {
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
background: #f9f9f9;
}
.camera-controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.camera-controls button {
padding: 8px 16px;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
cursor: pointer;
transition: background-color 0.2s;
}
.camera-controls button:hover:not(:disabled) {
background: #e9ecef;
}
.camera-controls button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.camera-status {
margin-bottom: 20px;
padding: 15px;
border-radius: 4px;
}
.status-error {
background: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.status-info {
background: #d1ecf1;
border: 1px solid #bee5eb;
color: #0c5460;
}
.status-scanning {
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.status-detail {
margin-top: 5px;
font-size: 0.9em;
opacity: 0.8;
}
.camera-container {
position: relative;
width: 100%;
max-width: 400px;
height: 300px;
margin: 20px auto;
border: 2px solid #ddd;
border-radius: 8px;
overflow: hidden;
}
.camera-video {
width: 100%;
height: 100%;
object-fit: cover;
}
.scanning-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
}
.scan-frame {
width: 200px;
height: 200px;
border: 2px solid #00ff00;
border-radius: 8px;
position: relative;
}
.scan-line {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: #00ff00;
animation: scan 2s linear infinite;
}
@keyframes scan {
0% { top: 0; }
100% { top: 100%; }
}
.scan-results {
margin-top: 20px;
}
.results-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 4px;
background: #fff;
margin-bottom: 15px;
}
.result-item {
padding: 12px;
border-bottom: 1px solid #eee;
}
.result-item:last-child {
border-bottom: none;
}
.result-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.result-number {
font-weight: bold;
color: #007bff;
}
.result-time {
font-size: 0.85em;
color: #666;
}
.result-content {
margin-bottom: 10px;
}
.qr-data,
.qr-format {
margin-bottom: 5px;
word-break: break-all;
}
.result-actions {
display: flex;
gap: 8px;
}
.copy-btn,
.remove-btn {
padding: 4px 8px;
border: 1px solid #ccc;
border-radius: 3px;
background: #fff;
cursor: pointer;
font-size: 0.8em;
}
.copy-btn:hover {
background: #e9ecef;
}
.remove-btn:hover {
background: #f8d7da;
color: #721c24;
}
.results-actions {
display: flex;
gap: 10px;
}
.clear-btn,
.export-btn {
padding: 8px 16px;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
cursor: pointer;
}
.clear-btn:hover {
background: #f8d7da;
color: #721c24;
}
.export-btn:hover {
background: #d4edda;
color: #155724;
}
.settings-panel {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 4px;
}
.setting-group {
margin-bottom: 15px;
}
.setting-group label {
display: flex;
align-items: center;
gap: 8px;
font-weight: bold;
margin-bottom: 5px;
}
.setting-group input[type="number"] {
width: 100px;
padding: 4px 8px;
border: 1px solid #ccc;
border-radius: 3px;
}
.setting-description {
font-size: 0.85em;
color: #666;
margin-top: 3px;
margin-left: 24px;
}
</style>