emmanuelbarrameda

take-picture.blade.php

Apr 14th, 2025
23
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 23.42 KB | None | 0 0
  1. @php
  2.     $isDisabled = $field->isDisabled();
  3. @endphp
  4.  
  5. <x-dynamic-component
  6.     :component="$getFieldWrapperView()"
  7.     :field="$field"
  8. >
  9.     <div
  10.         x-data="{
  11.            photoData: $wire.entangle('{{ $getStatePath() }}'),
  12.            photoSelected: false,
  13.            webcamActive: false,
  14.            webcamError: null,
  15.            cameraStream: null,
  16.            availableCameras: [],
  17.            selectedCameraId: null,
  18.            modalOpen: false,
  19.            aspectRatio: '{{ $getAspect() }}',
  20.            imageQuality: {{ $getImageQuality() }},
  21.            mirroredView: true,
  22.            isDisabled: {{ json_encode($isDisabled) }},
  23.            urlPrefix: '{{ $getImageUrlPrefix() }}',
  24.            
  25.            getImageUrl(path) {
  26.                if (!path) return null;
  27.                if (path.startsWith('data:image/')) return path;
  28.                // Only prepend the URL prefix if it's a path and not already a full URL
  29.                if (!path.startsWith('http://') && !path.startsWith('https://')) {
  30.                    return this.urlPrefix + path;
  31.                }
  32.                return path;
  33.            },
  34.  
  35.            async getCameras() {
  36.                try {
  37.                    const devices = await navigator.mediaDevices.enumerateDevices();
  38.                    this.availableCameras = devices.filter(device => device.kind === 'videoinput');
  39.                    
  40.                    if (this.availableCameras.length > 0 && !this.selectedCameraId) {
  41.                        // Default to the first camera (usually front-facing on mobile)
  42.                        this.selectedCameraId = this.availableCameras[0].deviceId;
  43.                    }
  44.                    
  45.                    return this.availableCameras;
  46.                } catch (error) {
  47.                    console.error('Error getting camera devices:', error);
  48.                    this.webcamError = '{{ __('Unable to detect available cameras') }}';
  49.                    return [];
  50.                }
  51.            },
  52.            
  53.            async initWebcam() {
  54.                if (this.isDisabled) return;
  55.                this.webcamActive = true;
  56.                this.webcamError = null;
  57.                
  58.                if ({{ $getShowCameraSelector() ? 'true' : 'false' }} && this.availableCameras.length === 0) {
  59.                    await this.getCameras();
  60.                }
  61.                
  62.                // Calculate aspect ratio for constraints
  63.                let aspectWidth = 16;
  64.                let aspectHeight = 9;
  65.                
  66.                if (this.aspectRatio) {
  67.                    const parts = this.aspectRatio.split(':');
  68.                    if (parts.length === 2) {
  69.                        aspectWidth = parseInt(parts[0]);
  70.                        aspectHeight = parseInt(parts[1]);
  71.                    }
  72.                }
  73.                
  74.                const constraints = {
  75.                    video: {
  76.                        facingMode: 'user',
  77.                        width: { ideal: aspectWidth * 120 },
  78.                        height: { ideal: aspectHeight * 120 }
  79.                    }
  80.                };
  81.                audio: false
  82.                
  83.                // If a specific camera is selected, use its deviceId
  84.                if (this.selectedCameraId) {
  85.                    constraints.video.deviceId = { exact: this.selectedCameraId };
  86.                }
  87.                
  88.                try {
  89.                    const stream = await navigator.mediaDevices.getUserMedia(constraints);
  90.                    this.cameraStream = stream;
  91.                    this.$refs.video.srcObject = stream;
  92.                    
  93.                    // Auto-open modal if configured
  94.                    if ({{ $getUseModal() ? 'true' : 'false' }} && !this.modalOpen) {
  95.                        this.openModal();
  96.                    }
  97.                } catch (error) {
  98.                    console.error('Error accessing webcam:', error);
  99.                    this.handleWebcamError(error);
  100.                }
  101.            },
  102.            
  103.            handleWebcamError(error) {
  104.                switch (error.name) {
  105.                    case 'NotAllowedError':
  106.                    case 'PermissionDeniedError':
  107.                        this.webcamError = '{{ __('Permission denied. Please allow camera access') }}';
  108.                        break;
  109.                    case 'NotFoundError':
  110.                    case 'DevicesNotFoundError':
  111.                        this.webcamError = '{{ __('No available or connected camera found') }}';
  112.                        break;
  113.                    case 'NotReadableError':
  114.                    case 'TrackStartError':
  115.                        this.webcamError = '{{ __('The camera is in use by another application or cannot be accessed') }}';
  116.                        break;
  117.                    case 'OverconstrainedError':
  118.                        this.webcamError = '{{ __('Could not meet the requested camera constraints') }}';
  119.                        break;
  120.                    case 'SecurityError':
  121.                        this.webcamError = '{{ __('Access blocked for security reasons. Use HTTPS or a trusted browser') }}';
  122.                        break;
  123.                    case 'AbortError':
  124.                        this.webcamError = '{{ __('The camera access operation was canceled') }}';
  125.                        break;
  126.                    default:
  127.                        this.webcamError = '{{ __('An unknown error occurred while trying to open the camera') }}';
  128.                }
  129.            },
  130.            
  131.            async changeCamera(cameraId) {
  132.                this.selectedCameraId = cameraId;
  133.                if (this.webcamActive) {
  134.                    this.stopCamera();
  135.                    await this.$nextTick();
  136.                    this.initWebcam();
  137.                }
  138.            },
  139.            
  140.            capturePhoto() {
  141.                const video = this.$refs.video;
  142.                const canvas = document.createElement('canvas');
  143.                
  144.                // Use actual video dimensions for better quality
  145.                canvas.width = video.videoWidth;
  146.                canvas.height = video.videoHeight;
  147.                
  148.                const context = canvas.getContext('2d');
  149.                
  150.                // Handle mirroring correctly during capture
  151.                if (this.mirroredView) {
  152.                    // If mirrored view, we need to flip the capture back to normal
  153.                    context.translate(canvas.width, 0);
  154.                    context.scale(-1, 1);
  155.                }
  156.                
  157.                context.drawImage(video, 0, 0, canvas.width, canvas.height);
  158.                
  159.                // Apply quality setting (0-1)
  160.                const quality = this.imageQuality / 100;
  161.                this.photoData = canvas.toDataURL('image/jpeg', quality);
  162.                
  163.                // Automatically shut down camera after capturing
  164.                this.stopCamera();
  165.                
  166.                // Close modal if it was open
  167.                if (this.modalOpen) {
  168.                    this.closeModal();
  169.                }
  170.            },
  171.            
  172.            usePhoto() {
  173.                this.photoSelected = true;
  174.                // Already set in photoData via entangle
  175.            },
  176.            
  177.            retakePhoto() {
  178.                this.photoSelected = false;
  179.                this.initWebcam();
  180.            },
  181.            
  182.            stopCamera() {
  183.                this.webcamActive = false;
  184.                if (this.cameraStream) {
  185.                    this.cameraStream.getTracks().forEach(track => track.stop());
  186.                    this.cameraStream = null;
  187.                }
  188.            },
  189.            
  190.            toggleCamera() {
  191.                if (this.webcamActive) {
  192.                    this.stopCamera();
  193.                } else {
  194.                    this.initWebcam();
  195.                }
  196.            },
  197.            
  198.            toggleMirror() {
  199.                this.mirroredView = !this.mirroredView;
  200.            },
  201.            
  202.            isBase64Image() {
  203.                return this.photoData && this.photoData.startsWith('data:image/');
  204.            },
  205.            
  206.            clearPhoto() {
  207.                this.photoData = null;
  208.                this.photoSelected = false;
  209.            },
  210.            
  211.            openModal() {
  212.                this.modalOpen = true;
  213.                document.body.classList.add('overflow-hidden');
  214.            },
  215.            
  216.            closeModal() {
  217.                this.modalOpen = false;
  218.                document.body.classList.remove('overflow-hidden');
  219.                this.stopCamera();
  220.            }
  221.        }"
  222.         x-init="() => {
  223.            if (!photoData) {
  224.                if ({{ $getUseModal() ? 'false' : 'true' }}) {
  225.                    initWebcam();
  226.                }
  227.            } else if (!isBase64Image()) {
  228.                photoSelected = true;
  229.            }
  230.            
  231.            if ({{ $getShowCameraSelector() ? 'true' : 'false' }}) {
  232.                getCameras();
  233.            }
  234.        }"
  235.         @keydown.escape.window="if (modalOpen) { closeModal(); } else { stopCamera(); }"
  236.         class="flex flex-col space-y-4"
  237.     >
  238.         <!-- preview thumbnail -->
  239.         <div class="flex items-center space-x-4">
  240.            
  241.             <div class="relative w-20 h-20">
  242.  
  243.                 <!-- photo-preview available -->
  244.                 <template x-if="photoData">
  245.                     <div
  246.                         @click="!{{ json_encode($isDisabled) }} && initWebcam()"
  247.                         class="w-24 h-24 rounded-lg overflow-hidden bg-gray-100 cursor-pointer shadow-sm hover:shadow-md transition-all border border-gray-300"
  248.                         :class="{'cursor-default': {{ json_encode($isDisabled) }}, 'cursor-pointer': !{{ json_encode($isDisabled) }}}"
  249.                     >
  250.                         <img :src="photoData ? getImageUrl(photoData) : ''" class="w-full h-full object-cover">
  251.                         <div class="absolute bottom-0 right-0 p-1 bg-gray-800 bg-opacity-70 rounded-tl" x-show="!{{ json_encode($isDisabled) }}">
  252.                             <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-white" viewBox="0 0 20 20" fill="currentColor">
  253.                                 <path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
  254.                             </svg>
  255.                         </div>
  256.                     </div>
  257.                 </template>
  258.                
  259.                 <!-- take photo button -->
  260.                 <template x-if="!photoData">
  261.                     <button
  262.                         type="button"
  263.                         @click="!{{ json_encode($isDisabled) }} && initWebcam()"
  264.                         class="w-24 h-24 rounded-lg border border-dashed border-gray-400 flex flex-col items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors"
  265.                         :class="{'cursor-default pointer-events-none opacity-70': {{ json_encode($isDisabled) }}, 'cursor-pointer': !{{ json_encode($isDisabled) }}}"
  266.                     >
  267.                         <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  268.                             <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
  269.                             <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
  270.                         </svg>
  271.                         <span class="mt-1 text-xs text-gray-600">{{ __('Take Photo') }}</span>
  272.                     </button>
  273.                 </template>
  274.                
  275.                 <!-- clear button -->
  276.                 <template x-if="photoData && !{{ json_encode($isDisabled) }}">
  277.                     <button
  278.                         type="button"
  279.                         @click.stop="clearPhoto()"
  280.                         class="absolute -top-2 -right-2 p-1 bg-red-500 text-white rounded-full shadow-sm hover:bg-red-600 transition-colors"
  281.                     >
  282.                         <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
  283.                             <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
  284.                         </svg>
  285.                     </button>
  286.                 </template>
  287.             </div>
  288.            
  289.             <!-- help text -->
  290.             @if (!$isDisabled)
  291.                 <div class="text-sm ml-4">
  292.                     <p class="text-gray-700 font-medium mb-1">{{ $getLabel() }}</p>
  293.                     <p class="text-gray-500 text-xs">{{ __('Click to capture a new photo') }}</p>
  294.                 </div>
  295.             @endif
  296.         </div>
  297.        
  298.         <!-- display error message, when accessing the camera -->
  299.         <template x-if="webcamError && !modalOpen">
  300.             <div class="text-red-500 bg-red-50 py-2 px-3 rounded text-sm">
  301.                 <span x-text="webcamError"></span>
  302.             </div>
  303.         </template>
  304.  
  305.         <!-- field to store the captured picture -->
  306.         <input type="hidden" {{ $applyStateBindingModifiers('wire:model') }}="{{ $getStatePath() }}">
  307.        
  308.         <!-- MODAL -->
  309.         <template x-teleport="body">
  310.             <div
  311.                 x-show="modalOpen"
  312.                 @click.self="closeModal()"
  313.                 x-transition:enter="transition ease-out duration-200"
  314.                 x-transition:enter-start="opacity-0"
  315.                 x-transition:enter-end="opacity-100"
  316.                 x-transition:leave="transition ease-in duration-150"
  317.                 x-transition:leave-start="opacity-100"
  318.                 x-transition:leave-end="opacity-0"
  319.                 class="fixed inset-0 z-50 bg-black bg-opacity-75 flex items-center justify-center p-4"
  320.                 style="display: none;"
  321.             >
  322.                 <div
  323.                     @click.stop
  324.                     x-transition:enter="transition ease-out duration-200"
  325.                     x-transition:enter-start="opacity-0 scale-95"
  326.                     x-transition:enter-end="opacity-100 scale-100"
  327.                     x-transition:leave="transition ease-in duration-150"
  328.                     x-transition:leave-start="opacity-100 scale-100"
  329.                     x-transition:leave-end="opacity-0 scale-95"
  330.                     class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-lg overflow-hidden"
  331.                 >
  332.                     <!-- MODAL HEADER -->
  333.                     <div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
  334.                         <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
  335.                             {{ __('Take Photo') }}
  336.                         </h3>
  337.                         <button
  338.                             type="button"
  339.                             @click="closeModal()"
  340.                             class="text-gray-400 hover:text-gray-500"
  341.                         >
  342.                             <svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
  343.                                 <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
  344.                             </svg>
  345.                         </button>
  346.                     </div>
  347.                    
  348.                     <!-- MODAL BODY -->
  349.                     <div class="p-4">
  350.                         <!-- CAMERA VIEW  -->
  351.                         <div class="relative bg-black rounded-lg overflow-hidden mb-4">
  352.                             <!-- PRVIEW -->
  353.                             <template x-if="webcamActive && !webcamError">
  354.                                 <div class="aspect-video flex items-center justify-center">
  355.                                     <video
  356.                                         x-ref="video"
  357.                                         autoplay
  358.                                         playsinline
  359.                                         :style="mirroredView ? 'transform: scaleX(-1);' : ''"
  360.                                         class="max-w-full max-h-[60vh] object-contain"
  361.                                     ></video>
  362.                                 </div>
  363.                             </template>
  364.                            
  365.                             <!-- ERROR -->
  366.                             <template x-if="webcamError">
  367.                                 <div class="aspect-video bg-gray-800 flex flex-col items-center justify-center text-center p-6">
  368.                                     <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-red-500 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  369.                                         <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
  370.                                     </svg>
  371.                                     <span class="text-white text-lg font-medium" x-text="webcamError"></span>
  372.  
  373.                                     <button
  374.                                         type="button"
  375.                                         @click="webcamError = null; initWebcam()"
  376.                                         class="mt-4 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700"
  377.                                     >
  378.                                         {{ __('Try Again') }}
  379.                                     </button>
  380.                                    
  381.                                 </div>
  382.                             </template>
  383.                            
  384.                             <!-- TAKE PHOTO BUTTON -->
  385.                             <template x-if="webcamActive && !webcamError">
  386.                                 <div class="absolute bottom-4 left-0 right-0 flex justify-center">
  387.                                     <button
  388.                                         type="button"
  389.                                         @click="capturePhoto()"
  390.                                         class="w-16 h-16 rounded-full bg-primary-600 hover:bg-primary-700 border-4 border-white flex items-center justify-center shadow-lg"
  391.                                         title="{{ __('Take Photo') }}"
  392.                                     >
  393.                                         <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  394.                                             <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
  395.                                         </svg>
  396.                                     </button>
  397.                                 </div>
  398.                             </template>
  399.                            
  400.                             <!-- MIRROR -->
  401.                             <template x-if="webcamActive && !webcamError">
  402.                                 <div class="absolute top-4 right-4">
  403.                                     <button
  404.                                         type="button"
  405.                                         @click="toggleMirror()"
  406.                                         class="w-10 h-10 rounded-full bg-black bg-opacity-50 text-white flex items-center justify-center"
  407.                                         :title="mirroredView ? disableMirrorText : enableMirrorText"
  408.                                     >
  409.                                         <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  410.                                             <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
  411.                                         </svg>
  412.                                     </button>
  413.                                 </div>
  414.                             </template>
  415.  
  416.  
  417.                         </div>
  418.                        
  419.                         <!-- CAMERA SELECTOR DROPDOWN -->
  420.                         <template x-if="{{ $getShowCameraSelector() ? 'true' : 'false' }} && availableCameras.length > 1">
  421.                             <div class="mb-4">
  422.                                 <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
  423.                                     {{ __('Select Camera') }}
  424.                                 </label>
  425.                                 <select
  426.                                     x-model="selectedCameraId"
  427.                                     @change="changeCamera($event.target.value)"
  428.                                     class="block w-full border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-primary-500 focus:ring-primary-500 rounded-md shadow-sm"
  429.                                 >
  430.                                     <template x-for="(camera, index) in availableCameras" :key="camera.deviceId">
  431.                                         <option :value="camera.deviceId" x-text="`Camera ${index + 1} (${camera.label || 'Unnamed Camera'})`"></option>
  432.                                     </template>
  433.                                 </select>
  434.                             </div>
  435.                         </template>
  436.                        
  437.                         <!-- ACTION BUTTONS -->
  438.                         <div class="flex justify-end space-x-3">
  439.                             <button
  440.                                 type="button"
  441.                                 @click="closeModal()"
  442.                                 class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600"
  443.                             >
  444.                                 {{ __('Cancel') }}
  445.                             </button>
  446.                            
  447.                             <!-- TAKE PHOTO BTN -->
  448.                             <button
  449.                                 type="button"
  450.                                 @click="capturePhoto()"
  451.                                 class="px-4 py-2 text-sm font-medium text-white bg-primary-600 border border-transparent rounded-md shadow-sm hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
  452.                             >
  453.                                 {{ __('Take Photo') }}
  454.                             </button>
  455.                         </div>
  456.                     </div>
  457.                 </div>
  458.             </div>
  459.         </template>
  460.     </div>
  461. </x-dynamic-component>
Add Comment
Please, Sign In to add comment