Browse Source

Merge pull request 'add a share_target for people to add a photo' (#115) from share-photo into master

Reviewed-on: https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/pulls/115
kb/add-usage-guide
trentlarson 7 months ago
parent
commit
03ac31d981
  1. 2
      src/components/GiftedDialog.vue
  2. 66
      src/components/PhotoDialog.vue
  3. 10
      src/components/QuickNav.vue
  4. 2
      src/constants/app.ts
  5. 26
      src/db/index.ts
  6. 13
      src/db/tables/temp.ts
  7. 5
      src/router/index.ts
  8. 99
      src/views/AccountViewView.vue
  9. 87
      src/views/GiftedDetails.vue
  10. 45
      src/views/HelpView.vue
  11. 2
      src/views/NewEditProjectView.vue
  12. 24
      src/views/ProjectViewView.vue
  13. 5
      src/views/SeedBackupView.vue
  14. 188
      src/views/SharedPhotoView.vue
  15. 55
      src/views/TestView.vue
  16. 21
      sw_scripts/additional-scripts.js
  17. 14
      sw_scripts/safari-notifications.js
  18. 21
      vite.config.mjs

2
src/components/GiftedDialog.vue

@ -15,7 +15,7 @@
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20" class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20"
@click="changeUnitCode()" @click="changeUnitCode()"
> >
{{ libsUtil.UNIT_SHORT[unitCode] }} {{ libsUtil.UNIT_SHORT[unitCode] || unitCode }}
</span> </span>
<div <div
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2" class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"

66
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 =
height: imageHeight, (await cameraComponent?.snapshot({
width: imageWidth, height: imageHeight,
}); // png is default; if that changes, change extension in formData.append width: imageWidth,
})) || 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"); // png is set in snapshot() 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;
} }
} }

10
src/components/QuickNav.vue

@ -12,7 +12,7 @@
}" }"
> >
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1"> <router-link :to="{ name: 'home' }" class="block text-center py-3 px-1">
<fa icon="house-chimney" class="fa-fw"></fa> <fa icon="house-chimney" class="fa-fw" />
</router-link> </router-link>
</li> </li>
<!-- Search --> <!-- Search -->
@ -28,7 +28,7 @@
:to="{ name: 'discover' }" :to="{ name: 'discover' }"
class="block text-center py-3 px-1" class="block text-center py-3 px-1"
> >
<fa icon="magnifying-glass" class="fa-fw"></fa> <fa icon="magnifying-glass" class="fa-fw" />
</router-link> </router-link>
</li> </li>
<!-- Projects --> <!-- Projects -->
@ -44,7 +44,7 @@
:to="{ name: 'projects' }" :to="{ name: 'projects' }"
class="block text-center py-3 px-1" class="block text-center py-3 px-1"
> >
<fa icon="hand" class="fa-fw"></fa> <fa icon="hand" class="fa-fw" />
</router-link> </router-link>
</li> </li>
<!-- Contacts --> <!-- Contacts -->
@ -60,7 +60,7 @@
:to="{ name: 'contacts' }" :to="{ name: 'contacts' }"
class="block text-center py-3 px-1" class="block text-center py-3 px-1"
> >
<fa icon="users" class="fa-fw"></fa> <fa icon="users" class="fa-fw" />
</router-link> </router-link>
</li> </li>
<!-- Profile --> <!-- Profile -->
@ -76,7 +76,7 @@
:to="{ name: 'account' }" :to="{ name: 'account' }"
class="block text-center py-3 px-1" class="block text-center py-3 px-1"
> >
<fa icon="circle-user" class="fa-fw"></fa> <fa icon="circle-user" class="fa-fw" />
</router-link> </router-link>
</li> </li>
</ul> </ul>

2
src/constants/app.ts

@ -30,6 +30,8 @@ export const DEFAULT_IMAGE_API_SERVER =
export const DEFAULT_PUSH_SERVER = export const DEFAULT_PUSH_SERVER =
window.location.protocol + "//" + window.location.host; window.location.protocol + "//" + window.location.host;
export const IMAGE_TYPE_PROFILE = "profile";
/** /**
* The possible values for "group" and "type" are in App.vue. * The possible values for "group" and "type" are in App.vue.
* From the notiwind package * From the notiwind package

26
src/db/index.ts

@ -8,6 +8,7 @@ import {
Settings, Settings,
SettingsSchema, SettingsSchema,
} from "./tables/settings"; } from "./tables/settings";
import { Temp, TempSchema } from "./tables/temp";
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app"; import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
// Define types for tables that hold sensitive and non-sensitive data // Define types for tables that hold sensitive and non-sensitive data
@ -16,6 +17,7 @@ type NonsensitiveTables = {
contacts: Table<Contact>; contacts: Table<Contact>;
logs: Table<Log>; logs: Table<Log>;
settings: Table<Settings>; settings: Table<Settings>;
temp: Table<Temp>;
}; };
// Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings // Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings
@ -25,14 +27,7 @@ export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
// Initialize Dexie databases for sensitive and non-sensitive data // Initialize Dexie databases for sensitive and non-sensitive data
export const accountsDB = new BaseDexie("TimeSafariAccounts") as SensitiveDexie; export const accountsDB = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
const SensitiveSchemas = { ...AccountsSchema };
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie; export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
const NonsensitiveSchemas = {
...ContactSchema,
...LogSchema,
...SettingsSchema,
};
// Manage the encryption key. If not present in localStorage, create and store it. // Manage the encryption key. If not present in localStorage, create and store it.
const secret = const secret =
@ -42,11 +37,18 @@ if (!localStorage.getItem("secret")) localStorage.setItem("secret", secret);
// Apply encryption to the sensitive database using the secret key // Apply encryption to the sensitive database using the secret key
encrypted(accountsDB, { secretKey: secret }); encrypted(accountsDB, { secretKey: secret });
// Define the schema for our databases // Define the schemas for our databases
accountsDB.version(1).stores(SensitiveSchemas); // Only the tables with index modifications need listing. https://dexie.org/docs/Tutorial/Design#database-versioning
// v1 was contacts & settings accountsDB.version(1).stores(AccountsSchema);
// v2 added logs // v1 also had contacts & settings
db.version(2).stores(NonsensitiveSchemas); // v2 added Log
db.version(2).stores({
...ContactSchema,
...LogSchema,
...SettingsSchema,
});
// v3 added Temp
db.version(3).stores(TempSchema);
// Event handler to initialize the non-sensitive database with default settings // Event handler to initialize the non-sensitive database with default settings
db.on("populate", () => { db.on("populate", () => {

13
src/db/tables/temp.ts

@ -0,0 +1,13 @@
// for ephemeral uses, eg. passing a blob from the service worker to the main thread
export type Temp = {
id: string;
blob?: Blob;
};
/**
* Schema for the Temp table in the database.
*/
export const TempSchema = {
temp: "id",
};

5
src/router/index.ts

@ -169,6 +169,11 @@ const routes: Array<RouteRecordRaw> = [
name: "seed-backup", name: "seed-backup",
component: () => import("../views/SeedBackupView.vue"), component: () => import("../views/SeedBackupView.vue"),
}, },
{
path: "/shared-photo",
name: "shared-photo",
component: () => import("@/views/SharedPhotoView.vue"),
},
{ {
path: "/start", path: "/start",
name: "start", name: "start",

99
src/views/AccountViewView.vue

@ -4,16 +4,6 @@
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back -->
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
<!-- Heading --> <!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-4"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-4">
Your Identity Your Identity
@ -96,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">
<fa <div class>
icon="camera" <fa
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" icon="camera"
@click="openPhotoDialog" 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(undefined, undefined)"
</span> />
<GiftedPhotoDialog ref="photoDialog" /> </div>
<div>
<input type="file" @change="uploadPhotoFile" />
</div>
</div>
<PhotoDialog ref="photoDialog" />
</div> </div>
<div class="mt-4"> <div class="mt-6">
<div class="flex justify-center"> <div class="flex justify-center text-center">
... and those without your image see this (if you let them see your People without your image see this:
activity): <br />
(if you've let them see your activity)
</div> </div>
<div class="flex justify-center"> <div class="flex justify-center">
<EntityIcon <EntityIcon
@ -587,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 />
@ -624,13 +620,14 @@ 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 {
AppString, AppString,
DEFAULT_IMAGE_API_SERVER, DEFAULT_IMAGE_API_SERVER,
DEFAULT_PUSH_SERVER, DEFAULT_PUSH_SERVER,
IMAGE_TYPE_PROFILE,
NotificationIface, NotificationIface,
} from "@/constants/app"; } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
@ -654,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;
@ -1129,17 +1127,36 @@ export default class AccountViewView extends Vue {
console.error("Export Error:", error); console.error("Export Error:", error);
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any async uploadPhotoFile(event: Event) {
async uploadFile(event: any) { inputPhotoFileNameRef.value = event.target.files[0];
inputFileNameRef.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",
@ -1148,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,
); );
@ -1160,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, { await Dexie.import(inputImportFileNameRef.value as Blob, {
progressCallback: this.progressCallback, progressCallback: this.progressCallback,
}); });
} }
@ -1376,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, {
@ -1387,7 +1404,9 @@ export default class AccountViewView extends Vue {
//console.log("Got image URL:", imgUrl); //console.log("Got image URL:", imgUrl);
}, },
true, true,
"profile", IMAGE_TYPE_PROFILE,
blob,
fileName,
); );
} }

87
src/views/GiftedDetails.vue

@ -5,10 +5,13 @@
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back --> <!-- Back -->
<div class="text-lg text-center font-light relative px-7"> <div
v-if="!hideBackButton"
class="text-lg text-center font-light relative px-7"
>
<h1 <h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="cancel()" @click="cancelBack()"
> >
<fa icon="chevron-left" class="fa-fw"></fa> <fa icon="chevron-left" class="fa-fw"></fa>
</h1> </h1>
@ -31,7 +34,7 @@
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20" class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20"
@click="changeUnitCode()" @click="changeUnitCode()"
> >
{{ libsUtil.UNIT_SHORT[unitCode] }} {{ libsUtil.UNIT_SHORT[unitCode] || unitCode }}
</span> </span>
<div <div
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2" class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@ -71,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
@ -127,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,
}, },
@ -144,9 +147,11 @@ export default class GiftedDetails extends Vue {
amountInput = "0"; amountInput = "0";
description = ""; description = "";
destinationNameAfter = "";
givenToUser = false; givenToUser = false;
giverDid: string | undefined; giverDid: string | undefined;
giverName = ""; giverName = "";
hideBackButton = false;
imageUrl = ""; imageUrl = "";
isTrade = false; isTrade = false;
message = ""; message = "";
@ -161,26 +166,39 @@ export default class GiftedDetails extends Vue {
libsUtil = libsUtil; libsUtil = libsUtil;
async mounted() { async mounted() {
this.amountInput = this.$route.query.amountInput as string; this.amountInput =
this.description = this.$route.query.description as string; (this.$route.query.amountInput as string) || this.amountInput;
this.description = (this.$route.query.description as string) || "";
this.destinationNameAfter = this.$route.query
.destinationNameAfter as string;
this.giverDid = this.$route.query.giverDid as string; this.giverDid = this.$route.query.giverDid as string;
this.giverName = this.$route.query.giverName as string; this.giverName = (this.$route.query.giverName as string) || "";
if (this.giverDid && !this.giverName) { this.hideBackButton = this.$route.query.hideBackButton === "true";
this.giverName = this.message = (this.$route.query.message as string) || "";
this.giverDid === this.activeDid ? "you" : "someone not named";
}
this.message = this.$route.query.message as string;
this.offerId = this.$route.query.offerId as string; this.offerId = this.$route.query.offerId as string;
this.projectId = this.$route.query.projectId as string; this.projectId = this.$route.query.projectId as string;
this.recipientDid = this.$route.query.recipientDid as string; this.recipientDid = this.$route.query.recipientDid as string;
this.recipientName = this.$route.query.recipientName as string; this.recipientName = (this.$route.query.recipientName as string) || "";
if (this.recipientDid && !this.recipientName) { this.unitCode = (this.$route.query.unitCode as string) || this.unitCode;
this.recipientName =
this.recipientDid === this.activeDid ? "you" : "someone not named"; this.imageUrl =
(this.$route.query.imageUrl as string) ||
localStorage.getItem("imageUrl") ||
"";
// this is an endpoint for sharing project info to highlight something given
// https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target
if (this.$route.query.shareTitle) {
this.description = this.$route.query.shareTitle as string;
}
if (this.$route.query.shareText) {
this.description =
(this.description ? this.description + " " : "") +
(this.$route.query.shareText as string);
}
if (this.$route.query.shareUrl) {
this.imageUrl = this.$route.query.shareUrl as string;
} }
this.unitCode = this.$route.query.unitCode as string;
this.imageUrl = localStorage.getItem("imageUrl") || "";
try { try {
await db.open(); await db.open();
@ -188,7 +206,15 @@ export default class GiftedDetails extends Vue {
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
if (this.giverDid && !this.giverName) {
this.giverName =
this.giverDid === this.activeDid ? "you" : "someone not named";
}
this.givenToUser = this.recipientDid === this.activeDid; this.givenToUser = this.recipientDid === this.activeDid;
if (this.recipientDid && !this.recipientName) {
this.recipientName =
this.recipientDid === this.activeDid ? "you" : "someone not named";
}
this.showGivenToUser = this.showGivenToUser =
!this.projectId && this.recipientDid === this.activeDid; !this.projectId && this.recipientDid === this.activeDid;
@ -240,12 +266,21 @@ export default class GiftedDetails extends Vue {
} }
cancel() { cancel() {
this.deleteImage(); // not awaiting, so they'll go back immediately
if (this.destinationNameAfter) {
this.$router.push({ name: this.destinationNameAfter });
} else {
this.$router.back();
}
}
cancelBack() {
this.deleteImage(); // not awaiting, so they'll go back immediately this.deleteImage(); // not awaiting, so they'll go back immediately
this.$router.back(); this.$router.back();
} }
openPhotoDialog() { openPhotoDialog() {
(this.$refs.photoDialog as GiftedPhotoDialog).open((imgUrl) => { (this.$refs.photoDialog as PhotoDialog).open((imgUrl) => {
this.imageUrl = imgUrl; this.imageUrl = imgUrl;
}); });
} }
@ -284,7 +319,7 @@ export default class GiftedDetails extends Vue {
// don't bother with a notification // don't bother with a notification
// (either they'll simply continue or they're canceling and going back) // (either they'll simply continue or they're canceling and going back)
} else { } else {
console.error("Non-success deleting image:", response); console.error("Problem deleting image:", response);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -435,7 +470,11 @@ export default class GiftedDetails extends Vue {
5000, 5000,
); );
localStorage.removeItem("imageUrl"); localStorage.removeItem("imageUrl");
this.$router.back(); if (this.destinationNameAfter) {
this.$router.push({ name: this.destinationNameAfter });
} else {
this.$router.back();
}
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) { } catch (error: any) {

45
src/views/HelpView.vue

@ -100,8 +100,9 @@
<h2 class="text-xl font-semibold">How do I backup all my data?</h2> <h2 class="text-xl font-semibold">How do I backup all my data?</h2>
<p> <p>
There are two sets of data to backup: the identifier secrets and the There are three sets of data to backup: the identifier secrets;
other data that isn't quite a secret such as settings, contacts, etc. the non-public textual data that isn't quite a secret such as settings and contacts;
the non-public image for yourself; and the data that you have sent to the public.
</p> </p>
<div class="px-4"> <div class="px-4">
@ -122,7 +123,7 @@
</ul> </ul>
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">
How do I backup my other (non-identifier-secret) data? How do I backup my non-secret, non-public text data?
</h2> </h2>
<ul class="list-disc list-outside ml-4"> <ul class="list-disc list-outside ml-4">
<li> <li>
@ -134,6 +135,27 @@
won't lose it. won't lose it.
</li> </li>
</ul> </ul>
<h2 class="text-xl font-semibold">
How do I backup my non-secret, non-public image?
</h2>
<ul class="list-disc list-outside ml-4">
<li>
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page,
tap on your image, and save it.
</li>
</ul>
<h2 class="text-xl font-semibold">
How do I backup my public data?
</h2>
<ul class="list-disc list-outside ml-4">
<li>
This requires use of the API, so investigate the endpoints
<a href="https://api.endorser.ch/" target="_blank" class="text-blue-500">here</a>
(particularly the "claim" endpoints).
</li>
</ul>
</div> </div>
<h2 class="text-xl font-semibold">How do I restore my data?</h2> <h2 class="text-xl font-semibold">How do I restore my data?</h2>
@ -178,8 +200,7 @@
<h2 class="text-xl font-semibold">How do I erase my data?</h2> <h2 class="text-xl font-semibold">How do I erase my data?</h2>
<p> <p>
Before doing this, note the two kinds of data to backup: identity data, Before doing this, you may want to back up your data with the instructions above.
and other data for contacts and settings (see instructions above).
</p> </p>
<ul> <ul>
<li class="list-disc list-outside ml-4"> <li class="list-disc list-outside ml-4">
@ -198,11 +219,11 @@
<ul> <ul>
<li class="list-disc list-outside ml-4"> <li class="list-disc list-outside ml-4">
Chrome: Chrome:
Clear at chrome://settings/content/all and Clear at "chrome://settings/content/all" and
also clear under dev tools Application also clear under dev tools Application
</li> </li>
<li class="list-disc list-outside ml-4"> <li class="list-disc list-outside ml-4">
Firefox: <a href="about:preferences">go here</a>, Manage Data, Firefox: Navigate to "about:preferences", Manage Data,
find timesafari.app and select, hit Remove Selected, then Save find timesafari.app and select, hit Remove Selected, then Save
Changes Changes
</li> </li>
@ -232,7 +253,7 @@
<p> <p>
There is a even more functionality in a mobile app (and more There is a even more functionality in a mobile app (and more
documentation) at documentation) at
<a href="https://endorser.ch" class="text-blue-500"> <a href="https://endorser.ch" target="_blank" class="text-blue-500">
EndorserSearch.com EndorserSearch.com
</a> </a>
</p> </p>
@ -303,7 +324,7 @@
find "timesafari.app", and click "Unregister". find "timesafari.app", and click "Unregister".
</li> </li>
<li> <li>
<a href="https://duckduckgo.com/?q=unregister+service+worker" class="text-blue-500">Search</a> <a href="https://duckduckgo.com/?q=unregister+service+worker" target="_blank" class="text-blue-500">Search</a>
for instructions for other browsers.</li> for instructions for other browsers.</li>
</ul> </ul>
Then reload Time Safari. Then reload Time Safari.
@ -344,7 +365,7 @@
by disabling notifications on the Account <fa icon="circle-user" class="fa-fw" /> page. by disabling notifications on the Account <fa icon="circle-user" class="fa-fw" /> page.
<br /> <br />
For all other claim data, For all other claim data,
<a href="https://endorser.ch/privacy-policy" class="text-blue-500"> <a href="https://endorser.ch/privacy-policy" target="_blank" class="text-blue-500">
the Endorser Service has this Privacy Policy. the Endorser Service has this Privacy Policy.
</a> </a>
</p> </p>
@ -352,7 +373,7 @@
<h2 class="text-xl font-semibold">Where can I read more?</h2> <h2 class="text-xl font-semibold">Where can I read more?</h2>
<p> <p>
This is part of the This is part of the
<a href="https://livesofgiving.org" class="text-blue-500"> <a href="https://livesofgiving.org" target="_blank" class="text-blue-500">
Lives of Giving Lives of Giving
</a> </a>
initiative. initiative.
@ -362,7 +383,7 @@
<p>{{ package.version }} ({{ commitHash }})</p> <p>{{ package.version }} ({{ commitHash }})</p>
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">
For any other questions, including removing your data: For any other questions, including removing all your data from the public ledger:
</h2> </h2>
<p> <p>
Contact us at Contact us at

2
src/views/NewEditProjectView.vue

@ -227,7 +227,7 @@ export default class NewEditProjectView extends Vue {
return headers; return headers;
} }
async created() { async mounted() {
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = (settings?.activeDid as string) || ""; this.activeDid = (settings?.activeDid as string) || "";

24
src/views/ProjectViewView.vue

@ -505,7 +505,7 @@ export default class ProjectViewView extends Vue {
title: "Error", title: "Error",
text: "There was a problem getting that project. See logs for more info.", text: "There was a problem getting that project. See logs for more info.",
}, },
-1, 5000,
); );
} }
} catch (error: unknown) { } catch (error: unknown) {
@ -519,7 +519,7 @@ export default class ProjectViewView extends Vue {
title: "Error", title: "Error",
text: "That project does not exist.", text: "That project does not exist.",
}, },
-1, 5000,
); );
} else { } else {
this.$notify( this.$notify(
@ -529,7 +529,7 @@ export default class ProjectViewView extends Vue {
title: "Error", title: "Error",
text: "Something went wrong retrieving that project. See logs for more info.", text: "Something went wrong retrieving that project. See logs for more info.",
}, },
-1, 5000,
); );
} }
} }
@ -561,7 +561,7 @@ export default class ProjectViewView extends Vue {
title: "Error", title: "Error",
text: "Failed to retrieve plans fulfilled by this project.", text: "Failed to retrieve plans fulfilled by this project.",
}, },
-1, 5000,
); );
} }
} catch (error: unknown) { } catch (error: unknown) {
@ -573,7 +573,7 @@ export default class ProjectViewView extends Vue {
title: "Error", title: "Error",
text: "Something went wrong retrieving plans fulfilled by this project.", text: "Something went wrong retrieving plans fulfilled by this project.",
}, },
-1, 5000,
); );
console.error( console.error(
"Error retrieving plans fulfilled by this project:", "Error retrieving plans fulfilled by this project:",
@ -616,7 +616,7 @@ export default class ProjectViewView extends Vue {
title: "Error", title: "Error",
text: "Failed to retrieve more gives to this project.", text: "Failed to retrieve more gives to this project.",
}, },
-1, 5000,
); );
} }
} catch (error: unknown) { } catch (error: unknown) {
@ -628,7 +628,7 @@ export default class ProjectViewView extends Vue {
title: "Error", title: "Error",
text: "Something went wrong retrieving more gives to this project.", text: "Something went wrong retrieving more gives to this project.",
}, },
-1, 5000,
); );
console.error( console.error(
"Something went wrong retrieving more gives to this project:", "Something went wrong retrieving more gives to this project:",
@ -671,7 +671,7 @@ export default class ProjectViewView extends Vue {
title: "Error", title: "Error",
text: "Failed to retrieve more offers to this project.", text: "Failed to retrieve more offers to this project.",
}, },
-1, 5000,
); );
} }
} catch (error: unknown) { } catch (error: unknown) {
@ -683,7 +683,7 @@ export default class ProjectViewView extends Vue {
title: "Error", title: "Error",
text: "Something went wrong retrieving more offers to this project.", text: "Something went wrong retrieving more offers to this project.",
}, },
-1, 5000,
); );
console.error( console.error(
"Something went wrong retrieving more offers to this project:", "Something went wrong retrieving more offers to this project:",
@ -727,7 +727,7 @@ export default class ProjectViewView extends Vue {
title: "Error", title: "Error",
text: "Failed to retrieve more plans that fullfill this project.", text: "Failed to retrieve more plans that fullfill this project.",
}, },
-1, 5000,
); );
} }
} catch (error: unknown) { } catch (error: unknown) {
@ -739,7 +739,7 @@ export default class ProjectViewView extends Vue {
title: "Error", title: "Error",
text: "Something went wrong retrieving more plans that fulfull this project.", text: "Something went wrong retrieving more plans that fulfull this project.",
}, },
-1, 5000,
); );
console.error( console.error(
"Something went wrong retrieving more plans that fulfill this project:", "Something went wrong retrieving more plans that fulfill this project:",
@ -928,7 +928,7 @@ export default class ProjectViewView extends Vue {
title: "Error", title: "Error",
text: message, text: message,
}, },
-1, 5000,
); );
} }
} }

5
src/views/SeedBackupView.vue

@ -70,12 +70,9 @@ import * as R from "ramda";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app"; import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
interface Account {
mnemonic: string;
}
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class SeedBackupView extends Vue { export default class SeedBackupView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;

188
src/views/SharedPhotoView.vue

@ -0,0 +1,188 @@
<template>
<QuickNav />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Image
</h1>
<div v-if="imageBlob">
<div v-if="uploading" class="text-center mb-4">
<fa icon="spinner" class="fa-spin-pulse" />
</div>
<div v-else>
<div class="text-center mb-4">Choose how to use this image</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<button
@click="recordGift"
class="text-center text-md font-bold 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-3 rounded-md"
>
<fa icon="gift" class="fa-fw" />
Record a Gift
</button>
<button
@click="recordProfile"
class="text-center text-md font-bold 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-3 rounded-md"
>
<fa icon="circle-user" class="fa-fw" />
Save as Profile Image
</button>
<button
@click="cancel"
class="text-center text-md font-bold bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
>
<fa icon="ban" class="fa-fw" />
Cancel
</button>
</div>
</div>
<div class="flex justify-center">
<img
:src="URL.createObjectURL(imageBlob)"
alt="Shared Image"
class="rounded mt-4"
/>
</div>
</div>
<div v-else class="text-center mb-4">
<p>No image found.</p>
</div>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue";
import {
DEFAULT_IMAGE_API_SERVER,
IMAGE_TYPE_PROFILE,
NotificationIface,
} from "@/constants/app";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { getIdentity } from "@/libs/util";
import { accessToken } from "@/libs/crypto";
import axios from "axios";
@Component({ components: { QuickNav } })
export default class SharedPhotoView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
activeDid: string | undefined = undefined;
imageBlob: Blob | undefined = undefined;
imageFileName: string | undefined = undefined;
uploading = false;
URL = window.URL || window.webkitURL;
// 'created' hook runs when the Vue instance is first created
async mounted() {
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid as string;
const temp = await db.temp.get("shared-photo");
if (temp) {
this.imageBlob = temp.blob;
// clear the temp image
db.temp.delete("shared-photo");
this.imageFileName = this.$route.query.fileName as string;
}
} catch (err: unknown) {
console.error("Got an error loading an identifier:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Got an error loading this data.",
},
-1,
);
}
}
async recordGift() {
await this.sendToImageServer("GiveAction").then((url) => {
if (url) {
this.$router.push({
name: "gifted-details",
query: {
destinationNameAfter: "home",
hideBackButton: true,
imageUrl: url,
recipientDid: this.activeDid,
},
});
}
});
}
async recordProfile() {
await this.sendToImageServer(IMAGE_TYPE_PROFILE).then((url) => {
if (url) {
db.settings.update(MASTER_SETTINGS_KEY, {
profileImageUrl: url,
});
this.$router.push({ name: "account" });
}
});
}
async cancel() {
this.imageBlob = undefined;
this.imageFileName = undefined;
this.$router.push({ name: "home" });
}
async sendToImageServer(imageType: string) {
this.uploading = true;
let result;
try {
// send the image to the server
const identifier = await getIdentity(this.activeDid as string);
const token = await accessToken(identifier);
const headers = {
Authorization: "Bearer " + token,
};
const formData = new FormData();
formData.append(
"image",
this.imageBlob as Blob,
this.imageFileName as string,
);
formData.append("claimType", imageType);
const response = await axios.post(
DEFAULT_IMAGE_API_SERVER + "/image",
formData,
{ headers },
);
this.imageBlob = undefined;
this.imageFileName = undefined;
this.uploading = false;
result = response.data.url as string;
} catch (error) {
console.error("Error uploading the image", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was an error saving the picture. Please try again.",
},
5000,
);
this.uploading = false;
}
return result;
}
}
</script>

55
src/views/TestView.vue

@ -153,13 +153,66 @@
Notif OFF Notif OFF
</button> </button>
</div> </div>
<div>
<h2 class="text-xl font-bold mb-4">Share Image</h2>
Populates the "shared-photo" view as if they used "share_target".
<input type="file" @change="uploadFile" />
<router-link
v-if="showFileNextStep()"
:to="{
name: 'shared-photo',
query: { fileName },
}"
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2"
>
Go to Shared Page
</router-link>
</div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { db } from "@/db/index";
const inputFileNameRef = ref<Blob>();
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class Help extends Vue {} export default class Help extends Vue {
fileName?: string;
async uploadFile(event: Event) {
inputFileNameRef.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 = inputFileNameRef.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.fileName = file.name as string;
const temp = await db.temp.get("shared-photo");
if (temp) {
await db.temp.update("shared-photo", { blob });
} else {
await db.temp.add({ id: "shared-photo", blob });
}
}
};
reader.readAsArrayBuffer(file as Blob);
}
}
showFileNextStep() {
return !!inputFileNameRef.value;
}
}
</script> </script>

21
sw_scripts/additional-scripts.js

@ -81,7 +81,7 @@ self.addEventListener("push", function (event) {
} else { } else {
title = payload.title || "Update"; title = payload.title || "Update";
} }
// getNotificationCount is injected from safari-notifications.js at build time by the vue.config.js configureWebpack apply plugin // getNotificationCount is injected from safari-notifications.js at build time by the sw_combine.js script
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
message = await getNotificationCount(); message = await getNotificationCount();
} }
@ -131,8 +131,27 @@ self.addEventListener("notificationclick", (event) => {
); );
}); });
// This is invoked when the user chooses this as a share_target, mapped to share-target in the manifest.
self.addEventListener("fetch", (event) => { self.addEventListener("fetch", (event) => {
logConsoleAndDb("Service worker got fetch event.", event); logConsoleAndDb("Service worker got fetch event.", event);
// Regular requests not related to Web Share Target.
if (event.request.method !== "POST") {
event.respondWith(fetch(event.request));
return;
}
// Requests related to Web Share share-target Target.
event.respondWith(
(async () => {
const formData = await event.request.formData();
const photo = formData.get("photo");
// savePhoto is injected from safari-notifications.js at build time by the sw_combine.js script
// eslint-disable-next-line no-undef
await savePhoto(photo);
return Response.redirect("/shared-photo", 303);
})(),
);
}); });
self.addEventListener("error", (event) => { self.addEventListener("error", (event) => {

14
sw_scripts/safari-notifications.js

@ -566,6 +566,20 @@ async function getNotificationCount() {
return result; return result;
} }
// Store the image blob and go immediate to a page to upload it.
// @param photo - image Blob to store for later retrieval after redirect
async function savePhoto(photo) {
try {
const db = await openIndexedDB("TimeSafari");
const transaction = db.transaction("temp", "readwrite");
const store = transaction.objectStore("temp");
await updateRecord(store, { id: "shared-photo", blob: photo });
transaction.oncomplete = () => db.close();
} catch (error) {
console.error("safari-notifications logMessage IndexedDB error", error);
}
}
self.appendDailyLog = appendDailyLog; self.appendDailyLog = appendDailyLog;
self.getNotificationCount = getNotificationCount; self.getNotificationCount = getNotificationCount;
self.decodeBase64 = decodeBase64; self.decodeBase64 = decodeBase64;

21
vite.config.mjs

@ -17,6 +17,27 @@ export default defineConfig({
filename: 'sw_scripts-combined.js', filename: 'sw_scripts-combined.js',
manifest: { manifest: {
name: process.env.TIME_SAFARI_APP_TITLE || require('./package.json').name, name: process.env.TIME_SAFARI_APP_TITLE || require('./package.json').name,
short_name: process.env.TIME_SAFARI_APP_TITLE || require('./package.json').name,
// 192x192 and 512x512 are important for Chrome to show that it's installable
"icons":[
{"src":"./img/icons/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},
{"src":"./img/icons/android-chrome-512x512.png","sizes":"512x512","type":"image/png"},
{"src":"./img/icons/android-chrome-maskable-192x192.png","sizes":"192x192","type":"image/png","purpose":"maskable"},
{"src":"./img/icons/android-chrome-maskable-512x512.png","sizes":"512x512","type":"image/png","purpose":"maskable"}
],
share_target: {
action: '/share-target',
method: 'POST',
enctype: 'multipart/form-data',
params: {
files: [
{
name: 'photo',
accept: ['image/jpg', 'image/jpeg', 'image/png'],
},
],
},
},
}, },
}), }),
], ],

Loading…
Cancel
Save