fix camera ios 18.x check inline page camera
This commit is contained in:
parent
30a8f01441
commit
9e69d33963
5 changed files with 243 additions and 41 deletions
|
|
@ -8,3 +8,6 @@
|
||||||
|
|
||||||
- [Position Orientation Change Fix](issue_position_orientation_change_fix.md) - Fix for position map not displaying on screen orientation changes
|
- [Position Orientation Change Fix](issue_position_orientation_change_fix.md) - Fix for position map not displaying on screen orientation changes
|
||||||
- [Active Camera Scope](project_active_camera_scope.md) - Current camera issue work should stay in HomeView.vue, not legacy MapView.vue
|
- [Active Camera Scope](project_active_camera_scope.md) - Current camera issue work should stay in HomeView.vue, not legacy MapView.vue
|
||||||
|
- [Front Camera Preview Alignment](issue_front_camera_preview_alignment.md) - Keep popup preview orientation aligned with normalized saved photos
|
||||||
|
- [iOS Native Photo Mirroring](issue_ios_native_photo_mirroring.md) - Do not flip iOS native-capture photos during normalization
|
||||||
|
- [iOS Native Camera Popup](issue_ios_native_camera_popup_control.md) - Inline-first on iOS; native activates only after inline fails (prior native-first seed was reverted)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
name: front_camera_preview_alignment
|
||||||
|
description: HomeView inline camera flow should show a non-mirrored preview and keep the saved/uploaded image in the same orientation the user saw while shooting
|
||||||
|
type: project
|
||||||
|
---
|
||||||
|
|
||||||
|
When fixing front-camera capture issues in `HomeView.vue`, keep the in-page preview and the final saved/uploaded image in the same non-mirrored orientation.
|
||||||
|
|
||||||
|
**Why:** Users rejected flows where the live in-page preview looked mirrored or differed from the final image. The expected behavior is “what I see while shooting is what gets used.”
|
||||||
|
|
||||||
|
**How to apply:** In the inline `simple-vue-camera` flow, avoid extra front-camera mirroring in either preview styling or snapshot normalization. Treat the in-page preview as the canonical orientation for `img`/`fileImg`.
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
name: ios_native_camera_popup_control
|
||||||
|
description: iOS uses inline camera first (same as Android); native fallback triggers only after inline fails. The prior "native-first on iOS" approach was reverted per product requirement.
|
||||||
|
type: project
|
||||||
|
---
|
||||||
|
|
||||||
|
In `HomeView.vue`, the inline camera is **always attempted first** on iOS (and every other platform). The hidden `<input type="file" capture="user">` is only activated by `switchToNativePhotoCapture()` / `enableNativePhotoCaptureFallback()` after inline capture is proven to fail.
|
||||||
|
|
||||||
|
**Why:** The product requirement is "use inline camera whenever the device supports inline capture; use native device capture only when inline is not supported or fails." iOS Safari supports `getUserMedia` since iOS 14.3, so inline-first is viable. A prior session hard-coded `useNativePhotoCapture = computed(() => isIOSDevice || preferNativePhotoCapture.value)`, bypassing inline on iOS entirely — that line was reverted.
|
||||||
|
|
||||||
|
**How to apply:** `preferNativePhotoCapture` must start `false` on all devices. Never seed it with `isIOSDevice`. All failure paths (`openCamera` permission denied, `camera.start()` throw, invalid snapshot blob, invalid image on submit) already call `switchToNativePhotoCapture()` which sets `preferNativePhotoCapture = true` so subsequent taps go directly to native — covering the first-tap fallback automatically.
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
name: ios_native_photo_mirroring
|
||||||
|
description: Native photo capture on iOS in HomeView should not be horizontally mirrored during normalization
|
||||||
|
type: project
|
||||||
|
---
|
||||||
|
|
||||||
|
For `HomeView.vue`, native photo capture on iOS should not apply horizontal mirroring during image normalization.
|
||||||
|
|
||||||
|
**Why:** The iOS fallback uses the native still-photo picker, and the returned photo can already be in the correct orientation. Applying the same mirror correction used for other front-camera paths flips the image incorrectly on iOS.
|
||||||
|
|
||||||
|
**How to apply:** Keep platform-specific normalization for native capture. Do not assume the iOS file-input photo path behaves the same as `simple-vue-camera` snapshots or Android fallback capture.
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted, watch, onBeforeUnmount, computed } from 'vue'
|
import {
|
||||||
|
ref,
|
||||||
|
reactive,
|
||||||
|
onMounted,
|
||||||
|
watch,
|
||||||
|
onBeforeUnmount,
|
||||||
|
computed,
|
||||||
|
} from 'vue'
|
||||||
import { useQuasar } from 'quasar'
|
import { useQuasar } from 'quasar'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
import Camera from 'simple-vue-camera'
|
import Camera from 'simple-vue-camera'
|
||||||
|
|
@ -19,6 +26,7 @@ const mixin = useCounterMixin()
|
||||||
const { date2Thai, showLoader, hideLoader, messageError, dialogConfirm } = mixin
|
const { date2Thai, showLoader, hideLoader, messageError, dialogConfirm } = mixin
|
||||||
const $q = useQuasar()
|
const $q = useQuasar()
|
||||||
const {
|
const {
|
||||||
|
cameraPermissionState,
|
||||||
checkPrivacyAccepted,
|
checkPrivacyAccepted,
|
||||||
syncPermissionStates,
|
syncPermissionStates,
|
||||||
requestCameraPermission,
|
requestCameraPermission,
|
||||||
|
|
@ -225,9 +233,8 @@ const currentCameraIndex = ref<number>(0)
|
||||||
const currentCameraType = ref<'front' | 'back' | 'unknown'>('unknown')
|
const currentCameraType = ref<'front' | 'back' | 'unknown'>('unknown')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use the native still-photo picker only on real iOS/iPadOS devices.
|
* Detect real iOS/iPadOS devices. Desktop mobile emulation can spoof the
|
||||||
* Desktop mobile emulation can spoof the iPhone user-agent, so rely on
|
* iPhone user-agent, so rely on platform + touch capability instead.
|
||||||
* platform + touch capability instead of user-agent alone.
|
|
||||||
*/
|
*/
|
||||||
const isIOSDevice: boolean = (() => {
|
const isIOSDevice: boolean = (() => {
|
||||||
const platform = navigator.platform || ''
|
const platform = navigator.platform || ''
|
||||||
|
|
@ -240,10 +247,16 @@ const isIOSDevice: boolean = (() => {
|
||||||
})()
|
})()
|
||||||
|
|
||||||
const isAndroidDevice: boolean = /Android/i.test(navigator.userAgent)
|
const isAndroidDevice: boolean = /Android/i.test(navigator.userAgent)
|
||||||
|
/**
|
||||||
|
* `preferNativePhotoCapture` starts false on every device — inline camera is
|
||||||
|
* always attempted first. It is set to true only by
|
||||||
|
* `switchToNativePhotoCapture()` / `enableNativePhotoCaptureFallback()` when
|
||||||
|
* inline capture is proven to be unsupported or has failed. This covers iOS
|
||||||
|
* Safari too: getUserMedia is supported there since iOS 14.3, so we try
|
||||||
|
* inline first and fall back gracefully on first failure.
|
||||||
|
*/
|
||||||
const preferNativePhotoCapture = ref<boolean>(false)
|
const preferNativePhotoCapture = ref<boolean>(false)
|
||||||
const useNativePhotoCapture = computed(
|
const useNativePhotoCapture = computed(() => preferNativePhotoCapture.value)
|
||||||
() => isIOSDevice || preferNativePhotoCapture.value
|
|
||||||
)
|
|
||||||
const centeredPreviewImageStyle = Object.freeze({
|
const centeredPreviewImageStyle = Object.freeze({
|
||||||
objectFit: 'cover',
|
objectFit: 'cover',
|
||||||
objectPosition: 'center center',
|
objectPosition: 'center center',
|
||||||
|
|
@ -251,6 +264,13 @@ const centeredPreviewImageStyle = Object.freeze({
|
||||||
|
|
||||||
/** Ref for the hidden file input used as native camera fallback */
|
/** Ref for the hidden file input used as native camera fallback */
|
||||||
const nativePhotoInput = ref<HTMLInputElement | null>(null)
|
const nativePhotoInput = ref<HTMLInputElement | null>(null)
|
||||||
|
const DEFAULT_NORMALIZED_IMAGE_QUALITY = 0.92
|
||||||
|
|
||||||
|
interface NormalizePhotoOptions {
|
||||||
|
fileName?: string
|
||||||
|
mirrorHorizontally?: boolean
|
||||||
|
quality?: number
|
||||||
|
}
|
||||||
|
|
||||||
function clearSelectedPhoto() {
|
function clearSelectedPhoto() {
|
||||||
if (img.value) {
|
if (img.value) {
|
||||||
|
|
@ -298,6 +318,70 @@ async function isValidCapturedImage(blob: Blob) {
|
||||||
return canDecodeImageBlob(blob)
|
return canDecodeImageBlob(blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadImageFromBlob(blob: Blob) {
|
||||||
|
const imageUrl = URL.createObjectURL(blob)
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await new Promise<HTMLImageElement>((resolve, reject) => {
|
||||||
|
const image = new Image()
|
||||||
|
image.onload = () => resolve(image)
|
||||||
|
image.onerror = () => reject(new Error('Image load failed'))
|
||||||
|
image.src = imageUrl
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
URL.revokeObjectURL(imageUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function normalizePhotoFile(
|
||||||
|
source: Blob,
|
||||||
|
options: NormalizePhotoOptions = {}
|
||||||
|
) {
|
||||||
|
const fileName =
|
||||||
|
options.fileName ??
|
||||||
|
(source instanceof File && source.name
|
||||||
|
? source.name
|
||||||
|
: `photo_${Date.now()}.jpg`)
|
||||||
|
const imageType =
|
||||||
|
source.type && source.type.startsWith('image/') ? source.type : 'image/jpeg'
|
||||||
|
|
||||||
|
if (!options.mirrorHorizontally) {
|
||||||
|
if (source instanceof File && source.name === fileName) {
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
|
||||||
|
return new File([source], fileName, { type: imageType })
|
||||||
|
}
|
||||||
|
|
||||||
|
const image = await loadImageFromBlob(source)
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = image.naturalWidth
|
||||||
|
canvas.height = image.naturalHeight
|
||||||
|
|
||||||
|
const context = canvas.getContext('2d')
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('Canvas context unavailable')
|
||||||
|
}
|
||||||
|
|
||||||
|
context.translate(canvas.width, 0)
|
||||||
|
context.scale(-1, 1)
|
||||||
|
context.drawImage(image, 0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
|
const normalizedBlob = await new Promise<Blob | null>((resolve) => {
|
||||||
|
canvas.toBlob(
|
||||||
|
resolve,
|
||||||
|
imageType,
|
||||||
|
options.quality ?? DEFAULT_NORMALIZED_IMAGE_QUALITY
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!normalizedBlob) {
|
||||||
|
throw new Error('Failed to normalize image')
|
||||||
|
}
|
||||||
|
|
||||||
|
return new File([normalizedBlob], fileName, { type: imageType })
|
||||||
|
}
|
||||||
|
|
||||||
async function assignSelectedPhoto(file: File) {
|
async function assignSelectedPhoto(file: File) {
|
||||||
clearSelectedPhoto()
|
clearSelectedPhoto()
|
||||||
fileImg.value = file
|
fileImg.value = file
|
||||||
|
|
@ -316,9 +400,21 @@ function openNativePhotoCapture() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enableNativePhotoCaptureFallback() {
|
||||||
|
preferNativePhotoCapture.value = true
|
||||||
|
camera.value?.stop()
|
||||||
|
cameraIsOn.value = false
|
||||||
|
clearSelectedPhoto()
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchToNativePhotoCapture() {
|
||||||
|
enableNativePhotoCaptureFallback()
|
||||||
|
openNativePhotoCapture()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the user selects / captures a photo via the native file-input
|
* Called when the user selects / captures a photo via the native file-input
|
||||||
* fallback. Mirrors the same `img` + `fileImg` state that capturePhoto()
|
* fallback. Keeps the same `img` + `fileImg` state that capturePhoto()
|
||||||
* sets so the upload flow is identical.
|
* sets so the upload flow is identical.
|
||||||
*/
|
*/
|
||||||
async function onNativePhotoSelected(event: Event) {
|
async function onNativePhotoSelected(event: Event) {
|
||||||
|
|
@ -337,7 +433,22 @@ async function onNativePhotoSelected(event: Event) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await assignSelectedPhoto(file)
|
try {
|
||||||
|
const normalizedFile = await normalizePhotoFile(file, {
|
||||||
|
fileName: file.name,
|
||||||
|
mirrorHorizontally: !isIOSDevice,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!(await isValidCapturedImage(normalizedFile))) {
|
||||||
|
throw new Error('Normalized image is invalid')
|
||||||
|
}
|
||||||
|
|
||||||
|
await assignSelectedPhoto(normalizedFile)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error normalizing native photo:', error)
|
||||||
|
clearSelectedPhoto()
|
||||||
|
messageError($q, error, 'ไม่สามารถเตรียมรูปภาพได้ กรุณาถ่ายรูปใหม่อีกครั้ง')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const intervalId = ref<ReturnType<typeof setInterval> | undefined>(undefined)
|
const intervalId = ref<ReturnType<typeof setInterval> | undefined>(undefined)
|
||||||
|
|
@ -460,6 +571,14 @@ function identifyCameraType(label: string): 'front' | 'back' | 'unknown' {
|
||||||
return 'unknown'
|
return 'unknown'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveCameraType(
|
||||||
|
label: string,
|
||||||
|
fallbackType: 'front' | 'back'
|
||||||
|
): 'front' | 'back' {
|
||||||
|
const detectedType = identifyCameraType(label)
|
||||||
|
return detectedType === 'unknown' ? fallbackType : detectedType
|
||||||
|
}
|
||||||
|
|
||||||
/** function เปิดกล้อง*/
|
/** function เปิดกล้อง*/
|
||||||
async function openCamera() {
|
async function openCamera() {
|
||||||
// เช็คสิทธิ์ privacy ก่อนเปิดกล้อง
|
// เช็คสิทธิ์ privacy ก่อนเปิดกล้อง
|
||||||
|
|
@ -467,9 +586,8 @@ async function openCamera() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// iOS Safari fallback: getUserMedia can open native video-recording UI and
|
// If inline capture previously failed on this device, fall back to the
|
||||||
// canvas.drawImage may produce a blank blob. Use the hidden file input
|
// native still-photo picker instead of reopening the live camera.
|
||||||
// with capture="user" instead — this opens the native camera in photo mode.
|
|
||||||
if (useNativePhotoCapture.value) {
|
if (useNativePhotoCapture.value) {
|
||||||
openNativePhotoCapture()
|
openNativePhotoCapture()
|
||||||
return
|
return
|
||||||
|
|
@ -481,8 +599,23 @@ async function openCamera() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(isIOSDevice || isAndroidDevice) &&
|
||||||
|
!useNativePhotoCapture.value &&
|
||||||
|
(cameraPermissionState.value === 'denied' ||
|
||||||
|
cameraPermissionState.value === 'unsupported')
|
||||||
|
) {
|
||||||
|
switchToNativePhotoCapture()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const hasCameraPermission = await requestCameraPermission()
|
const hasCameraPermission = await requestCameraPermission()
|
||||||
if (!hasCameraPermission) {
|
if (!hasCameraPermission) {
|
||||||
|
if ((isIOSDevice || isAndroidDevice) && !useNativePhotoCapture.value) {
|
||||||
|
switchToNativePhotoCapture()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
messageError(
|
messageError(
|
||||||
$q,
|
$q,
|
||||||
'',
|
'',
|
||||||
|
|
@ -492,6 +625,8 @@ async function openCamera() {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
currentCameraType.value = 'front'
|
||||||
|
|
||||||
// Keep the initial stream aligned with the Camera component's front-camera constraint.
|
// Keep the initial stream aligned with the Camera component's front-camera constraint.
|
||||||
await camera.value?.start()
|
await camera.value?.start()
|
||||||
const devices: any = await camera.value?.devices(['videoinput'])
|
const devices: any = await camera.value?.devices(['videoinput'])
|
||||||
|
|
@ -504,13 +639,20 @@ async function openCamera() {
|
||||||
? devices.findIndex((d: any) => d.deviceId === activeId)
|
? devices.findIndex((d: any) => d.deviceId === activeId)
|
||||||
: -1
|
: -1
|
||||||
currentCameraIndex.value = activeIdx >= 0 ? activeIdx : 0
|
currentCameraIndex.value = activeIdx >= 0 ? activeIdx : 0
|
||||||
currentCameraType.value = identifyCameraType(
|
currentCameraType.value = resolveCameraType(
|
||||||
devices[currentCameraIndex.value]?.label || ''
|
devices[currentCameraIndex.value]?.label || '',
|
||||||
|
'front'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
cameraIsOn.value = true
|
cameraIsOn.value = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error opening camera:', error)
|
console.error('Error opening camera:', error)
|
||||||
|
|
||||||
|
if ((isIOSDevice || isAndroidDevice) && !useNativePhotoCapture.value) {
|
||||||
|
switchToNativePhotoCapture()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
messageError($q, error, 'ไม่สามารถเปิดกล้องได้ กรุณาลองใหม่อีกครั้ง')
|
messageError($q, error, 'ไม่สามารถเปิดกล้องได้ กรุณาลองใหม่อีกครั้ง')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -529,9 +671,15 @@ async function changeCamera(targetCameraType?: 'front' | 'back') {
|
||||||
|
|
||||||
if (devices.length === 1 || !targetCameraType) {
|
if (devices.length === 1 || !targetCameraType) {
|
||||||
const device = devices[0]
|
const device = devices[0]
|
||||||
|
const fallbackType =
|
||||||
|
targetCameraType ??
|
||||||
|
(currentCameraType.value === 'back' ? 'back' : 'front')
|
||||||
await camera.value?.changeCamera(device.deviceId)
|
await camera.value?.changeCamera(device.deviceId)
|
||||||
currentCameraIndex.value = 0
|
currentCameraIndex.value = 0
|
||||||
currentCameraType.value = identifyCameraType(device.label || '')
|
currentCameraType.value = resolveCameraType(
|
||||||
|
device.label || '',
|
||||||
|
fallbackType
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -544,13 +692,19 @@ async function changeCamera(targetCameraType?: 'front' | 'back') {
|
||||||
const targetDevice = matchingCameras[0]
|
const targetDevice = matchingCameras[0]
|
||||||
await camera.value?.changeCamera(targetDevice.deviceId)
|
await camera.value?.changeCamera(targetDevice.deviceId)
|
||||||
currentCameraIndex.value = devices.indexOf(targetDevice)
|
currentCameraIndex.value = devices.indexOf(targetDevice)
|
||||||
currentCameraType.value = targetCameraType
|
currentCameraType.value = resolveCameraType(
|
||||||
|
targetDevice.label || '',
|
||||||
|
targetCameraType
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
const nextIndex = (currentCameraIndex.value + 1) % devices.length
|
const nextIndex = (currentCameraIndex.value + 1) % devices.length
|
||||||
const nextDevice = devices[nextIndex]
|
const nextDevice = devices[nextIndex]
|
||||||
await camera.value?.changeCamera(nextDevice.deviceId)
|
await camera.value?.changeCamera(nextDevice.deviceId)
|
||||||
currentCameraIndex.value = nextIndex
|
currentCameraIndex.value = nextIndex
|
||||||
currentCameraType.value = identifyCameraType(nextDevice.label || '')
|
currentCameraType.value = resolveCameraType(
|
||||||
|
nextDevice.label || '',
|
||||||
|
targetCameraType
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error switching camera:', error)
|
console.error('Error switching camera:', error)
|
||||||
|
|
@ -566,10 +720,14 @@ async function switchCamera() {
|
||||||
// สลับแค่ระหว่างกล้อง 2 ตัวแรก (กล้องหน้าและหลังหลัก)
|
// สลับแค่ระหว่างกล้อง 2 ตัวแรก (กล้องหน้าและหลังหลัก)
|
||||||
const targetIndex = currentCameraIndex.value === 0 ? 1 : 0
|
const targetIndex = currentCameraIndex.value === 0 ? 1 : 0
|
||||||
const targetDevice = availableCameras.value[targetIndex]
|
const targetDevice = availableCameras.value[targetIndex]
|
||||||
|
const fallbackType = currentCameraType.value === 'back' ? 'front' : 'back'
|
||||||
|
|
||||||
await camera.value?.changeCamera(targetDevice.deviceId)
|
await camera.value?.changeCamera(targetDevice.deviceId)
|
||||||
currentCameraIndex.value = targetIndex
|
currentCameraIndex.value = targetIndex
|
||||||
currentCameraType.value = identifyCameraType(targetDevice.label || '')
|
currentCameraType.value = resolveCameraType(
|
||||||
|
targetDevice.label || '',
|
||||||
|
fallbackType
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** function ถ่ายรูป*/
|
/** function ถ่ายรูป*/
|
||||||
|
|
@ -581,16 +739,8 @@ async function capturePhoto() {
|
||||||
0.8
|
0.8
|
||||||
)
|
)
|
||||||
if (!imageBlob || !(await isValidCapturedImage(imageBlob))) {
|
if (!imageBlob || !(await isValidCapturedImage(imageBlob))) {
|
||||||
if (isAndroidDevice) {
|
if (isIOSDevice || isAndroidDevice) {
|
||||||
preferNativePhotoCapture.value = true
|
switchToNativePhotoCapture()
|
||||||
camera.value?.stop()
|
|
||||||
cameraIsOn.value = false
|
|
||||||
clearSelectedPhoto()
|
|
||||||
messageError(
|
|
||||||
$q,
|
|
||||||
'',
|
|
||||||
'ไม่สามารถอ่านรูปจากกล้องของอุปกรณ์ได้ กรุณาถ่ายใหม่อีกครั้ง ระบบจะเปลี่ยนไปใช้โหมดถ่ายภาพของอุปกรณ์'
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -598,14 +748,28 @@ async function capturePhoto() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const fileName = `photo_${Date.now()}.jpg`
|
const fileName = `photo_${Date.now()}.jpg`
|
||||||
//ไฟล์รูป
|
const normalizedFile = await normalizePhotoFile(imageBlob, {
|
||||||
const file = new File([imageBlob], fileName, { type: 'image/jpeg' })
|
fileName,
|
||||||
await assignSelectedPhoto(file)
|
mirrorHorizontally: false,
|
||||||
|
quality: 0.8,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!(await isValidCapturedImage(normalizedFile))) {
|
||||||
|
throw new Error('Normalized image is invalid')
|
||||||
|
}
|
||||||
|
|
||||||
|
await assignSelectedPhoto(normalizedFile)
|
||||||
|
|
||||||
//แสดงรูป
|
//แสดงรูป
|
||||||
camera.value?.stop()
|
camera.value?.stop()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error capturing photo:', error)
|
console.error('Error capturing photo:', error)
|
||||||
|
|
||||||
|
if ((isIOSDevice || isAndroidDevice) && !useNativePhotoCapture.value) {
|
||||||
|
switchToNativePhotoCapture()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
messageError($q, error, 'ไม่สามารถถ่ายรูปได้ กรุณาลองใหม่อีกครั้ง')
|
messageError($q, error, 'ไม่สามารถถ่ายรูปได้ กรุณาลองใหม่อีกครั้ง')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -701,10 +865,8 @@ async function confirm() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fileImg.value || !(await isValidCapturedImage(fileImg.value))) {
|
if (!fileImg.value || !(await isValidCapturedImage(fileImg.value))) {
|
||||||
if (isAndroidDevice) {
|
if ((isIOSDevice || isAndroidDevice) && !useNativePhotoCapture.value) {
|
||||||
preferNativePhotoCapture.value = true
|
enableNativePhotoCaptureFallback()
|
||||||
camera.value?.stop()
|
|
||||||
cameraIsOn.value = false
|
|
||||||
}
|
}
|
||||||
clearSelectedPhoto()
|
clearSelectedPhoto()
|
||||||
disabledBtn.value = false
|
disabledBtn.value = false
|
||||||
|
|
@ -1069,6 +1231,9 @@ watch(
|
||||||
<Camera
|
<Camera
|
||||||
:resolution="{ width: photoWidth, height: photoHeight }"
|
:resolution="{ width: photoWidth, height: photoHeight }"
|
||||||
ref="camera"
|
ref="camera"
|
||||||
|
:class="[
|
||||||
|
'camera-preview',
|
||||||
|
]"
|
||||||
:autoplay="false"
|
:autoplay="false"
|
||||||
:playsinline="true"
|
:playsinline="true"
|
||||||
:facingMode="'user'"
|
:facingMode="'user'"
|
||||||
|
|
@ -1202,6 +1367,9 @@ watch(
|
||||||
height: photoHeight,
|
height: photoHeight,
|
||||||
}"
|
}"
|
||||||
ref="camera"
|
ref="camera"
|
||||||
|
:class="[
|
||||||
|
'camera-preview',
|
||||||
|
]"
|
||||||
:autoplay="false"
|
:autoplay="false"
|
||||||
:playsinline="true"
|
:playsinline="true"
|
||||||
:facingMode="'user'"
|
:facingMode="'user'"
|
||||||
|
|
@ -1686,11 +1854,10 @@ watch(
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Native still-image fallback: hidden file input.
|
Native still-image fallback: hidden file input.
|
||||||
Used for iOS by default and for Android only after the live snapshot path
|
Activated only after inline camera is proven unsupported or fails — on any
|
||||||
produces an unreadable image, so the user can retake with the device camera.
|
platform including iOS. Never used as the first-choice path.
|
||||||
-->
|
-->
|
||||||
<input
|
<input
|
||||||
v-if="useNativePhotoCapture"
|
|
||||||
ref="nativePhotoInput"
|
ref="nativePhotoInput"
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
|
|
@ -1750,9 +1917,8 @@ watch(
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* iOS-specific video fixes */
|
/* Keep the live camera preview visually aligned with the normalized saved photo. */
|
||||||
.card-container video,
|
.camera-preview :deep(video) {
|
||||||
.card-container-xs video {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue