Compare commits
13 Commits
registrati
...
cross-plat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ee4a7e411 | ||
|
|
1c8528fb20 | ||
|
|
b8a7771edf | ||
|
|
5d845fb112 | ||
|
|
660f2170de | ||
|
|
94bd649003 | ||
|
|
b2d628cfeb | ||
|
|
00e52f8dca | ||
|
|
073ce24f43 | ||
|
|
2c84bb50b3 | ||
|
|
abf18835f6 | ||
|
|
f72562804d | ||
|
|
bdc5ffafc1 |
@@ -26,6 +26,7 @@ module.exports = {
|
||||
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-unnecessary-type-constraint": "off"
|
||||
"@typescript-eslint/no-unnecessary-type-constraint": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
|
||||
},
|
||||
};
|
||||
|
||||
6
.gitignore
vendored
@@ -51,5 +51,7 @@ vendor/
|
||||
# Build logs
|
||||
build_logs/
|
||||
|
||||
# PWA icon files generated by capacitor-assets
|
||||
icons
|
||||
android/app/src/main/assets/public
|
||||
android/app/src/main/res
|
||||
android/.gradle/buildOutputCleanup/buildOutputCleanup.lock
|
||||
android/.gradle/file-system.probe
|
||||
@@ -187,11 +187,7 @@ Prerequisites: macOS with Xcode installed
|
||||
3. Copy the assets:
|
||||
|
||||
```bash
|
||||
# It makes no sense why capacitor-assets will not run without these but it actually changes the contents.
|
||||
mkdir -p ios/App/App/Assets.xcassets/AppIcon.appiconset
|
||||
echo '{"images":[]}' > ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json
|
||||
mkdir -p ios/App/App/Assets.xcassets/Splash.imageset
|
||||
echo '{"images":[]}' > ios/App/App/Assets.xcassets/Splash.imageset/Contents.json
|
||||
npx capacitor-assets generate --ios
|
||||
```
|
||||
|
||||
|
||||
5
android/.gitignore
vendored
@@ -102,8 +102,3 @@ app/src/main/assets/public
|
||||
app/src/main/assets/capacitor.config.json
|
||||
app/src/main/assets/capacitor.plugins.json
|
||||
app/src/main/res/xml/config.xml
|
||||
|
||||
# Generated Icons from capacitor-assets
|
||||
app/src/main/res/drawable/*.png
|
||||
app/src/main/res/drawable-*/*.png
|
||||
app/src/main/res/mipmap-*/*.png
|
||||
|
||||
@@ -10,6 +10,8 @@ android {
|
||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
implementation project(':capacitor-app')
|
||||
implementation project(':capacitor-camera')
|
||||
implementation project(':capacitor-filesystem')
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -2,5 +2,13 @@
|
||||
{
|
||||
"pkg": "@capacitor/app",
|
||||
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/camera",
|
||||
"classpath": "com.capacitorjs.plugins.camera.CameraPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/filesystem",
|
||||
"classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin"
|
||||
}
|
||||
]
|
||||
|
||||
BIN
android/app/src/main/res/drawable-land-hdpi/splash.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
android/app/src/main/res/drawable-land-mdpi/splash.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
android/app/src/main/res/drawable-land-xhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
android/app/src/main/res/drawable-land-xxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
android/app/src/main/res/drawable-land-xxxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
android/app/src/main/res/drawable-port-hdpi/splash.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
android/app/src/main/res/drawable-port-mdpi/splash.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
android/app/src/main/res/drawable-port-xhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
android/app/src/main/res/drawable-port-xxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
android/app/src/main/res/drawable-port-xxxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
android/app/src/main/res/drawable/splash.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
@@ -1,9 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background>
|
||||
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
|
||||
</background>
|
||||
<foreground>
|
||||
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
|
||||
</foreground>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,9 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background>
|
||||
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
|
||||
</background>
|
||||
<foreground>
|
||||
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
|
||||
</foreground>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 15 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
@@ -7,7 +7,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.2.1'
|
||||
classpath 'com.android.tools.build:gradle:8.9.0'
|
||||
classpath 'com.google.gms:google-services:4.4.0'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
|
||||
@@ -4,3 +4,9 @@ project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/
|
||||
|
||||
include ':capacitor-app'
|
||||
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
||||
|
||||
include ':capacitor-camera'
|
||||
project(':capacitor-camera').projectDir = new File('../node_modules/@capacitor/camera/android')
|
||||
|
||||
include ':capacitor-filesystem'
|
||||
project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
BIN
assets/icon-only.jpg
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
assets/icon.png
|
Before Width: | Height: | Size: 99 KiB |
4
ios/.gitignore
vendored
@@ -21,7 +21,3 @@ fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots
|
||||
fastlane/test_output
|
||||
|
||||
# Generated Icons from capacitor-assets (also Contents.json which is confusing; see BUILDING.md)
|
||||
App/App/Assets.xcassets/AppIcon.appiconset
|
||||
App/App/Assets.xcassets/Splash.imageset
|
||||
|
||||
|
After Width: | Height: | Size: 116 KiB |
14
ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"idiom": "universal",
|
||||
"size": "1024x1024",
|
||||
"filename": "AppIcon-512@2x.png",
|
||||
"platform": "ios"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
23
ios/App/App/Assets.xcassets/Splash.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "splash-2732x2732-2.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "splash-2732x2732-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "splash-2732x2732.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png
vendored
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png
vendored
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png
vendored
Normal file
|
After Width: | Height: | Size: 40 KiB |
22
ios/fastlane/Fastfile
Normal file
@@ -0,0 +1,22 @@
|
||||
default_platform(:ios)
|
||||
|
||||
platform :ios do
|
||||
desc "Build and deploy iOS app"
|
||||
lane :beta do
|
||||
build_ios_app(
|
||||
scheme: "App",
|
||||
workspace: "App.xcworkspace",
|
||||
export_method: "app-store"
|
||||
)
|
||||
upload_to_testflight
|
||||
end
|
||||
|
||||
lane :release do
|
||||
build_ios_app(
|
||||
scheme: "App",
|
||||
workspace: "App.xcworkspace",
|
||||
export_method: "app-store"
|
||||
)
|
||||
upload_to_app_store
|
||||
end
|
||||
end
|
||||
229
package-lock.json
generated
@@ -10,8 +10,10 @@
|
||||
"dependencies": {
|
||||
"@capacitor/android": "^6.2.0",
|
||||
"@capacitor/app": "^6.0.0",
|
||||
"@capacitor/camera": "^6.0.0",
|
||||
"@capacitor/cli": "^6.2.0",
|
||||
"@capacitor/core": "^6.2.0",
|
||||
"@capacitor/filesystem": "^6.0.0",
|
||||
"@capacitor/ios": "^6.2.0",
|
||||
"@dicebear/collection": "^5.4.1",
|
||||
"@dicebear/core": "^5.4.1",
|
||||
@@ -2768,6 +2770,15 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/camera": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/camera/-/camera-6.0.0.tgz",
|
||||
"integrity": "sha512-AZ/gfVPC3lsKbk9/yHI60ygNyOkN5jsCb4bHxXFbW0bss3XYtR/J1XWFJGkFNiRErNnTz6jnDrhsGCr7+JPfiA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/cli": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-6.2.1.tgz",
|
||||
@@ -2878,6 +2889,15 @@
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/filesystem": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/filesystem/-/filesystem-6.0.0.tgz",
|
||||
"integrity": "sha512-GnC4CBfky7fvG9zSV/aQnZaGs6ZJ90AaQorr53z81ArTCqcrSUeBMuCxWmvti9HrdXLhBavyA1UOjvRGObOFjg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/ios": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-6.2.1.tgz",
|
||||
@@ -5181,9 +5201,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/cli": {
|
||||
"version": "0.22.23",
|
||||
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.22.23.tgz",
|
||||
"integrity": "sha512-LXFKu2jnk9ClVD+kw0sJCQ89zei01wz2t4EJwc9P7EwYb8gabC8FtPyM/X7NIE5jtrnTLTUtjW5ovxQSBL7pJQ==",
|
||||
"version": "0.22.24",
|
||||
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.22.24.tgz",
|
||||
"integrity": "sha512-lhdenxBC8/x/vL39j79eXE09mOaqNNLmiSDdY/PblnI+UNzGgsQ48hBTYa/MQhd0ioXXVKurZL2941dLKwcxJw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
@@ -5201,12 +5221,12 @@
|
||||
"@expo/osascript": "^2.1.6",
|
||||
"@expo/package-manager": "^1.7.2",
|
||||
"@expo/plist": "^0.2.2",
|
||||
"@expo/prebuild-config": "^8.0.30",
|
||||
"@expo/prebuild-config": "^8.0.31",
|
||||
"@expo/rudder-sdk-node": "^1.1.1",
|
||||
"@expo/spawn-async": "^1.7.2",
|
||||
"@expo/ws-tunnel": "^1.0.1",
|
||||
"@expo/xcpretty": "^4.3.0",
|
||||
"@react-native/dev-middleware": "0.76.8",
|
||||
"@react-native/dev-middleware": "0.76.9",
|
||||
"@urql/core": "^5.0.6",
|
||||
"@urql/exchange-retry": "^1.3.0",
|
||||
"accepts": "^1.3.8",
|
||||
@@ -6602,9 +6622,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/prebuild-config": {
|
||||
"version": "8.0.30",
|
||||
"resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-8.0.30.tgz",
|
||||
"integrity": "sha512-xNHWGh0xLZjxBXwVbDW+TPeexuQ95FZX2ZRrzJkALxhQiwYQswQSFE7CVUFMC2USIKVklCcgfEvtqnguTBQVxQ==",
|
||||
"version": "8.0.31",
|
||||
"resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-8.0.31.tgz",
|
||||
"integrity": "sha512-YTuS5ic9KolD/WA3GqgLcZytHQU1dpitlZ/cbDq8ZqkY+1ae5YWX+GkYEZf2VyECPaWnHYuDGddaTQVw5miTRg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
@@ -6614,7 +6634,7 @@
|
||||
"@expo/config-types": "^52.0.5",
|
||||
"@expo/image-utils": "^0.6.5",
|
||||
"@expo/json-file": "^9.0.2",
|
||||
"@react-native/normalize-colors": "0.76.8",
|
||||
"@react-native/normalize-colors": "0.76.9",
|
||||
"debug": "^4.3.1",
|
||||
"fs-extra": "^9.0.0",
|
||||
"resolve-from": "^5.0.0",
|
||||
@@ -8010,23 +8030,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/babel-plugin-codegen": {
|
||||
"version": "0.76.8",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.76.8.tgz",
|
||||
"integrity": "sha512-84RUEhDZS+q7vPtxKi0iMZLd5/W0VN7NOyqX5f+burV3xMYpUhpF5TDJ2Ysol7dJrvEZHm6ISAriO85++V8YDw==",
|
||||
"version": "0.76.9",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.76.9.tgz",
|
||||
"integrity": "sha512-vxL/vtDEIYHfWKm5oTaEmwcnNGsua/i9OjIxBDBFiJDu5i5RU3bpmDiXQm/bJxrJNPRp5lW0I0kpGihVhnMAIQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@react-native/codegen": "0.76.8"
|
||||
"@react-native/codegen": "0.76.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/babel-preset": {
|
||||
"version": "0.76.8",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.76.8.tgz",
|
||||
"integrity": "sha512-xrP+r3orRzzxtC2TrfGIP6IYi1f4AiWlnSiWf4zxEdMFzKrYdmxhD0FPtAZb77B0DqFIW5AcBFlm4grfL/VgfA==",
|
||||
"version": "0.76.9",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.76.9.tgz",
|
||||
"integrity": "sha512-TbSeCplCM6WhL3hR2MjC/E1a9cRnMLz7i767T7mP90oWkklEjyPxWl+0GGoVGnJ8FC/jLUupg/HvREKjjif6lw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
@@ -8072,7 +8092,7 @@
|
||||
"@babel/plugin-transform-typescript": "^7.25.2",
|
||||
"@babel/plugin-transform-unicode-regex": "^7.24.7",
|
||||
"@babel/template": "^7.25.0",
|
||||
"@react-native/babel-plugin-codegen": "0.76.8",
|
||||
"@react-native/babel-plugin-codegen": "0.76.9",
|
||||
"babel-plugin-syntax-hermes-parser": "^0.25.1",
|
||||
"babel-plugin-transform-flow-enums": "^0.0.2",
|
||||
"react-refresh": "^0.14.0"
|
||||
@@ -8085,9 +8105,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/codegen": {
|
||||
"version": "0.76.8",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.76.8.tgz",
|
||||
"integrity": "sha512-qvKhcYBkRHJFkeWrYm66kEomQOTVXWiHBkZ8VF9oC/71OJkLszpTpVOuPIyyib6fqhjy9l7mHYGYenSpfYI5Ww==",
|
||||
"version": "0.76.9",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.76.9.tgz",
|
||||
"integrity": "sha512-AzlCHMTKrAVC2709V4ZGtBXmGVtWTpWm3Ruv5vXcd3/anH4mGucfJ4rjbWKdaYQJMpXa3ytGomQrsIsT/s8kgA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
@@ -8224,9 +8244,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/debugger-frontend": {
|
||||
"version": "0.76.8",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.76.8.tgz",
|
||||
"integrity": "sha512-kSukBw2C++5ENLUCAp/1uEeiFgiHi/MBa71Wgym3UD5qwu2vOSPOTSKRX7q2Jb676MUzTcrIaJBZ/r2qk25u7Q==",
|
||||
"version": "0.76.9",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.76.9.tgz",
|
||||
"integrity": "sha512-0Ru72Bm066xmxFuOXhhvrryxvb57uI79yDSFf+hxRpktkC98NMuRenlJhslMrbJ6WjCu1vOe/9UjWNYyxXTRTA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
@@ -8235,15 +8255,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/dev-middleware": {
|
||||
"version": "0.76.8",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.76.8.tgz",
|
||||
"integrity": "sha512-KYx7hFME2uYQRCDCqb19ghw51TAdh48PZ5EMpoU2kPA1SKKO9c1bUbpsKRhVZ0bv1QqEX6fjox3c4/WYRozHQA==",
|
||||
"version": "0.76.9",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.76.9.tgz",
|
||||
"integrity": "sha512-xkd3C3dRcmZLjFTEAOvC14q3apMLouIvJViCZY/p1EfCMrNND31dgE1dYrLTiI045WAWMt5bD15i6f7dE2/QWA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@isaacs/ttlcache": "^1.4.1",
|
||||
"@react-native/debugger-frontend": "0.76.8",
|
||||
"@react-native/debugger-frontend": "0.76.9",
|
||||
"chrome-launcher": "^0.15.2",
|
||||
"chromium-edge-launcher": "^0.2.0",
|
||||
"connect": "^3.6.5",
|
||||
@@ -8571,9 +8591,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/normalize-colors": {
|
||||
"version": "0.76.8",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.76.8.tgz",
|
||||
"integrity": "sha512-FRjRvs7RgsXjkbGSOjYSxhX5V70c0IzA/jy3HXeYpATMwD9fOR1DbveLW497QGsVdCa0vThbJUtR8rIzAfpHQA==",
|
||||
"version": "0.76.9",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.76.9.tgz",
|
||||
"integrity": "sha512-TUdMG2JGk72M9d8DYbubdOlrzTYjw+YMe/xOnLU4viDgWRHsCbtRS9x0IAxRjs3amj/7zmK3Atm8jUPvdAc8qw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
@@ -9757,9 +9777,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__generator": {
|
||||
"version": "7.6.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz",
|
||||
"integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==",
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
|
||||
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
@@ -9946,9 +9966,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/luxon": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.0.tgz",
|
||||
"integrity": "sha512-RtEj20xRyG7cRp142MkQpV3GRF8Wo2MtDkKLz65MQs7rM1Lh8bz+HtfPXCCJEYpnDFu6VwAq/Iv2Ikyp9Jw/hw==",
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz",
|
||||
"integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -12320,9 +12340,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/babel-preset-expo": {
|
||||
"version": "12.0.10",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-12.0.10.tgz",
|
||||
"integrity": "sha512-6QE52Bxsp5XRE8t0taKRFTFsmTG0ThQE+PTgCgLY9s8v2Aeh8R+E+riXhSHX6hP+diDmBFBdvLCUTq7kroJb1Q==",
|
||||
"version": "12.0.11",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-12.0.11.tgz",
|
||||
"integrity": "sha512-4m6D92nKEieg+7DXa8uSvpr0GjfuRfM/G0t0I/Q5hF8HleEv5ms3z4dJ+p52qXSJsm760tMqLdO93Ywuoi7cCQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
@@ -12333,7 +12353,7 @@
|
||||
"@babel/plugin-transform-parameters": "^7.22.15",
|
||||
"@babel/preset-react": "^7.22.15",
|
||||
"@babel/preset-typescript": "^7.23.0",
|
||||
"@react-native/babel-preset": "0.76.8",
|
||||
"@react-native/babel-preset": "0.76.9",
|
||||
"babel-plugin-react-native-web": "~0.19.13",
|
||||
"react-refresh": "^0.14.2"
|
||||
},
|
||||
@@ -12393,9 +12413,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/bare-fs": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.0.2.tgz",
|
||||
"integrity": "sha512-S5mmkMesiduMqnz51Bfh0Et9EX0aTCJxhsI4bvzFFLs8Z1AV8RDHadfY5CyLwdoLHgXbNBEN1gQcbEtGwuvixw==",
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.2.tgz",
|
||||
"integrity": "sha512-8wSeOia5B7LwD4+h465y73KOdj5QHsbbuoUfPBi+pXgFJIPuG7SsiOdJuijWMyfid49eD+WivpfY7KT8gbAzBA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
@@ -12547,9 +12567,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bignumber.js": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz",
|
||||
"integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==",
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.2.0.tgz",
|
||||
"integrity": "sha512-JocpCSOixzy5XFJi2ub6IMmV/G9i8Lrm2lZvwBv9xPdglmZM0ufDVBbjbrfU/zuLvBfD7Bv2eYxz9i+OHTgkew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
@@ -13193,9 +13213,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001707",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz",
|
||||
"integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==",
|
||||
"version": "1.0.30001712",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001712.tgz",
|
||||
"integrity": "sha512-MBqPpGYYdQ7/hfKiet9SCI+nmN5/hp4ZzveOJubl5DTAMa5oggjAuoi0Z4onBpKPFI2ePGnQuQIzF3VxDjDJig==",
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -13947,9 +13967,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/config-file-ts/node_modules/typescript": {
|
||||
"version": "5.8.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
|
||||
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -15601,9 +15621,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.129",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.129.tgz",
|
||||
"integrity": "sha512-JlXUemX4s0+9f8mLqib/bHH8gOHf5elKS6KeWG3sk3xozb/JTq/RLXIv8OKUWiK4Ah00Wm88EFj5PYkFr4RUPA==",
|
||||
"version": "1.5.132",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.132.tgz",
|
||||
"integrity": "sha512-QgX9EBvWGmvSRa74zqfnG7+Eno0Ak0vftBll0Pt2/z5b3bEGYL6OUXLgKPtvx73dn3dvwrlyVkjPKRRlhLYTEg==",
|
||||
"devOptional": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -16027,14 +16047,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-prettier": {
|
||||
"version": "5.2.5",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.5.tgz",
|
||||
"integrity": "sha512-IKKP8R87pJyMl7WWamLgPkloB16dagPIdd2FjBDbyRYPKo93wS/NbCOPh6gH+ieNLC+XZrhJt/kWj0PS/DFdmg==",
|
||||
"version": "5.2.6",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.6.tgz",
|
||||
"integrity": "sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prettier-linter-helpers": "^1.0.0",
|
||||
"synckit": "^0.10.2"
|
||||
"synckit": "^0.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
@@ -16642,21 +16662,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/expo": {
|
||||
"version": "52.0.42",
|
||||
"resolved": "https://registry.npmjs.org/expo/-/expo-52.0.42.tgz",
|
||||
"integrity": "sha512-t+PRYIzzPFAlF99OVJOjZwM1glLhN85XGD6vmeg6uwpADDILl9yw4dfy0DXL4hot5GJkAGaZ+uOHUljV4kC2Bg==",
|
||||
"version": "52.0.44",
|
||||
"resolved": "https://registry.npmjs.org/expo/-/expo-52.0.44.tgz",
|
||||
"integrity": "sha512-qj3+MWxmqLyBaYQ8jDOvVLEgSqNplH3cf+nDhxCo4C1cpTPD1u/HGh1foibtaeuCYLHsE5km1lrcOpRbFJ4luQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.0",
|
||||
"@expo/cli": "0.22.23",
|
||||
"@expo/cli": "0.22.24",
|
||||
"@expo/config": "~10.0.11",
|
||||
"@expo/config-plugins": "~9.0.17",
|
||||
"@expo/fingerprint": "0.11.11",
|
||||
"@expo/metro-config": "0.19.12",
|
||||
"@expo/vector-icons": "^14.0.0",
|
||||
"babel-preset-expo": "~12.0.10",
|
||||
"babel-preset-expo": "~12.0.11",
|
||||
"expo-asset": "~11.0.5",
|
||||
"expo-constants": "~17.0.8",
|
||||
"expo-file-system": "~18.0.12",
|
||||
@@ -18566,9 +18586,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/image-size": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.0.tgz",
|
||||
"integrity": "sha512-4S8fwbO6w3GeCVN6OPtA9I5IGKkcDMPcKndtUlpJuCwu7JLjtj7JZpwqLuyY2nrmQT3AWsCJLSKPsc2mPBSl3w==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
|
||||
"integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
@@ -23524,9 +23544,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nostr-tools": {
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.11.1.tgz",
|
||||
"integrity": "sha512-+Oj5t+behIkU9kh3go5wg8Aa5oR7euBU9gOItUNapJe5Gaa+KPzMuTIN+rMRK3DaZ4Zt6RM4kR/ddwstzGKf7g==",
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.12.0.tgz",
|
||||
"integrity": "sha512-pUWEb020gTvt1XZvTa8AKNIHWFapjsv2NKyk43Ez2nnvz6WSXsrTFE0XtkNLSRBjPn6EpxumKeNiVzLz74jNSA==",
|
||||
"license": "Unlicense",
|
||||
"dependencies": {
|
||||
"@noble/ciphers": "^0.5.1",
|
||||
@@ -28790,9 +28810,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/synckit": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.10.3.tgz",
|
||||
"integrity": "sha512-R1urvuyiTaWfeCggqEvpDJwAlDVdsT9NM+IP//Tk2x7qHCkSvBk/fwFgw/TLAHzZlrAnnazMcRw0ZD8HlYFTEQ==",
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.2.tgz",
|
||||
"integrity": "sha512-1IUffI8zZ8qUMB3NUJIjk0RpLroG/8NkQDAWH1NbB2iJ0/5pn3M8rxfNzMz4GH9OnYaGYn31LEDSXJp/qIlxgA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -28803,7 +28823,7 @@
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/unts"
|
||||
"url": "https://opencollective.com/synckit"
|
||||
}
|
||||
},
|
||||
"node_modules/synckit/node_modules/tslib": {
|
||||
@@ -29614,24 +29634,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typeorm": {
|
||||
"version": "0.3.21",
|
||||
"resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.21.tgz",
|
||||
"integrity": "sha512-lh4rUWl1liZGjyPTWpwcK8RNI5x4ekln+/JJOox1wCd7xbucYDOXWD+1cSzTN3L0wbTGxxOtloM5JlxbOxEufA==",
|
||||
"version": "0.3.22",
|
||||
"resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.22.tgz",
|
||||
"integrity": "sha512-P/Tsz3UpJ9+K0oryC0twK5PO27zejLYYwMsE8SISfZc1lVHX+ajigiOyWsKbuXpEFMjD9z7UjLzY3+ElVOMMDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sqltools/formatter": "^1.2.5",
|
||||
"ansis": "^3.9.0",
|
||||
"ansis": "^3.17.0",
|
||||
"app-root-path": "^3.1.0",
|
||||
"buffer": "^6.0.3",
|
||||
"dayjs": "^1.11.9",
|
||||
"debug": "^4.3.4",
|
||||
"dotenv": "^16.0.3",
|
||||
"dayjs": "^1.11.13",
|
||||
"debug": "^4.4.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"glob": "^10.4.5",
|
||||
"sha.js": "^2.4.11",
|
||||
"sql-highlight": "^6.0.0",
|
||||
"tslib": "^2.5.0",
|
||||
"uuid": "^11.0.5",
|
||||
"yargs": "^17.6.2"
|
||||
"tslib": "^2.8.1",
|
||||
"uuid": "^11.1.0",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"typeorm": "cli.js",
|
||||
@@ -29645,12 +29665,12 @@
|
||||
"url": "https://opencollective.com/typeorm"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@google-cloud/spanner": "^5.18.0",
|
||||
"@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0",
|
||||
"@sap/hana-client": "^2.12.25",
|
||||
"better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
|
||||
"hdb-pool": "^0.1.6",
|
||||
"ioredis": "^5.0.4",
|
||||
"mongodb": "^5.8.0",
|
||||
"mongodb": "^5.8.0 || ^6.0.0",
|
||||
"mssql": "^9.1.1 || ^10.0.1 || ^11.0.1",
|
||||
"mysql2": "^2.2.5 || ^3.0.1",
|
||||
"oracledb": "^6.3.0",
|
||||
@@ -29662,7 +29682,7 @@
|
||||
"sql.js": "^1.4.0",
|
||||
"sqlite3": "^5.0.3",
|
||||
"ts-node": "^10.7.0",
|
||||
"typeorm-aurora-data-api-driver": "^2.0.0"
|
||||
"typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@google-cloud/spanner": {
|
||||
@@ -29718,6 +29738,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/typeorm/node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/typeorm/node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
@@ -29753,6 +29790,18 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/typeorm/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typeorm/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/typeorm/node_modules/uuid": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
@@ -30206,9 +30255,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.16",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.16.tgz",
|
||||
"integrity": "sha512-Y5gnfp4NemVfgOTDQAunSD4346fal44L9mszGGY/e+qxsRT5y1sMlS/8tiQ8AFAp+MFgYNSINdfEchJiPm41vQ==",
|
||||
"version": "5.4.17",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.17.tgz",
|
||||
"integrity": "sha512-5+VqZryDj4wgCs55o9Lp+p8GE78TLVg0lasCH5xFZ4jacZjtqZa6JUw9/p0WeAojaOfncSM6v77InkFPGnvPvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -44,8 +44,10 @@
|
||||
"dependencies": {
|
||||
"@capacitor/android": "^6.2.0",
|
||||
"@capacitor/app": "^6.0.0",
|
||||
"@capacitor/camera": "^6.0.0",
|
||||
"@capacitor/cli": "^6.2.0",
|
||||
"@capacitor/core": "^6.2.0",
|
||||
"@capacitor/filesystem": "^6.0.0",
|
||||
"@capacitor/ios": "^6.2.0",
|
||||
"@dicebear/collection": "^5.4.1",
|
||||
"@dicebear/core": "^5.4.1",
|
||||
|
||||
193
src/components/DataExportSection.vue
Normal file
@@ -0,0 +1,193 @@
|
||||
/** * Data Export Section Component * * Provides UI and functionality for
|
||||
exporting user data and backing up identifier seeds. * Includes buttons for seed
|
||||
backup and database export, with platform-specific download instructions. * *
|
||||
@component * @displayName DataExportSection * @example * ```vue *
|
||||
<DataExportSection :active-did="currentDid" />
|
||||
* ``` */
|
||||
|
||||
<template>
|
||||
<div
|
||||
id="sectionDataExport"
|
||||
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
|
||||
>
|
||||
<div class="mb-2 font-bold">Data Export</div>
|
||||
<router-link
|
||||
v-if="activeDid"
|
||||
:to="{ name: 'seed-backup' }"
|
||||
class="block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2"
|
||||
>
|
||||
Backup Identifier Seed
|
||||
</router-link>
|
||||
|
||||
<button
|
||||
:class="computedStartDownloadLinkClassNames()"
|
||||
class="block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
|
||||
@click="exportDatabase()"
|
||||
>
|
||||
Download Settings & Contacts
|
||||
<br />
|
||||
(excluding Identifier Data)
|
||||
</button>
|
||||
<a
|
||||
v-if="platformService?.needsSecondaryDownloadLink()"
|
||||
ref="downloadLink"
|
||||
:class="computedDownloadLinkClassNames()"
|
||||
class="block w-full text-center text-md bg-gradient-to-b from-green-500 to-green-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
|
||||
>
|
||||
If no download happened yet, click again here to download now.
|
||||
</a>
|
||||
<div
|
||||
v-if="platformService?.getExportInstructions().length > 0"
|
||||
class="mt-4"
|
||||
>
|
||||
<p
|
||||
v-for="instruction in platformService?.getExportInstructions()"
|
||||
:key="instruction"
|
||||
class="list-disc list-outside ml-4"
|
||||
>
|
||||
{{ instruction }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-facing-decorator";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { db } from "../db/index";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||
import { PlatformService } from "../services/PlatformService";
|
||||
|
||||
/**
|
||||
* @vue-component
|
||||
* Data Export Section Component
|
||||
* Handles database export and seed backup functionality with platform-specific behavior
|
||||
*/
|
||||
@Component
|
||||
export default class DataExportSection extends Vue {
|
||||
/**
|
||||
* Notification function injected by Vue
|
||||
* Used to show success/error messages to the user
|
||||
*/
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
/**
|
||||
* Active DID (Decentralized Identifier) of the user
|
||||
* Controls visibility of seed backup option
|
||||
* @required
|
||||
*/
|
||||
@Prop({ required: true }) readonly activeDid!: string;
|
||||
|
||||
/**
|
||||
* URL for the database export download
|
||||
* Created and revoked dynamically during export process
|
||||
* Only used in web platform
|
||||
*/
|
||||
downloadUrl = "";
|
||||
|
||||
/**
|
||||
* Platform service instance for platform-specific operations
|
||||
*/
|
||||
private platformService?: PlatformService;
|
||||
|
||||
/**
|
||||
* Lifecycle hook to clean up resources
|
||||
* Revokes object URL when component is unmounted (web platform only)
|
||||
*/
|
||||
beforeUnmount() {
|
||||
if (this.downloadUrl && this.platformService?.needsDownloadCleanup()) {
|
||||
URL.revokeObjectURL(this.downloadUrl);
|
||||
}
|
||||
}
|
||||
|
||||
async mounted() {
|
||||
this.platformService = await PlatformServiceFactory.getInstance();
|
||||
logger.log(
|
||||
"DataExportSection mounted on platform:",
|
||||
process.env.VITE_PLATFORM,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the database to a JSON file
|
||||
* Uses platform-specific methods for saving the exported data
|
||||
* Shows success/error notifications to user
|
||||
*
|
||||
* @throws {Error} If export fails
|
||||
* @emits {Notification} Success or error notification
|
||||
*/
|
||||
public async exportDatabase() {
|
||||
try {
|
||||
if (!this.platformService) {
|
||||
this.platformService = await PlatformServiceFactory.getInstance();
|
||||
}
|
||||
logger.log(
|
||||
"Starting database export on platform:",
|
||||
process.env.VITE_PLATFORM,
|
||||
);
|
||||
|
||||
const blob = await db.export({ prettyJson: true });
|
||||
const fileName = `${db.name}-backup.json`;
|
||||
logger.log("Database export details:", {
|
||||
fileName,
|
||||
blobSize: `${blob.size} bytes`,
|
||||
platform: process.env.VITE_PLATFORM,
|
||||
});
|
||||
|
||||
await this.platformService.exportDatabase(blob, fileName);
|
||||
logger.log("Database export completed successfully:", {
|
||||
fileName,
|
||||
platform: process.env.VITE_PLATFORM,
|
||||
});
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Export Successful",
|
||||
text: this.platformService.getExportSuccessMessage(),
|
||||
},
|
||||
-1,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Database export failed:", {
|
||||
error,
|
||||
platform: process.env.VITE_PLATFORM,
|
||||
});
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Export Error",
|
||||
text: "There was an error exporting the data.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes class names for the initial download button
|
||||
* @returns Object with 'hidden' class when download is in progress (web platform only)
|
||||
*/
|
||||
public computedStartDownloadLinkClassNames() {
|
||||
return {
|
||||
hidden:
|
||||
this.downloadUrl && this.platformService?.needsSecondaryDownloadLink(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes class names for the secondary download link
|
||||
* @returns Object with 'hidden' class when no download is available or not on web platform
|
||||
*/
|
||||
public computedDownloadLinkClassNames() {
|
||||
return {
|
||||
hidden:
|
||||
!this.downloadUrl ||
|
||||
!this.platformService?.needsSecondaryDownloadLink(),
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -40,11 +40,6 @@
|
||||
}"
|
||||
class="max-h-[90vh] max-w-[90vw] object-contain"
|
||||
/>
|
||||
<!-- This gives a round cropper.
|
||||
:presetMode="{
|
||||
mode: 'round',
|
||||
}"
|
||||
-->
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="flex justify-center">
|
||||
@@ -74,95 +69,80 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else ref="cameraContainer">
|
||||
<!--
|
||||
Camera "resolution" doesn't change how it shows on screen but rather stretches the result,
|
||||
eg. the following which just stretches it vertically:
|
||||
:resolution="{ width: 375, height: 812 }"
|
||||
-->
|
||||
<camera
|
||||
ref="camera"
|
||||
facing-mode="environment"
|
||||
autoplay
|
||||
@started="cameraStarted()"
|
||||
>
|
||||
<div
|
||||
class="absolute portrait:bottom-0 portrait:left-0 portrait:right-0 portrait:pb-2 landscape:right-0 landscape:top-0 landscape:bottom-0 landscape:pr-4 flex landscape:flex-row justify-center items-center"
|
||||
<div v-else>
|
||||
<div class="flex flex-col items-center justify-center gap-4 p-4">
|
||||
<button
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
||||
@click="takePhoto"
|
||||
>
|
||||
<button
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
||||
@click="takeImage()"
|
||||
>
|
||||
<font-awesome icon="camera" class="w-[1em]"></font-awesome>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="absolute portrait:bottom-2 portrait:right-16 landscape:right-0 landscape:bottom-16 landscape:pr-4 flex justify-center items-center"
|
||||
<font-awesome icon="camera" class="w-[1em]" />
|
||||
</button>
|
||||
<button
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
||||
@click="pickPhoto"
|
||||
>
|
||||
<button
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
||||
@click="swapMirrorClass()"
|
||||
>
|
||||
<font-awesome icon="left-right" class="w-[1em]"></font-awesome>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="numDevices > 1" class="absolute bottom-2 right-4">
|
||||
<button
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
||||
@click="switchCamera()"
|
||||
>
|
||||
<font-awesome icon="rotate" class="w-[1em]"></font-awesome>
|
||||
</button>
|
||||
</div>
|
||||
</camera>
|
||||
<font-awesome icon="image" class="w-[1em]" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/**
|
||||
* PhotoDialog.vue - Cross-platform photo capture and selection component
|
||||
*
|
||||
* This component provides a unified interface for taking photos and selecting images
|
||||
* across different platforms using the PlatformService.
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @file PhotoDialog.vue
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import Camera from "simple-vue-camera";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import VuePictureCropper, { cropper } from "vue-picture-cropper";
|
||||
|
||||
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
|
||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { accessToken } from "../libs/crypto";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||
import { PlatformService } from "../services/PlatformService";
|
||||
|
||||
@Component({ components: { Camera, VuePictureCropper } })
|
||||
@Component({ components: { VuePictureCropper } })
|
||||
export default class PhotoDialog extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
activeDeviceNumber = 0;
|
||||
activeDid = "";
|
||||
blob?: Blob;
|
||||
claimType = "";
|
||||
crop = false;
|
||||
fileName?: string;
|
||||
mirror = false;
|
||||
numDevices = 0;
|
||||
setImageCallback: (arg: string) => void = () => {};
|
||||
showRetry = true;
|
||||
uploading = false;
|
||||
visible = false;
|
||||
|
||||
private platformService?: PlatformService;
|
||||
URL = window.URL || window.webkitURL;
|
||||
|
||||
async mounted() {
|
||||
try {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
this.platformService = await PlatformServiceFactory.getInstance();
|
||||
} catch (err: unknown) {
|
||||
logger.error("Error retrieving settings from database:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: err.message || "There was an error retrieving your settings.",
|
||||
text:
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "There was an error retrieving your settings.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
@@ -173,7 +153,7 @@ export default class PhotoDialog extends Vue {
|
||||
setImageFn: (arg: string) => void,
|
||||
claimType: string,
|
||||
crop?: boolean,
|
||||
blob?: Blob, // for image upload, just to use the cropping function
|
||||
blob?: Blob,
|
||||
inputFileName?: string,
|
||||
) {
|
||||
this.visible = true;
|
||||
@@ -187,7 +167,6 @@ export default class PhotoDialog extends Vue {
|
||||
if (blob) {
|
||||
this.blob = blob;
|
||||
this.fileName = inputFileName;
|
||||
// doesn't make sense to retry the file upload; they can cancel if they picked the wrong one
|
||||
this.showRetry = false;
|
||||
} else {
|
||||
this.blob = undefined;
|
||||
@@ -205,85 +184,47 @@ export default class PhotoDialog extends Vue {
|
||||
this.blob = undefined;
|
||||
}
|
||||
|
||||
async cameraStarted() {
|
||||
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
|
||||
if (cameraComponent) {
|
||||
this.numDevices = (await cameraComponent.devices(["videoinput"])).length;
|
||||
this.mirror = cameraComponent.facingMode === "user";
|
||||
// figure out which device is active
|
||||
const currentDeviceId = cameraComponent.currentDeviceID();
|
||||
const devices = await cameraComponent.devices(["videoinput"]);
|
||||
this.activeDeviceNumber = devices.findIndex(
|
||||
(device) => device.deviceId === currentDeviceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async switchCamera() {
|
||||
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
|
||||
this.activeDeviceNumber = (this.activeDeviceNumber + 1) % this.numDevices;
|
||||
const devices = await cameraComponent?.devices(["videoinput"]);
|
||||
await cameraComponent?.changeCamera(
|
||||
devices[this.activeDeviceNumber].deviceId,
|
||||
);
|
||||
}
|
||||
|
||||
async takeImage(/* payload: MouseEvent */) {
|
||||
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
|
||||
|
||||
/**
|
||||
* This logic to set the image height & width correctly.
|
||||
* Without it, the portrait orientation ends up with an image that is stretched horizontally.
|
||||
* Note that it's the same with raw browser Javascript; see the "drawImage" example below.
|
||||
* Now that I've done it, I can't explain why it works.
|
||||
*/
|
||||
let imageHeight = cameraComponent?.resolution?.height;
|
||||
let imageWidth = cameraComponent?.resolution?.width;
|
||||
const initialImageRatio = imageWidth / imageHeight;
|
||||
const windowRatio = window.innerWidth / window.innerHeight;
|
||||
if (initialImageRatio > 1 && windowRatio < 1) {
|
||||
// the image is wider than it is tall, and the window is taller than it is wide
|
||||
// For some reason, mobile in portrait orientation renders a horizontally-stretched image.
|
||||
// We're gonna force it opposite.
|
||||
imageHeight = cameraComponent?.resolution?.width;
|
||||
imageWidth = cameraComponent?.resolution?.height;
|
||||
} else if (initialImageRatio < 1 && windowRatio > 1) {
|
||||
// the image is taller than it is wide, and the window is wider than it is tall
|
||||
// Haven't seen this happen, but we'll do it just in case.
|
||||
imageHeight = cameraComponent?.resolution?.width;
|
||||
imageWidth = cameraComponent?.resolution?.height;
|
||||
}
|
||||
const newImageRatio = imageWidth / imageHeight;
|
||||
if (newImageRatio < windowRatio) {
|
||||
// the image is a taller ratio than the window, so fit the height first
|
||||
imageHeight = window.innerHeight / 2;
|
||||
imageWidth = imageHeight * newImageRatio;
|
||||
} else {
|
||||
// the image is a wider ratio than the window, so fit the width first
|
||||
imageWidth = window.innerWidth / 2;
|
||||
imageHeight = imageWidth / newImageRatio;
|
||||
}
|
||||
|
||||
// The resolution is only necessary because of that mobile portrait-orientation case.
|
||||
// The mobile emulation in a browser shows something stretched vertically, but real devices work fine.
|
||||
this.blob =
|
||||
(await cameraComponent?.snapshot({
|
||||
height: imageHeight,
|
||||
width: imageWidth,
|
||||
})) || undefined;
|
||||
// png is default
|
||||
this.fileName = "snapshot.png";
|
||||
if (!this.blob) {
|
||||
async takePhoto() {
|
||||
try {
|
||||
if (!this.platformService) {
|
||||
this.platformService = await PlatformServiceFactory.getInstance();
|
||||
}
|
||||
const result = await this.platformService.takePicture();
|
||||
this.blob = result.blob;
|
||||
this.fileName = result.fileName;
|
||||
} catch (error: unknown) {
|
||||
logger.error("Error taking picture:", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "There was an error taking the picture. Please try again.",
|
||||
text: "Failed to take picture. Please try again.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async pickPhoto() {
|
||||
try {
|
||||
if (!this.platformService) {
|
||||
this.platformService = await PlatformServiceFactory.getInstance();
|
||||
}
|
||||
const result = await this.platformService.pickImage();
|
||||
this.blob = result.blob;
|
||||
this.fileName = result.fileName;
|
||||
} catch (error: unknown) {
|
||||
logger.error("Error picking image:", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Failed to pick image. Please try again.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,51 +236,6 @@ export default class PhotoDialog extends Vue {
|
||||
this.blob = undefined;
|
||||
}
|
||||
|
||||
/****
|
||||
|
||||
Here's an approach to photo capture without a library. It has similar quirks.
|
||||
Now that we've fixed styling for simple-vue-camera, it's not critical to refactor. Maybe someday.
|
||||
|
||||
<button id="start-camera" @click="cameraClicked">Start Camera</button>
|
||||
<video id="video" width="320" height="240" autoplay></video>
|
||||
<button id="snap-photo" @click="photoSnapped">Snap Photo</button>
|
||||
<canvas id="canvas" width="320" height="240"></canvas>
|
||||
|
||||
async cameraClicked() {
|
||||
const video = document.querySelector("#video");
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: true,
|
||||
audio: false,
|
||||
});
|
||||
if (video instanceof HTMLVideoElement) {
|
||||
video.srcObject = stream;
|
||||
}
|
||||
}
|
||||
photoSnapped() {
|
||||
const video = document.querySelector("#video");
|
||||
const canvas = document.querySelector("#canvas");
|
||||
if (
|
||||
canvas instanceof HTMLCanvasElement &&
|
||||
video instanceof HTMLVideoElement
|
||||
) {
|
||||
canvas
|
||||
?.getContext("2d")
|
||||
?.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
// ... or set the blob:
|
||||
// canvas?.toBlob(
|
||||
// (blob) => {
|
||||
// this.blob = blob;
|
||||
// },
|
||||
// "image/jpeg",
|
||||
// 1,
|
||||
// );
|
||||
|
||||
// data url of the image
|
||||
const image_data_url = canvas?.toDataURL("image/jpeg");
|
||||
}
|
||||
}
|
||||
****/
|
||||
|
||||
async uploadImage() {
|
||||
this.uploading = true;
|
||||
|
||||
@@ -350,11 +246,9 @@ export default class PhotoDialog extends Vue {
|
||||
const token = await accessToken(this.activeDid);
|
||||
const headers = {
|
||||
Authorization: "Bearer " + token,
|
||||
// axios fills in Content-Type of multipart/form-data
|
||||
};
|
||||
const formData = new FormData();
|
||||
if (!this.blob) {
|
||||
// yeah, this should never happen, but it helps with subsequent type checking
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -367,7 +261,7 @@ export default class PhotoDialog extends Vue {
|
||||
this.uploading = false;
|
||||
return;
|
||||
}
|
||||
formData.append("image", this.blob, this.fileName || "snapshot.png");
|
||||
formData.append("image", this.blob, this.fileName || "photo.jpg");
|
||||
formData.append("claimType", this.claimType);
|
||||
try {
|
||||
if (
|
||||
@@ -387,14 +281,64 @@ export default class PhotoDialog extends Vue {
|
||||
|
||||
this.close();
|
||||
this.setImageCallback(response.data.url as string);
|
||||
} catch (error) {
|
||||
logger.error("Error uploading the image", error);
|
||||
} catch (error: unknown) {
|
||||
// Log the raw error first
|
||||
logger.error("Raw error object:", JSON.stringify(error, null, 2));
|
||||
|
||||
let errorMessage = "There was an error saving the picture.";
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
const status = error.response?.status;
|
||||
const statusText = error.response?.statusText;
|
||||
const data = error.response?.data;
|
||||
|
||||
// Log detailed error information
|
||||
logger.error("Upload error details:", {
|
||||
status,
|
||||
statusText,
|
||||
data: JSON.stringify(data, null, 2),
|
||||
message: error.message,
|
||||
config: {
|
||||
url: error.config?.url,
|
||||
method: error.config?.method,
|
||||
headers: error.config?.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (status === 401) {
|
||||
errorMessage = "Authentication failed. Please try logging in again.";
|
||||
} else if (status === 413) {
|
||||
errorMessage = "Image file is too large. Please try a smaller image.";
|
||||
} else if (status === 415) {
|
||||
errorMessage =
|
||||
"Unsupported image format. Please try a different image.";
|
||||
} else if (status && status >= 500) {
|
||||
errorMessage = "Server error. Please try again later.";
|
||||
} else if (data?.message) {
|
||||
errorMessage = data.message;
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
// Log non-Axios error with full details
|
||||
logger.error("Non-Axios error details:", {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
error: JSON.stringify(error, Object.getOwnPropertyNames(error), 2),
|
||||
});
|
||||
} else {
|
||||
// Log any other type of error
|
||||
logger.error("Unknown error type:", {
|
||||
error: JSON.stringify(error, null, 2),
|
||||
type: typeof error,
|
||||
});
|
||||
}
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "There was an error saving the picture.",
|
||||
text: errorMessage,
|
||||
},
|
||||
5000,
|
||||
);
|
||||
@@ -402,17 +346,6 @@ export default class PhotoDialog extends Vue {
|
||||
this.blob = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
swapMirrorClass() {
|
||||
this.mirror = !this.mirror;
|
||||
if (this.mirror) {
|
||||
(this.$refs.cameraContainer as HTMLElement).classList.add("mirror-video");
|
||||
} else {
|
||||
(this.$refs.cameraContainer as HTMLElement).classList.remove(
|
||||
"mirror-video",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -438,12 +371,4 @@ export default class PhotoDialog extends Vue {
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.mirror-video {
|
||||
transform: scaleX(-1);
|
||||
-webkit-transform: scaleX(-1); /* For Safari */
|
||||
-moz-transform: scaleX(-1); /* For Firefox */
|
||||
-ms-transform: scaleX(-1); /* For IE */
|
||||
-o-transform: scaleX(-1); /* For Opera */
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
<template>
|
||||
<div v-if="visible" class="dialog-overlay">
|
||||
<div class="dialog">
|
||||
<h1 class="text-xl font-bold text-center mb-4">Registration Needed</h1>
|
||||
|
||||
Before you can perform certain actions in the app, you need to register an account. It's easy, and it's FREE!
|
||||
|
||||
<div class="mt-8">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2"
|
||||
@click="onClickSaveChanges()"
|
||||
>
|
||||
Register
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2"
|
||||
@click="onClickCancel()"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from "vue-facing-decorator";
|
||||
|
||||
import { NotificationIface } from "../constants/app";
|
||||
|
||||
@Component
|
||||
export default class RegistrationGate extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
callback: (text: string, expiresAt: string) => void = () => {};
|
||||
inviteIdentifier = "";
|
||||
text = "";
|
||||
visible = false;
|
||||
expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 7)
|
||||
.toISOString()
|
||||
.substring(0, 10);
|
||||
|
||||
async open(
|
||||
inviteIdentifier: string,
|
||||
aCallback: (text: string, expiresAt: string) => void,
|
||||
) {
|
||||
this.callback = aCallback;
|
||||
this.inviteIdentifier = inviteIdentifier;
|
||||
this.visible = true;
|
||||
}
|
||||
|
||||
async onClickSaveChanges() {
|
||||
if (!this.expiresAt) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "warning",
|
||||
title: "Needs Expiration",
|
||||
text: "You must select an expiration date.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
} else {
|
||||
this.callback(this.text, this.expiresAt);
|
||||
this.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
onClickCancel() {
|
||||
this.visible = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dialog-overlay {
|
||||
z-index: 50;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
</style>
|
||||
127
src/services/PlatformService.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Represents the result of an image capture or selection operation.
|
||||
* Contains both the image data as a Blob and the associated filename.
|
||||
*/
|
||||
export interface ImageResult {
|
||||
/** The image data as a Blob object */
|
||||
blob: Blob;
|
||||
/** The filename associated with the image */
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform-agnostic interface for handling platform-specific operations.
|
||||
* Provides a common API for file system operations, camera interactions,
|
||||
* platform detection, and deep linking across different platforms
|
||||
* (web, mobile, desktop).
|
||||
*/
|
||||
export interface PlatformService {
|
||||
// File system operations
|
||||
/**
|
||||
* Reads the contents of a file at the specified path.
|
||||
* @param path - The path to the file to read
|
||||
* @returns Promise resolving to the file contents as a string
|
||||
*/
|
||||
readFile(path: string): Promise<string>;
|
||||
|
||||
/**
|
||||
* Writes content to a file at the specified path.
|
||||
* @param path - The path where the file should be written
|
||||
* @param content - The content to write to the file
|
||||
* @returns Promise that resolves when the write is complete
|
||||
*/
|
||||
writeFile(path: string, content: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Deletes a file at the specified path.
|
||||
* @param path - The path to the file to delete
|
||||
* @returns Promise that resolves when the deletion is complete
|
||||
*/
|
||||
deleteFile(path: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Lists all files in the specified directory.
|
||||
* @param directory - The directory path to list
|
||||
* @returns Promise resolving to an array of filenames
|
||||
*/
|
||||
listFiles(directory: string): Promise<string[]>;
|
||||
|
||||
/**
|
||||
* Exports a database blob to a file, handling platform-specific save operations.
|
||||
* @param blob - The database blob to export
|
||||
* @param fileName - The name of the file to save
|
||||
* @returns Promise that resolves when the export is complete
|
||||
* @throws Error if export fails
|
||||
*/
|
||||
exportDatabase(blob: Blob, fileName: string): Promise<void>;
|
||||
|
||||
// Camera operations
|
||||
/**
|
||||
* Activates the device camera to take a picture.
|
||||
* @returns Promise resolving to the captured image result
|
||||
*/
|
||||
takePicture(): Promise<ImageResult>;
|
||||
|
||||
/**
|
||||
* Opens a file picker to select an existing image.
|
||||
* @returns Promise resolving to the selected image result
|
||||
*/
|
||||
pickImage(): Promise<ImageResult>;
|
||||
|
||||
// Platform specific features
|
||||
/**
|
||||
* Checks if the current platform is Capacitor (mobile).
|
||||
* @returns true if running on Capacitor
|
||||
*/
|
||||
isCapacitor(): boolean;
|
||||
|
||||
/**
|
||||
* Checks if the current platform is Electron (desktop).
|
||||
* @returns true if running on Electron
|
||||
*/
|
||||
isElectron(): boolean;
|
||||
|
||||
/**
|
||||
* Checks if the current platform is PyWebView.
|
||||
* @returns true if running on PyWebView
|
||||
*/
|
||||
isPyWebView(): boolean;
|
||||
|
||||
/**
|
||||
* Checks if the current platform is web browser.
|
||||
* @returns true if running in a web browser
|
||||
*/
|
||||
isWeb(): boolean;
|
||||
|
||||
// Deep linking
|
||||
/**
|
||||
* Handles deep link URLs for the application.
|
||||
* @param url - The deep link URL to handle
|
||||
* @returns Promise that resolves when the deep link has been handled
|
||||
*/
|
||||
handleDeepLink(url: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Gets platform-specific instructions for saving exported files
|
||||
* @returns Array of instruction strings for the current platform
|
||||
*/
|
||||
getExportInstructions(): string[];
|
||||
|
||||
/**
|
||||
* Gets the success message for database export
|
||||
* @returns Success message appropriate for the current platform
|
||||
*/
|
||||
getExportSuccessMessage(): string;
|
||||
|
||||
/**
|
||||
* Checks if the platform requires a secondary download link
|
||||
* @returns true if platform needs a secondary download link
|
||||
*/
|
||||
needsSecondaryDownloadLink(): boolean;
|
||||
|
||||
/**
|
||||
* Checks if the platform needs cleanup after download
|
||||
* @returns true if platform needs cleanup after download
|
||||
*/
|
||||
needsDownloadCleanup(): boolean;
|
||||
}
|
||||
69
src/services/PlatformServiceFactory.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { PlatformService } from "./PlatformService";
|
||||
import { WebPlatformService } from "./platforms/WebPlatformService";
|
||||
|
||||
/**
|
||||
* Factory class for creating platform-specific service implementations.
|
||||
* Implements the Singleton pattern to ensure only one instance of PlatformService exists.
|
||||
*
|
||||
* The factory determines which platform implementation to use based on the VITE_PLATFORM
|
||||
* environment variable. Supported platforms are:
|
||||
* - capacitor: Mobile platform using Capacitor
|
||||
* - electron: Desktop platform using Electron
|
||||
* - pywebview: Python WebView implementation
|
||||
* - web: Default web platform (fallback)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const platformService = PlatformServiceFactory.getInstance();
|
||||
* await platformService.takePicture();
|
||||
* ```
|
||||
*/
|
||||
export class PlatformServiceFactory {
|
||||
private static instance: PlatformService | null = null;
|
||||
|
||||
/**
|
||||
* Gets or creates the singleton instance of PlatformService.
|
||||
* Creates the appropriate platform-specific implementation based on environment.
|
||||
*
|
||||
* @returns {PlatformService} The singleton instance of PlatformService
|
||||
*/
|
||||
public static async getInstance(): Promise<PlatformService> {
|
||||
if (PlatformServiceFactory.instance) {
|
||||
return PlatformServiceFactory.instance;
|
||||
}
|
||||
|
||||
const platform = process.env.VITE_PLATFORM || "web";
|
||||
let service: PlatformService;
|
||||
|
||||
switch (platform) {
|
||||
case "capacitor": {
|
||||
const { CapacitorPlatformService } = await import(
|
||||
"./platforms/CapacitorPlatformService"
|
||||
);
|
||||
service = new CapacitorPlatformService();
|
||||
break;
|
||||
}
|
||||
case "electron": {
|
||||
const { ElectronPlatformService } = await import(
|
||||
"./platforms/ElectronPlatformService"
|
||||
);
|
||||
service = new ElectronPlatformService();
|
||||
break;
|
||||
}
|
||||
case "pywebview": {
|
||||
const { PyWebViewPlatformService } = await import(
|
||||
"./platforms/PyWebViewPlatformService"
|
||||
);
|
||||
service = new PyWebViewPlatformService();
|
||||
break;
|
||||
}
|
||||
case "web":
|
||||
default:
|
||||
service = new WebPlatformService();
|
||||
break;
|
||||
}
|
||||
|
||||
PlatformServiceFactory.instance = service;
|
||||
return service;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,40 @@
|
||||
/**
|
||||
* API error handling utilities for the application.
|
||||
* Provides centralized error handling for API requests with platform-specific logging.
|
||||
*
|
||||
* @module api
|
||||
*/
|
||||
|
||||
import { AxiosError } from "axios";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
/**
|
||||
* Handles API errors with platform-specific logging and error processing.
|
||||
*
|
||||
* @param error - The Axios error object from the failed request
|
||||
* @param endpoint - The API endpoint that was called
|
||||
* @returns null for rate limit errors (400), throws the error otherwise
|
||||
* @throws The original error for non-rate-limit cases
|
||||
*
|
||||
* @remarks
|
||||
* Special handling includes:
|
||||
* - Enhanced logging for Capacitor platform
|
||||
* - Rate limit detection and handling
|
||||
* - Detailed error information logging including:
|
||||
* - Error message
|
||||
* - HTTP status
|
||||
* - Response data
|
||||
* - Request configuration (URL, method, headers)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* try {
|
||||
* await api.getData();
|
||||
* } catch (error) {
|
||||
* handleApiError(error as AxiosError, '/api/data');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const handleApiError = (error: AxiosError, endpoint: string) => {
|
||||
if (process.env.VITE_PLATFORM === "capacitor") {
|
||||
logger.error(`[Capacitor API Error] ${endpoint}:`, {
|
||||
|
||||
@@ -23,6 +23,23 @@
|
||||
* - Query parameter validation and sanitization
|
||||
* - Type-safe parameter passing to router
|
||||
*
|
||||
* Deep Link Format:
|
||||
* timesafari://<route>[/<param>][?queryParam1=value1&queryParam2=value2]
|
||||
*
|
||||
* Supported Routes:
|
||||
* - user-profile: View user profile
|
||||
* - project-details: View project details
|
||||
* - onboard-meeting-setup: Setup onboarding meeting
|
||||
* - invite-one-accept: Accept invitation
|
||||
* - contact-import: Import contacts
|
||||
* - confirm-gift: Confirm gift
|
||||
* - claim: View claim
|
||||
* - claim-cert: View claim certificate
|
||||
* - claim-add-raw: Add raw claim
|
||||
* - contact-edit: Edit contact
|
||||
* - contacts: View contacts
|
||||
* - did: View DID
|
||||
*
|
||||
* @example
|
||||
* const handler = new DeepLinkHandler(router);
|
||||
* await handler.handleDeepLink("timesafari://claim/123?view=details");
|
||||
@@ -38,15 +55,28 @@ import {
|
||||
import { logConsoleAndDb } from "../db";
|
||||
import type { DeepLinkError } from "../interfaces/deepLinks";
|
||||
|
||||
/**
|
||||
* Handles processing and routing of deep links in the application.
|
||||
* Provides validation, error handling, and routing for deep link URLs.
|
||||
*/
|
||||
export class DeepLinkHandler {
|
||||
private router: Router;
|
||||
|
||||
/**
|
||||
* Creates a new DeepLinkHandler instance.
|
||||
* @param router - Vue Router instance for navigation
|
||||
*/
|
||||
constructor(router: Router) {
|
||||
this.router = router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses deep link URL into path, params and query components
|
||||
* Parses deep link URL into path, params and query components.
|
||||
* Validates URL structure using Zod schemas.
|
||||
*
|
||||
* @param url - The deep link URL to parse (format: scheme://path[?query])
|
||||
* @throws {DeepLinkError} If URL format is invalid
|
||||
* @returns Parsed URL components (path, params, query)
|
||||
*/
|
||||
private parseDeepLink(url: string) {
|
||||
const parts = url.split("://");
|
||||
@@ -79,8 +109,11 @@ export class DeepLinkHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes incoming deep links and routes them appropriately
|
||||
* @param url The deep link URL to process
|
||||
* Processes incoming deep links and routes them appropriately.
|
||||
* Handles validation, error handling, and routing to the correct view.
|
||||
*
|
||||
* @param url - The deep link URL to process
|
||||
* @throws {DeepLinkError} If URL processing fails
|
||||
*/
|
||||
async handleDeepLink(url: string): Promise<void> {
|
||||
try {
|
||||
@@ -107,7 +140,13 @@ export class DeepLinkHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes the deep link to appropriate view with validated parameters
|
||||
* Routes the deep link to appropriate view with validated parameters.
|
||||
* Validates route and parameters using Zod schemas before routing.
|
||||
*
|
||||
* @param path - The route path from the deep link
|
||||
* @param params - URL parameters
|
||||
* @param query - Query string parameters
|
||||
* @throws {DeepLinkError} If validation fails or route is invalid
|
||||
*/
|
||||
private async validateAndRoute(
|
||||
path: string,
|
||||
|
||||
@@ -1,12 +1,55 @@
|
||||
/**
|
||||
* Plan service module for handling plan and claim data loading.
|
||||
* Provides functionality to load plans with retry mechanism and error handling.
|
||||
*
|
||||
* @module plan
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
/**
|
||||
* Response interface for plan loading operations.
|
||||
* Represents the structure of both successful and error responses.
|
||||
*/
|
||||
interface PlanResponse {
|
||||
/** The response data payload */
|
||||
data?: unknown;
|
||||
/** HTTP status code of the response */
|
||||
status?: number;
|
||||
/** Error message in case of failure */
|
||||
error?: string;
|
||||
/** Response headers */
|
||||
headers?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a plan with automatic retry mechanism.
|
||||
* Attempts to load the plan multiple times in case of failure.
|
||||
*
|
||||
* @param handle - The unique identifier for the plan or claim
|
||||
* @param retries - Number of retry attempts (default: 3)
|
||||
* @returns Promise resolving to PlanResponse
|
||||
*
|
||||
* @remarks
|
||||
* - Implements exponential backoff with 1 second delay between retries
|
||||
* - Provides detailed logging of each attempt and any errors
|
||||
* - Handles both plan and claim flows based on handle content
|
||||
* - Logs comprehensive error information including:
|
||||
* - HTTP status and headers
|
||||
* - Response data
|
||||
* - Request configuration
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const response = await loadPlanWithRetry('plan-123');
|
||||
* if (response.error) {
|
||||
* console.error(response.error);
|
||||
* } else {
|
||||
* console.log(response.data);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const loadPlanWithRetry = async (
|
||||
handle: string,
|
||||
retries = 3,
|
||||
@@ -58,6 +101,22 @@ export const loadPlanWithRetry = async (
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes a single API request to load a plan or claim.
|
||||
* Determines the appropriate endpoint based on the handle.
|
||||
*
|
||||
* @param handle - The unique identifier for the plan or claim
|
||||
* @returns Promise resolving to PlanResponse
|
||||
* @throws Will throw an error if the API request fails
|
||||
*
|
||||
* @remarks
|
||||
* - Automatically detects claim vs plan endpoints based on handle
|
||||
* - Uses axios for HTTP requests
|
||||
* - Provides detailed error logging
|
||||
* - Different endpoints:
|
||||
* - Claims: /api/claims/{handle}
|
||||
* - Plans: /api/plans/{handle}
|
||||
*/
|
||||
export const loadPlan = async (handle: string): Promise<PlanResponse> => {
|
||||
logger.log(`[Plan Service] Making API request for plan ${handle}`);
|
||||
|
||||
|
||||
281
src/services/platforms/CapacitorPlatformService.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
import { ImageResult, PlatformService } from "../PlatformService";
|
||||
import { Filesystem, Directory } from "@capacitor/filesystem";
|
||||
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { Share } from "@capacitor/share";
|
||||
|
||||
/**
|
||||
* Platform service implementation for Capacitor (mobile) platform.
|
||||
* Provides native mobile functionality through Capacitor plugins for:
|
||||
* - File system operations
|
||||
* - Camera and image picker
|
||||
* - Platform-specific features
|
||||
*/
|
||||
export class CapacitorPlatformService implements PlatformService {
|
||||
/**
|
||||
* Reads a file from the app's data directory.
|
||||
* @param path - Relative path to the file in the app's data directory
|
||||
* @returns Promise resolving to the file contents as string
|
||||
* @throws Error if file cannot be read or doesn't exist
|
||||
*/
|
||||
async readFile(path: string): Promise<string> {
|
||||
const file = await Filesystem.readFile({
|
||||
path,
|
||||
directory: Directory.Data,
|
||||
});
|
||||
if (file.data instanceof Blob) {
|
||||
return await file.data.text();
|
||||
}
|
||||
return file.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes content to a file in the app's data directory.
|
||||
* @param path - Relative path where to write the file
|
||||
* @param content - Content to write to the file
|
||||
* @throws Error if write operation fails
|
||||
*/
|
||||
async writeFile(path: string, content: string): Promise<void> {
|
||||
await Filesystem.writeFile({
|
||||
path,
|
||||
data: content,
|
||||
directory: Directory.Data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a file from the app's data directory.
|
||||
* @param path - Relative path to the file to delete
|
||||
* @throws Error if deletion fails or file doesn't exist
|
||||
*/
|
||||
async deleteFile(path: string): Promise<void> {
|
||||
await Filesystem.deleteFile({
|
||||
path,
|
||||
directory: Directory.Data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists files in the specified directory within app's data directory.
|
||||
* @param directory - Relative path to the directory to list
|
||||
* @returns Promise resolving to array of filenames
|
||||
* @throws Error if directory cannot be read or doesn't exist
|
||||
*/
|
||||
async listFiles(directory: string): Promise<string[]> {
|
||||
const result = await Filesystem.readdir({
|
||||
path: directory,
|
||||
directory: Directory.Data,
|
||||
});
|
||||
return result.files.map((file) =>
|
||||
typeof file === "string" ? file : file.name,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the device camera to take a picture.
|
||||
* Configures camera for high quality images with editing enabled.
|
||||
* @returns Promise resolving to the captured image data
|
||||
* @throws Error if camera access fails or user cancels
|
||||
*/
|
||||
async takePicture(): Promise<ImageResult> {
|
||||
try {
|
||||
const image = await Camera.getPhoto({
|
||||
quality: 90,
|
||||
allowEditing: true,
|
||||
resultType: CameraResultType.Base64,
|
||||
source: CameraSource.Camera,
|
||||
});
|
||||
|
||||
const blob = await this.processImageData(image.base64String);
|
||||
return {
|
||||
blob,
|
||||
fileName: `photo_${Date.now()}.${image.format || "jpg"}`,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error("Error taking picture with Capacitor:", error);
|
||||
throw new Error("Failed to take picture");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the device photo gallery to pick an existing image.
|
||||
* Configures picker for high quality images with editing enabled.
|
||||
* @returns Promise resolving to the selected image data
|
||||
* @throws Error if gallery access fails or user cancels
|
||||
*/
|
||||
async pickImage(): Promise<ImageResult> {
|
||||
try {
|
||||
const image = await Camera.getPhoto({
|
||||
quality: 90,
|
||||
allowEditing: true,
|
||||
resultType: CameraResultType.Base64,
|
||||
source: CameraSource.Photos,
|
||||
});
|
||||
|
||||
const blob = await this.processImageData(image.base64String);
|
||||
return {
|
||||
blob,
|
||||
fileName: `photo_${Date.now()}.${image.format || "jpg"}`,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error("Error picking image with Capacitor:", error);
|
||||
throw new Error("Failed to pick image");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts base64 image data to a Blob.
|
||||
* @param base64String - Base64 encoded image data
|
||||
* @returns Promise resolving to image Blob
|
||||
* @throws Error if conversion fails
|
||||
*/
|
||||
private async processImageData(base64String?: string): Promise<Blob> {
|
||||
if (!base64String) {
|
||||
throw new Error("No image data received");
|
||||
}
|
||||
|
||||
// Convert base64 to blob
|
||||
const byteCharacters = atob(base64String);
|
||||
const byteArrays = [];
|
||||
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
|
||||
const slice = byteCharacters.slice(offset, offset + 512);
|
||||
const byteNumbers = new Array(slice.length);
|
||||
for (let i = 0; i < slice.length; i++) {
|
||||
byteNumbers[i] = slice.charCodeAt(i);
|
||||
}
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
byteArrays.push(byteArray);
|
||||
}
|
||||
return new Blob(byteArrays, { type: "image/jpeg" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if running on Capacitor platform.
|
||||
* @returns true, as this is the Capacitor implementation
|
||||
*/
|
||||
isCapacitor(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if running on Electron platform.
|
||||
* @returns false, as this is not Electron
|
||||
*/
|
||||
isElectron(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if running on PyWebView platform.
|
||||
* @returns false, as this is not PyWebView
|
||||
*/
|
||||
isPyWebView(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if running on web platform.
|
||||
* @returns false, as this is not web
|
||||
*/
|
||||
isWeb(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles deep link URLs for the application.
|
||||
* Note: Capacitor handles deep links automatically.
|
||||
* @param _url - The deep link URL (unused)
|
||||
*/
|
||||
async handleDeepLink(_url: string): Promise<void> {
|
||||
// Capacitor handles deep links automatically
|
||||
// This is just a placeholder for the interface
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
getExportInstructions(): string[] {
|
||||
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||
if (isIOS) {
|
||||
return [
|
||||
"On iOS: Choose 'More...' and select a place in iCloud, or go 'Back' and save to another location.",
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
"On Android: Choose 'Open' and then share to your preferred place.",
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
getExportSuccessMessage(): string {
|
||||
return "The backup has been saved to your device.";
|
||||
}
|
||||
|
||||
needsSecondaryDownloadLink(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
needsDownloadCleanup(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
async exportDatabase(blob: Blob, fileName: string): Promise<void> {
|
||||
logger.log("Starting database export on Capacitor platform:", {
|
||||
fileName,
|
||||
blobSize: `${blob.size} bytes`,
|
||||
});
|
||||
|
||||
// Create a File object from the Blob
|
||||
const file = new File([blob], fileName, { type: "application/json" });
|
||||
|
||||
try {
|
||||
logger.log("Attempting to use native share sheet");
|
||||
// Use the native share sheet
|
||||
await navigator.share({
|
||||
files: [file],
|
||||
title: fileName,
|
||||
});
|
||||
logger.log("Database export completed via native share sheet");
|
||||
} catch (error) {
|
||||
logger.log("Native share failed, falling back to Capacitor Share API");
|
||||
// Fallback to Capacitor Share API if Web Share API fails
|
||||
// First save to temporary file
|
||||
const base64Data = await this.blobToBase64(blob);
|
||||
const result = await Filesystem.writeFile({
|
||||
path: fileName,
|
||||
data: base64Data,
|
||||
directory: Directory.Cache, // Use Cache instead of Documents for temporary files
|
||||
recursive: true,
|
||||
});
|
||||
logger.log("Temporary file created for sharing:", result.uri);
|
||||
|
||||
// Then share using Capacitor Share API
|
||||
await Share.share({
|
||||
title: fileName,
|
||||
url: result.uri,
|
||||
});
|
||||
logger.log("Database export completed via Capacitor Share API");
|
||||
|
||||
// Clean up the temporary file
|
||||
try {
|
||||
await Filesystem.deleteFile({
|
||||
path: fileName,
|
||||
directory: Directory.Cache,
|
||||
});
|
||||
logger.log("Temporary file cleaned up successfully");
|
||||
} catch (cleanupError) {
|
||||
logger.warn("Failed to clean up temporary file:", cleanupError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async blobToBase64(blob: Blob): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const base64data = reader.result as string;
|
||||
resolve(base64data.split(",")[1]);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
}
|
||||
136
src/services/platforms/ElectronPlatformService.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { ImageResult, PlatformService } from "../PlatformService";
|
||||
import { logger } from "../../utils/logger";
|
||||
|
||||
/**
|
||||
* Platform service implementation for Electron (desktop) platform.
|
||||
* Note: This is a placeholder implementation with most methods currently unimplemented.
|
||||
* Implements the PlatformService interface but throws "Not implemented" errors for most operations.
|
||||
*
|
||||
* @remarks
|
||||
* This service is intended for desktop application functionality through Electron.
|
||||
* Future implementations should provide:
|
||||
* - Native file system access
|
||||
* - Desktop camera integration
|
||||
* - System-level features
|
||||
*/
|
||||
export class ElectronPlatformService implements PlatformService {
|
||||
/**
|
||||
* Reads a file from the filesystem.
|
||||
* @param _path - Path to the file to read
|
||||
* @returns Promise that should resolve to file contents
|
||||
* @throws Error with "Not implemented" message
|
||||
* @todo Implement file reading using Electron's file system API
|
||||
*/
|
||||
async readFile(_path: string): Promise<string> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes content to a file.
|
||||
* @param _path - Path where to write the file
|
||||
* @param _content - Content to write to the file
|
||||
* @throws Error with "Not implemented" message
|
||||
* @todo Implement file writing using Electron's file system API
|
||||
*/
|
||||
async writeFile(_path: string, _content: string): Promise<void> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a file from the filesystem.
|
||||
* @param _path - Path to the file to delete
|
||||
* @throws Error with "Not implemented" message
|
||||
* @todo Implement file deletion using Electron's file system API
|
||||
*/
|
||||
async deleteFile(_path: string): Promise<void> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists files in the specified directory.
|
||||
* @param _directory - Path to the directory to list
|
||||
* @returns Promise that should resolve to array of filenames
|
||||
* @throws Error with "Not implemented" message
|
||||
* @todo Implement directory listing using Electron's file system API
|
||||
*/
|
||||
async listFiles(_directory: string): Promise<string[]> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Should open system camera to take a picture.
|
||||
* @returns Promise that should resolve to captured image data
|
||||
* @throws Error with "Not implemented" message
|
||||
* @todo Implement camera access using Electron's media APIs
|
||||
*/
|
||||
async takePicture(): Promise<ImageResult> {
|
||||
logger.error("takePicture not implemented in Electron platform");
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Should open system file picker for selecting an image.
|
||||
* @returns Promise that should resolve to selected image data
|
||||
* @throws Error with "Not implemented" message
|
||||
* @todo Implement file picker using Electron's dialog API
|
||||
*/
|
||||
async pickImage(): Promise<ImageResult> {
|
||||
logger.error("pickImage not implemented in Electron platform");
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if running on Capacitor platform.
|
||||
* @returns false, as this is not Capacitor
|
||||
*/
|
||||
isCapacitor(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if running on Electron platform.
|
||||
* @returns true, as this is the Electron implementation
|
||||
*/
|
||||
isElectron(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if running on PyWebView platform.
|
||||
* @returns false, as this is not PyWebView
|
||||
*/
|
||||
isPyWebView(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if running on web platform.
|
||||
* @returns false, as this is not web
|
||||
*/
|
||||
isWeb(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should handle deep link URLs for the desktop application.
|
||||
* @param _url - The deep link URL to handle
|
||||
* @throws Error with "Not implemented" message
|
||||
* @todo Implement deep link handling using Electron's protocol handler
|
||||
*/
|
||||
async handleDeepLink(_url: string): Promise<void> {
|
||||
logger.error("handleDeepLink not implemented in Electron platform");
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a database blob to a file.
|
||||
* @param _blob - The database blob to export
|
||||
* @param _fileName - The name of the file to save
|
||||
* @throws Error with "Not implemented" message
|
||||
* @todo Implement file export using Electron's file system API
|
||||
*/
|
||||
async exportDatabase(_blob: Blob, _fileName: string): Promise<void> {
|
||||
logger.error("exportDatabase not implemented in Electron platform");
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
}
|
||||
137
src/services/platforms/PyWebViewPlatformService.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { ImageResult, PlatformService } from "../PlatformService";
|
||||
import { logger } from "../../utils/logger";
|
||||
|
||||
/**
|
||||
* Platform service implementation for PyWebView platform.
|
||||
* Note: This is a placeholder implementation with most methods currently unimplemented.
|
||||
* Implements the PlatformService interface but throws "Not implemented" errors for most operations.
|
||||
*
|
||||
* @remarks
|
||||
* This service is intended for Python-based desktop applications using pywebview.
|
||||
* Future implementations should provide:
|
||||
* - Integration with Python backend file operations
|
||||
* - System camera access through Python
|
||||
* - Native system dialogs via pywebview
|
||||
* - Python-JavaScript bridge functionality
|
||||
*/
|
||||
export class PyWebViewPlatformService implements PlatformService {
|
||||
/**
|
||||
* Reads a file using the Python backend.
|
||||
* @param _path - Path to the file to read
|
||||
* @returns Promise that should resolve to file contents
|
||||
* @throws Error with "Not implemented" message
|
||||
* @todo Implement file reading through pywebview's Python-JavaScript bridge
|
||||
*/
|
||||
async readFile(_path: string): Promise<string> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes content to a file using the Python backend.
|
||||
* @param _path - Path where to write the file
|
||||
* @param _content - Content to write to the file
|
||||
* @throws Error with "Not implemented" message
|
||||
* @todo Implement file writing through pywebview's Python-JavaScript bridge
|
||||
*/
|
||||
async writeFile(_path: string, _content: string): Promise<void> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a file using the Python backend.
|
||||
* @param _path - Path to the file to delete
|
||||
* @throws Error with "Not implemented" message
|
||||
* @todo Implement file deletion through pywebview's Python-JavaScript bridge
|
||||
*/
|
||||
async deleteFile(_path: string): Promise<void> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists files in the specified directory using the Python backend.
|
||||
* @param _directory - Path to the directory to list
|
||||
* @returns Promise that should resolve to array of filenames
|
||||
* @throws Error with "Not implemented" message
|
||||
* @todo Implement directory listing through pywebview's Python-JavaScript bridge
|
||||
*/
|
||||
async listFiles(_directory: string): Promise<string[]> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Should open system camera through Python backend.
|
||||
* @returns Promise that should resolve to captured image data
|
||||
* @throws Error with "Not implemented" message
|
||||
* @todo Implement camera access using Python's camera libraries
|
||||
*/
|
||||
async takePicture(): Promise<ImageResult> {
|
||||
logger.error("takePicture not implemented in PyWebView platform");
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Should open system file picker through pywebview.
|
||||
* @returns Promise that should resolve to selected image data
|
||||
* @throws Error with "Not implemented" message
|
||||
* @todo Implement file picker using pywebview's file dialog API
|
||||
*/
|
||||
async pickImage(): Promise<ImageResult> {
|
||||
logger.error("pickImage not implemented in PyWebView platform");
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if running on Capacitor platform.
|
||||
* @returns false, as this is not Capacitor
|
||||
*/
|
||||
isCapacitor(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if running on Electron platform.
|
||||
* @returns false, as this is not Electron
|
||||
*/
|
||||
isElectron(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if running on PyWebView platform.
|
||||
* @returns true, as this is the PyWebView implementation
|
||||
*/
|
||||
isPyWebView(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if running on web platform.
|
||||
* @returns false, as this is not web
|
||||
*/
|
||||
isWeb(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should handle deep link URLs through the Python backend.
|
||||
* @param _url - The deep link URL to handle
|
||||
* @throws Error with "Not implemented" message
|
||||
* @todo Implement deep link handling using Python's URL handling capabilities
|
||||
*/
|
||||
async handleDeepLink(_url: string): Promise<void> {
|
||||
logger.error("handleDeepLink not implemented in PyWebView platform");
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a database blob to a file using the Python backend.
|
||||
* @param _blob - The database blob to export
|
||||
* @param _fileName - The name of the file to save
|
||||
* @throws Error with "Not implemented" message
|
||||
* @todo Implement file export through pywebview's Python-JavaScript bridge
|
||||
*/
|
||||
async exportDatabase(_blob: Blob, _fileName: string): Promise<void> {
|
||||
logger.error("exportDatabase not implemented in PyWebView platform");
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
}
|
||||
261
src/services/platforms/WebPlatformService.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import { ImageResult, PlatformService } from "../PlatformService";
|
||||
import { logger } from "../../utils/logger";
|
||||
|
||||
/**
|
||||
* Platform service implementation for web browser platform.
|
||||
* Implements the PlatformService interface with web-specific functionality.
|
||||
*
|
||||
* @remarks
|
||||
* This service provides web-based implementations for:
|
||||
* - Image capture using the browser's file input
|
||||
* - Image selection from local filesystem
|
||||
* - Image processing and conversion
|
||||
*
|
||||
* Note: File system operations are not available in the web platform
|
||||
* due to browser security restrictions. These methods throw appropriate errors.
|
||||
*/
|
||||
export class WebPlatformService implements PlatformService {
|
||||
/**
|
||||
* Not supported in web platform.
|
||||
* @param _path - Unused path parameter
|
||||
* @throws Error indicating file system access is not available
|
||||
*/
|
||||
async readFile(_path: string): Promise<string> {
|
||||
throw new Error("File system access not available in web platform");
|
||||
}
|
||||
|
||||
/**
|
||||
* Not supported in web platform.
|
||||
* @param _path - Unused path parameter
|
||||
* @param _content - Unused content parameter
|
||||
* @throws Error indicating file system access is not available
|
||||
*/
|
||||
async writeFile(_path: string, _content: string): Promise<void> {
|
||||
throw new Error("File system access not available in web platform");
|
||||
}
|
||||
|
||||
/**
|
||||
* Not supported in web platform.
|
||||
* @param _path - Unused path parameter
|
||||
* @throws Error indicating file system access is not available
|
||||
*/
|
||||
async deleteFile(_path: string): Promise<void> {
|
||||
throw new Error("File system access not available in web platform");
|
||||
}
|
||||
|
||||
/**
|
||||
* Not supported in web platform.
|
||||
* @param _directory - Unused directory parameter
|
||||
* @throws Error indicating file system access is not available
|
||||
*/
|
||||
async listFiles(_directory: string): Promise<string[]> {
|
||||
throw new Error("File system access not available in web platform");
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a file input dialog configured for camera capture.
|
||||
* Creates a temporary file input element to access the device camera.
|
||||
*
|
||||
* @returns Promise resolving to the captured image data
|
||||
* @throws Error if image capture fails or no image is selected
|
||||
*
|
||||
* @remarks
|
||||
* Uses the 'capture' attribute to prefer the device camera.
|
||||
* Falls back to file selection if camera is not available.
|
||||
* Processes the captured image to ensure consistent format.
|
||||
*/
|
||||
async takePicture(): Promise<ImageResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = "image/*";
|
||||
input.capture = "environment";
|
||||
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (file) {
|
||||
try {
|
||||
const blob = await this.processImageFile(file);
|
||||
resolve({
|
||||
blob,
|
||||
fileName: file.name || "photo.jpg",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error processing camera image:", error);
|
||||
reject(new Error("Failed to process camera image"));
|
||||
}
|
||||
} else {
|
||||
reject(new Error("No image captured"));
|
||||
}
|
||||
};
|
||||
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a file input dialog for selecting an image file.
|
||||
* Creates a temporary file input element to access local files.
|
||||
*
|
||||
* @returns Promise resolving to the selected image data
|
||||
* @throws Error if image processing fails or no image is selected
|
||||
*
|
||||
* @remarks
|
||||
* Allows selection of any image file type.
|
||||
* Processes the selected image to ensure consistent format.
|
||||
*/
|
||||
async pickImage(): Promise<ImageResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = "image/*";
|
||||
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (file) {
|
||||
try {
|
||||
const blob = await this.processImageFile(file);
|
||||
resolve({
|
||||
blob,
|
||||
fileName: file.name || "photo.jpg",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error processing picked image:", error);
|
||||
reject(new Error("Failed to process picked image"));
|
||||
}
|
||||
} else {
|
||||
reject(new Error("No image selected"));
|
||||
}
|
||||
};
|
||||
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes an image file to ensure consistent format.
|
||||
* Converts the file to a data URL and then to a Blob.
|
||||
*
|
||||
* @param file - The image File object to process
|
||||
* @returns Promise resolving to processed image Blob
|
||||
* @throws Error if file reading or conversion fails
|
||||
*
|
||||
* @remarks
|
||||
* This method ensures consistent image format across different
|
||||
* input sources by converting through data URL to Blob.
|
||||
*/
|
||||
private async processImageFile(file: File): Promise<Blob> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const dataUrl = event.target?.result as string;
|
||||
// Convert to blob to ensure consistent format
|
||||
fetch(dataUrl)
|
||||
.then((res) => res.blob())
|
||||
.then((blob) => resolve(blob))
|
||||
.catch((error) => {
|
||||
logger.error("Error converting data URL to blob:", error);
|
||||
reject(error);
|
||||
});
|
||||
};
|
||||
reader.onerror = (error) => {
|
||||
logger.error("Error reading file:", error);
|
||||
reject(error);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if running on Capacitor platform.
|
||||
* @returns false, as this is not Capacitor
|
||||
*/
|
||||
isCapacitor(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if running on Electron platform.
|
||||
* @returns false, as this is not Electron
|
||||
*/
|
||||
isElectron(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if running on PyWebView platform.
|
||||
* @returns false, as this is not PyWebView
|
||||
*/
|
||||
isPyWebView(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if running on web platform.
|
||||
* @returns true, as this is the web implementation
|
||||
*/
|
||||
isWeb(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles deep link URLs in the web platform.
|
||||
* Deep links are handled through URL parameters in the web environment.
|
||||
*
|
||||
* @param _url - The deep link URL to handle (unused in web implementation)
|
||||
* @returns Promise that resolves immediately as web handles URLs naturally
|
||||
*/
|
||||
async handleDeepLink(_url: string): Promise<void> {
|
||||
// Web platform can handle deep links through URL parameters
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
getExportInstructions(): string[] {
|
||||
return [
|
||||
"After the download, you can save the file in your preferred storage location.",
|
||||
];
|
||||
}
|
||||
|
||||
getExportSuccessMessage(): string {
|
||||
return "See your downloads directory for the backup. It is in the Dexie format.";
|
||||
}
|
||||
|
||||
needsSecondaryDownloadLink(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
needsDownloadCleanup(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
async exportDatabase(blob: Blob, fileName: string): Promise<void> {
|
||||
logger.log("Starting database export on web platform:", {
|
||||
fileName,
|
||||
blobSize: `${blob.size} bytes`,
|
||||
});
|
||||
|
||||
try {
|
||||
logger.log("Creating download link for database export");
|
||||
// Create a download link
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
|
||||
logger.log("Triggering download");
|
||||
// Trigger the download
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
logger.log("Cleaning up download link");
|
||||
// Clean up
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
logger.log("Database export completed successfully");
|
||||
} catch (error) {
|
||||
logger.error("Failed to export database:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -420,53 +420,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="sectionDataExport"
|
||||
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
|
||||
>
|
||||
<div class="mb-2 font-bold">Data Export</div>
|
||||
<router-link
|
||||
v-if="activeDid"
|
||||
:to="{ name: 'seed-backup' }"
|
||||
class="block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2"
|
||||
>
|
||||
Backup Identifier Seed
|
||||
</router-link>
|
||||
|
||||
<button
|
||||
:class="computedStartDownloadLinkClassNames()"
|
||||
class="block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
|
||||
@click="exportDatabase()"
|
||||
>
|
||||
Download Settings & Contacts
|
||||
<br />
|
||||
(excluding Identifier Data)
|
||||
</button>
|
||||
<a
|
||||
ref="downloadLink"
|
||||
:class="computedDownloadLinkClassNames()"
|
||||
class="block w-full text-center text-md bg-gradient-to-b from-green-500 to-green-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
|
||||
>
|
||||
If no download happened yet, click again here to download now.
|
||||
</a>
|
||||
<div class="mt-4">
|
||||
<p>
|
||||
After the download, you can save the file in your preferred storage
|
||||
location.
|
||||
</p>
|
||||
<ul>
|
||||
<li class="list-disc list-outside ml-4">
|
||||
On iOS: Choose "More..." and select a place in iCloud, or go "Back"
|
||||
and save to another location.
|
||||
</li>
|
||||
<li class="list-disc list-outside ml-4">
|
||||
On Android: Choose "Open" and then share
|
||||
<font-awesome icon="share-nodes" class="fa-fw" />
|
||||
to your prefered place.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<DataExportSection :active-did="activeDid" />
|
||||
|
||||
<!-- id used by puppeteer test script -->
|
||||
<h3
|
||||
@@ -946,6 +900,7 @@ import PushNotificationPermission from "../components/PushNotificationPermission
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import TopMessage from "../components/TopMessage.vue";
|
||||
import UserNameDialog from "../components/UserNameDialog.vue";
|
||||
import DataExportSection from "../components/DataExportSection.vue";
|
||||
import {
|
||||
AppString,
|
||||
DEFAULT_IMAGE_API_SERVER,
|
||||
@@ -999,6 +954,7 @@ const inputImportFileNameRef = ref<Blob>();
|
||||
QuickNav,
|
||||
TopMessage,
|
||||
UserNameDialog,
|
||||
DataExportSection,
|
||||
},
|
||||
})
|
||||
export default class AccountViewView extends Vue {
|
||||
|
||||
@@ -137,7 +137,7 @@ export default class SharedPhotoView extends Vue {
|
||||
// this might be wrong since "name" goes with params, but it works so test well when you change it
|
||||
query: {
|
||||
destinationPathAfter: "/",
|
||||
hideBackButton: true,
|
||||
hideBackButton: "true",
|
||||
imageUrl: url,
|
||||
recipientDid: this.activeDid,
|
||||
},
|
||||
@@ -221,13 +221,63 @@ export default class SharedPhotoView extends Vue {
|
||||
|
||||
this.uploading = false;
|
||||
} catch (error) {
|
||||
logger.error("Error uploading the image", error);
|
||||
// Log the raw error first
|
||||
logger.error("Raw error object:", JSON.stringify(error, null, 2));
|
||||
|
||||
let errorMessage = "There was an error saving the picture.";
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
const status = error.response?.status;
|
||||
const statusText = error.response?.statusText;
|
||||
const data = error.response?.data;
|
||||
|
||||
// Log detailed error information
|
||||
logger.error("Upload error details:", {
|
||||
status,
|
||||
statusText,
|
||||
data: JSON.stringify(data, null, 2),
|
||||
message: error.message,
|
||||
config: {
|
||||
url: error.config?.url,
|
||||
method: error.config?.method,
|
||||
headers: error.config?.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (status === 401) {
|
||||
errorMessage = "Authentication failed. Please try logging in again.";
|
||||
} else if (status === 413) {
|
||||
errorMessage = "Image file is too large. Please try a smaller image.";
|
||||
} else if (status === 415) {
|
||||
errorMessage =
|
||||
"Unsupported image format. Please try a different image.";
|
||||
} else if (status && status >= 500) {
|
||||
errorMessage = "Server error. Please try again later.";
|
||||
} else if (data?.message) {
|
||||
errorMessage = data.message;
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
// Log non-Axios error with full details
|
||||
logger.error("Non-Axios error details:", {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
error: JSON.stringify(error, Object.getOwnPropertyNames(error), 2),
|
||||
});
|
||||
} else {
|
||||
// Log any other type of error
|
||||
logger.error("Unknown error type:", {
|
||||
error: JSON.stringify(error, null, 2),
|
||||
type: typeof error,
|
||||
});
|
||||
}
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "There was an error saving the picture.",
|
||||
text: errorMessage,
|
||||
},
|
||||
5000,
|
||||
);
|
||||
|
||||
@@ -1,35 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020", // Latest ECMAScript features that are widely supported by modern browsers
|
||||
"module": "ESNext", // Use ES modules
|
||||
"strict": true, // Enable all strict type checking options
|
||||
"jsx": "preserve", // Preserves JSX to be transformed by Babel or another transpiler
|
||||
"moduleResolution": "node", // Use Node.js style module resolution
|
||||
"experimentalDecorators": true,
|
||||
"esModuleInterop": true, // Enables compatibility with CommonJS modules for default imports
|
||||
"allowSyntheticDefaultImports": true, // Allow default imports from modules with no default export
|
||||
"forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references to the same file
|
||||
"useDefineForClassFields": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": "./src", // Base directory to resolve non-relative module names
|
||||
"module": "ESNext", // Use ES modules
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve", // Preserves JSX to be transformed by Babel or another transpiler
|
||||
"strict": true, // Enable all strict type checking options
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"experimentalDecorators": true,
|
||||
"paths": {
|
||||
"@/components/*": ["components/*"],
|
||||
"@/views/*": ["views/*"],
|
||||
"@/db/*": ["db/*"],
|
||||
"@/libs/*": ["libs/*"],
|
||||
"@/constants/*": ["constants/*"],
|
||||
"@/store/*": ["store/*"]
|
||||
},
|
||||
"lib": ["ES2020", "dom", "dom.iterable"], // Include typings for ES2020 and DOM APIs
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"test-playwright/**/*.ts",
|
||||
"test-playwright/**/*.tsx"
|
||||
"src/**/*.vue"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
||||
10
tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.*"]
|
||||
}
|
||||
@@ -36,7 +36,21 @@ export async function createBuildConfig(mode: string) {
|
||||
assetsDir: 'assets',
|
||||
chunkSizeWarningLimit: 1000,
|
||||
rollupOptions: {
|
||||
external: isCapacitor ? ['@capacitor/app'] : []
|
||||
external: isCapacitor
|
||||
? [
|
||||
'@capacitor/app',
|
||||
'@capacitor/share',
|
||||
'@capacitor/filesystem',
|
||||
'@capacitor/camera',
|
||||
'@capacitor/core'
|
||||
]
|
||||
: [
|
||||
'@capacitor/app',
|
||||
'@capacitor/share',
|
||||
'@capacitor/filesystem',
|
||||
'@capacitor/camera',
|
||||
'@capacitor/core'
|
||||
]
|
||||
}
|
||||
},
|
||||
define: {
|
||||
|
||||