|
|
@@ -5,7 +5,7 @@
|
|
|
<div
|
|
|
class="photo-circle"
|
|
|
:class="form.avatar ? 'photo-circle--done' : 'photo-circle--pending'"
|
|
|
- @click="openDefaultPicker"
|
|
|
+ @click="openPicker"
|
|
|
>
|
|
|
<img
|
|
|
v-if="avatarPreview"
|
|
|
@@ -24,44 +24,14 @@
|
|
|
@change="onFileSelected"
|
|
|
/>
|
|
|
|
|
|
- <input
|
|
|
- ref="cameraInputRef"
|
|
|
- accept="image/jpeg,image/png,image/webp"
|
|
|
- capture="user"
|
|
|
- class="hidden"
|
|
|
- type="file"
|
|
|
- @change="onFileSelected"
|
|
|
- />
|
|
|
-
|
|
|
- <template v-if="isNativeApp && !form.avatar">
|
|
|
- <q-btn
|
|
|
- color="primary-button"
|
|
|
- no-caps
|
|
|
- padding="10px 28px"
|
|
|
- rounded
|
|
|
- :label="$t('auth.avatar_step_take_photo')"
|
|
|
- @click="cameraInputRef.click()"
|
|
|
- />
|
|
|
-
|
|
|
- <q-btn
|
|
|
- color="primary"
|
|
|
- flat
|
|
|
- no-caps
|
|
|
- padding="8px 16px"
|
|
|
- rounded
|
|
|
- :label="$t('auth.avatar_step_choose_photo')"
|
|
|
- @click="uploadInputRef.click()"
|
|
|
- />
|
|
|
- </template>
|
|
|
-
|
|
|
<q-btn
|
|
|
- v-else-if="!form.avatar"
|
|
|
+ v-if="!form.avatar"
|
|
|
color="primary-button"
|
|
|
no-caps
|
|
|
padding="10px 28px"
|
|
|
rounded
|
|
|
:label="$t('auth.avatar_step_button')"
|
|
|
- @click="uploadInputRef.click()"
|
|
|
+ @click="openPicker"
|
|
|
/>
|
|
|
|
|
|
<q-btn
|
|
|
@@ -84,36 +54,77 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
+import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
|
|
|
import { computed, onBeforeUnmount, ref, watch } from "vue";
|
|
|
import { useQuasar } from "quasar";
|
|
|
import { useI18n } from "vue-i18n";
|
|
|
|
|
|
-const form = defineModel({ type: Object, required: true });
|
|
|
-
|
|
|
const $q = useQuasar();
|
|
|
const { t } = useI18n();
|
|
|
|
|
|
+const form = defineModel({ type: Object, required: true });
|
|
|
+
|
|
|
const uploadInputRef = ref(null);
|
|
|
-const cameraInputRef = ref(null);
|
|
|
const avatarPreview = ref(null);
|
|
|
-const maxAvatarSize = 5 * 1024 * 1024;
|
|
|
-const acceptedAvatarTypes = ["image/jpeg", "image/png", "image/webp"];
|
|
|
|
|
|
const isNativeApp = computed(() => {
|
|
|
return $q.platform.is.capacitor || $q.platform.is.cordova;
|
|
|
});
|
|
|
|
|
|
-const revokePreview = () => {
|
|
|
- if (!avatarPreview.value) return;
|
|
|
+const maxAvatarSize = 5 * 1024 * 1024;
|
|
|
+const acceptedAvatarTypes = ["image/jpeg", "image/png", "image/webp"];
|
|
|
|
|
|
- URL.revokeObjectURL(avatarPreview.value);
|
|
|
+const base64ToFile = (base64, filename, mimeType = "image/jpeg") => {
|
|
|
+ const byteString = atob(base64);
|
|
|
+ const ab = new ArrayBuffer(byteString.length);
|
|
|
+ const ia = new Uint8Array(ab);
|
|
|
|
|
|
- avatarPreview.value = null;
|
|
|
+ for (let i = 0; i < byteString.length; i++) {
|
|
|
+ ia[i] = byteString.charCodeAt(i);
|
|
|
+ }
|
|
|
+
|
|
|
+ return new File([ab], filename, { type: mimeType });
|
|
|
};
|
|
|
|
|
|
-const openDefaultPicker = () => {
|
|
|
+//
|
|
|
+
|
|
|
+const openNativePicker = async () => {
|
|
|
+ try {
|
|
|
+ const photo = await Camera.getPhoto({
|
|
|
+ resultType: CameraResultType.Base64,
|
|
|
+ source: CameraSource.Prompt,
|
|
|
+ quality: 85,
|
|
|
+ correctOrientation: true,
|
|
|
+ promptLabelHeader: t("auth.avatar_step_prompt_header"),
|
|
|
+ promptLabelPhoto: t("auth.avatar_step_prompt_gallery"),
|
|
|
+ promptLabelPicture: t("auth.avatar_step_prompt_camera"),
|
|
|
+ promptLabelCancel: t("auth.avatar_step_prompt_cancel"),
|
|
|
+ });
|
|
|
+
|
|
|
+ const base64 = photo.base64String.replace(/\s/g, "");
|
|
|
+ const mime = `image/${photo.format}`;
|
|
|
+ const file = base64ToFile(base64, `avatar.${photo.format}`, mime);
|
|
|
+
|
|
|
+ validateAndSetFile(file);
|
|
|
+ } catch (err) {
|
|
|
+ const msg = (err?.message || "").toLowerCase();
|
|
|
+
|
|
|
+ if (
|
|
|
+ !msg.includes("cancel") &&
|
|
|
+ !msg.includes("dismiss") &&
|
|
|
+ !msg.includes("no image")
|
|
|
+ ) {
|
|
|
+ $q.notify({
|
|
|
+ type: "negative",
|
|
|
+ message: t("auth.avatar_step_invalid"),
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const openPicker = () => {
|
|
|
if (isNativeApp.value) {
|
|
|
- cameraInputRef.value?.click();
|
|
|
+ openNativePicker();
|
|
|
|
|
|
return;
|
|
|
}
|
|
|
@@ -121,9 +132,35 @@ const openDefaultPicker = () => {
|
|
|
uploadInputRef.value?.click();
|
|
|
};
|
|
|
|
|
|
+const revokePreview = () => {
|
|
|
+ if (!avatarPreview.value) return;
|
|
|
+
|
|
|
+ URL.revokeObjectURL(avatarPreview.value);
|
|
|
+
|
|
|
+ avatarPreview.value = null;
|
|
|
+};
|
|
|
+
|
|
|
+//
|
|
|
+
|
|
|
const onFileSelected = (event) => {
|
|
|
const file = event.target.files?.[0] ?? null;
|
|
|
|
|
|
+ validateAndSetFile(file);
|
|
|
+
|
|
|
+ event.target.value = "";
|
|
|
+};
|
|
|
+
|
|
|
+const removeAvatar = () => {
|
|
|
+ revokePreview();
|
|
|
+
|
|
|
+ form.value.avatar = null;
|
|
|
+
|
|
|
+ if (uploadInputRef.value) {
|
|
|
+ uploadInputRef.value.value = "";
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const validateAndSetFile = (file) => {
|
|
|
if (!file) return;
|
|
|
|
|
|
if (!acceptedAvatarTypes.includes(file.type) || file.size > maxAvatarSize) {
|
|
|
@@ -132,8 +169,6 @@ const onFileSelected = (event) => {
|
|
|
message: t("auth.avatar_step_invalid"),
|
|
|
});
|
|
|
|
|
|
- event.target.value = "";
|
|
|
-
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -144,20 +179,6 @@ const onFileSelected = (event) => {
|
|
|
avatarPreview.value = URL.createObjectURL(file);
|
|
|
};
|
|
|
|
|
|
-const removeAvatar = () => {
|
|
|
- revokePreview();
|
|
|
-
|
|
|
- form.value.avatar = null;
|
|
|
-
|
|
|
- if (uploadInputRef.value) {
|
|
|
- uploadInputRef.value.value = "";
|
|
|
- }
|
|
|
-
|
|
|
- if (cameraInputRef.value) {
|
|
|
- cameraInputRef.value.value = "";
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
watch(
|
|
|
() => form.value.avatar,
|
|
|
(avatar) => {
|