Browse Source

feat: implement user-accessible file storage with improved share dialog handling

- Add timeout mechanism for share dialogs to prevent hanging (15s timeout)
- Implement centralized share dialog handling with robust error handling
- Update file storage to use app's external storage (accessible via file managers)
- Add directory picker functionality for custom save locations
- Add listUserAccessibleFiles method to show saved files
- Add testListUserFiles functionality for debugging
- Improve logging and error handling throughout file operations
- Ensure compatibility with Android 11+ storage restrictions

Files are now saved to:
- Android: /storage/emulated/0/Android/data/app.timesafari.app/files/TimeSafari/
- iOS: /Documents/ (accessible via Files app)

Users can access files through file managers, app's file listing,
or use share dialog to save to Downloads folder.
capacitor-local-save
Matthew Raymer 3 days ago
parent
commit
2d516b90b0
  1. 254
      package-lock.json
  2. 8
      src/components/DataExportSection.vue
  3. 19
      src/libs/capacitor/app.ts
  4. 33
      src/services/PlatformService.ts
  5. 615
      src/services/platforms/CapacitorPlatformService.ts
  6. 72
      src/services/platforms/ElectronPlatformService.ts
  7. 77
      src/services/platforms/WebPlatformService.ts
  8. 143
      src/views/TestView.vue

254
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "timesafari", "name": "timesafari",
"version": "0.4.8", "version": "0.5.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "timesafari", "name": "timesafari",
"version": "0.4.8", "version": "0.5.1",
"dependencies": { "dependencies": {
"@capacitor-community/sqlite": "6.0.2", "@capacitor-community/sqlite": "6.0.2",
"@capacitor-mlkit/barcode-scanning": "^6.0.0", "@capacitor-mlkit/barcode-scanning": "^6.0.0",
@ -3835,9 +3835,9 @@
} }
}, },
"node_modules/@electron/asar/node_modules/brace-expansion": { "node_modules/@electron/asar/node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -4524,9 +4524,9 @@
} }
}, },
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": { "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -6686,9 +6686,9 @@
} }
}, },
"node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -8217,9 +8217,9 @@
} }
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.42.0", "version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.42.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.43.0.tgz",
"integrity": "sha512-gldmAyS9hpj+H6LpRNlcjQWbuKUtb94lodB9uCz71Jm+7BxK1VIOo7y62tZZwxhA7j1ylv/yQz080L5WkS+LoQ==", "integrity": "sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -8231,9 +8231,9 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.42.0", "version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.42.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.43.0.tgz",
"integrity": "sha512-bpRipfTgmGFdCZDFLRvIkSNO1/3RGS74aWkJJTFJBH7h3MRV4UijkaEUeOMbi9wxtxYmtAbVcnMtHTPBhLEkaw==", "integrity": "sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -8271,9 +8271,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-arm64": { "node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.42.0", "version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.42.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.43.0.tgz",
"integrity": "sha512-fJcN4uSGPWdpVmvLuMtALUFwCHgb2XiQjuECkHT3lWLZhSQ3MBQ9pq+WoWeJq2PrNxr9rPM1Qx+IjyGj8/c6zQ==", "integrity": "sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -8285,9 +8285,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-x64": { "node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.42.0", "version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.42.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.43.0.tgz",
"integrity": "sha512-CziHfyzpp8hJpCVE/ZdTizw58gr+m7Y2Xq5VOuCSrZR++th2xWAz4Nqk52MoIIrV3JHtVBhbBsJcAxs6NammOQ==", "integrity": "sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -8299,9 +8299,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.42.0", "version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.42.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.43.0.tgz",
"integrity": "sha512-UsQD5fyLWm2Fe5CDM7VPYAo+UC7+2Px4Y+N3AcPh/LdZu23YcuGPegQly++XEVaC8XUTFVPscl5y5Cl1twEI4A==", "integrity": "sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -8313,9 +8313,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.42.0", "version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.42.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.43.0.tgz",
"integrity": "sha512-/i8NIrlgc/+4n1lnoWl1zgH7Uo0XK5xK3EDqVTf38KvyYgCU/Rm04+o1VvvzJZnVS5/cWSd07owkzcVasgfIkQ==", "integrity": "sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -8353,9 +8353,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-loongarch64-gnu": { "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.42.0", "version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.42.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.43.0.tgz",
"integrity": "sha512-O8AplvIeavK5ABmZlKBq9/STdZlnQo7Sle0LLhVA7QT+CiGpNVe197/t8Aph9bhJqbDVGCHpY2i7QyfEDDStDg==", "integrity": "sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@ -8367,9 +8367,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": { "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.42.0", "version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.42.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.43.0.tgz",
"integrity": "sha512-6Qb66tbKVN7VyQrekhEzbHRxXXFFD8QKiFAwX5v9Xt6FiJ3BnCVBuyBxa2fkFGqxOCSGGYNejxd8ht+q5SnmtA==", "integrity": "sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -8381,9 +8381,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.42.0", "version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.42.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.43.0.tgz",
"integrity": "sha512-KQETDSEBamQFvg/d8jajtRwLNBlGc3aKpaGiP/LvEbnmVUKlFta1vqJqTrvPtsYsfbE/DLg5CC9zyXRX3fnBiA==", "integrity": "sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -8395,9 +8395,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-musl": { "node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.42.0", "version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.42.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.43.0.tgz",
"integrity": "sha512-qMvnyjcU37sCo/tuC+JqeDKSuukGAd+pVlRl/oyDbkvPJ3awk6G6ua7tyum02O3lI+fio+eM5wsVd66X0jQtxw==", "integrity": "sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -8409,9 +8409,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.42.0", "version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.42.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.43.0.tgz",
"integrity": "sha512-I2Y1ZUgTgU2RLddUHXTIgyrdOwljjkmcZ/VilvaEumtS3Fkuhbw4p4hgHc39Ypwvo2o7sBFNl2MquNvGCa55Iw==", "integrity": "sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -8462,9 +8462,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.42.0", "version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.42.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.43.0.tgz",
"integrity": "sha512-F+5J9pelstXKwRSDq92J0TEBXn2nfUrQGg+HK1+Tk7VOL09e0gBqUHugZv7SW4MGrYj41oNCUe3IKCDGVlis2g==", "integrity": "sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -12199,9 +12199,9 @@
} }
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "2.0.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
@ -12648,9 +12648,9 @@
} }
}, },
"node_modules/cacache/node_modules/rimraf/node_modules/brace-expansion": { "node_modules/cacache/node_modules/rimraf/node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -12868,9 +12868,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001721", "version": "1.0.30001722",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001722.tgz",
"integrity": "sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==", "integrity": "sha512-DCQHBBZtiK6JVkAGw7drvAMK0Q0POD/xZvEmDp6baiMMP6QXXk9HpD6mNYBZWhOPG6LvIDb82ITqtWjhDckHCA==",
"devOptional": true, "devOptional": true,
"funding": [ "funding": [
{ {
@ -14931,9 +14931,9 @@
} }
}, },
"node_modules/dir-compare/node_modules/brace-expansion": { "node_modules/dir-compare/node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -15865,9 +15865,9 @@
} }
}, },
"node_modules/eslint/node_modules/brace-expansion": { "node_modules/eslint/node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -17617,9 +17617,9 @@
} }
}, },
"node_modules/glob/node_modules/brace-expansion": { "node_modules/glob/node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -19127,9 +19127,9 @@
} }
}, },
"node_modules/jake/node_modules/brace-expansion": { "node_modules/jake/node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -25514,9 +25514,9 @@
} }
}, },
"node_modules/replace/node_modules/brace-expansion": { "node_modules/replace/node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -26094,9 +26094,9 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.42.0", "version": "4.43.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.42.0.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.43.0.tgz",
"integrity": "sha512-LW+Vse3BJPyGJGAJt1j8pWDKPd73QM8cRXYK1IxOBgL2AGLu7Xd2YOW0M2sLUBCkF5MshXXtMApyEAEzMVMsnw==", "integrity": "sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -26110,33 +26110,33 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.42.0", "@rollup/rollup-android-arm-eabi": "4.43.0",
"@rollup/rollup-android-arm64": "4.42.0", "@rollup/rollup-android-arm64": "4.43.0",
"@rollup/rollup-darwin-arm64": "4.42.0", "@rollup/rollup-darwin-arm64": "4.43.0",
"@rollup/rollup-darwin-x64": "4.42.0", "@rollup/rollup-darwin-x64": "4.43.0",
"@rollup/rollup-freebsd-arm64": "4.42.0", "@rollup/rollup-freebsd-arm64": "4.43.0",
"@rollup/rollup-freebsd-x64": "4.42.0", "@rollup/rollup-freebsd-x64": "4.43.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.42.0", "@rollup/rollup-linux-arm-gnueabihf": "4.43.0",
"@rollup/rollup-linux-arm-musleabihf": "4.42.0", "@rollup/rollup-linux-arm-musleabihf": "4.43.0",
"@rollup/rollup-linux-arm64-gnu": "4.42.0", "@rollup/rollup-linux-arm64-gnu": "4.43.0",
"@rollup/rollup-linux-arm64-musl": "4.42.0", "@rollup/rollup-linux-arm64-musl": "4.43.0",
"@rollup/rollup-linux-loongarch64-gnu": "4.42.0", "@rollup/rollup-linux-loongarch64-gnu": "4.43.0",
"@rollup/rollup-linux-powerpc64le-gnu": "4.42.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.43.0",
"@rollup/rollup-linux-riscv64-gnu": "4.42.0", "@rollup/rollup-linux-riscv64-gnu": "4.43.0",
"@rollup/rollup-linux-riscv64-musl": "4.42.0", "@rollup/rollup-linux-riscv64-musl": "4.43.0",
"@rollup/rollup-linux-s390x-gnu": "4.42.0", "@rollup/rollup-linux-s390x-gnu": "4.43.0",
"@rollup/rollup-linux-x64-gnu": "4.42.0", "@rollup/rollup-linux-x64-gnu": "4.43.0",
"@rollup/rollup-linux-x64-musl": "4.42.0", "@rollup/rollup-linux-x64-musl": "4.43.0",
"@rollup/rollup-win32-arm64-msvc": "4.42.0", "@rollup/rollup-win32-arm64-msvc": "4.43.0",
"@rollup/rollup-win32-ia32-msvc": "4.42.0", "@rollup/rollup-win32-ia32-msvc": "4.43.0",
"@rollup/rollup-win32-x64-msvc": "4.42.0", "@rollup/rollup-win32-x64-msvc": "4.43.0",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/rollup/node_modules/@rollup/rollup-darwin-arm64": { "node_modules/rollup/node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.42.0", "version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.42.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.43.0.tgz",
"integrity": "sha512-JxHtA081izPBVCHLKnl6GEA0w3920mlJPLh89NojpU2GsBSB6ypu4erFg/Wx1qbpUbepn0jY4dVWMGZM8gplgA==", "integrity": "sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -26148,9 +26148,9 @@
] ]
}, },
"node_modules/rollup/node_modules/@rollup/rollup-darwin-x64": { "node_modules/rollup/node_modules/@rollup/rollup-darwin-x64": {
"version": "4.42.0", "version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.42.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.43.0.tgz",
"integrity": "sha512-rv5UZaWVIJTDMyQ3dCEK+m0SAn6G7H3PRc2AZmExvbDvtaDc+qXkei0knQWcI3+c9tEs7iL/4I4pTQoPbNL2SA==", "integrity": "sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -26162,9 +26162,9 @@
] ]
}, },
"node_modules/rollup/node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/rollup/node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.42.0", "version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.42.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.43.0.tgz",
"integrity": "sha512-eoujJFOvoIBjZEi9hJnXAbWg+Vo1Ov8n/0IKZZcPZ7JhBzxh2A+2NFyeMZIRkY9iwBvSjloKgcvnjTbGKHE44Q==", "integrity": "sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -26176,9 +26176,9 @@
] ]
}, },
"node_modules/rollup/node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/rollup/node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.42.0", "version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.42.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.43.0.tgz",
"integrity": "sha512-/3NrcOWFSR7RQUQIuZQChLND36aTU9IYE4j+TB40VU78S+RA0IiqHR30oSh6P1S9f9/wVOenHQnacs/Byb824g==", "integrity": "sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -26190,9 +26190,9 @@
] ]
}, },
"node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.42.0", "version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.42.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.43.0.tgz",
"integrity": "sha512-Gfm6cV6mj3hCUY8TqWa63DB8Mx3NADoFwiJrMpoZ1uESbK8FQV3LXkhfry+8bOniq9pqY1OdsjFWNsSbfjPugw==", "integrity": "sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -26204,9 +26204,9 @@
] ]
}, },
"node_modules/rollup/node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.42.0", "version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.42.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.43.0.tgz",
"integrity": "sha512-g86PF8YZ9GRqkdi0VoGlcDUb4rYtQKyTD1IVtxxN4Hpe7YqLBShA7oHMKU6oKTCi3uxwW4VkIGnOaH/El8de3w==", "integrity": "sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -26218,9 +26218,9 @@
] ]
}, },
"node_modules/rollup/node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/rollup/node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.42.0", "version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.42.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.43.0.tgz",
"integrity": "sha512-+axkdyDGSp6hjyzQ5m1pgcvQScfHnMCcsXkx8pTgy/6qBmWVhtRVlgxjWwDp67wEXXUr0x+vD6tp5W4x6V7u1A==", "integrity": "sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -26232,9 +26232,9 @@
] ]
}, },
"node_modules/rollup/node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/rollup/node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.42.0", "version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.42.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.43.0.tgz",
"integrity": "sha512-LpHiJRwkaVz/LqjHjK8LCi8osq7elmpwujwbXKNW88bM8eeGxavJIKKjkjpMHAh/2xfnrt1ZSnhTv41WYUHYmA==", "integrity": "sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -26424,9 +26424,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/sdp": { "node_modules/sdp": {
"version": "3.2.0", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz", "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.1.tgz",
"integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==", "integrity": "sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/secp256k1": { "node_modules/secp256k1": {
@ -28495,9 +28495,9 @@
} }
}, },
"node_modules/test-exclude/node_modules/brace-expansion": { "node_modules/test-exclude/node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true, "peer": true,
@ -31045,9 +31045,9 @@
} }
}, },
"node_modules/zod": { "node_modules/zod": {
"version": "3.25.58", "version": "3.25.61",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.58.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.61.tgz",
"integrity": "sha512-DVLmMQzSZwNYzQoMaM3MQWnxr2eq+AtM9Hx3w1/Yl0pH8sLTSjN4jGP7w6f7uand6Hw44tsnSu1hz1AOA6qI2Q==", "integrity": "sha512-fzfJgUw78LTNnHujj9re1Ov/JJQkRZZGDMcYqSx7Hp4rPOkKywaFHq0S6GoHeXs0wGNE/sIOutkXgnwzrVOGCQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"

8
src/components/DataExportSection.vue

@ -162,13 +162,13 @@ export default class DataExportSection extends Vue {
downloadAnchor.click(); downloadAnchor.click();
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000); setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
} else if (this.platformCapabilities.hasFileSystem) { } else if (this.platformCapabilities.hasFileSystem) {
// Native platform: Write to app directory with enhanced options // Native platform: Write to user-accessible location and share
const result = await this.platformService.writeAndShareFile( const result = await this.platformService.writeAndShareFile(
fileName, fileName,
jsonStr, jsonStr,
{ {
allowLocationSelection: true, allowLocationSelection: true,
saveToDownloads: false, showLocationSelectionDialog: true,
mimeType: "application/json" mimeType: "application/json"
} }
); );
@ -188,9 +188,9 @@ export default class DataExportSection extends Vue {
title: "Export Successful", title: "Export Successful",
text: this.platformCapabilities.hasFileDownload text: this.platformCapabilities.hasFileDownload
? "See your downloads directory for the backup." ? "See your downloads directory for the backup."
: "The backup file has been saved.", : "Backup saved to multiple locations. Use the share dialog to access your file and choose where to save it permanently.",
}, },
3000, 5000,
); );
} catch (error) { } catch (error) {
logger.error("Export Error:", error); logger.error("Export Error:", error);

19
src/libs/capacitor/app.ts

@ -12,6 +12,14 @@ import type { PluginListenerHandle } from "@capacitor/core";
* Supports 'backButton' and 'appUrlOpen' events from Capacitor * Supports 'backButton' and 'appUrlOpen' events from Capacitor
*/ */
interface AppInterface { interface AppInterface {
/**
* Force exit the app. This should only be used in conjunction with the `backButton` handler for Android to
* exit the app when navigation is complete.
*
* @returns Promise that resolves when the app has been exited
*/
exitApp(): Promise<void>;
/** /**
* Add listener for back button events * Add listener for back button events
* @param eventName - Must be 'backButton' * @param eventName - Must be 'backButton'
@ -38,8 +46,19 @@ interface AppInterface {
/** /**
* App wrapper for Capacitor functionality * App wrapper for Capacitor functionality
* Provides type-safe event listeners for back button and URL open events * Provides type-safe event listeners for back button and URL open events
* and app exit functionality
*/ */
export const App: AppInterface = { export const App: AppInterface = {
/**
* Force exit the app. This should only be used in conjunction with the `backButton` handler for Android to
* exit the app when navigation is complete.
*
* @returns Promise that resolves when the app has been exited
*/
exitApp(): Promise<void> {
return CapacitorApp.exitApp();
},
addListener( addListener(
eventName: "backButton" | "appUrlOpen", eventName: "backButton" | "appUrlOpen",
listenerFunc: BackButtonListener | ((data: AppLaunchUrl) => void), listenerFunc: BackButtonListener | ((data: AppLaunchUrl) => void),

33
src/services/PlatformService.ts

@ -74,7 +74,10 @@ export interface PlatformService {
options?: { options?: {
allowLocationSelection?: boolean; allowLocationSelection?: boolean;
saveToDownloads?: boolean; saveToDownloads?: boolean;
saveToPrivateStorage?: boolean;
mimeType?: string; mimeType?: string;
showShareDialog?: boolean;
showLocationSelectionDialog?: boolean;
} }
): Promise<{ saved: boolean; uri?: string; shared: boolean; error?: string }>; ): Promise<{ saved: boolean; uri?: string; shared: boolean; error?: string }>;
@ -92,6 +95,36 @@ export interface PlatformService {
*/ */
listFiles(directory: string): Promise<string[]>; listFiles(directory: string): Promise<string[]>;
/**
* Tests the file sharing functionality by creating and sharing a test file.
* @returns Promise resolving to a test result message
*/
testFileSharing(): Promise<string>;
/**
* Tests saving a file without showing the share dialog.
* @returns Promise resolving to a test result message
*/
testFileSaveOnly(): Promise<string>;
/**
* Tests the location selection functionality using the file picker.
* @returns Promise resolving to a test result message
*/
testLocationSelection(): Promise<string>;
/**
* Tests location selection without showing the dialog (restores original behavior).
* @returns Promise resolving to a test result message
*/
testLocationSelectionSilent(): Promise<string>;
/**
* Tests listing user-accessible files saved by the app.
* @returns Promise resolving to a test result message
*/
testListUserFiles(): Promise<string>;
// Camera operations // Camera operations
/** /**
* Activates the device camera to take a picture. * Activates the device camera to take a picture.

615
src/services/platforms/CapacitorPlatformService.ts

@ -6,6 +6,7 @@ import {
CameraDirection, CameraDirection,
} from "@capacitor/camera"; } from "@capacitor/camera";
import { Share } from "@capacitor/share"; import { Share } from "@capacitor/share";
import { FilePicker } from "@capawesome/capacitor-file-picker";
import { import {
SQLiteConnection, SQLiteConnection,
SQLiteDBConnection, SQLiteDBConnection,
@ -271,22 +272,24 @@ export class CapacitorPlatformService implements PlatformService {
); );
if (this.getCapabilities().isIOS) { if (this.getCapabilities().isIOS) {
// iOS uses different permission model // iOS uses different permission model - Documents directory is accessible
logger.log("iOS platform - Documents directory is accessible by default");
return; return;
} }
// Try to access a test directory to check permissions // For Android, try to access external storage to check permissions
try { try {
await Filesystem.stat({ // Try to list files in external storage to check permissions
path: "/storage/emulated/0/Download", await Filesystem.readdir({
directory: Directory.Documents, path: ".",
directory: Directory.External,
}); });
logger.log( logger.log(
"Storage permissions already granted", "External storage permissions already granted",
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2), JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
); );
return; return;
} catch (error: unknown) { } catch (error) {
const err = error as Error; const err = error as Error;
const errorLogData = { const errorLogData = {
error: { error: {
@ -297,19 +300,11 @@ export class CapacitorPlatformService implements PlatformService {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}; };
// "File does not exist" is expected and not a permission error
if (err.message === "File does not exist") {
logger.log(
"Directory does not exist (expected), proceeding with write",
JSON.stringify(errorLogData, null, 2),
);
return;
}
// Check for actual permission errors // Check for actual permission errors
if ( if (
err.message.includes("permission") || err.message.includes("permission") ||
err.message.includes("access") err.message.includes("access") ||
err.message.includes("denied")
) { ) {
logger.log( logger.log(
"Permission check failed, requesting permissions", "Permission check failed, requesting permissions",
@ -319,45 +314,33 @@ export class CapacitorPlatformService implements PlatformService {
// The Filesystem plugin will automatically request permissions when needed // The Filesystem plugin will automatically request permissions when needed
// We just need to try the operation again // We just need to try the operation again
try { try {
await Filesystem.stat({ await Filesystem.readdir({
path: "/storage/emulated/0/Download", path: ".",
directory: Directory.Documents, directory: Directory.External,
}); });
logger.log( logger.log(
"Storage permissions granted after request", "External storage permissions granted after request",
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2), JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
); );
return; return;
} catch (retryError: unknown) { } catch (retryError: unknown) {
const retryErr = retryError as Error; const retryErr = retryError as Error;
throw new Error( throw new Error(
`Failed to obtain storage permissions: ${retryErr.message}`, `Failed to obtain external storage permissions: ${retryErr.message}`,
); );
} }
} }
// For any other error, log it but don't treat as permission error // For any other error, log it but don't treat as permission error
logger.log( logger.log(
"Unexpected error during permission check", "Unexpected error during permission check, proceeding anyway",
JSON.stringify(errorLogData, null, 2), JSON.stringify(errorLogData, null, 2),
); );
return; return;
} }
} catch (error: unknown) { } catch (error) {
const err = error as Error; logger.error("Error in checkStoragePermissions:", error);
const errorLogData = { throw error;
error: {
message: err.message,
name: err.name,
stack: err.stack,
},
timestamp: new Date().toISOString(),
};
logger.error(
"Error checking/requesting permissions",
JSON.stringify(errorLogData, null, 2),
);
throw new Error(`Failed to obtain storage permissions: ${err.message}`);
} }
} }
@ -382,8 +365,8 @@ export class CapacitorPlatformService implements PlatformService {
* Enhanced file save and share functionality with location selection. * Enhanced file save and share functionality with location selection.
* *
* Provides multiple options for saving files: * Provides multiple options for saving files:
* 1. Save to app-private storage and share (current behavior) * 1. Save to user-accessible storage (Downloads/Documents) and share (default behavior)
* 2. Save to device Downloads folder (Android) or Documents (iOS) * 2. Save to app-private storage and share (for sensitive data)
* 3. Allow user to choose save location via file picker * 3. Allow user to choose save location via file picker
* 4. Direct share without saving locally * 4. Direct share without saving locally
* *
@ -398,7 +381,10 @@ export class CapacitorPlatformService implements PlatformService {
options: { options: {
allowLocationSelection?: boolean; allowLocationSelection?: boolean;
saveToDownloads?: boolean; saveToDownloads?: boolean;
saveToPrivateStorage?: boolean;
mimeType?: string; mimeType?: string;
showShareDialog?: boolean;
showLocationSelectionDialog?: boolean;
} = {} } = {}
): Promise<{ saved: boolean; uri?: string; shared: boolean; error?: string }> { ): Promise<{ saved: boolean; uri?: string; shared: boolean; error?: string }> {
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
@ -420,15 +406,11 @@ export class CapacitorPlatformService implements PlatformService {
// Determine save strategy based on options and platform // Determine save strategy based on options and platform
if (options.allowLocationSelection) { if (options.allowLocationSelection) {
// Use file picker to let user choose location // Use enhanced location selection with multiple save options
fileUri = await this.saveFileWithPicker(fileName, content, options.mimeType); fileUri = await this.saveWithLocationOptions(fileName, content, options.mimeType, options.showLocationSelectionDialog);
saved = true; saved = true;
} else if (options.saveToDownloads) { } else if (options.saveToPrivateStorage) {
// Save directly to Downloads folder // Save to app-private storage (for sensitive data)
fileUri = await this.saveToDownloads(fileName, content);
saved = true;
} else {
// Fallback to app-private storage (current behavior)
const result = await Filesystem.writeFile({ const result = await Filesystem.writeFile({
path: fileName, path: fileName,
data: content, data: content,
@ -438,6 +420,10 @@ export class CapacitorPlatformService implements PlatformService {
}); });
fileUri = result.uri; fileUri = result.uri;
saved = true; saved = true;
} else {
// Default: Save to user-accessible location (Downloads/Documents)
fileUri = await this.saveToDownloads(fileName, content);
saved = true;
} }
logger.log("[CapacitorPlatformService] File write successful:", { logger.log("[CapacitorPlatformService] File write successful:", {
@ -446,20 +432,53 @@ export class CapacitorPlatformService implements PlatformService {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
// Share the file // Share the file (unless explicitly disabled)
let shared = false; let shared = false;
try { if (options.showShareDialog !== false && !options.allowLocationSelection) {
await Share.share({ try {
title: "TimeSafari Backup", logger.log("[CapacitorPlatformService] Starting share operation:", {
text: "Here is your backup file.", uri: fileUri,
url: fileUri, timestamp: new Date().toISOString(),
dialogTitle: "Share your backup file", });
});
// Share the file with improved timeout handling
const shareResult = await this.handleShareDialog({
title: "TimeSafari Backup",
text: "Here is your backup file.",
url: fileUri,
dialogTitle: "Share your backup file",
});
shared = true;
logger.log("[CapacitorPlatformService] Share dialog completed successfully:", {
activityType: shareResult?.activityType,
timestamp: new Date().toISOString(),
});
} catch (shareError) {
const shareErr = shareError as Error;
logger.warn("[CapacitorPlatformService] Share operation completed (may have been cancelled or timed out):", {
error: shareErr.message,
timestamp: new Date().toISOString(),
});
// Check if it's a user cancellation, timeout, or other expected completion
if (shareErr.message.includes("cancel") ||
shareErr.message.includes("dismiss") ||
shareErr.message.includes("timeout") ||
shareErr.message.includes("Share dialog timeout")) {
logger.log("[CapacitorPlatformService] Share dialog completed (cancelled/timeout) - this is expected behavior");
// Don't treat cancellation/timeout as an error, file is still saved
// The dialog should close automatically on timeout
} else {
// Log other share errors but don't fail the operation
logger.error("[CapacitorPlatformService] Unexpected share error:", shareErr);
}
}
} else if (options.allowLocationSelection) {
// Location selection already handled the sharing, mark as shared
shared = true; shared = true;
logger.log("[CapacitorPlatformService] File shared successfully"); logger.log("[CapacitorPlatformService] Location selection handled sharing");
} catch (shareError) {
logger.warn("[CapacitorPlatformService] Share failed, but file was saved:", shareError);
// Don't throw error if sharing fails, file is still saved
} }
return { saved, uri: fileUri, shared }; return { saved, uri: fileUri, shared };
@ -479,7 +498,59 @@ export class CapacitorPlatformService implements PlatformService {
} }
/** /**
* Saves a file using the file picker to let user choose location. * Handles share dialog with improved timeout and dismissal handling.
* @param shareOptions - Options for the share dialog
* @param timeoutMs - Timeout in milliseconds (default: 15 seconds)
* @returns Promise that resolves when share operation completes
*/
private async handleShareDialog(shareOptions: any, timeoutMs: number = 15000): Promise<any> {
try {
logger.log("[CapacitorPlatformService] Starting share dialog with timeout:", {
timeoutMs,
timestamp: new Date().toISOString(),
});
const sharePromise = Share.share(shareOptions);
// Add timeout to prevent hanging
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
logger.warn("[CapacitorPlatformService] Share dialog timeout reached - forcing completion");
reject(new Error("Share dialog timeout - forcing completion"));
}, timeoutMs);
});
// Race between share completion and timeout
const result = await Promise.race([sharePromise, timeoutPromise]);
logger.log("[CapacitorPlatformService] Share dialog completed successfully:", {
activityType: result?.activityType,
timestamp: new Date().toISOString(),
});
return result;
} catch (error) {
const err = error as Error;
// Check if it's an expected completion (timeout, cancellation, etc.)
if (err.message.includes("timeout") ||
err.message.includes("cancel") ||
err.message.includes("dismiss")) {
logger.log("[CapacitorPlatformService] Share dialog completed (expected):", {
reason: err.message,
timestamp: new Date().toISOString(),
});
// Return a success result even for timeout/cancellation
return { activityType: "timeout_or_cancellation" };
} else {
// Re-throw unexpected errors
throw error;
}
}
}
/**
* Saves a file using the native share dialog to let user choose save location.
* @param fileName - Name of the file to save * @param fileName - Name of the file to save
* @param content - File content * @param content - File content
* @param mimeType - MIME type of the file * @param mimeType - MIME type of the file
@ -491,29 +562,135 @@ export class CapacitorPlatformService implements PlatformService {
mimeType: string = "application/json" mimeType: string = "application/json"
): Promise<string> { ): Promise<string> {
try { try {
// For now, fallback to regular save since file picker save API is complex logger.log("[CapacitorPlatformService] Using native share dialog for save location selection:", {
// Save to app-private storage and let user share to choose location fileName,
const result = await Filesystem.writeFile({ mimeType,
path: fileName, platform: this.getCapabilities().isIOS ? "iOS" : "Android",
timestamp: new Date().toISOString(),
});
// First, save the file to a temporary location
const tempFileName = `temp_${Date.now()}_${fileName}`;
const tempResult = await Filesystem.writeFile({
path: tempFileName,
data: content,
directory: Directory.Data,
encoding: Encoding.UTF8,
});
logger.log("[CapacitorPlatformService] File saved to temp location:", {
tempUri: tempResult.uri,
timestamp: new Date().toISOString(),
});
// Use the native share dialog which includes "Save to Files" options
// This allows users to choose where to save the file using the system's file manager
await this.handleShareDialog({
title: "Save TimeSafari File",
text: `Save ${fileName} to your preferred location`,
url: tempResult.uri,
dialogTitle: "Choose where to save your file",
});
logger.log("[CapacitorPlatformService] Native share dialog completed for location selection");
// Return the temp URI - the user can save it wherever they want via the share dialog
// The share dialog will provide options like "Save to Files", "Copy to Downloads", etc.
return tempResult.uri;
} catch (error) {
logger.error("[CapacitorPlatformService] Native share dialog for location selection failed:", error);
throw new Error(`Failed to open location selection dialog: ${error}`);
}
}
/**
* Provides multiple save location options for the user to choose from.
* @param fileName - Name of the file to save
* @param content - File content
* @param mimeType - MIME type of the file
* @param showDialog - Whether to show the location selection dialog
* @returns Promise resolving to the saved file URI
*/
private async saveWithLocationOptions(
fileName: string,
content: string,
mimeType: string = "application/json",
showDialog: boolean = true
): Promise<string> {
try {
logger.log("[CapacitorPlatformService] Providing save location options:", {
fileName,
mimeType,
showDialog,
platform: this.getCapabilities().isIOS ? "iOS" : "Android",
timestamp: new Date().toISOString(),
});
// Save to multiple locations and let user choose via share dialog
const locations = [];
// Save to Documents (iOS) or Downloads (Android)
const primaryLocation = await this.saveToDownloads(fileName, content);
locations.push(primaryLocation);
// Save to app data directory as backup
const backupFileName = `backup_${Date.now()}_${fileName}`;
const backupResult = await Filesystem.writeFile({
path: backupFileName,
data: content, data: content,
directory: Directory.Data, directory: Directory.Data,
encoding: Encoding.UTF8, encoding: Encoding.UTF8,
}); });
locations.push(backupResult.uri);
logger.log("[CapacitorPlatformService] File saved to app storage for picker fallback:", { logger.log("[CapacitorPlatformService] File saved to multiple locations:", {
uri: result.uri, primaryLocation,
backupLocation: backupResult.uri,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
return result.uri; // Use share dialog to let user choose which location to use (if enabled)
if (showDialog) {
try {
await this.handleShareDialog({
title: "TimeSafari File Saved",
text: `Your file has been saved to multiple locations. Choose where to access it.`,
url: primaryLocation,
dialogTitle: "Access your saved file",
});
logger.log("[CapacitorPlatformService] Share dialog completed for location selection");
} catch (shareError) {
const shareErr = shareError as Error;
logger.warn("[CapacitorPlatformService] Share dialog failed, but file was saved:", {
error: shareErr.message,
timestamp: new Date().toISOString(),
});
// Check if it's a user cancellation or timeout
if (shareErr.message.includes("cancel") ||
shareErr.message.includes("dismiss") ||
shareErr.message.includes("timeout")) {
logger.log("[CapacitorPlatformService] User cancelled location selection share dialog or timeout occurred");
} else {
logger.error("[CapacitorPlatformService] Location selection share error:", shareErr);
}
}
} else {
logger.log("[CapacitorPlatformService] Location selection dialog disabled, file saved silently");
}
// Return the primary location (user can access others via share dialog)
return primaryLocation;
} catch (error) { } catch (error) {
logger.error("[CapacitorPlatformService] File picker save failed:", error); logger.error("[CapacitorPlatformService] Save with location options failed:", error);
throw new Error(`Failed to save file with picker: ${error}`); throw new Error(`Failed to save with location options: ${error}`);
} }
} }
/** /**
* Saves a file directly to the Downloads folder (Android) or Documents (iOS). * Saves a file directly to the Downloads folder (Android) or Documents (iOS).
* These locations are user-accessible through file managers and the app.
* @param fileName - Name of the file to save * @param fileName - Name of the file to save
* @param content - File content * @param content - File content
* @returns Promise resolving to the saved file URI * @returns Promise resolving to the saved file URI
@ -521,22 +698,43 @@ export class CapacitorPlatformService implements PlatformService {
private async saveToDownloads(fileName: string, content: string): Promise<string> { private async saveToDownloads(fileName: string, content: string): Promise<string> {
try { try {
if (this.getCapabilities().isIOS) { if (this.getCapabilities().isIOS) {
// iOS: Save to Documents directory // iOS: Save to Documents directory (user accessible)
const result = await Filesystem.writeFile({ const result = await Filesystem.writeFile({
path: fileName, path: fileName,
data: content, data: content,
directory: Directory.Documents, directory: Directory.Documents,
encoding: Encoding.UTF8, encoding: Encoding.UTF8,
}); });
logger.log("[CapacitorPlatformService] File saved to iOS Documents:", {
uri: result.uri,
fileName,
timestamp: new Date().toISOString(),
});
return result.uri; return result.uri;
} else { } else {
// Android: Save to Downloads directory // Android: Save to app's external storage (accessible via file managers)
// Due to Android 11+ restrictions, we can't directly write to public Downloads
// Users can access files through file managers or use share dialog to save to Downloads
const downloadsPath = `TimeSafari/${fileName}`;
const result = await Filesystem.writeFile({ const result = await Filesystem.writeFile({
path: fileName, path: downloadsPath,
data: content, data: content,
directory: Directory.External, directory: Directory.External, // App's external storage (accessible via file managers)
encoding: Encoding.UTF8, encoding: Encoding.UTF8,
recursive: true,
});
logger.log("[CapacitorPlatformService] File saved to Android external storage:", {
uri: result.uri,
fileName,
downloadsPath,
note: "File is accessible via file managers. Use share dialog to save to Downloads.",
timestamp: new Date().toISOString(),
}); });
return result.uri; return result.uri;
} }
} catch (error) { } catch (error) {
@ -573,6 +771,175 @@ export class CapacitorPlatformService implements PlatformService {
); );
} }
/**
* Lists user-accessible files saved by the app.
* Returns files from Downloads (Android) or Documents (iOS) directories.
* @returns Promise resolving to array of file information
*/
async listUserAccessibleFiles(): Promise<Array<{name: string, uri: string, size?: number}>> {
try {
if (this.getCapabilities().isIOS) {
// iOS: List files in Documents directory
const result = await Filesystem.readdir({
path: ".",
directory: Directory.Documents,
});
return result.files.map((file) => ({
name: typeof file === "string" ? file : file.name,
uri: `file://${file.uri || file}`,
size: typeof file === "string" ? undefined : file.size,
}));
} else {
// Android: List files in app's external storage (TimeSafari subdirectory)
try {
const result = await Filesystem.readdir({
path: "TimeSafari",
directory: Directory.External,
});
return result.files.map((file) => ({
name: typeof file === "string" ? file : file.name,
uri: `file://${file.uri || file}`,
size: typeof file === "string" ? undefined : file.size,
}));
} catch (downloadsError) {
logger.warn("[CapacitorPlatformService] Could not read external storage directory:", downloadsError);
return [];
}
}
} catch (error) {
logger.error("[CapacitorPlatformService] Failed to list user accessible files:", error);
return [];
}
}
/**
* Tests the file sharing functionality by creating and sharing a test file.
* @returns Promise resolving to a test result message
*/
async testFileSharing(): Promise<string> {
try {
const testContent = {
message: "This is a test file for TimeSafari file sharing",
timestamp: new Date().toISOString(),
platform: this.getCapabilities().isIOS ? "iOS" : "Android",
test: true
};
const fileName = `timesafari-test-${Date.now()}.json`;
const content = JSON.stringify(testContent, null, 2);
const result = await this.writeAndShareFile(fileName, content, {
mimeType: "application/json"
});
if (result.saved) {
return `✅ File saved successfully! URI: ${result.uri}. Shared: ${result.shared ? 'Yes' : 'No'}`;
} else {
return `❌ File save failed: ${result.error}`;
}
} catch (error) {
const err = error as Error;
return `❌ Test failed: ${err.message}`;
}
}
/**
* Tests saving a file without showing the share dialog.
* @returns Promise resolving to a test result message
*/
async testFileSaveOnly(): Promise<string> {
try {
const testContent = {
message: "This is a test file saved without sharing",
timestamp: new Date().toISOString(),
platform: this.getCapabilities().isIOS ? "iOS" : "Android",
test: true,
saveOnly: true
};
const fileName = `timesafari-save-only-${Date.now()}.json`;
const content = JSON.stringify(testContent, null, 2);
const result = await this.writeAndShareFile(fileName, content, {
mimeType: "application/json",
showShareDialog: false
});
if (result.saved) {
return `✅ File saved successfully without sharing! URI: ${result.uri}`;
} else {
return `❌ File save failed: ${result.error}`;
}
} catch (error) {
const err = error as Error;
return `❌ Test failed: ${err.message}`;
}
}
/**
* Tests the location selection functionality using the file picker.
* @returns Promise resolving to a test result message
*/
async testLocationSelection(): Promise<string> {
try {
const testContent = {
message: "This is a test file for location selection",
timestamp: new Date().toISOString(),
platform: this.getCapabilities().isIOS ? "iOS" : "Android",
test: true,
locationSelection: true
};
const fileName = `timesafari-location-test-${Date.now()}.json`;
const content = JSON.stringify(testContent, null, 2);
// Use the FilePicker to let user choose where to save the file
const result = await this.saveFileWithLocationPicker(fileName, content, "application/json");
return `✅ Location selection test successful! File saved using directory picker. URI: ${result}`;
} catch (error) {
const err = error as Error;
return `❌ Location selection test failed: ${err.message}`;
}
}
/**
* Tests location selection without showing the dialog (restores original behavior).
* @returns Promise resolving to a test result message
*/
async testLocationSelectionSilent(): Promise<string> {
try {
const testContent = {
message: "This is a test file for silent location selection",
timestamp: new Date().toISOString(),
platform: this.getCapabilities().isIOS ? "iOS" : "Android",
test: true,
locationSelection: true,
silent: true
};
const fileName = `timesafari-silent-location-test-${Date.now()}.json`;
const content = JSON.stringify(testContent, null, 2);
const result = await this.writeAndShareFile(fileName, content, {
allowLocationSelection: true,
showLocationSelectionDialog: false,
mimeType: "application/json"
});
if (result.saved) {
return `✅ Silent location selection test successful! File saved to multiple locations without showing dialog. URI: ${result.uri}`;
} else {
return `❌ Silent location selection test failed: ${result.error}`;
}
} catch (error) {
const err = error as Error;
return `❌ Silent location selection test failed: ${err.message}`;
}
}
/** /**
* Opens the device camera to take a picture. * Opens the device camera to take a picture.
* Configures camera for high quality images with editing enabled. * Configures camera for high quality images with editing enabled.
@ -752,4 +1119,104 @@ export class CapacitorPlatformService implements PlatformService {
throw new Error(`Failed to save file: ${err.message}`); throw new Error(`Failed to save file: ${err.message}`);
} }
} }
/**
* Saves a file using the FilePicker to let user choose the save location.
* This provides true location selection rather than using a share dialog.
* @param fileName - Name of the file to save
* @param content - File content
* @param mimeType - MIME type of the file
* @returns Promise resolving to the saved file URI
*/
private async saveFileWithLocationPicker(
fileName: string,
content: string,
mimeType: string = "application/json"
): Promise<string> {
try {
logger.log("[CapacitorPlatformService] Using FilePicker pickDirectory for location selection:", {
fileName,
mimeType,
platform: this.getCapabilities().isIOS ? "iOS" : "Android",
timestamp: new Date().toISOString(),
});
// Use FilePicker to let user choose a directory to save the file
const directoryResult = await FilePicker.pickDirectory();
logger.log("[CapacitorPlatformService] User selected directory:", {
selectedPath: directoryResult.path,
timestamp: new Date().toISOString(),
});
// Save the file to the selected directory
const fullPath = `${directoryResult.path}/${fileName}`;
// Try to save to the user-selected directory
try {
const result = await Filesystem.writeFile({
path: fullPath,
data: content,
directory: Directory.ExternalStorage, // Use external storage for user-selected location
encoding: Encoding.UTF8,
recursive: true,
});
logger.log("[CapacitorPlatformService] File saved to selected directory:", {
finalUri: result.uri,
fullPath,
timestamp: new Date().toISOString(),
});
return result.uri;
} catch (writeError) {
logger.warn("[CapacitorPlatformService] Failed to write to selected directory, trying alternative:", writeError);
// Fallback: Save to Downloads with the selected filename
const downloadsResult = await this.saveToDownloads(fileName, content);
logger.log("[CapacitorPlatformService] File saved to Downloads as fallback:", {
finalUri: downloadsResult,
originalPath: fullPath,
timestamp: new Date().toISOString(),
});
return downloadsResult;
}
} catch (error) {
logger.error("[CapacitorPlatformService] FilePicker directory selection failed:", error);
// If directory picker fails, fallback to Downloads
try {
logger.log("[CapacitorPlatformService] Falling back to Downloads directory");
const fallbackResult = await this.saveToDownloads(fileName, content);
return fallbackResult;
} catch (fallbackError) {
throw new Error(`Failed to select directory with FilePicker and fallback failed: ${error}`);
}
}
}
/**
* Tests listing user-accessible files saved by the app.
* @returns Promise resolving to a test result message
*/
async testListUserFiles(): Promise<string> {
try {
const files = await this.listUserAccessibleFiles();
if (files.length === 0) {
return `📁 No user-accessible files found. Try saving some files first using the other test buttons.`;
}
const fileList = files.map(file =>
`- ${file.name} (${file.size ? `${file.size} bytes` : 'size unknown'})`
).join('\n');
return `📁 Found ${files.length} user-accessible file(s):\n${fileList}`;
} catch (error) {
const err = error as Error;
return `❌ Failed to list user files: ${err.message}`;
}
}
} }

72
src/services/platforms/ElectronPlatformService.ts

@ -205,6 +205,7 @@ export class ElectronPlatformService implements PlatformService {
isIOS: false, isIOS: false,
hasFileDownload: false, // Not implemented yet hasFileDownload: false, // Not implemented yet
needsFileHandlingInstructions: false, needsFileHandlingInstructions: false,
isNativeApp: true,
}; };
} }
@ -234,11 +235,27 @@ export class ElectronPlatformService implements PlatformService {
* Writes content to a file and opens the system share dialog. * Writes content to a file and opens the system share dialog.
* @param _fileName - Name of the file to create * @param _fileName - Name of the file to create
* @param _content - Content to write to the file * @param _content - Content to write to the file
* @param _options - Options for file saving behavior
* @throws Error with "Not implemented" message * @throws Error with "Not implemented" message
* @todo Implement using Electron's dialog and file system APIs * @todo Implement using Electron's dialog and file system APIs
*/ */
async writeAndShareFile(_fileName: string, _content: string): Promise<void> { async writeAndShareFile(
throw new Error("Not implemented"); _fileName: string,
_content: string,
_options?: {
allowLocationSelection?: boolean;
saveToDownloads?: boolean;
saveToPrivateStorage?: boolean;
mimeType?: string;
showShareDialog?: boolean;
showLocationSelectionDialog?: boolean;
}
): Promise<{ saved: boolean; uri?: string; shared: boolean; error?: string }> {
return {
saved: false,
shared: false,
error: "Not implemented in Electron platform"
};
} }
/** /**
@ -284,6 +301,17 @@ export class ElectronPlatformService implements PlatformService {
throw new Error("Not implemented"); throw new Error("Not implemented");
} }
/**
* Should rotate the camera between front and back cameras.
* @returns Promise that resolves when the camera is rotated
* @throws Error with "Not implemented" message
* @todo Implement camera rotation using Electron's media APIs
*/
async rotateCamera(): Promise<void> {
logger.error("rotateCamera not implemented in Electron platform");
throw new Error("Not implemented");
}
/** /**
* Should handle deep link URLs for the desktop application. * Should handle deep link URLs for the desktop application.
* @param _url - The deep link URL to handle * @param _url - The deep link URL to handle
@ -345,4 +373,44 @@ export class ElectronPlatformService implements PlatformService {
); );
} }
} }
/**
* Tests the file sharing functionality.
* @returns Promise resolving to a test result message
*/
async testFileSharing(): Promise<string> {
return "File sharing not available in Electron platform - not implemented";
}
/**
* Tests saving a file without showing the share dialog.
* @returns Promise resolving to a test result message
*/
async testFileSaveOnly(): Promise<string> {
return "File save only not available in Electron platform - not implemented";
}
/**
* Tests the location selection functionality using the file picker.
* @returns Promise resolving to a test result message
*/
async testLocationSelection(): Promise<string> {
return "Location selection not available in Electron platform - not implemented";
}
/**
* Tests location selection without showing the dialog (restores original behavior).
* @returns Promise resolving to a test result message
*/
async testLocationSelectionSilent(): Promise<string> {
return "Location selection not available in Electron platform - not implemented";
}
/**
* Tests listing user-accessible files saved by the app.
* @returns Promise resolving to a test result message
*/
async testListUserFiles(): Promise<string> {
return "File listing not available in Electron platform - not implemented";
}
} }

77
src/services/platforms/WebPlatformService.ts

@ -29,10 +29,13 @@ export class WebPlatformService implements PlatformService {
return { return {
hasFileSystem: false, hasFileSystem: false,
hasCamera: true, // Through file input with capture hasCamera: true, // Through file input with capture
isMobile: /iPhone|iPad|iPod|Android/i.test(navigator.userAgent), isMobile: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent,
),
isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent), isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
hasFileDownload: true, hasFileDownload: true,
needsFileHandlingInstructions: false, needsFileHandlingInstructions: false,
isNativeApp: false,
}; };
} }
@ -356,10 +359,26 @@ export class WebPlatformService implements PlatformService {
* Not supported in web platform. * Not supported in web platform.
* @param _fileName - Unused fileName parameter * @param _fileName - Unused fileName parameter
* @param _content - Unused content parameter * @param _content - Unused content parameter
* @throws Error indicating file system access is not available * @param _options - Unused options parameter
* @returns Promise that resolves to a failure result
*/ */
async writeAndShareFile(_fileName: string, _content: string): Promise<void> { async writeAndShareFile(
throw new Error("File system access not available in web platform"); _fileName: string,
_content: string,
_options?: {
allowLocationSelection?: boolean;
saveToDownloads?: boolean;
saveToPrivateStorage?: boolean;
mimeType?: string;
showShareDialog?: boolean;
showLocationSelectionDialog?: boolean;
}
): Promise<{ saved: boolean; uri?: string; shared: boolean; error?: string }> {
return {
saved: false,
shared: false,
error: "File system access not available in web platform"
};
} }
/** /**
@ -390,4 +409,54 @@ export class WebPlatformService implements PlatformService {
.query(sql, params) .query(sql, params)
.then((result: QueryExecResult[]) => result[0]?.values[0]); .then((result: QueryExecResult[]) => result[0]?.values[0]);
} }
/**
* Tests the file sharing functionality.
* @returns Promise resolving to a test result message
*/
async testFileSharing(): Promise<string> {
return "File sharing not available in web platform - use download instead";
}
/**
* Tests saving a file without showing the share dialog.
* @returns Promise resolving to a test result message
*/
async testFileSaveOnly(): Promise<string> {
return "File saving not available in web platform - use download instead";
}
/**
* Tests the location selection functionality using the file picker.
* @returns Promise resolving to a test result message
*/
async testLocationSelection(): Promise<string> {
return "Location selection not available in web platform - use download instead";
}
/**
* Tests location selection without showing the dialog (restores original behavior).
* @returns Promise resolving to a test result message
*/
async testLocationSelectionSilent(): Promise<string> {
return "Location selection not available in web platform - use download instead";
}
/**
* Tests listing user-accessible files saved by the app.
* @returns Promise resolving to a test result message
*/
async testListUserFiles(): Promise<string> {
return "File listing not available in web platform - files are downloaded directly";
}
/**
* Rotates the camera between front and back cameras.
* Not supported in web platform.
* @returns Promise that resolves immediately
*/
async rotateCamera(): Promise<void> {
// Not supported in web platform
return Promise.resolve();
}
} }

143
src/views/TestView.vue

@ -215,6 +215,46 @@
</div> </div>
</div> </div>
<div class="mt-8">
<h2 class="text-xl font-bold mb-4">File Sharing Test</h2>
Test the new file sharing functionality that saves to user-accessible locations.
<div>
<button
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
@click="testFileSharing()"
>
Test File Sharing
</button>
<button
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
@click="testFileSaveOnly()"
>
Test Save Only (No Share Dialog)
</button>
<button
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
@click="testLocationSelection()"
>
Test Location Selection
</button>
<button
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
@click="testLocationSelectionSilent()"
>
Test Silent Location Selection
</button>
<button
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
@click="testListUserFiles()"
>
List User Files
</button>
<div v-if="fileSharingResult" class="mt-2 p-2 bg-gray-100 rounded">
<strong>Result:</strong> {{ fileSharingResult }}
</div>
</div>
</div>
<div class="mt-8"> <div class="mt-8">
<h2 class="text-xl font-bold mb-4">Image Sharing</h2> <h2 class="text-xl font-bold mb-4">Image Sharing</h2>
Populates the "shared-photo" view as if they used "share_target". Populates the "shared-photo" view as if they used "share_target".
@ -387,6 +427,9 @@ export default class Help extends Vue {
sqlQuery = ""; sqlQuery = "";
sqlResult: unknown = null; sqlResult: unknown = null;
// for file sharing test
fileSharingResult = "";
cryptoLib = cryptoLib; cryptoLib = cryptoLib;
async mounted() { async mounted() {
@ -620,5 +663,105 @@ export default class Help extends Vue {
); );
} }
} }
public async testFileSharing() {
const platformService = PlatformServiceFactory.getInstance();
try {
const result = await platformService.testFileSharing();
this.fileSharingResult = result;
logger.log("File Sharing Test Result:", this.fileSharingResult);
} catch (error) {
logger.error("File Sharing Test Error:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "File Sharing Error",
text: error instanceof Error ? error.message : String(error),
},
5000,
);
}
}
public async testFileSaveOnly() {
const platformService = PlatformServiceFactory.getInstance();
try {
const result = await platformService.testFileSaveOnly();
this.fileSharingResult = result;
logger.log("File Save Only Test Result:", this.fileSharingResult);
} catch (error) {
logger.error("File Save Only Test Error:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "File Save Only Error",
text: error instanceof Error ? error.message : String(error),
},
5000,
);
}
}
public async testLocationSelection() {
const platformService = PlatformServiceFactory.getInstance();
try {
const result = await platformService.testLocationSelection();
this.fileSharingResult = result;
logger.log("Location Selection Test Result:", this.fileSharingResult);
} catch (error) {
logger.error("Location Selection Test Error:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Location Selection Error",
text: error instanceof Error ? error.message : String(error),
},
5000,
);
}
}
public async testLocationSelectionSilent() {
const platformService = PlatformServiceFactory.getInstance();
try {
const result = await platformService.testLocationSelectionSilent();
this.fileSharingResult = result;
logger.log("Silent Location Selection Test Result:", this.fileSharingResult);
} catch (error) {
logger.error("Silent Location Selection Test Error:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Silent Location Selection Error",
text: error instanceof Error ? error.message : String(error),
},
5000,
);
}
}
public async testListUserFiles() {
const platformService = PlatformServiceFactory.getInstance();
try {
const result = await platformService.testListUserFiles();
this.fileSharingResult = result;
logger.log("List User Files Test Result:", this.fileSharingResult);
} catch (error) {
logger.error("List User Files Test Error:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "List User Files Error",
text: error instanceof Error ? error.message : String(error),
},
5000,
);
}
}
} }
</script> </script>

Loading…
Cancel
Save