From 94bd64900373db23318295ec0126d43432b7222b Mon Sep 17 00:00:00 2001
From: Matthew Raymer <matthew.raymer@anomalistdesign.com>
Date: Mon, 7 Apr 2025 07:17:43 +0000
Subject: [PATCH] refactor: improve camera controls and modularize data export

- Add detailed error logging for image upload failures in PhotoDialog and SharedPhotoView
- Extract DataExportSection into standalone component with proper prop handling
- Fix Backup Identifier Seed visibility by passing activeDid prop
---
 src/components/DataExportSection.vue | 114 +++++++++++++++++++++++++++
 src/components/PhotoDialog.vue       |  21 ++---
 src/views/AccountViewView.vue        |  50 +-----------
 src/views/SharedPhotoView.vue        |  21 ++---
 4 files changed, 139 insertions(+), 67 deletions(-)
 create mode 100644 src/components/DataExportSection.vue

diff --git a/src/components/DataExportSection.vue b/src/components/DataExportSection.vue
new file mode 100644
index 00000000..8a195918
--- /dev/null
+++ b/src/components/DataExportSection.vue
@@ -0,0 +1,114 @@
+<template>
+  <div
+    id="sectionDataExport"
+    class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
+  >
+    <div class="mb-2 font-bold">Data Export</div>
+    <router-link
+      v-if="activeDid"
+      :to="{ name: 'seed-backup' }"
+      class="block w-full 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-2 mt-2"
+    >
+      Backup Identifier Seed
+    </router-link>
+
+    <button
+      :class="computedStartDownloadLinkClassNames()"
+      class="block w-full 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"
+      @click="exportDatabase()"
+    >
+      Download Settings & Contacts
+      <br />
+      (excluding Identifier Data)
+    </button>
+    <a
+      ref="downloadLink"
+      :class="computedDownloadLinkClassNames()"
+      class="block w-full text-center text-md bg-gradient-to-b from-green-500 to-green-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
+    >
+      If no download happened yet, click again here to download now.
+    </a>
+    <div class="mt-4">
+      <p>
+        After the download, you can save the file in your preferred storage
+        location.
+      </p>
+      <ul>
+        <li class="list-disc list-outside ml-4">
+          On iOS: Choose "More..." and select a place in iCloud, or go "Back"
+          and save to another location.
+        </li>
+        <li class="list-disc list-outside ml-4">
+          On Android: Choose "Open" and then share
+          <font-awesome icon="share-nodes" class="fa-fw" />
+          to your prefered place.
+        </li>
+      </ul>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from "vue-facing-decorator";
+import { NotificationIface } from "../constants/app";
+import { db } from "../db/index";
+import { logger } from "../utils/logger";
+
+@Component
+export default class DataExportSection extends Vue {
+  $notify!: (notification: NotificationIface, timeout?: number) => void;
+
+  @Prop({ required: true }) readonly activeDid!: string;
+  downloadUrl = "";
+
+  beforeUnmount() {
+    if (this.downloadUrl) {
+      URL.revokeObjectURL(this.downloadUrl);
+    }
+  }
+
+  public async exportDatabase() {
+    try {
+      const blob = await db.export({ prettyJson: true });
+      this.downloadUrl = URL.createObjectURL(blob);
+      const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
+      downloadAnchor.href = this.downloadUrl;
+      downloadAnchor.download = `${db.name}-backup.json`;
+      downloadAnchor.click();
+      this.$notify(
+        {
+          group: "alert",
+          type: "success",
+          title: "Download Started",
+          text: "See your downloads directory for the backup. It is in the Dexie format.",
+        },
+        -1,
+      );
+      setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
+    } catch (error) {
+      logger.error("Export Error:", error);
+      this.$notify(
+        {
+          group: "alert",
+          type: "danger",
+          title: "Export Error",
+          text: "There was an error exporting the data.",
+        },
+        3000,
+      );
+    }
+  }
+
+  public computedStartDownloadLinkClassNames() {
+    return {
+      hidden: this.downloadUrl,
+    };
+  }
+
+  public computedDownloadLinkClassNames() {
+    return {
+      hidden: !this.downloadUrl,
+    };
+  }
+}
+</script>
diff --git a/src/components/PhotoDialog.vue b/src/components/PhotoDialog.vue
index f0f5b631..a5962f71 100644
--- a/src/components/PhotoDialog.vue
+++ b/src/components/PhotoDialog.vue
@@ -273,14 +273,14 @@ export default class PhotoDialog extends Vue {
     } catch (error) {
       // Log the raw error first
       logger.error("Raw error object:", JSON.stringify(error, null, 2));
-      
+
       let errorMessage = "There was an error saving the picture.";
-      
+
       if (axios.isAxiosError(error)) {
         const status = error.response?.status;
         const statusText = error.response?.statusText;
         const data = error.response?.data;
-        
+
         // Log detailed error information
         logger.error("Upload error details:", {
           status,
@@ -290,16 +290,17 @@ export default class PhotoDialog extends Vue {
           config: {
             url: error.config?.url,
             method: error.config?.method,
-            headers: error.config?.headers
-          }
+            headers: error.config?.headers,
+          },
         });
-        
+
         if (status === 401) {
           errorMessage = "Authentication failed. Please try logging in again.";
         } else if (status === 413) {
           errorMessage = "Image file is too large. Please try a smaller image.";
         } else if (status === 415) {
-          errorMessage = "Unsupported image format. Please try a different image.";
+          errorMessage =
+            "Unsupported image format. Please try a different image.";
         } else if (status && status >= 500) {
           errorMessage = "Server error. Please try again later.";
         } else if (data?.message) {
@@ -311,16 +312,16 @@ export default class PhotoDialog extends Vue {
           name: error.name,
           message: error.message,
           stack: error.stack,
-          error: JSON.stringify(error, Object.getOwnPropertyNames(error), 2)
+          error: JSON.stringify(error, Object.getOwnPropertyNames(error), 2),
         });
       } else {
         // Log any other type of error
         logger.error("Unknown error type:", {
           error: JSON.stringify(error, null, 2),
-          type: typeof error
+          type: typeof error,
         });
       }
-      
+
       this.$notify(
         {
           group: "alert",
diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue
index f9d76cd5..24712a6e 100644
--- a/src/views/AccountViewView.vue
+++ b/src/views/AccountViewView.vue
@@ -420,53 +420,7 @@
       </button>
     </div>
 
-    <div
-      id="sectionDataExport"
-      class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
-    >
-      <div class="mb-2 font-bold">Data Export</div>
-      <router-link
-        v-if="activeDid"
-        :to="{ name: 'seed-backup' }"
-        class="block w-full 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-2 mt-2"
-      >
-        Backup Identifier Seed
-      </router-link>
-
-      <button
-        :class="computedStartDownloadLinkClassNames()"
-        class="block w-full 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"
-        @click="exportDatabase()"
-      >
-        Download Settings & Contacts
-        <br />
-        (excluding Identifier Data)
-      </button>
-      <a
-        ref="downloadLink"
-        :class="computedDownloadLinkClassNames()"
-        class="block w-full text-center text-md bg-gradient-to-b from-green-500 to-green-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
-      >
-        If no download happened yet, click again here to download now.
-      </a>
-      <div class="mt-4">
-        <p>
-          After the download, you can save the file in your preferred storage
-          location.
-        </p>
-        <ul>
-          <li class="list-disc list-outside ml-4">
-            On iOS: Choose "More..." and select a place in iCloud, or go "Back"
-            and save to another location.
-          </li>
-          <li class="list-disc list-outside ml-4">
-            On Android: Choose "Open" and then share
-            <font-awesome icon="share-nodes" class="fa-fw" />
-            to your prefered place.
-          </li>
-        </ul>
-      </div>
-    </div>
+    <DataExportSection :active-did="activeDid" />
 
     <!-- id used by puppeteer test script -->
     <h3
@@ -946,6 +900,7 @@ import PushNotificationPermission from "../components/PushNotificationPermission
 import QuickNav from "../components/QuickNav.vue";
 import TopMessage from "../components/TopMessage.vue";
 import UserNameDialog from "../components/UserNameDialog.vue";
+import DataExportSection from "../components/DataExportSection.vue";
 import {
   AppString,
   DEFAULT_IMAGE_API_SERVER,
@@ -999,6 +954,7 @@ const inputImportFileNameRef = ref<Blob>();
     QuickNav,
     TopMessage,
     UserNameDialog,
+    DataExportSection,
   },
 })
 export default class AccountViewView extends Vue {
diff --git a/src/views/SharedPhotoView.vue b/src/views/SharedPhotoView.vue
index 815e1b17..26f05672 100644
--- a/src/views/SharedPhotoView.vue
+++ b/src/views/SharedPhotoView.vue
@@ -223,14 +223,14 @@ export default class SharedPhotoView extends Vue {
     } catch (error) {
       // Log the raw error first
       logger.error("Raw error object:", JSON.stringify(error, null, 2));
-      
+
       let errorMessage = "There was an error saving the picture.";
-      
+
       if (axios.isAxiosError(error)) {
         const status = error.response?.status;
         const statusText = error.response?.statusText;
         const data = error.response?.data;
-        
+
         // Log detailed error information
         logger.error("Upload error details:", {
           status,
@@ -240,16 +240,17 @@ export default class SharedPhotoView extends Vue {
           config: {
             url: error.config?.url,
             method: error.config?.method,
-            headers: error.config?.headers
-          }
+            headers: error.config?.headers,
+          },
         });
-        
+
         if (status === 401) {
           errorMessage = "Authentication failed. Please try logging in again.";
         } else if (status === 413) {
           errorMessage = "Image file is too large. Please try a smaller image.";
         } else if (status === 415) {
-          errorMessage = "Unsupported image format. Please try a different image.";
+          errorMessage =
+            "Unsupported image format. Please try a different image.";
         } else if (status && status >= 500) {
           errorMessage = "Server error. Please try again later.";
         } else if (data?.message) {
@@ -261,16 +262,16 @@ export default class SharedPhotoView extends Vue {
           name: error.name,
           message: error.message,
           stack: error.stack,
-          error: JSON.stringify(error, Object.getOwnPropertyNames(error), 2)
+          error: JSON.stringify(error, Object.getOwnPropertyNames(error), 2),
         });
       } else {
         // Log any other type of error
         logger.error("Unknown error type:", {
           error: JSON.stringify(error, null, 2),
-          type: typeof error
+          type: typeof error,
         });
       }
-      
+
       this.$notify(
         {
           group: "alert",