PersonalSettingAvatarCameraFlyout.vue 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { useUserMedia, usePermission } from '@vueuse/core'
  4. import { computed, ref, watch } from 'vue'
  5. import type { ImageFileData } from '#shared/utils/files.ts'
  6. import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
  7. import CommonFlyout from '#desktop/components/CommonFlyout/CommonFlyout.vue'
  8. defineEmits<{
  9. 'avatar-captured': [ImageFileData | undefined]
  10. }>()
  11. const image = ref<ImageFileData>()
  12. const canvasHeight = 256
  13. const canvasWidth = 256
  14. const cameraAccess = usePermission('camera')
  15. const cameraIsDisabled = computed(
  16. () => !cameraAccess.value || cameraAccess.value === 'denied',
  17. )
  18. const cameraIcon = computed(() =>
  19. cameraIsDisabled.value ? 'camera-video-off' : 'camera-video',
  20. )
  21. const { stream, start, stop } = useUserMedia({
  22. constraints: {
  23. video: {
  24. width: 256,
  25. height: 256,
  26. },
  27. },
  28. })
  29. if (!cameraIsDisabled.value) start()
  30. const getCanvasObject = () => {
  31. const canvas = document.querySelector('canvas')
  32. if (!canvas) return
  33. return canvas
  34. }
  35. const getCanvas2dContext = (canvas: HTMLCanvasElement) => {
  36. if (!canvas) return
  37. const context = canvas.getContext('2d')
  38. if (!context) return
  39. return context
  40. }
  41. const discardImage = () => {
  42. if (image.value) {
  43. image.value = undefined
  44. }
  45. const canvas = getCanvasObject()
  46. if (!canvas) return
  47. getCanvas2dContext(canvas)?.clearRect(0, 0, canvas.width, canvas.height)
  48. }
  49. watch(cameraIsDisabled, (isDisabled) => {
  50. if (isDisabled) {
  51. discardImage()
  52. stop()
  53. return
  54. }
  55. start()
  56. })
  57. const captureImage = () => {
  58. if (!stream.value) return
  59. const canvas = getCanvasObject()
  60. if (!canvas) return
  61. const context = getCanvas2dContext(canvas)
  62. if (!context) return
  63. canvas.width = canvasWidth
  64. canvas.height = canvasHeight
  65. const video = document.querySelector('video')
  66. if (!video) return
  67. context.translate(canvasWidth, 0)
  68. context.scale(-1, 1)
  69. context.drawImage(
  70. video,
  71. (video.videoWidth - video.videoHeight) / 2,
  72. 0,
  73. video.videoHeight,
  74. video.videoHeight,
  75. 0,
  76. 0,
  77. canvasWidth,
  78. canvasHeight,
  79. )
  80. image.value = {
  81. content: canvas.toDataURL('image/png'),
  82. name: 'avatar.png',
  83. type: 'image/png',
  84. }
  85. }
  86. </script>
  87. <template>
  88. <CommonFlyout
  89. :header-title="__('Camera')"
  90. :footer-action-options="{
  91. actionLabel: __('Save'),
  92. actionButton: { variant: 'submit', disabled: !image },
  93. }"
  94. header-icon="camera"
  95. name="avatar-camera-capture"
  96. @action="$emit('avatar-captured', image)"
  97. @close="stop"
  98. >
  99. <div class="flex flex-col items-center gap-6 pb-10 pt-12">
  100. <canvas
  101. v-show="image"
  102. class="h-64 min-h-64 w-64 min-w-64 rounded-full border border-black dark:border-white"
  103. >
  104. </canvas>
  105. <div
  106. v-if="!image"
  107. class="relative h-64 min-h-64 w-64 min-w-64 overflow-hidden rounded-full border border-black bg-blue-200 text-stone-200 dark:border-white dark:bg-gray-700 dark:text-neutral-500"
  108. >
  109. <CommonIcon
  110. :name="cameraIcon"
  111. size="xl"
  112. class="absolute top-1/2 -translate-y-1/2 ltr:left-1/2 ltr:-translate-x-1/2 rtl:right-1/2 rtl:translate-x-1/2"
  113. />
  114. <!-- eslint-disable vuejs-accessibility/media-has-caption -->
  115. <video
  116. v-show="!cameraIsDisabled"
  117. class="h-full w-full object-cover"
  118. :aria-label="$t('Use the camera to take a photo for the avatar.')"
  119. :srcObject="stream"
  120. autoplay
  121. />
  122. </div>
  123. <CommonAlert v-if="cameraIsDisabled" variant="danger">
  124. {{
  125. $t('Accessing your camera is forbidden. Please check your settings.')
  126. }}
  127. </CommonAlert>
  128. <div v-else class="flex flex-row gap-2">
  129. <CommonButton
  130. v-if="!image"
  131. variant="primary"
  132. size="medium"
  133. @click="captureImage"
  134. >
  135. {{ $t('Capture From Camera') }}
  136. </CommonButton>
  137. <CommonButton
  138. v-else
  139. variant="remove"
  140. size="medium"
  141. @click="discardImage"
  142. >
  143. {{ $t('Discard Snapshot') }}
  144. </CommonButton>
  145. </div>
  146. </div>
  147. </CommonFlyout>
  148. </template>
  149. <style scoped>
  150. video {
  151. transform: rotateY(180deg);
  152. }
  153. </style>