Browse Source

add file-chooser to the profile image selection

Trent Larson 7 months ago
parent
commit
b81c096fe4
  1. 62
      src/components/PhotoDialog.vue
  2. 77
      src/views/AccountViewView.vue
  3. 8
      src/views/GiftedDetails.vue

62
src/components/GiftedPhotoDialog.vue → src/components/PhotoDialog.vue

@ -34,7 +34,7 @@
backgroundColor: '#f8f8f8', backgroundColor: '#f8f8f8',
margin: 'auto', margin: 'auto',
}" }"
:img="URL.createObjectURL(blob)" :img="createBlobURL(blob)"
:options="{ :options="{
viewMode: 1, viewMode: 1,
dragMode: 'crop', dragMode: 'crop',
@ -49,7 +49,7 @@
</div> </div>
<div v-else> <div v-else>
<div class="flex justify-center"> <div class="flex justify-center">
<img :src="URL.createObjectURL(blob)" class="mt-2 rounded" /> <img :src="createBlobURL(blob)" class="mt-2 rounded" />
</div> </div>
</div> </div>
<div class="absolute bottom-[1rem] left-[1rem] px-2 py-1"> <div class="absolute bottom-[1rem] left-[1rem] px-2 py-1">
@ -60,7 +60,10 @@
<span>Upload</span> <span>Upload</span>
</button> </button>
</div> </div>
<div class="absolute bottom-[1rem] right-[1rem] px-2 py-1"> <div
v-if="showRetry"
class="absolute bottom-[1rem] right-[1rem] px-2 py-1"
>
<button <button
@click="retryImage" @click="retryImage"
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-1 px-2 rounded-md" class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-1 px-2 rounded-md"
@ -127,17 +130,19 @@ import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
@Component({ components: { Camera, VuePictureCropper } }) @Component({ components: { Camera, VuePictureCropper } })
export default class GiftedPhotoDialog extends Vue { export default class PhotoDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
activeDeviceNumber = 0; activeDeviceNumber = 0;
activeDid = ""; activeDid = "";
blob: Blob | null = null; blob?: Blob;
claimType = "GiveAction"; claimType = "GiveAction";
crop = false; crop = false;
fileName?: string;
mirror = false; mirror = false;
numDevices = 0; numDevices = 0;
setImage: (arg: string) => void = () => {}; setImageCallback: (arg: string) => void = () => {};
showRetry = true;
uploading = false; uploading = false;
visible = false; visible = false;
@ -163,7 +168,13 @@ export default class GiftedPhotoDialog extends Vue {
} }
} }
open(setImageFn: (arg: string) => void, crop?: boolean, claimType?: string) { open(
setImageFn: (arg: string) => void,
crop?: boolean,
claimType?: string,
blob?: Blob, // for image upload, just to use the cropping function
inputFileName?: string,
) {
this.visible = true; this.visible = true;
this.crop = !!crop; this.crop = !!crop;
this.claimType = claimType || "GiveAction"; this.claimType = claimType || "GiveAction";
@ -171,7 +182,16 @@ export default class GiftedPhotoDialog extends Vue {
if (bottomNav) { if (bottomNav) {
bottomNav.style.display = "none"; bottomNav.style.display = "none";
} }
this.setImage = setImageFn; this.setImageCallback = setImageFn;
if (blob) {
this.blob = blob;
this.fileName = inputFileName;
this.showRetry = false;
} else {
this.blob = undefined;
this.fileName = undefined;
this.showRetry = true;
}
} }
close() { close() {
@ -180,7 +200,7 @@ export default class GiftedPhotoDialog extends Vue {
if (bottomNav) { if (bottomNav) {
bottomNav.style.display = ""; bottomNav.style.display = "";
} }
this.blob = null; this.blob = undefined;
} }
async cameraStarted() { async cameraStarted() {
@ -236,10 +256,13 @@ export default class GiftedPhotoDialog extends Vue {
// The resolution is only necessary because of that mobile portrait-orientation case. // The resolution is only necessary because of that mobile portrait-orientation case.
// The mobile emulation in a browser shows something stretched vertically, but real devices work fine. // The mobile emulation in a browser shows something stretched vertically, but real devices work fine.
this.blob = await cameraComponent?.snapshot({ this.blob =
(await cameraComponent?.snapshot({
height: imageHeight, height: imageHeight,
width: imageWidth, width: imageWidth,
}); // png is default; if that changes, change extension in formData.append })) || undefined;
// png is default
this.fileName = "snapshot.png";
if (!this.blob) { if (!this.blob) {
this.$notify( this.$notify(
{ {
@ -254,8 +277,13 @@ export default class GiftedPhotoDialog extends Vue {
} }
} }
private createBlobURL(blob: Blob): string {
console.log("blob", blob);
return URL.createObjectURL(blob);
}
async retryImage() { async retryImage() {
this.blob = null; this.blob = undefined;
} }
/**** /****
@ -307,7 +335,7 @@ export default class GiftedPhotoDialog extends Vue {
this.uploading = true; this.uploading = true;
if (this.crop) { if (this.crop) {
this.blob = await cropper?.getBlob(); this.blob = (await cropper?.getBlob()) || undefined;
} }
const identifier = await getIdentity(this.activeDid); const identifier = await getIdentity(this.activeDid);
@ -330,7 +358,7 @@ export default class GiftedPhotoDialog extends Vue {
this.uploading = false; this.uploading = false;
return; return;
} }
formData.append("image", this.blob, "snapshot.png"); formData.append("image", this.blob, this.fileName || "snapshot.png");
formData.append("claimType", this.claimType); formData.append("claimType", this.claimType);
try { try {
const response = await axios.post( const response = await axios.post(
@ -341,8 +369,8 @@ export default class GiftedPhotoDialog extends Vue {
this.uploading = false; this.uploading = false;
this.visible = false; this.visible = false;
this.blob = null; this.blob = undefined;
this.setImage(response.data.url as string); this.setImageCallback(response.data.url as string);
} catch (error) { } catch (error) {
console.error("Error uploading the image", error); console.error("Error uploading the image", error);
this.$notify( this.$notify(
@ -355,7 +383,7 @@ export default class GiftedPhotoDialog extends Vue {
5000, 5000,
); );
this.uploading = false; this.uploading = false;
this.blob = null; this.blob = undefined;
} }
} }

77
src/views/AccountViewView.vue

@ -86,19 +86,25 @@
class="text-red-500 fa-fw ml-8 mt-8 w-12 h-12" class="text-red-500 fa-fw ml-8 mt-8 w-12 h-12"
/> />
</span> </span>
<span v-else> <div v-else class="text-center">
<div class>
<fa <fa
icon="camera" icon="camera"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-md" class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-md"
@click="openPhotoDialog" @click="openPhotoDialog(undefined, undefined)"
/> />
</span>
<GiftedPhotoDialog ref="photoDialog" />
</div> </div>
<div class="mt-4"> <div>
<div class="flex justify-center"> <input type="file" @change="uploadPhotoFile" />
... and those without your image see this (if you let them see your </div>
activity): </div>
<PhotoDialog ref="photoDialog" />
</div>
<div class="mt-6">
<div class="flex justify-center text-center">
People without your image see this:
<br />
(if you've let them see your activity)
</div> </div>
<div class="flex justify-center"> <div class="flex justify-center">
<EntityIcon <EntityIcon
@ -577,11 +583,11 @@
<div class="ml-4 mt-2"> <div class="ml-4 mt-2">
Import Import
<input type="file" @change="uploadFile" class="ml-2" /> <input type="file" @change="uploadImportFile" class="ml-2" />
<div v-if="showContactImport()"> <div v-if="showContactImport()">
<button <button
class="block text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6" class="block text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
@click="confirmSubmitFile()" @click="confirmSubmitImportFile()"
> >
Import Settings & Contacts Import Settings & Contacts
<br /> <br />
@ -614,7 +620,7 @@ import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import GiftedPhotoDialog from "@/components/GiftedPhotoDialog.vue"; import PhotoDialog from "@/components/PhotoDialog.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { import {
@ -645,10 +651,11 @@ interface IAccount {
derivationPath: string; derivationPath: string;
} }
const inputFileNameRef = ref<Blob>(); const inputImportFileNameRef = ref<Blob>();
const inputPhotoFileNameRef = ref<Blob>();
@Component({ @Component({
components: { EntityIcon, GiftedPhotoDialog, QuickNav, TopMessage }, components: { EntityIcon, PhotoDialog, QuickNav, TopMessage },
}) })
export default class AccountViewView extends Vue { export default class AccountViewView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
@ -1120,16 +1127,36 @@ export default class AccountViewView extends Vue {
console.error("Export Error:", error); console.error("Export Error:", error);
} }
async uploadFile(event: Event) { async uploadPhotoFile(event: Event) {
inputFileNameRef.value = event.target.files[0]; inputPhotoFileNameRef.value = event.target.files[0];
// https://developer.mozilla.org/en-US/docs/Web/API/File
// ... plus it has a `type` property from my testing
const file = inputPhotoFileNameRef.value;
if (file != null) {
const reader = new FileReader();
reader.onload = async (e) => {
const data = e.target?.result as ArrayBuffer;
if (data) {
const blob = new Blob([new Uint8Array(data)], {
type: file.type,
});
this.openPhotoDialog(blob, file.name as string);
}
};
reader.readAsArrayBuffer(file as Blob);
}
}
async uploadImportFile(event: Event) {
inputImportFileNameRef.value = event.target.files[0];
} }
showContactImport() { showContactImport() {
return !!inputFileNameRef.value; return !!inputImportFileNameRef.value;
} }
confirmSubmitFile() { confirmSubmitImportFile() {
if (inputFileNameRef.value != null) { if (inputImportFileNameRef.value != null) {
this.$notify( this.$notify(
{ {
group: "modal", group: "modal",
@ -1138,7 +1165,7 @@ export default class AccountViewView extends Vue {
text: text:
"This will replace all settings and contacts, so we recommend you first do the backup step above." + "This will replace all settings and contacts, so we recommend you first do the backup step above." +
" Are you sure you want to import and replace all contacts and settings?", " Are you sure you want to import and replace all contacts and settings?",
onYes: this.submitFile, onYes: this.submitImportFile,
}, },
-1, -1,
); );
@ -1150,10 +1177,10 @@ export default class AccountViewView extends Vue {
* *
* @throws Will notify the user if there is an export error. * @throws Will notify the user if there is an export error.
*/ */
async submitFile() { async submitImportFile() {
if (inputFileNameRef.value != null) { if (inputImportFileNameRef.value != null) {
await db.delete(); await db.delete();
await Dexie.import(inputFileNameRef.value as Blob, { await Dexie.import(inputImportFileNameRef.value as Blob, {
progressCallback: this.progressCallback, progressCallback: this.progressCallback,
}); });
} }
@ -1366,8 +1393,8 @@ export default class AccountViewView extends Vue {
); );
} }
openPhotoDialog() { openPhotoDialog(blob?: Blob, fileName?: string) {
(this.$refs.photoDialog as GiftedPhotoDialog).open( (this.$refs.photoDialog as PhotoDialog).open(
async (imgUrl) => { async (imgUrl) => {
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
@ -1378,6 +1405,8 @@ export default class AccountViewView extends Vue {
}, },
true, true,
IMAGE_TYPE_PROFILE, IMAGE_TYPE_PROFILE,
blob,
fileName,
); );
} }

8
src/views/GiftedDetails.vue

@ -74,7 +74,7 @@
/> />
</span> </span>
</div> </div>
<GiftedPhotoDialog ref="photoDialog" /> <PhotoDialog ref="photoDialog" />
<div v-if="projectId" class="mt-4"> <div v-if="projectId" class="mt-4">
<fa <fa
@ -130,11 +130,11 @@ import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { createAndSubmitGive, getPlanFromCache } from "@/libs/endorserServer"; import { createAndSubmitGive, getPlanFromCache } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import GiftedPhotoDialog from "@/components/GiftedPhotoDialog.vue"; import PhotoDialog from "@/components/PhotoDialog.vue";
@Component({ @Component({
components: { components: {
GiftedPhotoDialog, PhotoDialog,
QuickNav, QuickNav,
TopMessage, TopMessage,
}, },
@ -280,7 +280,7 @@ export default class GiftedDetails extends Vue {
} }
openPhotoDialog() { openPhotoDialog() {
(this.$refs.photoDialog as GiftedPhotoDialog).open((imgUrl) => { (this.$refs.photoDialog as PhotoDialog).open((imgUrl) => {
this.imageUrl = imgUrl; this.imageUrl = imgUrl;
}); });
} }

Loading…
Cancel
Save