Browse Source

add file-chooser to the profile image selection

pull/115/head
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',
margin: 'auto',
}"
:img="URL.createObjectURL(blob)"
:img="createBlobURL(blob)"
:options="{
viewMode: 1,
dragMode: 'crop',
@ -49,7 +49,7 @@
</div>
<div v-else>
<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 class="absolute bottom-[1rem] left-[1rem] px-2 py-1">
@ -60,7 +60,10 @@
<span>Upload</span>
</button>
</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
@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"
@ -127,17 +130,19 @@ import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
@Component({ components: { Camera, VuePictureCropper } })
export default class GiftedPhotoDialog extends Vue {
export default class PhotoDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
activeDeviceNumber = 0;
activeDid = "";
blob: Blob | null = null;
blob?: Blob;
claimType = "GiveAction";
crop = false;
fileName?: string;
mirror = false;
numDevices = 0;
setImage: (arg: string) => void = () => {};
setImageCallback: (arg: string) => void = () => {};
showRetry = true;
uploading = 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.crop = !!crop;
this.claimType = claimType || "GiveAction";
@ -171,7 +182,16 @@ export default class GiftedPhotoDialog extends Vue {
if (bottomNav) {
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() {
@ -180,7 +200,7 @@ export default class GiftedPhotoDialog extends Vue {
if (bottomNav) {
bottomNav.style.display = "";
}
this.blob = null;
this.blob = undefined;
}
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 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,
width: imageWidth,
}); // png is default; if that changes, change extension in formData.append
})) || undefined;
// png is default
this.fileName = "snapshot.png";
if (!this.blob) {
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() {
this.blob = null;
this.blob = undefined;
}
/****
@ -307,7 +335,7 @@ export default class GiftedPhotoDialog extends Vue {
this.uploading = true;
if (this.crop) {
this.blob = await cropper?.getBlob();
this.blob = (await cropper?.getBlob()) || undefined;
}
const identifier = await getIdentity(this.activeDid);
@ -330,7 +358,7 @@ export default class GiftedPhotoDialog extends Vue {
this.uploading = false;
return;
}
formData.append("image", this.blob, "snapshot.png");
formData.append("image", this.blob, this.fileName || "snapshot.png");
formData.append("claimType", this.claimType);
try {
const response = await axios.post(
@ -341,8 +369,8 @@ export default class GiftedPhotoDialog extends Vue {
this.uploading = false;
this.visible = false;
this.blob = null;
this.setImage(response.data.url as string);
this.blob = undefined;
this.setImageCallback(response.data.url as string);
} catch (error) {
console.error("Error uploading the image", error);
this.$notify(
@ -355,7 +383,7 @@ export default class GiftedPhotoDialog extends Vue {
5000,
);
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"
/>
</span>
<span v-else>
<div v-else class="text-center">
<div class>
<fa
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"
@click="openPhotoDialog"
@click="openPhotoDialog(undefined, undefined)"
/>
</span>
<GiftedPhotoDialog ref="photoDialog" />
</div>
<div class="mt-4">
<div class="flex justify-center">
... and those without your image see this (if you let them see your
activity):
<div>
<input type="file" @change="uploadPhotoFile" />
</div>
</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 class="flex justify-center">
<EntityIcon
@ -577,11 +583,11 @@
<div class="ml-4 mt-2">
Import
<input type="file" @change="uploadFile" class="ml-2" />
<input type="file" @change="uploadImportFile" class="ml-2" />
<div v-if="showContactImport()">
<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"
@click="confirmSubmitFile()"
@click="confirmSubmitImportFile()"
>
Import Settings & Contacts
<br />
@ -614,7 +620,7 @@ import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import GiftedPhotoDialog from "@/components/GiftedPhotoDialog.vue";
import PhotoDialog from "@/components/PhotoDialog.vue";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import {
@ -645,10 +651,11 @@ interface IAccount {
derivationPath: string;
}
const inputFileNameRef = ref<Blob>();
const inputImportFileNameRef = ref<Blob>();
const inputPhotoFileNameRef = ref<Blob>();
@Component({
components: { EntityIcon, GiftedPhotoDialog, QuickNav, TopMessage },
components: { EntityIcon, PhotoDialog, QuickNav, TopMessage },
})
export default class AccountViewView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
@ -1120,16 +1127,36 @@ export default class AccountViewView extends Vue {
console.error("Export Error:", error);
}
async uploadFile(event: Event) {
inputFileNameRef.value = event.target.files[0];
async uploadPhotoFile(event: Event) {
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() {
return !!inputFileNameRef.value;
return !!inputImportFileNameRef.value;
}
confirmSubmitFile() {
if (inputFileNameRef.value != null) {
confirmSubmitImportFile() {
if (inputImportFileNameRef.value != null) {
this.$notify(
{
group: "modal",
@ -1138,7 +1165,7 @@ export default class AccountViewView extends Vue {
text:
"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?",
onYes: this.submitFile,
onYes: this.submitImportFile,
},
-1,
);
@ -1150,10 +1177,10 @@ export default class AccountViewView extends Vue {
*
* @throws Will notify the user if there is an export error.
*/
async submitFile() {
if (inputFileNameRef.value != null) {
async submitImportFile() {
if (inputImportFileNameRef.value != null) {
await db.delete();
await Dexie.import(inputFileNameRef.value as Blob, {
await Dexie.import(inputImportFileNameRef.value as Blob, {
progressCallback: this.progressCallback,
});
}
@ -1366,8 +1393,8 @@ export default class AccountViewView extends Vue {
);
}
openPhotoDialog() {
(this.$refs.photoDialog as GiftedPhotoDialog).open(
openPhotoDialog(blob?: Blob, fileName?: string) {
(this.$refs.photoDialog as PhotoDialog).open(
async (imgUrl) => {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
@ -1378,6 +1405,8 @@ export default class AccountViewView extends Vue {
},
true,
IMAGE_TYPE_PROFILE,
blob,
fileName,
);
}

8
src/views/GiftedDetails.vue

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

Loading…
Cancel
Save