Browse Source

Merge pull request 'tweak imagery so that it doesn't get stretched on a mobile device' (#107) from photo-ratio into master

Reviewed-on: https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/pulls/107
pull/108/head
trentlarson 8 months ago
parent
commit
ce05f7d003
  1. 2
      CHANGELOG.md
  2. 2
      README.md
  3. 4
      package-lock.json
  4. 2
      package.json
  5. 7
      project.task.yaml
  6. 151
      src/components/GiftedPhotoDialog.vue

2
CHANGELOG.md

@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Nothing - Nothing
## [0.3.2] - 2024.03.17 ## [0.3.3] - 2024.03.18
### Added ### Added
- Photo on gift record - Photo on gift record
### Fixed ### Fixed

2
README.md

@ -42,7 +42,7 @@ npm run lint
``` ```
# (See .env.development for more details.) # (See .env.development for more details.)
# The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there. # The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.
APP_TITLE="TimeSafari_Test" VUE_APP_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK VUE_APP_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VUE_APP_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app npm run build TIME_SAFARI_APP_TITLE="TimeSafari_Test" VUE_APP_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK VUE_APP_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VUE_APP_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app npm run build
``` ```
* Production * Production

4
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "TimeSafari", "name": "TimeSafari",
"version": "0.3.2", "version": "0.3.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "TimeSafari", "name": "TimeSafari",
"version": "0.3.2", "version": "0.3.3",
"dependencies": { "dependencies": {
"@dicebear/collection": "^5.3.5", "@dicebear/collection": "^5.3.5",
"@dicebear/core": "^5.3.5", "@dicebear/core": "^5.3.5",

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "TimeSafari", "name": "TimeSafari",
"version": "0.3.2", "version": "0.3.3",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",

7
project.task.yaml

@ -1,8 +1,12 @@
tasks : tasks :
- bug - landscape doesn't show full camera
- but - portrait stretches pic
- add to readme - check version, close tabs & restart phone if necessary
- bug maybe - a new give remembers the previous project - bug maybe - a new give remembers the previous project
- alert & stop if give amount < 0 - alert & stop if give amount < 0
- add warning that all data (except ID) is public
- onboarding video - onboarding video
- .1 on feed, don't show "to someone anonymous" if it's to a project - .1 on feed, don't show "to someone anonymous" if it's to a project
@ -55,7 +59,7 @@ tasks :
- .1 hide project-create button on project page if not registered - .1 hide project-create button on project page if not registered
- .1 hide offer & give buttons on project list page if not registered - .1 hide offer & give buttons on project list page if not registered
- .1 add cursor-pointer on the icons for giving on the project page, and on the list of projects on the discover page - .1 add cursor-pointer on the icons for giving on the project page, and on the list of projects on the discover page
- .2 record when InfiniteScroll hits the end of the list and don't trigger any more loads - .2 record when InfiniteScroll hits the end of the list and don't trigger any more loads (feed, project list, give & offer lists)
- bug - turning notifications on from the help screen did not stay, though account screen toggle did stay (From Jason on iPhone.) - bug - turning notifications on from the help screen did not stay, though account screen toggle did stay (From Jason on iPhone.)
- refactor - supply the projectId to the OfferDialog just like we do with the GiftedDialog offerId (in the "open" method, maybe as well as an attribute) - refactor - supply the projectId to the OfferDialog just like we do with the GiftedDialog offerId (in the "open" method, maybe as well as an attribute)
@ -119,6 +123,7 @@ tasks :
- .5 show seed phrase in a QR code for transfer to another device - .5 show seed phrase in a QR code for transfer to another device
- .5 on DiscoverView, switch to a filter UI (eg. just from friend - .5 on DiscoverView, switch to a filter UI (eg. just from friend
- .5 don't show "Offer" on project screen if they aren't registered - .5 don't show "Offer" on project screen if they aren't registered
- 01 especially for iOS, check for new version & update, eg. https://stackoverflow.com/questions/52221805/any-way-yet-to-auto-update-or-just-clear-the-cache-on-a-pwa-on-ios
- 24 Move to Vite - 24 Move to Vite
- 32 accept images for projects - 32 accept images for projects

151
src/components/GiftedPhotoDialog.vue

@ -1,43 +1,47 @@
<template> <template>
<div v-if="visible" class="dialog-overlay"> <div v-if="visible" class="dialog-overlay z-[60]">
<!-- Breadcrumb --> <div class="dialog relative">
<div class="dialog"> <div class="text-lg text-center font-light relative z-50">
<!-- Back --> <div
<div class="text-lg text-center font-light relative px-7"> id="ViewHeading"
<h1 class="text-center font-bold absolute top-0 left-0 right-0 px-4 py-2 bg-black/50 text-white leading-none"
class="text-lg text-center px-2 py-1 absolute -right-2 -top-1"
@click="close()"
> >
<fa icon="xmark" class="fa-fw"></fa>
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
<span v-if="uploading"> Uploading... </span> <span v-if="uploading"> Uploading... </span>
<span v-else-if="blob"> Look Good? </span> <span v-else-if="blob"> Look Good? </span>
<span v-else> Say "Cheese"! </span> <span v-else> Say "Cheese"! </span>
</h1> </div>
<div
class="text-lg text-center p-2 leading-none absolute right-0 top-0 text-white"
@click="close()"
>
<fa icon="xmark" class="w-[1em]"></fa>
</div>
</div>
<div v-if="uploading" class="flex justify-center"> <div v-if="uploading" class="flex justify-center">
<fa icon="spinner" class="fa-spin fa-3x text-center block" /> <fa icon="spinner" class="fa-spin fa-3x text-center block" />
</div> </div>
<div v-else-if="blob"> <div v-else-if="blob">
<div class="flex justify-around"> <div
class="flex justify-center gap-2 absolute bottom-[1rem] left-[1rem] right-[1rem] bg-black/50 px-4 py-2"
>
<button <button
@click="uploadImage" @click="uploadImage"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-2 rounded-full" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full"
> >
<span>Upload</span> <span>Upload</span>
</button> </button>
<button <button
@click="retryImage" @click="retryImage"
class="bg-slate-500 hover:bg-slate-700 text-white font-bold py-2 px-2 rounded-full" class="bg-slate-500 hover:bg-slate-700 text-white font-bold py-2 px-4 rounded-full"
> >
<span>Retry</span> <span>Retry</span>
</button> </button>
</div> </div>
<img :src="URL.createObjectURL(blob)" class="mt-2 w-full" /> <div class="flex justify-center">
<img :src="URL.createObjectURL(blob)" class="mt-2 rounded" />
</div>
</div> </div>
<div v-else> <div v-else>
<!-- <!--
@ -45,13 +49,14 @@
:resolution="{ width: 375, height: 812 }" :resolution="{ width: 375, height: 812 }"
--> -->
<camera facingMode="environment" autoplay ref="camera"> <camera facingMode="environment" autoplay ref="camera">
<div class="absolute bottom-0 w-full flex justify-center pb-4"> <div
<!-- Button --> class="absolute portrait:bottom-0 portrait:left-0 portrait:right-0 landscape:right-0 landscape:top-0 landscape:bottom-0 flex landscape:flex-row justify-center items-center portrait:pb-2 landscape:pr-4"
>
<button <button
@click="takeImage" @click="takeImage"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-2 rounded-full" class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
> >
<fa icon="camera" class="fa-fw"></fa> <fa icon="camera" class="w-[1em]"></fa>
</button> </button>
</div> </div>
</camera> </camera>
@ -78,6 +83,9 @@ export default class GiftedPhotoDialog extends Vue {
activeDid = ""; activeDid = "";
blob: Blob | null = null; blob: Blob | null = null;
setImage: (arg: string) => void = () => {}; setImage: (arg: string) => void = () => {};
imageHeight?: number = window.innerHeight / 2;
imageWidth?: number = window.innerWidth / 2;
imageWarning = ".";
uploading = false; uploading = false;
visible = false; visible = false;
@ -105,17 +113,64 @@ export default class GiftedPhotoDialog extends Vue {
open(setImageFn: (arg: string) => void) { open(setImageFn: (arg: string) => void) {
this.visible = true; this.visible = true;
const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
if (bottomNav) {
bottomNav.style.display = "none";
}
this.setImage = setImageFn; this.setImage = setImageFn;
} }
close() { close() {
this.visible = false; this.visible = false;
const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
if (bottomNav) {
bottomNav.style.display = "";
}
this.blob = null; this.blob = null;
} }
async takeImage(/* payload: MouseEvent */) { async takeImage(/* payload: MouseEvent */) {
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>; const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
this.blob = await cameraComponent?.snapshot(); // png is default; if that changes, change extension in formData.append
/**
* This logic to set the image height & width correctly.
* Without it, the portrait orientation ends up with an image that is stretched horizontally.
* Note that it's the same with raw browser Javascript; see the "drawImage" example below.
* Now that I've done it, I can't explain why it works.
*/
let imageHeight = cameraComponent?.resolution?.height;
let imageWidth = cameraComponent?.resolution?.width;
const initialImageRatio = imageWidth / imageHeight;
const windowRatio = window.innerWidth / window.innerHeight;
if (initialImageRatio > 1 && windowRatio < 1) {
// the image is wider than it is tall, and the window is taller than it is wide
// For some reason, mobile in portrait orientation renders a horizontally-stretched image.
// We're gonna force it opposite.
imageHeight = cameraComponent?.resolution?.width;
imageWidth = cameraComponent?.resolution?.height;
} else if (initialImageRatio < 1 && windowRatio > 1) {
// the image is taller than it is wide, and the window is wider than it is tall
// Haven't seen this happen, but we'll do it just in case.
imageHeight = cameraComponent?.resolution?.width;
imageWidth = cameraComponent?.resolution?.height;
}
const newImageRatio = imageWidth / imageHeight;
if (newImageRatio < windowRatio) {
// the image is a taller ratio than the window, so fit the height first
imageHeight = window.innerHeight / 2;
imageWidth = imageHeight * newImageRatio;
} else {
// the image is a wider ratio than the window, so fit the width first
imageWidth = window.innerWidth / 2;
imageHeight = imageWidth / newImageRatio;
}
// 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({
height: imageHeight,
width: imageWidth,
}); // png is default; if that changes, change extension in formData.append
if (!this.blob) { if (!this.blob) {
this.$notify( this.$notify(
{ {
@ -134,6 +189,54 @@ export default class GiftedPhotoDialog extends Vue {
this.blob = null; this.blob = null;
} }
/****
Here's an approach to photo capture without a library. It has similar quirks.
Now that we've fixed styling for simple-vue-camera, it's not critical to refactor. Maybe someday.
<button id="start-camera" @click="cameraClicked">Start Camera</button>
<video id="video" width="320" height="240" autoplay></video>
<button id="snap-photo" @click="photoSnapped">Snap Photo</button>
<canvas id="canvas" width="320" height="240"></canvas>
async cameraClicked() {
console.log("camera_button clicked");
const video = document.querySelector("#video");
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: false,
});
if (video instanceof HTMLVideoElement) {
video.srcObject = stream;
}
}
photoSnapped() {
console.log("snap_photo clicked");
const video = document.querySelector("#video");
const canvas = document.querySelector("#canvas");
if (
canvas instanceof HTMLCanvasElement &&
video instanceof HTMLVideoElement
) {
canvas
?.getContext("2d")
?.drawImage(video, 0, 0, canvas.width, canvas.height);
// ... or set the blob:
// canvas?.toBlob(
// (blob) => {
// this.blob = blob;
// },
// "image/jpeg",
// 1,
// );
// data url of the image
const image_data_url = canvas?.toDataURL("image/jpeg");
console.log(image_data_url);
}
}
****/
async uploadImage() { async uploadImage() {
this.uploading = true; this.uploading = true;
const identifier = await getIdentity(this.activeDid); const identifier = await getIdentity(this.activeDid);

Loading…
Cancel
Save