diff --git a/package-lock.json b/package-lock.json index 32c3ce63..18a623a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/src/components/DataExportSection.vue b/src/components/DataExportSection.vue index 858742d8..5ec3a43e 100644 --- a/src/components/DataExportSection.vue +++ b/src/components/DataExportSection.vue @@ -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); diff --git a/src/libs/capacitor/app.ts b/src/libs/capacitor/app.ts index 038fe679..3ba64b00 100644 --- a/src/libs/capacitor/app.ts +++ b/src/libs/capacitor/app.ts @@ -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; + /** * 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 { + return CapacitorApp.exitApp(); + }, + addListener( eventName: "backButton" | "appUrlOpen", listenerFunc: BackButtonListener | ((data: AppLaunchUrl) => void), diff --git a/src/services/PlatformService.ts b/src/services/PlatformService.ts index 52e22b80..e91f9711 100644 --- a/src/services/PlatformService.ts +++ b/src/services/PlatformService.ts @@ -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; + /** + * Tests the file sharing functionality by creating and sharing a test file. + * @returns Promise resolving to a test result message + */ + testFileSharing(): Promise; + + /** + * Tests saving a file without showing the share dialog. + * @returns Promise resolving to a test result message + */ + testFileSaveOnly(): Promise; + + /** + * Tests the location selection functionality using the file picker. + * @returns Promise resolving to a test result message + */ + testLocationSelection(): Promise; + + /** + * Tests location selection without showing the dialog (restores original behavior). + * @returns Promise resolving to a test result message + */ + testLocationSelectionSilent(): Promise; + + /** + * Tests listing user-accessible files saved by the app. + * @returns Promise resolving to a test result message + */ + testListUserFiles(): Promise; + // Camera operations /** * Activates the device camera to take a picture. diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts index 55e2e62b..2bcdb6bc 100644 --- a/src/services/platforms/CapacitorPlatformService.ts +++ b/src/services/platforms/CapacitorPlatformService.ts @@ -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 { + 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((_, 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 { 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 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 { + 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 app storage for picker fallback:", { - uri: result.uri, + logger.log("[CapacitorPlatformService] File saved to multiple locations:", { + primaryLocation, + backupLocation: backupResult.uri, 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) { - logger.error("[CapacitorPlatformService] File picker save failed:", error); - throw new Error(`Failed to save file with picker: ${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 { 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> { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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}`; + } + } } diff --git a/src/services/platforms/ElectronPlatformService.ts b/src/services/platforms/ElectronPlatformService.ts index 5700f7d6..7ae9d737 100644 --- a/src/services/platforms/ElectronPlatformService.ts +++ b/src/services/platforms/ElectronPlatformService.ts @@ -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 { - 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + return "File listing not available in Electron platform - not implemented"; + } } diff --git a/src/services/platforms/WebPlatformService.ts b/src/services/platforms/WebPlatformService.ts index 6f66367d..01a52d78 100644 --- a/src/services/platforms/WebPlatformService.ts +++ b/src/services/platforms/WebPlatformService.ts @@ -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 { - 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + // Not supported in web platform + return Promise.resolve(); + } } diff --git a/src/views/TestView.vue b/src/views/TestView.vue index 91dd673b..1675ac83 100644 --- a/src/views/TestView.vue +++ b/src/views/TestView.vue @@ -215,6 +215,46 @@ +
+

File Sharing Test

+ Test the new file sharing functionality that saves to user-accessible locations. +
+ + + + + +
+ Result: {{ fileSharingResult }} +
+
+
+

Image Sharing

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, + ); + } + } }