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.
This commit is contained in:
254
package-lock.json
generated
254
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "timesafari",
|
||||
"version": "0.4.8",
|
||||
"version": "0.5.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "timesafari",
|
||||
"version": "0.4.8",
|
||||
"version": "0.5.1",
|
||||
"dependencies": {
|
||||
"@capacitor-community/sqlite": "6.0.2",
|
||||
"@capacitor-mlkit/barcode-scanning": "^6.0.0",
|
||||
@@ -3835,9 +3835,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/asar/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4524,9 +4524,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -6686,9 +6686,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -8217,9 +8217,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.42.0.tgz",
|
||||
"integrity": "sha512-gldmAyS9hpj+H6LpRNlcjQWbuKUtb94lodB9uCz71Jm+7BxK1VIOo7y62tZZwxhA7j1ylv/yQz080L5WkS+LoQ==",
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.43.0.tgz",
|
||||
"integrity": "sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -8231,9 +8231,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.42.0.tgz",
|
||||
"integrity": "sha512-bpRipfTgmGFdCZDFLRvIkSNO1/3RGS74aWkJJTFJBH7h3MRV4UijkaEUeOMbi9wxtxYmtAbVcnMtHTPBhLEkaw==",
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.43.0.tgz",
|
||||
"integrity": "sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -8271,9 +8271,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.42.0.tgz",
|
||||
"integrity": "sha512-fJcN4uSGPWdpVmvLuMtALUFwCHgb2XiQjuECkHT3lWLZhSQ3MBQ9pq+WoWeJq2PrNxr9rPM1Qx+IjyGj8/c6zQ==",
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.43.0.tgz",
|
||||
"integrity": "sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -8285,9 +8285,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.42.0.tgz",
|
||||
"integrity": "sha512-CziHfyzpp8hJpCVE/ZdTizw58gr+m7Y2Xq5VOuCSrZR++th2xWAz4Nqk52MoIIrV3JHtVBhbBsJcAxs6NammOQ==",
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.43.0.tgz",
|
||||
"integrity": "sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -8299,9 +8299,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.42.0.tgz",
|
||||
"integrity": "sha512-UsQD5fyLWm2Fe5CDM7VPYAo+UC7+2Px4Y+N3AcPh/LdZu23YcuGPegQly++XEVaC8XUTFVPscl5y5Cl1twEI4A==",
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.43.0.tgz",
|
||||
"integrity": "sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -8313,9 +8313,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.42.0.tgz",
|
||||
"integrity": "sha512-/i8NIrlgc/+4n1lnoWl1zgH7Uo0XK5xK3EDqVTf38KvyYgCU/Rm04+o1VvvzJZnVS5/cWSd07owkzcVasgfIkQ==",
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.43.0.tgz",
|
||||
"integrity": "sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -8353,9 +8353,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
||||
"version": "4.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.42.0.tgz",
|
||||
"integrity": "sha512-O8AplvIeavK5ABmZlKBq9/STdZlnQo7Sle0LLhVA7QT+CiGpNVe197/t8Aph9bhJqbDVGCHpY2i7QyfEDDStDg==",
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.43.0.tgz",
|
||||
"integrity": "sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -8367,9 +8367,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.42.0.tgz",
|
||||
"integrity": "sha512-6Qb66tbKVN7VyQrekhEzbHRxXXFFD8QKiFAwX5v9Xt6FiJ3BnCVBuyBxa2fkFGqxOCSGGYNejxd8ht+q5SnmtA==",
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.43.0.tgz",
|
||||
"integrity": "sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -8381,9 +8381,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.42.0.tgz",
|
||||
"integrity": "sha512-KQETDSEBamQFvg/d8jajtRwLNBlGc3aKpaGiP/LvEbnmVUKlFta1vqJqTrvPtsYsfbE/DLg5CC9zyXRX3fnBiA==",
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.43.0.tgz",
|
||||
"integrity": "sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -8395,9 +8395,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.42.0.tgz",
|
||||
"integrity": "sha512-qMvnyjcU37sCo/tuC+JqeDKSuukGAd+pVlRl/oyDbkvPJ3awk6G6ua7tyum02O3lI+fio+eM5wsVd66X0jQtxw==",
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.43.0.tgz",
|
||||
"integrity": "sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -8409,9 +8409,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.42.0.tgz",
|
||||
"integrity": "sha512-I2Y1ZUgTgU2RLddUHXTIgyrdOwljjkmcZ/VilvaEumtS3Fkuhbw4p4hgHc39Ypwvo2o7sBFNl2MquNvGCa55Iw==",
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.43.0.tgz",
|
||||
"integrity": "sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -8462,9 +8462,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.42.0.tgz",
|
||||
"integrity": "sha512-F+5J9pelstXKwRSDq92J0TEBXn2nfUrQGg+HK1+Tk7VOL09e0gBqUHugZv7SW4MGrYj41oNCUe3IKCDGVlis2g==",
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.43.0.tgz",
|
||||
"integrity": "sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -12199,9 +12199,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
@@ -12648,9 +12648,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cacache/node_modules/rimraf/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -12868,9 +12868,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001721",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz",
|
||||
"integrity": "sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==",
|
||||
"version": "1.0.30001722",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001722.tgz",
|
||||
"integrity": "sha512-DCQHBBZtiK6JVkAGw7drvAMK0Q0POD/xZvEmDp6baiMMP6QXXk9HpD6mNYBZWhOPG6LvIDb82ITqtWjhDckHCA==",
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -14931,9 +14931,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dir-compare/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -15865,9 +15865,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -17617,9 +17617,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -19127,9 +19127,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jake/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -25514,9 +25514,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/replace/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -26094,9 +26094,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.42.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.42.0.tgz",
|
||||
"integrity": "sha512-LW+Vse3BJPyGJGAJt1j8pWDKPd73QM8cRXYK1IxOBgL2AGLu7Xd2YOW0M2sLUBCkF5MshXXtMApyEAEzMVMsnw==",
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.43.0.tgz",
|
||||
"integrity": "sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -26110,33 +26110,33 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.42.0",
|
||||
"@rollup/rollup-android-arm64": "4.42.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.42.0",
|
||||
"@rollup/rollup-darwin-x64": "4.42.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.42.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.42.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.42.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.42.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.42.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.42.0",
|
||||
"@rollup/rollup-linux-loongarch64-gnu": "4.42.0",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.42.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.42.0",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.42.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.42.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.42.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.42.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.42.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.42.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.42.0",
|
||||
"@rollup/rollup-android-arm-eabi": "4.43.0",
|
||||
"@rollup/rollup-android-arm64": "4.43.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.43.0",
|
||||
"@rollup/rollup-darwin-x64": "4.43.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.43.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.43.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.43.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.43.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.43.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.43.0",
|
||||
"@rollup/rollup-linux-loongarch64-gnu": "4.43.0",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.43.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.43.0",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.43.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.43.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.43.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.43.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.43.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.43.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.43.0",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup/node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.42.0.tgz",
|
||||
"integrity": "sha512-JxHtA081izPBVCHLKnl6GEA0w3920mlJPLh89NojpU2GsBSB6ypu4erFg/Wx1qbpUbepn0jY4dVWMGZM8gplgA==",
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.43.0.tgz",
|
||||
"integrity": "sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -26148,9 +26148,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/rollup/node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.42.0.tgz",
|
||||
"integrity": "sha512-rv5UZaWVIJTDMyQ3dCEK+m0SAn6G7H3PRc2AZmExvbDvtaDc+qXkei0knQWcI3+c9tEs7iL/4I4pTQoPbNL2SA==",
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.43.0.tgz",
|
||||
"integrity": "sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -26162,9 +26162,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/rollup/node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.42.0.tgz",
|
||||
"integrity": "sha512-eoujJFOvoIBjZEi9hJnXAbWg+Vo1Ov8n/0IKZZcPZ7JhBzxh2A+2NFyeMZIRkY9iwBvSjloKgcvnjTbGKHE44Q==",
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.43.0.tgz",
|
||||
"integrity": "sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -26176,9 +26176,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/rollup/node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.42.0.tgz",
|
||||
"integrity": "sha512-/3NrcOWFSR7RQUQIuZQChLND36aTU9IYE4j+TB40VU78S+RA0IiqHR30oSh6P1S9f9/wVOenHQnacs/Byb824g==",
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.43.0.tgz",
|
||||
"integrity": "sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -26190,9 +26190,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.42.0.tgz",
|
||||
"integrity": "sha512-Gfm6cV6mj3hCUY8TqWa63DB8Mx3NADoFwiJrMpoZ1uESbK8FQV3LXkhfry+8bOniq9pqY1OdsjFWNsSbfjPugw==",
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.43.0.tgz",
|
||||
"integrity": "sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -26204,9 +26204,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/rollup/node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.42.0.tgz",
|
||||
"integrity": "sha512-g86PF8YZ9GRqkdi0VoGlcDUb4rYtQKyTD1IVtxxN4Hpe7YqLBShA7oHMKU6oKTCi3uxwW4VkIGnOaH/El8de3w==",
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.43.0.tgz",
|
||||
"integrity": "sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -26218,9 +26218,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/rollup/node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.42.0.tgz",
|
||||
"integrity": "sha512-+axkdyDGSp6hjyzQ5m1pgcvQScfHnMCcsXkx8pTgy/6qBmWVhtRVlgxjWwDp67wEXXUr0x+vD6tp5W4x6V7u1A==",
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.43.0.tgz",
|
||||
"integrity": "sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -26232,9 +26232,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/rollup/node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.42.0.tgz",
|
||||
"integrity": "sha512-LpHiJRwkaVz/LqjHjK8LCi8osq7elmpwujwbXKNW88bM8eeGxavJIKKjkjpMHAh/2xfnrt1ZSnhTv41WYUHYmA==",
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.43.0.tgz",
|
||||
"integrity": "sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -26424,9 +26424,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sdp": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz",
|
||||
"integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.1.tgz",
|
||||
"integrity": "sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/secp256k1": {
|
||||
@@ -28495,9 +28495,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
@@ -31045,9 +31045,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.58",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.58.tgz",
|
||||
"integrity": "sha512-DVLmMQzSZwNYzQoMaM3MQWnxr2eq+AtM9Hx3w1/Yl0pH8sLTSjN4jGP7w6f7uand6Hw44tsnSu1hz1AOA6qI2Q==",
|
||||
"version": "3.25.61",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.61.tgz",
|
||||
"integrity": "sha512-fzfJgUw78LTNnHujj9re1Ov/JJQkRZZGDMcYqSx7Hp4rPOkKywaFHq0S6GoHeXs0wGNE/sIOutkXgnwzrVOGCQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
|
||||
@@ -162,13 +162,13 @@ export default class DataExportSection extends Vue {
|
||||
downloadAnchor.click();
|
||||
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
|
||||
} 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(
|
||||
fileName,
|
||||
jsonStr,
|
||||
{
|
||||
allowLocationSelection: true,
|
||||
saveToDownloads: false,
|
||||
showLocationSelectionDialog: true,
|
||||
mimeType: "application/json"
|
||||
}
|
||||
);
|
||||
@@ -188,9 +188,9 @@ export default class DataExportSection extends Vue {
|
||||
title: "Export Successful",
|
||||
text: this.platformCapabilities.hasFileDownload
|
||||
? "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) {
|
||||
logger.error("Export Error:", error);
|
||||
|
||||
@@ -12,6 +12,14 @@ import type { PluginListenerHandle } from "@capacitor/core";
|
||||
* Supports 'backButton' and 'appUrlOpen' events from Capacitor
|
||||
*/
|
||||
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
|
||||
* @param eventName - Must be 'backButton'
|
||||
@@ -38,8 +46,19 @@ interface AppInterface {
|
||||
/**
|
||||
* App wrapper for Capacitor functionality
|
||||
* Provides type-safe event listeners for back button and URL open events
|
||||
* and app exit functionality
|
||||
*/
|
||||
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(
|
||||
eventName: "backButton" | "appUrlOpen",
|
||||
listenerFunc: BackButtonListener | ((data: AppLaunchUrl) => void),
|
||||
|
||||
@@ -74,7 +74,10 @@ export interface PlatformService {
|
||||
options?: {
|
||||
allowLocationSelection?: boolean;
|
||||
saveToDownloads?: boolean;
|
||||
saveToPrivateStorage?: boolean;
|
||||
mimeType?: string;
|
||||
showShareDialog?: boolean;
|
||||
showLocationSelectionDialog?: boolean;
|
||||
}
|
||||
): Promise<{ saved: boolean; uri?: string; shared: boolean; error?: string }>;
|
||||
|
||||
@@ -92,6 +95,36 @@ export interface PlatformService {
|
||||
*/
|
||||
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
|
||||
/**
|
||||
* Activates the device camera to take a picture.
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
CameraDirection,
|
||||
} from "@capacitor/camera";
|
||||
import { Share } from "@capacitor/share";
|
||||
import { FilePicker } from "@capawesome/capacitor-file-picker";
|
||||
import {
|
||||
SQLiteConnection,
|
||||
SQLiteDBConnection,
|
||||
@@ -271,22 +272,24 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Try to access a test directory to check permissions
|
||||
// For Android, try to access external storage to check permissions
|
||||
try {
|
||||
await Filesystem.stat({
|
||||
path: "/storage/emulated/0/Download",
|
||||
directory: Directory.Documents,
|
||||
// Try to list files in external storage to check permissions
|
||||
await Filesystem.readdir({
|
||||
path: ".",
|
||||
directory: Directory.External,
|
||||
});
|
||||
logger.log(
|
||||
"Storage permissions already granted",
|
||||
"External storage permissions already granted",
|
||||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
|
||||
);
|
||||
return;
|
||||
} catch (error: unknown) {
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
const errorLogData = {
|
||||
error: {
|
||||
@@ -297,19 +300,11 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
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
|
||||
if (
|
||||
err.message.includes("permission") ||
|
||||
err.message.includes("access")
|
||||
err.message.includes("access") ||
|
||||
err.message.includes("denied")
|
||||
) {
|
||||
logger.log(
|
||||
"Permission check failed, requesting permissions",
|
||||
@@ -319,45 +314,33 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
// The Filesystem plugin will automatically request permissions when needed
|
||||
// We just need to try the operation again
|
||||
try {
|
||||
await Filesystem.stat({
|
||||
path: "/storage/emulated/0/Download",
|
||||
directory: Directory.Documents,
|
||||
await Filesystem.readdir({
|
||||
path: ".",
|
||||
directory: Directory.External,
|
||||
});
|
||||
logger.log(
|
||||
"Storage permissions granted after request",
|
||||
"External storage permissions granted after request",
|
||||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
|
||||
);
|
||||
return;
|
||||
} catch (retryError: unknown) {
|
||||
const retryErr = retryError as 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
|
||||
logger.log(
|
||||
"Unexpected error during permission check",
|
||||
"Unexpected error during permission check, proceeding anyway",
|
||||
JSON.stringify(errorLogData, null, 2),
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
const errorLogData = {
|
||||
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}`);
|
||||
} catch (error) {
|
||||
logger.error("Error in checkStoragePermissions:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,8 +365,8 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
* Enhanced file save and share functionality with location selection.
|
||||
*
|
||||
* Provides multiple options for saving files:
|
||||
* 1. Save to app-private storage and share (current behavior)
|
||||
* 2. Save to device Downloads folder (Android) or Documents (iOS)
|
||||
* 1. Save to user-accessible storage (Downloads/Documents) and share (default behavior)
|
||||
* 2. Save to app-private storage and share (for sensitive data)
|
||||
* 3. Allow user to choose save location via file picker
|
||||
* 4. Direct share without saving locally
|
||||
*
|
||||
@@ -398,7 +381,10 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
options: {
|
||||
allowLocationSelection?: boolean;
|
||||
saveToDownloads?: boolean;
|
||||
saveToPrivateStorage?: boolean;
|
||||
mimeType?: string;
|
||||
showShareDialog?: boolean;
|
||||
showLocationSelectionDialog?: boolean;
|
||||
} = {}
|
||||
): Promise<{ saved: boolean; uri?: string; shared: boolean; error?: string }> {
|
||||
const timestamp = new Date().toISOString();
|
||||
@@ -420,15 +406,11 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
|
||||
// Determine save strategy based on options and platform
|
||||
if (options.allowLocationSelection) {
|
||||
// Use file picker to let user choose location
|
||||
fileUri = await this.saveFileWithPicker(fileName, content, options.mimeType);
|
||||
// Use enhanced location selection with multiple save options
|
||||
fileUri = await this.saveWithLocationOptions(fileName, content, options.mimeType, options.showLocationSelectionDialog);
|
||||
saved = true;
|
||||
} else if (options.saveToDownloads) {
|
||||
// Save directly to Downloads folder
|
||||
fileUri = await this.saveToDownloads(fileName, content);
|
||||
saved = true;
|
||||
} else {
|
||||
// Fallback to app-private storage (current behavior)
|
||||
} else if (options.saveToPrivateStorage) {
|
||||
// Save to app-private storage (for sensitive data)
|
||||
const result = await Filesystem.writeFile({
|
||||
path: fileName,
|
||||
data: content,
|
||||
@@ -438,6 +420,10 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
});
|
||||
fileUri = result.uri;
|
||||
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:", {
|
||||
@@ -446,20 +432,53 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Share the file
|
||||
// Share the file (unless explicitly disabled)
|
||||
let shared = false;
|
||||
try {
|
||||
await Share.share({
|
||||
title: "TimeSafari Backup",
|
||||
text: "Here is your backup file.",
|
||||
url: fileUri,
|
||||
dialogTitle: "Share your backup file",
|
||||
});
|
||||
if (options.showShareDialog !== false && !options.allowLocationSelection) {
|
||||
try {
|
||||
logger.log("[CapacitorPlatformService] Starting share operation:", {
|
||||
uri: fileUri,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// 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;
|
||||
logger.log("[CapacitorPlatformService] File shared successfully");
|
||||
} catch (shareError) {
|
||||
logger.warn("[CapacitorPlatformService] Share failed, but file was saved:", shareError);
|
||||
// Don't throw error if sharing fails, file is still saved
|
||||
logger.log("[CapacitorPlatformService] Location selection handled sharing");
|
||||
}
|
||||
|
||||
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 content - File content
|
||||
* @param mimeType - MIME type of the file
|
||||
@@ -491,29 +562,135 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
mimeType: string = "application/json"
|
||||
): Promise<string> {
|
||||
try {
|
||||
// For now, fallback to regular save since file picker save API is complex
|
||||
// Save to app-private storage and let user share to choose location
|
||||
const result = await Filesystem.writeFile({
|
||||
path: fileName,
|
||||
logger.log("[CapacitorPlatformService] Using native share dialog for save location selection:", {
|
||||
fileName,
|
||||
mimeType,
|
||||
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 app storage for picker fallback:", {
|
||||
uri: result.uri,
|
||||
logger.log("[CapacitorPlatformService] File saved to temp location:", {
|
||||
tempUri: tempResult.uri,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return result.uri;
|
||||
// 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] File picker save failed:", error);
|
||||
throw new Error(`Failed to save file with picker: ${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,
|
||||
directory: Directory.Data,
|
||||
encoding: Encoding.UTF8,
|
||||
});
|
||||
locations.push(backupResult.uri);
|
||||
|
||||
logger.log("[CapacitorPlatformService] File saved to multiple locations:", {
|
||||
primaryLocation,
|
||||
backupLocation: backupResult.uri,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// 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) {
|
||||
logger.error("[CapacitorPlatformService] Save with location options failed:", error);
|
||||
throw new Error(`Failed to save with location options: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 content - File content
|
||||
* @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> {
|
||||
try {
|
||||
if (this.getCapabilities().isIOS) {
|
||||
// iOS: Save to Documents directory
|
||||
// iOS: Save to Documents directory (user accessible)
|
||||
const result = await Filesystem.writeFile({
|
||||
path: fileName,
|
||||
data: content,
|
||||
directory: Directory.Documents,
|
||||
encoding: Encoding.UTF8,
|
||||
});
|
||||
|
||||
logger.log("[CapacitorPlatformService] File saved to iOS Documents:", {
|
||||
uri: result.uri,
|
||||
fileName,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return result.uri;
|
||||
} 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({
|
||||
path: fileName,
|
||||
path: downloadsPath,
|
||||
data: content,
|
||||
directory: Directory.External,
|
||||
directory: Directory.External, // App's external storage (accessible via file managers)
|
||||
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;
|
||||
}
|
||||
} 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.
|
||||
* 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}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,6 +205,7 @@ export class ElectronPlatformService implements PlatformService {
|
||||
isIOS: false,
|
||||
hasFileDownload: false, // Not implemented yet
|
||||
needsFileHandlingInstructions: false,
|
||||
isNativeApp: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -234,11 +235,27 @@ export class ElectronPlatformService implements PlatformService {
|
||||
* Writes content to a file and opens the system share dialog.
|
||||
* @param _fileName - Name of the file to create
|
||||
* @param _content - Content to write to the file
|
||||
* @param _options - Options for file saving behavior
|
||||
* @throws Error with "Not implemented" message
|
||||
* @todo Implement using Electron's dialog and file system APIs
|
||||
*/
|
||||
async writeAndShareFile(_fileName: string, _content: string): Promise<void> {
|
||||
throw new Error("Not implemented");
|
||||
async writeAndShareFile(
|
||||
_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");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,10 +29,13 @@ export class WebPlatformService implements PlatformService {
|
||||
return {
|
||||
hasFileSystem: false,
|
||||
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),
|
||||
hasFileDownload: true,
|
||||
needsFileHandlingInstructions: false,
|
||||
isNativeApp: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -356,10 +359,26 @@ export class WebPlatformService implements PlatformService {
|
||||
* Not supported in web platform.
|
||||
* @param _fileName - Unused fileName 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> {
|
||||
throw new Error("File system access not available in web platform");
|
||||
async writeAndShareFile(
|
||||
_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)
|
||||
.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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,6 +215,46 @@
|
||||
</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">
|
||||
<h2 class="text-xl font-bold mb-4">Image Sharing</h2>
|
||||
Populates the "shared-photo" view as if they used "share_target".
|
||||
@@ -387,6 +427,9 @@ export default class Help extends Vue {
|
||||
sqlQuery = "";
|
||||
sqlResult: unknown = null;
|
||||
|
||||
// for file sharing test
|
||||
fileSharingResult = "";
|
||||
|
||||
cryptoLib = cryptoLib;
|
||||
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user