Browse Source

allow file choice for gift, plus other UI fixes

Trent Larson 7 months ago
parent
commit
a8ef530d58
  1. 2
      README.md
  2. 124
      src/components/ImageMethodDialog.vue
  3. 17
      src/components/PhotoDialog.vue
  4. 2
      src/main.ts
  5. 61
      src/views/AccountViewView.vue
  6. 16
      src/views/GiftedDetails.vue
  7. 2
      src/views/SharedPhotoView.vue

2
README.md

@ -112,6 +112,7 @@ To add an icon, add to main.ts and reference with `fa` element and `icon` attrib
- On each page, verify the messaging, and that they cannot take action. - On each page, verify the messaging, and that they cannot take action.
- On the discovery page, check that they can see projects, and set a search area to see projects nearby. - On the discovery page, check that they can see projects, and set a search area to see projects nearby.
- On the contacts page, check that they can add a contact even without their own ID. - On the contacts page, check that they can add a contact even without their own ID.
- Install the PWA.
- As User 0 in another browser on the test API, add a give & a project. - As User 0 in another browser on the test API, add a give & a project.
- Note that some combinations of desktop with mobile emulation stretch the image. - Note that some combinations of desktop with mobile emulation stretch the image.
- Import User 0 with seed: `rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage` - Import User 0 with seed: `rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage`
@ -130,6 +131,7 @@ To add an icon, add to main.ts and reference with `fa` element and `icon` attrib
- Export & import, both seed and contacts & settings. - Export & import, both seed and contacts & settings.
- Choose location on the search map. - Choose location on the search map.
- Offer, deliver a give, and confirm. Create a third user and test connections. - Offer, deliver a give, and confirm. Create a third user and test connections.
- On mobile, share an image with the app.
- Switch to "no identifier" to see that things look OK without any ID. - Switch to "no identifier" to see that things look OK without any ID.
### Clear/Reset data & restart ### Clear/Reset data & restart

124
src/components/ImageMethodDialog.vue

@ -0,0 +1,124 @@
<template>
<div v-if="visible" class="dialog-overlay z-[60]">
<div class="dialog relative">
<div class="text-lg text-center font-light relative z-50">
<div
id="ViewHeading"
class="text-center font-bold absolute top-0 left-0 right-0 px-4 py-0.5 bg-black/50 text-white leading-none"
>
Camera or Other?
</div>
<div
class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white"
@click="close()"
>
<fa icon="xmark" class="w-[1em]"></fa>
</div>
</div>
<div>
<div class="text-center mt-8">
<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()"
/>
</div>
<div class="mt-4">
<input type="file" @change="uploadImageFile" />
</div>
</div>
</div>
</div>
</div>
<PhotoDialog ref="photoDialog" />
</template>
<script lang="ts">
import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator";
import PhotoDialog from "@/components/PhotoDialog.vue";
const inputImageFileNameRef = ref<Blob>();
@Component({
components: { PhotoDialog },
})
export default class ImageMethodDialog extends Vue {
claimType: string;
crop: boolean = false;
imageCallback: (imageUrl?: string) => void = () => {};
visible = false;
open(setImageFn: (arg: string) => void, claimType: string, crop?: boolean) {
this.claimType = claimType;
this.crop = !!crop;
this.imageCallback = setImageFn;
this.visible = true;
}
openPhotoDialog(blob?: Blob, fileName?: string) {
this.visible = false;
(this.$refs.photoDialog as PhotoDialog).open(
this.imageCallback,
this.claimType,
this.crop,
blob,
fileName,
);
}
async uploadImageFile(event: Event) {
this.visible = false;
inputImageFileNameRef.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 = inputImageFileNameRef.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);
}
}
close() {
this.visible = false;
}
}
</script>
<style>
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
padding: 1.5rem;
}
.dialog {
background-color: white;
padding: 1rem;
border-radius: 0.5rem;
width: 100%;
max-width: 700px;
}
</style>

17
src/components/PhotoDialog.vue

@ -29,7 +29,6 @@
<div v-if="crop"> <div v-if="crop">
<VuePictureCropper <VuePictureCropper
:boxStyle="{ :boxStyle="{
height: window80(),
backgroundColor: '#f8f8f8', backgroundColor: '#f8f8f8',
margin: 'auto', margin: 'auto',
}" }"
@ -39,6 +38,7 @@
dragMode: 'crop', dragMode: 'crop',
aspectRatio: 9 / 9, aspectRatio: 9 / 9,
}" }"
class="max-h-[90vh] max-w-[90vw] object-contain"
/> />
<!-- This gives a round cropper. <!-- This gives a round cropper.
:presetMode="{ :presetMode="{
@ -48,7 +48,10 @@
</div> </div>
<div v-else> <div v-else>
<div class="flex justify-center"> <div class="flex justify-center">
<img :src="createBlobURL(blob)" class="mt-2 rounded" /> <img
:src="createBlobURL(blob)"
class="mt-2 rounded max-h-[90vh] max-w-[90vw] object-contain"
/>
</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">
@ -135,7 +138,7 @@ export default class PhotoDialog extends Vue {
activeDeviceNumber = 0; activeDeviceNumber = 0;
activeDid = ""; activeDid = "";
blob?: Blob; blob?: Blob;
claimType = "GiveAction"; claimType = "";
crop = false; crop = false;
fileName?: string; fileName?: string;
mirror = false; mirror = false;
@ -169,14 +172,14 @@ export default class PhotoDialog extends Vue {
open( open(
setImageFn: (arg: string) => void, setImageFn: (arg: string) => void,
claimType: string,
crop?: boolean, crop?: boolean,
claimType?: string,
blob?: Blob, // for image upload, just to use the cropping function blob?: Blob, // for image upload, just to use the cropping function
inputFileName?: string, inputFileName?: string,
) { ) {
this.visible = true; this.visible = true;
this.claimType = claimType;
this.crop = !!crop; this.crop = !!crop;
this.claimType = claimType || "GiveAction";
const bottomNav = document.querySelector("#QuickNav") as HTMLElement; const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
if (bottomNav) { if (bottomNav) {
bottomNav.style.display = "none"; bottomNav.style.display = "none";
@ -185,6 +188,7 @@ export default class PhotoDialog extends Vue {
if (blob) { if (blob) {
this.blob = blob; this.blob = blob;
this.fileName = inputFileName; this.fileName = inputFileName;
// doesn't make sense to retry the file upload; they can cancel if they picked the wrong one
this.showRetry = false; this.showRetry = false;
} else { } else {
this.blob = undefined; this.blob = undefined;
@ -366,8 +370,7 @@ export default class PhotoDialog extends Vue {
); );
this.uploading = false; this.uploading = false;
this.visible = false; this.close();
this.blob = undefined;
this.setImageCallback(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);

2
src/main.ts

@ -49,6 +49,7 @@ import {
faHand, faHand,
faHandHoldingHeart, faHandHoldingHeart,
faHouseChimney, faHouseChimney,
faImagePortrait,
faLeftRight, faLeftRight,
faLocationDot, faLocationDot,
faLongArrowAltLeft, faLongArrowAltLeft,
@ -114,6 +115,7 @@ library.add(
faHand, faHand,
faHandHoldingHeart, faHandHoldingHeart,
faHouseChimney, faHouseChimney,
faImagePortrait,
faLeftRight, faLeftRight,
faLocationDot, faLocationDot,
faLongArrowAltLeft, faLongArrowAltLeft,

61
src/views/AccountViewView.vue

@ -87,22 +87,23 @@
/> />
</span> </span>
<div v-else class="text-center"> <div v-else class="text-center">
<div class> <div class @click="openImageDialog()">
<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-l"
@click="openPhotoDialog(undefined, undefined)" />
<fa
icon="image-portrait"
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-r"
@click="openImageDialog()"
/> />
</div>
<div>
<input type="file" @change="uploadPhotoFile" />
</div> </div>
</div> </div>
<PhotoDialog ref="photoDialog" /> <ImageMethodDialog ref="imageMethodDialog" />
</div> </div>
<div class="mt-6"> <div class="mt-6">
<div class="flex justify-center text-center"> <div class="flex justify-center text-center">
People without your image see this: People {{ profileImageUrl ? "without your image" : "" }} see this:
<br /> <br />
(if you've let them see your activity) (if you've let them see your activity)
</div> </div>
@ -620,7 +621,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 PhotoDialog from "@/components/PhotoDialog.vue"; import ImageMethodDialog from "@/components/ImageMethodDialog.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 {
@ -652,10 +653,9 @@ interface IAccount {
} }
const inputImportFileNameRef = ref<Blob>(); const inputImportFileNameRef = ref<Blob>();
const inputPhotoFileNameRef = ref<Blob>();
@Component({ @Component({
components: { EntityIcon, PhotoDialog, QuickNav, TopMessage }, components: { EntityIcon, ImageMethodDialog, 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;
@ -698,13 +698,13 @@ export default class AccountViewView extends Vue {
warnIfTestServer = false; warnIfTestServer = false;
/** /**
* Async function executed when the component is created. * Async function executed when the component is mounted.
* Initializes the component's state with values from the database, * Initializes the component's state with values from the database,
* handles identity-related tasks, and checks limitations. * handles identity-related tasks, and checks limitations.
* *
* @throws Will display specific messages to the user based on different errors. * @throws Will display specific messages to the user based on different errors.
*/ */
async created() { async mounted() {
try { try {
await db.open(); await db.open();
@ -718,18 +718,13 @@ export default class AccountViewView extends Vue {
if (identity) { if (identity) {
this.processIdentity(identity); this.processIdentity(identity);
} }
} catch (err: unknown) {
this.handleError(err);
}
}
async mounted() {
try {
const registration = await navigator.serviceWorker.ready; const registration = await navigator.serviceWorker.ready;
this.subscription = await registration.pushManager.getSubscription(); this.subscription = await registration.pushManager.getSubscription();
this.isSubscribed = !!this.subscription; this.isSubscribed = !!this.subscription;
} catch (error) { } catch (error) {
console.error("Mount error:", error); console.error("Mount error:", error);
this.handleError(error);
} }
} }
@ -1127,26 +1122,6 @@ export default class AccountViewView extends Vue {
console.error("Export Error:", error); console.error("Export Error:", error);
} }
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) { async uploadImportFile(event: Event) {
inputImportFileNameRef.value = event.target.files[0]; inputImportFileNameRef.value = event.target.files[0];
} }
@ -1393,8 +1368,8 @@ export default class AccountViewView extends Vue {
); );
} }
openPhotoDialog(blob?: Blob, fileName?: string) { openImageDialog() {
(this.$refs.photoDialog as PhotoDialog).open( (this.$refs.imageMethodDialog as ImageMethodDialog).open(
async (imgUrl) => { async (imgUrl) => {
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
@ -1403,10 +1378,8 @@ export default class AccountViewView extends Vue {
this.profileImageUrl = imgUrl; this.profileImageUrl = imgUrl;
//console.log("Got image URL:", imgUrl); //console.log("Got image URL:", imgUrl);
}, },
true,
IMAGE_TYPE_PROFILE, IMAGE_TYPE_PROFILE,
blob, true,
fileName,
); );
} }

16
src/views/GiftedDetails.vue

@ -70,11 +70,11 @@
<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="openImageDialog"
/> />
</span> </span>
</div> </div>
<PhotoDialog ref="photoDialog" /> <ImageMethodDialog ref="imageDialog" />
<div v-if="projectId" class="mt-4"> <div v-if="projectId" class="mt-4">
<fa <fa
@ -122,19 +122,19 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app"; import ImageMethodDialog from "@/components/ImageMethodDialog.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 { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import { db } from "@/db/index"; import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; 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 PhotoDialog from "@/components/PhotoDialog.vue";
@Component({ @Component({
components: { components: {
PhotoDialog, ImageMethodDialog,
QuickNav, QuickNav,
TopMessage, TopMessage,
}, },
@ -279,10 +279,10 @@ export default class GiftedDetails extends Vue {
this.$router.back(); this.$router.back();
} }
openPhotoDialog() { openImageDialog() {
(this.$refs.photoDialog as PhotoDialog).open((imgUrl) => { (this.$refs.imageDialog as ImageMethodDialog).open((imgUrl) => {
this.imageUrl = imgUrl; this.imageUrl = imgUrl;
}); }, "GiveAction");
} }
confirmDeleteImage() { confirmDeleteImage() {

2
src/views/SharedPhotoView.vue

@ -133,8 +133,8 @@ export default class SharedPhotoView extends Vue {
}); });
this.$router.push({ name: "account" }); this.$router.push({ name: "account" });
}, },
true,
IMAGE_TYPE_PROFILE, IMAGE_TYPE_PROFILE,
true,
this.imageBlob, this.imageBlob,
this.imageFileName, this.imageFileName,
); );

Loading…
Cancel
Save