diff --git a/src/composables/usePermissions.ts b/src/composables/usePermissions.ts index ecf1ef3..b92ed29 100644 --- a/src/composables/usePermissions.ts +++ b/src/composables/usePermissions.ts @@ -1,52 +1,171 @@ -import { useQuasar } from 'quasar' +import { onBeforeUnmount, ref } from 'vue' import { usePrivacyStore } from '@/stores/privacy' +type BrowserPermissionState = PermissionState | 'unsupported' + +type PermissionQueryName = 'camera' | 'geolocation' + export function usePermissions() { - const $q = useQuasar() const privacyStore = usePrivacyStore() + const cameraPermissionState = ref('prompt') + const locationPermissionState = ref('prompt') - // const checkCameraPermission = (): boolean => { - // if (!privacyStore.isAccepted) { - // privacyStore.modalPrivacy = true - // $q.notify({ - // type: 'warning', - // message: 'กรุณายอมรับนโยบายคุ้มครองข้อมูลส่วนบุคคลก่อนใช้งานกล้อง', - // position: 'top', - // }) - // return false - // } - // return true - // } + let cameraPermissionStatus: PermissionStatus | null = null + let locationPermissionStatus: PermissionStatus | null = null - // const checkLocationPermission = (): boolean => { - // if (!privacyStore.isAccepted) { - // privacyStore.modalPrivacy = true - // $q.notify({ - // type: 'warning', - // message: 'กรุณายอมรับนโยบายคุ้มครองข้อมูลส่วนบุคคลก่อนใช้งานแผนที่', - // position: 'top', - // }) - // return false - // } - // return true - // } + const isPermissionsApiSupported = () => + typeof navigator !== 'undefined' && 'permissions' in navigator + + const setPermissionState = ( + target: typeof cameraPermissionState | typeof locationPermissionState, + state: BrowserPermissionState + ) => { + target.value = state + } + + const setPermissionChangeListener = ( + name: PermissionQueryName, + status: PermissionStatus + ) => { + status.onchange = () => { + const target = + name === 'camera' ? cameraPermissionState : locationPermissionState + setPermissionState(target, status.state) + } + } + + async function queryPermissionState(name: PermissionQueryName) { + if (!isPermissionsApiSupported()) { + return null + } + + try { + return await navigator.permissions.query({ name } as PermissionDescriptor) + } catch (error) { + return null + } + } + + async function syncPermissionState(name: PermissionQueryName) { + const target = + name === 'camera' ? cameraPermissionState : locationPermissionState + const previousStatus = + name === 'camera' ? cameraPermissionStatus : locationPermissionStatus + + if (previousStatus) { + previousStatus.onchange = null + } + + const status = await queryPermissionState(name) + if (!status) { + if (target.value === 'prompt') { + setPermissionState(target, 'unsupported') + } + return + } + + if (name === 'camera') { + cameraPermissionStatus = status + } else { + locationPermissionStatus = status + } + + setPermissionState(target, status.state) + setPermissionChangeListener(name, status) + } + + async function syncPermissionStates() { + await Promise.all([ + syncPermissionState('camera'), + syncPermissionState('geolocation'), + ]) + } const checkPrivacyAccepted = (): boolean => { if (!privacyStore.isAccepted) { privacyStore.modalPrivacy = true - // $q.notify({ - // type: 'warning', - // message: 'กรุณายอมรับนโยบายคุ้มครองข้อมูลส่วนบุคคลก่อนใช้งาน', - // position: 'center', - // }) return false } return true } + async function requestCameraPermission() { + if (!checkPrivacyAccepted()) { + return false + } + + if (cameraPermissionState.value === 'granted') { + return true + } + + if (!navigator.mediaDevices?.getUserMedia) { + setPermissionState(cameraPermissionState, 'unsupported') + return false + } + + try { + const stream = await navigator.mediaDevices.getUserMedia({ + video: { facingMode: 'user' }, + audio: false, + }) + + stream.getTracks().forEach((track) => track.stop()) + setPermissionState(cameraPermissionState, 'granted') + return true + } catch (error) { + setPermissionState(cameraPermissionState, 'denied') + return false + } finally { + await syncPermissionState('camera') + } + } + + async function requestLocationPermission() { + if (!checkPrivacyAccepted()) { + return false + } + + if (locationPermissionState.value === 'granted') { + return true + } + + if (!navigator.geolocation) { + setPermissionState(locationPermissionState, 'unsupported') + return false + } + + return new Promise((resolve) => { + navigator.geolocation.getCurrentPosition( + async () => { + setPermissionState(locationPermissionState, 'granted') + await syncPermissionState('geolocation') + resolve(true) + }, + async () => { + setPermissionState(locationPermissionState, 'denied') + await syncPermissionState('geolocation') + resolve(false) + } + ) + }) + } + + onBeforeUnmount(() => { + if (cameraPermissionStatus) { + cameraPermissionStatus.onchange = null + } + + if (locationPermissionStatus) { + locationPermissionStatus.onchange = null + } + }) + return { - // checkCameraPermission, - // checkLocationPermission, + cameraPermissionState, + locationPermissionState, checkPrivacyAccepted, + syncPermissionStates, + requestCameraPermission, + requestLocationPermission, } } diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index f5f3b52..023778e 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -18,7 +18,11 @@ import MapCheck from '@/components/AscGISMap.vue' const mixin = useCounterMixin() const { date2Thai, showLoader, hideLoader, messageError, dialogConfirm } = mixin const $q = useQuasar() -const { checkPrivacyAccepted } = usePermissions() +const { + checkPrivacyAccepted, + syncPermissionStates, + requestCameraPermission, +} = usePermissions() const privacyStore = usePrivacyStore() const positionKeycloakStore = usePositionKeycloakStore() const MOCK_CHECK_DELAY_MS = 800 @@ -471,34 +475,14 @@ async function openCamera() { return } - if (!isPermissionCameraDenied.value) { - try { - if (cameraIsOn.value) { - camera.value?.stop() - } else { - // Keep the initial stream aligned with the Camera component's front-camera constraint. - await camera.value?.start() - const devices: any = await camera.value?.devices(['videoinput']) - if (devices) { - availableCameras.value = devices + if (cameraIsOn.value) { + camera.value?.stop() + cameraIsOn.value = false + return + } - // Avoid an extra stop/start cycle here; on iOS it can override the initial camera selection. - const activeId = camera.value?.currentDeviceID() - const activeIdx = activeId - ? devices.findIndex((d: any) => d.deviceId === activeId) - : -1 - currentCameraIndex.value = activeIdx >= 0 ? activeIdx : 0 - currentCameraType.value = identifyCameraType( - devices[currentCameraIndex.value]?.label || '' - ) - } - } - cameraIsOn.value = !cameraIsOn.value - } catch (error) { - console.error('Error opening camera:', error) - messageError($q, error, 'ไม่สามารถเปิดกล้องได้ กรุณาลองใหม่อีกครั้ง') - } - } else { + const hasCameraPermission = await requestCameraPermission() + if (!hasCameraPermission) { messageError( $q, '', @@ -506,6 +490,29 @@ async function openCamera() { ) return } + + try { + // Keep the initial stream aligned with the Camera component's front-camera constraint. + await camera.value?.start() + const devices: any = await camera.value?.devices(['videoinput']) + if (devices) { + availableCameras.value = devices + + // Avoid an extra stop/start cycle here; on iOS it can override the initial camera selection. + const activeId = camera.value?.currentDeviceID() + const activeIdx = activeId + ? devices.findIndex((d: any) => d.deviceId === activeId) + : -1 + currentCameraIndex.value = activeIdx >= 0 ? activeIdx : 0 + currentCameraType.value = identifyCameraType( + devices[currentCameraIndex.value]?.label || '' + ) + } + cameraIsOn.value = true + } catch (error) { + console.error('Error opening camera:', error) + messageError($q, error, 'ไม่สามารถเปิดกล้องได้ กรุณาลองใหม่อีกครั้ง') + } } /** change camera device*/ @@ -880,21 +887,6 @@ function handleVisibilityChange() { } } -const isPermissionCameraDenied = ref(false) // ตัวแปรสำหรับตรวจสอบการปฏิเสธสิทธิ์กล้อง - -async function requestCamera() { - try { - // Probe camera permission with the same front-camera constraint, then release the stream immediately. - const stream = await navigator.mediaDevices.getUserMedia({ - video: { facingMode: 'user' }, - audio: false, - }) - stream.getTracks().forEach((track) => track.stop()) - } catch (err) { - isPermissionCameraDenied.value = true - } -} - /** Hook*/ onMounted(async () => { // เริ่มต้น clock เสมอ @@ -911,11 +903,11 @@ onMounted(async () => { startChecking() } + await syncPermissionStates() + // เรียกแผนที่เฉพาะเมื่อยอมรับ privacy แล้ว if (privacyStore.isAccepted) { mapRef.value?.requestLocationPermission() - // iOS uses the file-input fallback and does not need a getUserMedia probe - if (!useNativePhotoCapture.value) requestCamera() } }) @@ -954,11 +946,10 @@ onBeforeUnmount(() => { watch( () => privacyStore.isAccepted, - (newVal) => { + async (newVal) => { if (newVal) { + await syncPermissionStates() mapRef.value?.requestLocationPermission() - // iOS uses the file-input fallback and does not need a getUserMedia probe - if (!useNativePhotoCapture.value) requestCamera() } } )