Compare commits
5 Commits
build-ios
...
registrati
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3dcbb3229 | ||
| 7b0ee2e44e | |||
| ac018997e8 | |||
| 6f449e9c1f | |||
| 543599a6a1 |
@@ -26,7 +26,6 @@ module.exports = {
|
|||||||
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn",
|
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn",
|
||||||
"@typescript-eslint/no-explicit-any": "warn",
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
"@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,7 +51,5 @@ vendor/
|
|||||||
# Build logs
|
# Build logs
|
||||||
build_logs/
|
build_logs/
|
||||||
|
|
||||||
android/app/src/main/assets/public
|
# PWA icon files generated by capacitor-assets
|
||||||
android/app/src/main/res
|
icons
|
||||||
android/.gradle/buildOutputCleanup/buildOutputCleanup.lock
|
|
||||||
android/.gradle/file-system.probe
|
|
||||||
|
|||||||
@@ -187,7 +187,11 @@ Prerequisites: macOS with Xcode installed
|
|||||||
3. Copy the assets:
|
3. Copy the assets:
|
||||||
|
|
||||||
```bash
|
```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
|
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
|
npx capacitor-assets generate --ios
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -215,9 +219,6 @@ Prerequisites: Android Studio with SDK installed
|
|||||||
rm -rf dist
|
rm -rf dist
|
||||||
npm run build:web
|
npm run build:web
|
||||||
npm run build:capacitor
|
npm run build:capacitor
|
||||||
cd android
|
|
||||||
./gradlew clean
|
|
||||||
./gradlew assembleDebug
|
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Update Android project with latest build:
|
2. Update Android project with latest build:
|
||||||
|
|||||||
5
android/.gitignore
vendored
@@ -102,3 +102,8 @@ app/src/main/assets/public
|
|||||||
app/src/main/assets/capacitor.config.json
|
app/src/main/assets/capacitor.config.json
|
||||||
app/src/main/assets/capacitor.plugins.json
|
app/src/main/assets/capacitor.plugins.json
|
||||||
app/src/main/res/xml/config.xml
|
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
|
||||||
|
|||||||
BIN
android/.gradle/buildOutputCleanup/buildOutputCleanup.lock
Normal file
@@ -1,2 +1,2 @@
|
|||||||
#Wed Apr 09 09:01:13 UTC 2025
|
#Fri Mar 21 07:27:50 UTC 2025
|
||||||
gradle.version=8.11.1
|
gradle.version=8.2.1
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ project.ext.MY_KEY_PASSWORD = System.getenv('ANDROID_KEY_PASSWORD') ?: ""
|
|||||||
|
|
||||||
// If no environment variables, try to load from secrets file
|
// If no environment variables, try to load from secrets file
|
||||||
if (!project.ext.MY_KEYSTORE_FILE) {
|
if (!project.ext.MY_KEYSTORE_FILE) {
|
||||||
def secretsPropertiesFile = rootProject.file("app/gradle.properties.secrets")
|
def secretsPropertiesFile = rootProject.file("gradle.properties.secrets")
|
||||||
if (secretsPropertiesFile.exists()) {
|
if (secretsPropertiesFile.exists()) {
|
||||||
Properties secretsProperties = new Properties()
|
Properties secretsProperties = new Properties()
|
||||||
secretsProperties.load(new FileInputStream(secretsPropertiesFile))
|
secretsProperties.load(new FileInputStream(secretsPropertiesFile))
|
||||||
@@ -31,7 +31,7 @@ android {
|
|||||||
applicationId "app.timesafari.app"
|
applicationId "app.timesafari.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 10
|
versionCode 9
|
||||||
versionName "0.4.4"
|
versionName "0.4.4"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
|
|||||||
@@ -10,10 +10,6 @@ android {
|
|||||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':capacitor-app')
|
implementation project(':capacitor-app')
|
||||||
implementation project(':capacitor-camera')
|
|
||||||
implementation project(':capacitor-filesystem')
|
|
||||||
implementation project(':capacitor-share')
|
|
||||||
implementation project(':capawesome-capacitor-file-picker')
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,6 @@ public class ExampleInstrumentedTest {
|
|||||||
// Context of the app under test.
|
// Context of the app under test.
|
||||||
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||||
|
|
||||||
assertEquals("app.timesafari.app", appContext.getPackageName());
|
assertEquals("app.timesafari", appContext.getPackageName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
@@ -7,6 +8,7 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||||
@@ -14,6 +16,7 @@
|
|||||||
android:label="@string/title_activity_main"
|
android:label="@string/title_activity_main"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:theme="@style/AppTheme.NoActionBarLaunch">
|
android:theme="@style/AppTheme.NoActionBarLaunch">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
@@ -25,6 +28,7 @@
|
|||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data android:scheme="timesafari" />
|
<data android:scheme="timesafari" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
@@ -32,13 +36,13 @@
|
|||||||
android:authorities="${applicationId}.fileprovider"
|
android:authorities="${applicationId}.fileprovider"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true">
|
||||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths"></meta-data>
|
||||||
</provider>
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
<!-- Permissions -->
|
<!-- Permissions -->
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -2,21 +2,5 @@
|
|||||||
{
|
{
|
||||||
"pkg": "@capacitor/app",
|
"pkg": "@capacitor/app",
|
||||||
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
|
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
|
||||||
},
|
|
||||||
{
|
|
||||||
"pkg": "@capacitor/camera",
|
|
||||||
"classpath": "com.capacitorjs.plugins.camera.CameraPlugin"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pkg": "@capacitor/filesystem",
|
|
||||||
"classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pkg": "@capacitor/share",
|
|
||||||
"classpath": "com.capacitorjs.plugins.share.SharePlugin"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pkg": "@capawesome/capacitor-file-picker",
|
|
||||||
"classpath": "io.capawesome.capacitorjs.plugins.filepicker.FilePickerPlugin"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
17
android/app/src/main/assets/public/index.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
<title>TimeSafari</title>
|
||||||
|
<script type="module" crossorigin src="/assets/index-CZMUlUNO.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but TimeSafari doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1,5 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background>
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
<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>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background>
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
<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>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 23 KiB |
@@ -2,5 +2,4 @@
|
|||||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<external-path name="my_images" path="." />
|
<external-path name="my_images" path="." />
|
||||||
<cache-path name="my_cache_images" path="." />
|
<cache-path name="my_cache_images" path="." />
|
||||||
<files-path name="my_files" path="." />
|
|
||||||
</paths>
|
</paths>
|
||||||
@@ -7,7 +7,7 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:8.9.1'
|
classpath 'com.android.tools.build:gradle:8.2.1'
|
||||||
classpath 'com.google.gms:google-services:4.4.0'
|
classpath 'com.google.gms:google-services:4.4.0'
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
|||||||
@@ -4,15 +4,3 @@ project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/
|
|||||||
|
|
||||||
include ':capacitor-app'
|
include ':capacitor-app'
|
||||||
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
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')
|
|
||||||
|
|
||||||
include ':capacitor-share'
|
|
||||||
project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android')
|
|
||||||
|
|
||||||
include ':capawesome-capacitor-file-picker'
|
|
||||||
project(':capawesome-capacitor-file-picker').projectDir = new File('../node_modules/@capawesome/capacitor-file-picker/android')
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 60 KiB |
BIN
assets/icon.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
31
ios/.gitignore
vendored
@@ -1,16 +1,27 @@
|
|||||||
App/build
|
App/build
|
||||||
App/output
|
|
||||||
App/Pods
|
App/Pods
|
||||||
|
App/output
|
||||||
App/*.xcodeproj/xcuserdata/
|
App/App/public
|
||||||
App/*.xcworkspace/xcuserdata/
|
DerivedData
|
||||||
App/*/public
|
xcuserdata
|
||||||
|
*.xcuserstate
|
||||||
# Generated Config files
|
|
||||||
App/*/capacitor.config.json
|
|
||||||
App/*/config.xml
|
|
||||||
|
|
||||||
# Cordova plugins for Capacitor
|
# Cordova plugins for Capacitor
|
||||||
capacitor-cordova-ios-plugins
|
capacitor-cordova-ios-plugins
|
||||||
|
|
||||||
DerivedData
|
# Generated Config files
|
||||||
|
App/App/capacitor.config.json
|
||||||
|
App/App/config.xml
|
||||||
|
|
||||||
|
# User-specific Xcode files
|
||||||
|
App/App.xcodeproj/xcuserdata/*.xcuserdatad/
|
||||||
|
App/App.xcodeproj/*.xcuserstate
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
2
ios/App/App.xcworkspace/contents.xcworkspacedata
generated
@@ -2,7 +2,7 @@
|
|||||||
<Workspace
|
<Workspace
|
||||||
version = "1.0">
|
version = "1.0">
|
||||||
<FileRef
|
<FileRef
|
||||||
location = "group:App.xcodeproj">
|
location = "group:Time Safari.xcodeproj">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
<FileRef
|
<FileRef
|
||||||
location = "group:Pods/Pods.xcodeproj">
|
location = "group:Pods/Pods.xcodeproj">
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
Before Width: | Height: | Size: 116 KiB |
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"images": [
|
|
||||||
{
|
|
||||||
"idiom": "universal",
|
|
||||||
"size": "1024x1024",
|
|
||||||
"filename": "AppIcon-512@2x.png",
|
|
||||||
"platform": "ios"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info": {
|
|
||||||
"author": "xcode",
|
|
||||||
"version": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 40 KiB |
@@ -22,10 +22,6 @@
|
|||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSCameraUsageDescription</key>
|
|
||||||
<string>Upload photos and scan friends' QR codes</string>
|
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
|
||||||
<string>Upload photos for gifts</string>
|
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
@@ -49,16 +45,5 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
</dict>
|
||||||
<key>CFBundleURLTypes</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleURLName</key>
|
|
||||||
<string>app.timesafari</string>
|
|
||||||
<key>CFBundleURLSchemes</key>
|
|
||||||
<array>
|
|
||||||
<string>timesafari</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</array></dict>
|
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ def capacitor_pods
|
|||||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
||||||
pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
|
|
||||||
pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem'
|
|
||||||
pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
|
|
||||||
pod 'CapawesomeCapacitorFilePicker', :path => '../../node_modules/@capawesome/capacitor-file-picker'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
target 'App' do
|
target 'App' do
|
||||||
|
|||||||
@@ -1,52 +1,28 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- Capacitor (6.2.1):
|
- Capacitor (6.2.0):
|
||||||
- CapacitorCordova
|
- CapacitorCordova
|
||||||
- CapacitorApp (6.0.2):
|
- CapacitorApp (6.0.2):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- CapacitorCamera (6.1.2):
|
- CapacitorCordova (6.2.0)
|
||||||
- Capacitor
|
|
||||||
- CapacitorCordova (6.2.1)
|
|
||||||
- CapacitorFilesystem (6.0.3):
|
|
||||||
- Capacitor
|
|
||||||
- CapacitorShare (6.0.3):
|
|
||||||
- Capacitor
|
|
||||||
- CapawesomeCapacitorFilePicker (6.2.0):
|
|
||||||
- Capacitor
|
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
|
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
|
||||||
- "CapacitorApp (from `../../node_modules/@capacitor/app`)"
|
- "CapacitorApp (from `../../node_modules/@capacitor/app`)"
|
||||||
- "CapacitorCamera (from `../../node_modules/@capacitor/camera`)"
|
|
||||||
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
|
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
|
||||||
- "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)"
|
|
||||||
- "CapacitorShare (from `../../node_modules/@capacitor/share`)"
|
|
||||||
- "CapawesomeCapacitorFilePicker (from `../../node_modules/@capawesome/capacitor-file-picker`)"
|
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
Capacitor:
|
Capacitor:
|
||||||
:path: "../../node_modules/@capacitor/ios"
|
:path: "../../node_modules/@capacitor/ios"
|
||||||
CapacitorApp:
|
CapacitorApp:
|
||||||
:path: "../../node_modules/@capacitor/app"
|
:path: "../../node_modules/@capacitor/app"
|
||||||
CapacitorCamera:
|
|
||||||
:path: "../../node_modules/@capacitor/camera"
|
|
||||||
CapacitorCordova:
|
CapacitorCordova:
|
||||||
:path: "../../node_modules/@capacitor/ios"
|
:path: "../../node_modules/@capacitor/ios"
|
||||||
CapacitorFilesystem:
|
|
||||||
:path: "../../node_modules/@capacitor/filesystem"
|
|
||||||
CapacitorShare:
|
|
||||||
:path: "../../node_modules/@capacitor/share"
|
|
||||||
CapawesomeCapacitorFilePicker:
|
|
||||||
:path: "../../node_modules/@capawesome/capacitor-file-picker"
|
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Capacitor: c95400d761e376be9da6be5a05f226c0e865cebf
|
Capacitor: 05d35014f4425b0740fc8776481f6a369ad071bf
|
||||||
CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7
|
CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7
|
||||||
CapacitorCamera: 9bc7b005d0e6f1d5f525b8137045b60cffffce79
|
CapacitorCordova: b33e7f4aa4ed105dd43283acdd940964374a87d9
|
||||||
CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff
|
|
||||||
CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74
|
|
||||||
CapacitorShare: d2a742baec21c8f3b92b361a2fbd2401cdd8288e
|
|
||||||
CapawesomeCapacitorFilePicker: c40822f0a39f86855321943c7829d52bca7f01bd
|
|
||||||
|
|
||||||
PODFILE CHECKSUM: 1e9280368fd410520414f5741bf8fdfe7847b965
|
PODFILE CHECKSUM: 4233f5c5f414604460ff96d372542c311b0fb7a8
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
2BC611FE3D7967BDB623FF21 /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E0C2082015AEE6A0776A3EAB /* Pods_App.framework */; };
|
|
||||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
|
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
|
||||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
|
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
|
||||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
|
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
|
||||||
@@ -15,22 +14,21 @@
|
|||||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
|
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
|
||||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
||||||
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
||||||
|
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
|
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
|
||||||
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||||
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
504EC3041FED79650016851F /* Time Safari.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Time Safari.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||||
504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||||
821226CEE4D47A540167CC8F /* Pods-Time Safari.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Time Safari.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Time Safari/Pods-Time Safari.debug.xcconfig"; sourceTree = "<group>"; };
|
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
|
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
E0C2082015AEE6A0776A3EAB /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
EF03C3F99471948925ED5AC3 /* Pods-Time Safari.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Time Safari.release.xcconfig"; path = "Pods/Target Support Files/Pods-Time Safari/Pods-Time Safari.release.xcconfig"; sourceTree = "<group>"; };
|
|
||||||
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
@@ -39,7 +37,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
2BC611FE3D7967BDB623FF21 /* Pods_App.framework in Frameworks */,
|
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -49,7 +47,7 @@
|
|||||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
|
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
E0C2082015AEE6A0776A3EAB /* Pods_App.framework */,
|
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -67,7 +65,7 @@
|
|||||||
504EC3051FED79650016851F /* Products */ = {
|
504EC3051FED79650016851F /* Products */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
504EC3041FED79650016851F /* App.app */,
|
504EC3041FED79650016851F /* Time Safari.app */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -92,8 +90,6 @@
|
|||||||
children = (
|
children = (
|
||||||
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
|
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
|
||||||
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
|
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
|
||||||
821226CEE4D47A540167CC8F /* Pods-Time Safari.debug.xcconfig */,
|
|
||||||
EF03C3F99471948925ED5AC3 /* Pods-Time Safari.release.xcconfig */,
|
|
||||||
);
|
);
|
||||||
name = Pods;
|
name = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -101,9 +97,9 @@
|
|||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
504EC3031FED79650016851F /* App */ = {
|
504EC3031FED79650016851F /* Time Safari */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
|
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "Time Safari" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
|
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
|
||||||
504EC3001FED79650016851F /* Sources */,
|
504EC3001FED79650016851F /* Sources */,
|
||||||
@@ -115,9 +111,9 @@
|
|||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
name = App;
|
name = "Time Safari";
|
||||||
productName = "Time Safari";
|
productName = App;
|
||||||
productReference = 504EC3041FED79650016851F /* App.app */;
|
productReference = 504EC3041FED79650016851F /* Time Safari.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
};
|
};
|
||||||
/* End PBXNativeTarget section */
|
/* End PBXNativeTarget section */
|
||||||
@@ -126,8 +122,8 @@
|
|||||||
504EC2FC1FED79650016851F /* Project object */ = {
|
504EC2FC1FED79650016851F /* Project object */ = {
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
LastSwiftUpdateCheck = 920;
|
LastSwiftUpdateCheck = 0920;
|
||||||
LastUpgradeCheck = 920;
|
LastUpgradeCheck = 0920;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
504EC3031FED79650016851F = {
|
504EC3031FED79650016851F = {
|
||||||
CreatedOnToolsVersion = 9.2;
|
CreatedOnToolsVersion = 9.2;
|
||||||
@@ -136,7 +132,7 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */;
|
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "Time Safari" */;
|
||||||
compatibilityVersion = "Xcode 8.0";
|
compatibilityVersion = "Xcode 8.0";
|
||||||
developmentRegion = en;
|
developmentRegion = en;
|
||||||
hasScannedForEncodings = 0;
|
hasScannedForEncodings = 0;
|
||||||
@@ -145,11 +141,13 @@
|
|||||||
Base,
|
Base,
|
||||||
);
|
);
|
||||||
mainGroup = 504EC2FB1FED79650016851F;
|
mainGroup = 504EC2FB1FED79650016851F;
|
||||||
|
packageReferences = (
|
||||||
|
);
|
||||||
productRefGroup = 504EC3051FED79650016851F /* Products */;
|
productRefGroup = 504EC3051FED79650016851F /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
504EC3031FED79650016851F /* App */,
|
504EC3031FED79650016851F /* Time Safari */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@@ -350,12 +348,14 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 12;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "Time Safari";
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 0.4.4;
|
MARKETING_VERSION = 1.0;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -371,12 +371,14 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 12;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "Time Safari";
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 0.4.4;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||||
@@ -388,7 +390,7 @@
|
|||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = {
|
504EC2FF1FED79650016851F /* Build configuration list for PBXProject "Time Safari" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
504EC3141FED79650016851F /* Debug */,
|
504EC3141FED79650016851F /* Debug */,
|
||||||
@@ -397,7 +399,7 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */ = {
|
504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "Time Safari" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
504EC3171FED79650016851F /* Debug */,
|
504EC3171FED79650016851F /* Debug */,
|
||||||
1077
package-lock.json
generated
@@ -27,7 +27,6 @@
|
|||||||
"build:web": "vite build --config vite.config.web.mts",
|
"build:web": "vite build --config vite.config.web.mts",
|
||||||
"electron:dev": "npm run build && electron dist-electron",
|
"electron:dev": "npm run build && electron dist-electron",
|
||||||
"electron:start": "electron dist-electron",
|
"electron:start": "electron dist-electron",
|
||||||
"build:android": "rm -rf dist && npm run build:web && npm run build:capacitor && cd android && ./gradlew clean && ./gradlew assembleDebug && cd .. && npx cap sync android && npx capacitor-assets generate --android && npx cap open android",
|
|
||||||
"electron:build-linux": "npm run build:electron && electron-builder --linux AppImage",
|
"electron:build-linux": "npm run build:electron && electron-builder --linux AppImage",
|
||||||
"electron:build-linux-deb": "npm run build:electron && electron-builder --linux deb",
|
"electron:build-linux-deb": "npm run build:electron && electron-builder --linux deb",
|
||||||
"electron:build-linux-prod": "NODE_ENV=production npm run build:electron && electron-builder --linux AppImage",
|
"electron:build-linux-prod": "NODE_ENV=production npm run build:electron && electron-builder --linux AppImage",
|
||||||
@@ -45,13 +44,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^6.2.0",
|
"@capacitor/android": "^6.2.0",
|
||||||
"@capacitor/app": "^6.0.0",
|
"@capacitor/app": "^6.0.0",
|
||||||
"@capacitor/camera": "^6.0.0",
|
|
||||||
"@capacitor/cli": "^6.2.0",
|
"@capacitor/cli": "^6.2.0",
|
||||||
"@capacitor/core": "^6.2.0",
|
"@capacitor/core": "^6.2.0",
|
||||||
"@capacitor/filesystem": "^6.0.0",
|
|
||||||
"@capacitor/ios": "^6.2.0",
|
"@capacitor/ios": "^6.2.0",
|
||||||
"@capacitor/share": "^6.0.3",
|
|
||||||
"@capawesome/capacitor-file-picker": "^6.2.0",
|
|
||||||
"@dicebear/collection": "^5.4.1",
|
"@dicebear/collection": "^5.4.1",
|
||||||
"@dicebear/core": "^5.4.1",
|
"@dicebear/core": "^5.4.1",
|
||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Clean the public directory
|
|
||||||
rm -rf android/app/src/main/assets/public/*
|
|
||||||
|
|
||||||
# Copy web assets
|
|
||||||
cp -r dist/* android/app/src/main/assets/public/
|
|
||||||
|
|
||||||
# Ensure the directory structure exists
|
|
||||||
mkdir -p android/app/src/main/assets/public/assets
|
|
||||||
|
|
||||||
# Copy the main index file
|
|
||||||
cp dist/index.html android/app/src/main/assets/public/
|
|
||||||
|
|
||||||
# Copy all assets
|
|
||||||
cp -r dist/assets/* android/app/src/main/assets/public/assets/
|
|
||||||
|
|
||||||
# Copy other necessary files
|
|
||||||
cp dist/favicon.ico android/app/src/main/assets/public/
|
|
||||||
cp dist/robots.txt android/app/src/main/assets/public/
|
|
||||||
|
|
||||||
echo "Web assets copied successfully!"
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Create directories if they don't exist
|
|
||||||
mkdir -p android/app/src/main/res/mipmap-mdpi
|
|
||||||
mkdir -p android/app/src/main/res/mipmap-hdpi
|
|
||||||
mkdir -p android/app/src/main/res/mipmap-xhdpi
|
|
||||||
mkdir -p android/app/src/main/res/mipmap-xxhdpi
|
|
||||||
mkdir -p android/app/src/main/res/mipmap-xxxhdpi
|
|
||||||
|
|
||||||
# Generate placeholder icons using ImageMagick
|
|
||||||
convert -size 48x48 xc:blue -gravity center -pointsize 20 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-mdpi/ic_launcher.png
|
|
||||||
convert -size 72x72 xc:blue -gravity center -pointsize 30 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-hdpi/ic_launcher.png
|
|
||||||
convert -size 96x96 xc:blue -gravity center -pointsize 40 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
|
|
||||||
convert -size 144x144 xc:blue -gravity center -pointsize 60 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
|
|
||||||
convert -size 192x192 xc:blue -gravity center -pointsize 80 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
|
|
||||||
|
|
||||||
# Copy to round versions
|
|
||||||
cp android/app/src/main/res/mipmap-mdpi/ic_launcher.png android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
|
|
||||||
cp android/app/src/main/res/mipmap-hdpi/ic_launcher.png android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
|
|
||||||
cp android/app/src/main/res/mipmap-xhdpi/ic_launcher.png android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
|
|
||||||
cp android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
|
|
||||||
cp android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
|
|
||||||
@@ -103,7 +103,7 @@ const cleanIosPlatform = async (log) => {
|
|||||||
// Get app name from package.json
|
// Get app name from package.json
|
||||||
const packageJson = JSON.parse(readFileSync('package.json', 'utf8'));
|
const packageJson = JSON.parse(readFileSync('package.json', 'utf8'));
|
||||||
const appName = packageJson.name || 'App';
|
const appName = packageJson.name || 'App';
|
||||||
const appId = packageJson.build.appId || 'io.ionic.starter';
|
const appId = packageJson.capacitor?.appId || 'io.ionic.starter';
|
||||||
|
|
||||||
// Create a minimal capacitor config
|
// Create a minimal capacitor config
|
||||||
const capacitorConfig = `
|
const capacitorConfig = `
|
||||||
@@ -467,12 +467,12 @@ const configureIosProject = async (log) => {
|
|||||||
// Build and test iOS project
|
// Build and test iOS project
|
||||||
const buildAndTestIos = async (log, simulator) => {
|
const buildAndTestIos = async (log, simulator) => {
|
||||||
const simulatorName = simulator[0].name;
|
const simulatorName = simulator[0].name;
|
||||||
log('🏗️ Building iOS project...', simulator[0]);
|
log('🏗️ Building iOS project...');
|
||||||
execSync('cd ios/App && xcodebuild clean -workspace App.xcworkspace -scheme App', { stdio: 'inherit' });
|
execSync('cd ios/App && xcodebuild clean -workspace App.xcworkspace -scheme App', { stdio: 'inherit' });
|
||||||
log('✅ Xcode clean completed');
|
log('✅ Xcode clean completed');
|
||||||
|
|
||||||
log(`🏗️ Building for simulator: ${simulatorName}`);
|
log(`🏗️ Building for simulator: ${simulatorName}`);
|
||||||
execSync(`cd ios/App && xcodebuild build -workspace App.xcworkspace -scheme App -destination "platform=iOS Simulator,OS=17.2,name=${simulatorName}"`, { stdio: 'inherit' });
|
execSync(`cd ios/App && xcodebuild build -workspace App.xcworkspace -scheme App -destination "platform=iOS Simulator,name=${simulatorName}"`, { stdio: 'inherit' });
|
||||||
log('✅ Xcode build completed');
|
log('✅ Xcode build completed');
|
||||||
|
|
||||||
// Check if the project is configured for testing by querying the scheme capabilities
|
// Check if the project is configured for testing by querying the scheme capabilities
|
||||||
|
|||||||
@@ -1,196 +0,0 @@
|
|||||||
/** * 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
|
|
||||||
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="platformCapabilities.needsFileHandlingInstructions" class="mt-4">
|
|
||||||
<p>
|
|
||||||
After the download, you can save the file in your preferred storage
|
|
||||||
location.
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li
|
|
||||||
v-if="platformCapabilities.isIOS"
|
|
||||||
class="list-disc list-outside ml-4"
|
|
||||||
>
|
|
||||||
On iOS: You will be prompted to choose a location to save your backup
|
|
||||||
file.
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
v-if="platformCapabilities.isMobile && !platformCapabilities.isIOS"
|
|
||||||
class="list-disc list-outside ml-4"
|
|
||||||
>
|
|
||||||
On Android: You will be prompted to choose a location to save your
|
|
||||||
backup file.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue } from "vue-facing-decorator";
|
|
||||||
import { NotificationIface } from "../constants/app";
|
|
||||||
import { db } from "../db/index";
|
|
||||||
import { logger } from "../utils/logger";
|
|
||||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
|
||||||
import {
|
|
||||||
PlatformService,
|
|
||||||
PlatformCapabilities,
|
|
||||||
} 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 =
|
|
||||||
PlatformServiceFactory.getInstance();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Platform capabilities for the current platform
|
|
||||||
*/
|
|
||||||
private get platformCapabilities(): PlatformCapabilities {
|
|
||||||
return this.platformService.getCapabilities();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lifecycle hook to clean up resources
|
|
||||||
* Revokes object URL when component is unmounted (web platform only)
|
|
||||||
*/
|
|
||||||
beforeUnmount() {
|
|
||||||
if (this.downloadUrl && this.platformCapabilities.hasFileDownload) {
|
|
||||||
URL.revokeObjectURL(this.downloadUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {
|
|
||||||
const blob = await db.export({ prettyJson: true });
|
|
||||||
const fileName = `${db.name}-backup.json`;
|
|
||||||
|
|
||||||
if (this.platformCapabilities.hasFileDownload) {
|
|
||||||
// Web platform: Use download link
|
|
||||||
this.downloadUrl = URL.createObjectURL(blob);
|
|
||||||
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
|
|
||||||
downloadAnchor.href = this.downloadUrl;
|
|
||||||
downloadAnchor.download = fileName;
|
|
||||||
downloadAnchor.click();
|
|
||||||
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
|
|
||||||
} else if (this.platformCapabilities.hasFileSystem) {
|
|
||||||
// Native platform: Write to app directory
|
|
||||||
const content = await blob.text();
|
|
||||||
await this.platformService.writeAndShareFile(fileName, content);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "success",
|
|
||||||
title: "Export Successful",
|
|
||||||
text: this.platformCapabilities.hasFileDownload
|
|
||||||
? "See your downloads directory for the backup. It is in the Dexie format."
|
|
||||||
: "Please choose a location to save your backup file.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Export Error:", error);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Export Error",
|
|
||||||
text: "There was an error exporting the data.",
|
|
||||||
},
|
|
||||||
3000,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.platformCapabilities.hasFileDownload,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.platformCapabilities.hasFileDownload,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -40,6 +40,11 @@
|
|||||||
}"
|
}"
|
||||||
class="max-h-[90vh] max-w-[90vw] object-contain"
|
class="max-h-[90vh] max-w-[90vw] object-contain"
|
||||||
/>
|
/>
|
||||||
|
<!-- This gives a round cropper.
|
||||||
|
:presetMode="{
|
||||||
|
mode: 'round',
|
||||||
|
}"
|
||||||
|
-->
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
@@ -69,68 +74,88 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else ref="cameraContainer">
|
||||||
<div class="flex flex-col items-center justify-center gap-4 p-4">
|
<!--
|
||||||
<button
|
Camera "resolution" doesn't change how it shows on screen but rather stretches the result,
|
||||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
eg. the following which just stretches it vertically:
|
||||||
@click="takePhoto"
|
: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"
|
||||||
>
|
>
|
||||||
<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"
|
||||||
<button
|
@click="takeImage()"
|
||||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
>
|
||||||
@click="pickPhoto"
|
<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="image" 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"
|
||||||
</div>
|
@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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<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 axios from "axios";
|
||||||
|
import Camera from "simple-vue-camera";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import VuePictureCropper, { cropper } from "vue-picture-cropper";
|
import VuePictureCropper, { cropper } from "vue-picture-cropper";
|
||||||
|
|
||||||
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
|
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
|
||||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
import { accessToken } from "../libs/crypto";
|
import { accessToken } from "../libs/crypto";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
|
||||||
|
|
||||||
@Component({ components: { VuePictureCropper } })
|
@Component({ components: { Camera, VuePictureCropper } })
|
||||||
export default class PhotoDialog extends Vue {
|
export default class PhotoDialog extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
|
activeDeviceNumber = 0;
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
blob?: Blob;
|
blob?: Blob;
|
||||||
claimType = "";
|
claimType = "";
|
||||||
crop = false;
|
crop = false;
|
||||||
fileName?: string;
|
fileName?: string;
|
||||||
|
mirror = false;
|
||||||
|
numDevices = 0;
|
||||||
setImageCallback: (arg: string) => void = () => {};
|
setImageCallback: (arg: string) => void = () => {};
|
||||||
showRetry = true;
|
showRetry = true;
|
||||||
uploading = false;
|
uploading = false;
|
||||||
visible = false;
|
visible = false;
|
||||||
|
|
||||||
private platformService = PlatformServiceFactory.getInstance();
|
|
||||||
URL = window.URL || window.webkitURL;
|
URL = window.URL || window.webkitURL;
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount();
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
} catch (err: unknown) {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (err: any) {
|
||||||
logger.error("Error retrieving settings from database:", err);
|
logger.error("Error retrieving settings from database:", err);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
@@ -148,7 +173,7 @@ export default class PhotoDialog extends Vue {
|
|||||||
setImageFn: (arg: string) => void,
|
setImageFn: (arg: string) => void,
|
||||||
claimType: string,
|
claimType: string,
|
||||||
crop?: boolean,
|
crop?: boolean,
|
||||||
blob?: Blob,
|
blob?: Blob, // for image upload, just to use the cropping function
|
||||||
inputFileName?: string,
|
inputFileName?: string,
|
||||||
) {
|
) {
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
@@ -162,6 +187,7 @@ export default class PhotoDialog extends Vue {
|
|||||||
if (blob) {
|
if (blob) {
|
||||||
this.blob = blob;
|
this.blob = blob;
|
||||||
this.fileName = inputFileName;
|
this.fileName = inputFileName;
|
||||||
|
// doesn't make sense to retry the file upload; they can cancel if they picked the wrong one
|
||||||
this.showRetry = false;
|
this.showRetry = false;
|
||||||
} else {
|
} else {
|
||||||
this.blob = undefined;
|
this.blob = undefined;
|
||||||
@@ -179,41 +205,85 @@ export default class PhotoDialog extends Vue {
|
|||||||
this.blob = undefined;
|
this.blob = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async takePhoto() {
|
async cameraStarted() {
|
||||||
try {
|
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
|
||||||
const result = await this.platformService.takePicture();
|
if (cameraComponent) {
|
||||||
this.blob = result.blob;
|
this.numDevices = (await cameraComponent.devices(["videoinput"])).length;
|
||||||
this.fileName = result.fileName;
|
this.mirror = cameraComponent.facingMode === "user";
|
||||||
} catch (error: unknown) {
|
// figure out which device is active
|
||||||
logger.error("Error taking picture:", error);
|
const currentDeviceId = cameraComponent.currentDeviceID();
|
||||||
this.$notify(
|
const devices = await cameraComponent.devices(["videoinput"]);
|
||||||
{
|
this.activeDeviceNumber = devices.findIndex(
|
||||||
group: "alert",
|
(device) => device.deviceId === currentDeviceId,
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: "Failed to take picture. Please try again.",
|
|
||||||
},
|
|
||||||
5000,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async pickPhoto() {
|
async switchCamera() {
|
||||||
try {
|
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
|
||||||
const result = await this.platformService.pickImage();
|
this.activeDeviceNumber = (this.activeDeviceNumber + 1) % this.numDevices;
|
||||||
this.blob = result.blob;
|
const devices = await cameraComponent?.devices(["videoinput"]);
|
||||||
this.fileName = result.fileName;
|
await cameraComponent?.changeCamera(
|
||||||
} catch (error: unknown) {
|
devices[this.activeDeviceNumber].deviceId,
|
||||||
logger.error("Error picking image:", error);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Failed to pick image. Please try again.",
|
text: "There was an error taking the picture. Please try again.",
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,6 +295,51 @@ export default class PhotoDialog extends Vue {
|
|||||||
this.blob = undefined;
|
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() {
|
async uploadImage() {
|
||||||
this.uploading = true;
|
this.uploading = true;
|
||||||
|
|
||||||
@@ -235,9 +350,11 @@ export default class PhotoDialog extends Vue {
|
|||||||
const token = await accessToken(this.activeDid);
|
const token = await accessToken(this.activeDid);
|
||||||
const headers = {
|
const headers = {
|
||||||
Authorization: "Bearer " + token,
|
Authorization: "Bearer " + token,
|
||||||
|
// axios fills in Content-Type of multipart/form-data
|
||||||
};
|
};
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
if (!this.blob) {
|
if (!this.blob) {
|
||||||
|
// yeah, this should never happen, but it helps with subsequent type checking
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -250,7 +367,7 @@ export default class PhotoDialog extends Vue {
|
|||||||
this.uploading = false;
|
this.uploading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
formData.append("image", this.blob, this.fileName || "photo.jpg");
|
formData.append("image", this.blob, this.fileName || "snapshot.png");
|
||||||
formData.append("claimType", this.claimType);
|
formData.append("claimType", this.claimType);
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
@@ -270,64 +387,14 @@ export default class PhotoDialog extends Vue {
|
|||||||
|
|
||||||
this.close();
|
this.close();
|
||||||
this.setImageCallback(response.data.url as string);
|
this.setImageCallback(response.data.url as string);
|
||||||
} catch (error: unknown) {
|
} catch (error) {
|
||||||
// Log the raw error first
|
logger.error("Error uploading the image", error);
|
||||||
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(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: errorMessage,
|
text: "There was an error saving the picture.",
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
@@ -335,6 +402,17 @@ export default class PhotoDialog extends Vue {
|
|||||||
this.blob = undefined;
|
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>
|
</script>
|
||||||
|
|
||||||
@@ -360,4 +438,12 @@ export default class PhotoDialog extends Vue {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 700px;
|
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>
|
</style>
|
||||||
|
|||||||
101
src/components/RegistrationGate.vue
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<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>
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 capabilities interface defining what features are available
|
|
||||||
* on the current platform implementation
|
|
||||||
*/
|
|
||||||
export interface PlatformCapabilities {
|
|
||||||
/** Whether the platform supports native file system access */
|
|
||||||
hasFileSystem: boolean;
|
|
||||||
/** Whether the platform supports native camera access */
|
|
||||||
hasCamera: boolean;
|
|
||||||
/** Whether the platform is a mobile device */
|
|
||||||
isMobile: boolean;
|
|
||||||
/** Whether the platform is iOS specifically */
|
|
||||||
isIOS: boolean;
|
|
||||||
/** Whether the platform supports native file download */
|
|
||||||
hasFileDownload: boolean;
|
|
||||||
/** Whether the platform requires special file handling instructions */
|
|
||||||
needsFileHandlingInstructions: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Platform-agnostic interface for handling platform-specific operations.
|
|
||||||
* Provides a common API for file system operations, camera interactions,
|
|
||||||
* and platform detection across different platforms (web, mobile, desktop).
|
|
||||||
*/
|
|
||||||
export interface PlatformService {
|
|
||||||
// Platform capabilities
|
|
||||||
/**
|
|
||||||
* Gets the current platform's capabilities
|
|
||||||
* @returns Object describing what features are available on this platform
|
|
||||||
*/
|
|
||||||
getCapabilities(): PlatformCapabilities;
|
|
||||||
|
|
||||||
// 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>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes content to a file at the specified path and shares it.
|
|
||||||
* @param fileName - The filename of the file to write
|
|
||||||
* @param content - The content to write to the file
|
|
||||||
* @returns Promise that resolves when the write is complete
|
|
||||||
*/
|
|
||||||
writeAndShareFile(fileName: 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[]>;
|
|
||||||
|
|
||||||
// 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>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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>;
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { PlatformService } from "./PlatformService";
|
|
||||||
import { WebPlatformService } from "./platforms/WebPlatformService";
|
|
||||||
import { CapacitorPlatformService } from "./platforms/CapacitorPlatformService";
|
|
||||||
import { ElectronPlatformService } from "./platforms/ElectronPlatformService";
|
|
||||||
import { PyWebViewPlatformService } from "./platforms/PyWebViewPlatformService";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 getInstance(): PlatformService {
|
|
||||||
if (PlatformServiceFactory.instance) {
|
|
||||||
return PlatformServiceFactory.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
const platform = process.env.VITE_PLATFORM || "web";
|
|
||||||
|
|
||||||
switch (platform) {
|
|
||||||
case "capacitor":
|
|
||||||
PlatformServiceFactory.instance = new CapacitorPlatformService();
|
|
||||||
break;
|
|
||||||
case "electron":
|
|
||||||
PlatformServiceFactory.instance = new ElectronPlatformService();
|
|
||||||
break;
|
|
||||||
case "pywebview":
|
|
||||||
PlatformServiceFactory.instance = new PyWebViewPlatformService();
|
|
||||||
break;
|
|
||||||
case "web":
|
|
||||||
default:
|
|
||||||
PlatformServiceFactory.instance = new WebPlatformService();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return PlatformServiceFactory.instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +1,5 @@
|
|||||||
/**
|
|
||||||
* 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 { AxiosError } from "axios";
|
||||||
import { logger } from "../utils/logger";
|
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) => {
|
export const handleApiError = (error: AxiosError, endpoint: string) => {
|
||||||
if (process.env.VITE_PLATFORM === "capacitor") {
|
if (process.env.VITE_PLATFORM === "capacitor") {
|
||||||
logger.error(`[Capacitor API Error] ${endpoint}:`, {
|
logger.error(`[Capacitor API Error] ${endpoint}:`, {
|
||||||
|
|||||||
@@ -23,23 +23,6 @@
|
|||||||
* - Query parameter validation and sanitization
|
* - Query parameter validation and sanitization
|
||||||
* - Type-safe parameter passing to router
|
* - 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
|
* @example
|
||||||
* const handler = new DeepLinkHandler(router);
|
* const handler = new DeepLinkHandler(router);
|
||||||
* await handler.handleDeepLink("timesafari://claim/123?view=details");
|
* await handler.handleDeepLink("timesafari://claim/123?view=details");
|
||||||
@@ -55,28 +38,15 @@ import {
|
|||||||
import { logConsoleAndDb } from "../db";
|
import { logConsoleAndDb } from "../db";
|
||||||
import type { DeepLinkError } from "../interfaces/deepLinks";
|
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 {
|
export class DeepLinkHandler {
|
||||||
private router: Router;
|
private router: Router;
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new DeepLinkHandler instance.
|
|
||||||
* @param router - Vue Router instance for navigation
|
|
||||||
*/
|
|
||||||
constructor(router: Router) {
|
constructor(router: Router) {
|
||||||
this.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) {
|
private parseDeepLink(url: string) {
|
||||||
const parts = url.split("://");
|
const parts = url.split("://");
|
||||||
@@ -109,11 +79,8 @@ export class DeepLinkHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes incoming deep links and routes them appropriately.
|
* 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
|
||||||
*
|
|
||||||
* @param url - The deep link URL to process
|
|
||||||
* @throws {DeepLinkError} If URL processing fails
|
|
||||||
*/
|
*/
|
||||||
async handleDeepLink(url: string): Promise<void> {
|
async handleDeepLink(url: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@@ -140,13 +107,7 @@ 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(
|
private async validateAndRoute(
|
||||||
path: string,
|
path: string,
|
||||||
|
|||||||
@@ -1,55 +1,12 @@
|
|||||||
/**
|
|
||||||
* 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 axios from "axios";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
/**
|
|
||||||
* Response interface for plan loading operations.
|
|
||||||
* Represents the structure of both successful and error responses.
|
|
||||||
*/
|
|
||||||
interface PlanResponse {
|
interface PlanResponse {
|
||||||
/** The response data payload */
|
|
||||||
data?: unknown;
|
data?: unknown;
|
||||||
/** HTTP status code of the response */
|
|
||||||
status?: number;
|
status?: number;
|
||||||
/** Error message in case of failure */
|
|
||||||
error?: string;
|
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 (
|
export const loadPlanWithRetry = async (
|
||||||
handle: string,
|
handle: string,
|
||||||
retries = 3,
|
retries = 3,
|
||||||
@@ -101,22 +58,6 @@ 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> => {
|
export const loadPlan = async (handle: string): Promise<PlanResponse> => {
|
||||||
logger.log(`[Plan Service] Making API request for plan ${handle}`);
|
logger.log(`[Plan Service] Making API request for plan ${handle}`);
|
||||||
|
|
||||||
|
|||||||
@@ -1,473 +0,0 @@
|
|||||||
import {
|
|
||||||
ImageResult,
|
|
||||||
PlatformService,
|
|
||||||
PlatformCapabilities,
|
|
||||||
} from "../PlatformService";
|
|
||||||
import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
|
|
||||||
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
|
|
||||||
import { Share } from "@capacitor/share";
|
|
||||||
import { logger } from "../../utils/logger";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {
|
|
||||||
/**
|
|
||||||
* Gets the capabilities of the Capacitor platform
|
|
||||||
* @returns Platform capabilities object
|
|
||||||
*/
|
|
||||||
getCapabilities(): PlatformCapabilities {
|
|
||||||
return {
|
|
||||||
hasFileSystem: true,
|
|
||||||
hasCamera: true,
|
|
||||||
isMobile: true,
|
|
||||||
isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
|
|
||||||
hasFileDownload: false,
|
|
||||||
needsFileHandlingInstructions: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks and requests storage permissions if needed
|
|
||||||
* @returns Promise that resolves when permissions are granted
|
|
||||||
* @throws Error if permissions are denied
|
|
||||||
*/
|
|
||||||
private async checkStoragePermissions(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const logData = {
|
|
||||||
platform: this.getCapabilities().isIOS ? "iOS" : "Android",
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
logger.log(
|
|
||||||
"Checking storage permissions",
|
|
||||||
JSON.stringify(logData, null, 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.getCapabilities().isIOS) {
|
|
||||||
// iOS uses different permission model
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to access a test directory to check permissions
|
|
||||||
try {
|
|
||||||
await Filesystem.stat({
|
|
||||||
path: "/storage/emulated/0/Download",
|
|
||||||
directory: Directory.Documents,
|
|
||||||
});
|
|
||||||
logger.log(
|
|
||||||
"Storage permissions already granted",
|
|
||||||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const err = error as Error;
|
|
||||||
const errorLogData = {
|
|
||||||
error: {
|
|
||||||
message: err.message,
|
|
||||||
name: err.name,
|
|
||||||
stack: err.stack,
|
|
||||||
},
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// "File does not exist" is expected and not a permission error
|
|
||||||
if (err.message === "File does not exist") {
|
|
||||||
logger.log(
|
|
||||||
"Directory does not exist (expected), proceeding with write",
|
|
||||||
JSON.stringify(errorLogData, null, 2),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for actual permission errors
|
|
||||||
if (
|
|
||||||
err.message.includes("permission") ||
|
|
||||||
err.message.includes("access")
|
|
||||||
) {
|
|
||||||
logger.log(
|
|
||||||
"Permission check failed, requesting permissions",
|
|
||||||
JSON.stringify(errorLogData, null, 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
// The Filesystem plugin will automatically request permissions when needed
|
|
||||||
// We just need to try the operation again
|
|
||||||
try {
|
|
||||||
await Filesystem.stat({
|
|
||||||
path: "/storage/emulated/0/Download",
|
|
||||||
directory: Directory.Documents,
|
|
||||||
});
|
|
||||||
logger.log(
|
|
||||||
"Storage permissions granted after request",
|
|
||||||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
} catch (retryError: unknown) {
|
|
||||||
const retryErr = retryError as Error;
|
|
||||||
throw new Error(
|
|
||||||
`Failed to obtain storage permissions: ${retryErr.message}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For any other error, log it but don't treat as permission error
|
|
||||||
logger.log(
|
|
||||||
"Unexpected error during permission check",
|
|
||||||
JSON.stringify(errorLogData, null, 2),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const err = error as Error;
|
|
||||||
const errorLogData = {
|
|
||||||
error: {
|
|
||||||
message: err.message,
|
|
||||||
name: err.name,
|
|
||||||
stack: err.stack,
|
|
||||||
},
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
logger.error(
|
|
||||||
"Error checking/requesting permissions",
|
|
||||||
JSON.stringify(errorLogData, null, 2),
|
|
||||||
);
|
|
||||||
throw new Error(`Failed to obtain storage permissions: ${err.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 safe storage and offers sharing.
|
|
||||||
*
|
|
||||||
* Platform-specific behavior:
|
|
||||||
* - Saves to app's Documents directory
|
|
||||||
* - Offers sharing functionality to move file elsewhere
|
|
||||||
*
|
|
||||||
* The method handles:
|
|
||||||
* 1. Writing to app-safe storage
|
|
||||||
* 2. Sharing the file with user's preferred app
|
|
||||||
* 3. Error handling and logging
|
|
||||||
*
|
|
||||||
* @param fileName - The name of the file to create (e.g. "backup.json")
|
|
||||||
* @param content - The content to write to the file
|
|
||||||
*
|
|
||||||
* @throws Error if:
|
|
||||||
* - File writing fails
|
|
||||||
* - Sharing fails
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* // Save and share a JSON file
|
|
||||||
* await platformService.writeFile(
|
|
||||||
* "backup.json",
|
|
||||||
* JSON.stringify(data)
|
|
||||||
* );
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
async writeFile(fileName: string, content: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
const logData = {
|
|
||||||
targetFileName: fileName,
|
|
||||||
contentLength: content.length,
|
|
||||||
platform: this.getCapabilities().isIOS ? "iOS" : "Android",
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
logger.log(
|
|
||||||
"Starting writeFile operation",
|
|
||||||
JSON.stringify(logData, null, 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
// For Android, we need to handle content URIs differently
|
|
||||||
if (this.getCapabilities().isIOS) {
|
|
||||||
// Write to app's Documents directory for iOS
|
|
||||||
const writeResult = await Filesystem.writeFile({
|
|
||||||
path: fileName,
|
|
||||||
data: content,
|
|
||||||
directory: Directory.Data,
|
|
||||||
encoding: Encoding.UTF8,
|
|
||||||
});
|
|
||||||
|
|
||||||
const writeSuccessLogData = {
|
|
||||||
path: writeResult.uri,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
logger.log(
|
|
||||||
"File write successful",
|
|
||||||
JSON.stringify(writeSuccessLogData, null, 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Offer to share the file
|
|
||||||
try {
|
|
||||||
await Share.share({
|
|
||||||
title: "TimeSafari Backup",
|
|
||||||
text: "Here is your TimeSafari backup file.",
|
|
||||||
url: writeResult.uri,
|
|
||||||
dialogTitle: "Share your backup",
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.log(
|
|
||||||
"Share dialog shown",
|
|
||||||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
|
|
||||||
);
|
|
||||||
} catch (shareError) {
|
|
||||||
// Log share error but don't fail the operation
|
|
||||||
logger.error(
|
|
||||||
"Share dialog failed",
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
error: shareError,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For Android, first write to app's Documents directory
|
|
||||||
const writeResult = await Filesystem.writeFile({
|
|
||||||
path: fileName,
|
|
||||||
data: content,
|
|
||||||
directory: Directory.Data,
|
|
||||||
encoding: Encoding.UTF8,
|
|
||||||
});
|
|
||||||
|
|
||||||
const writeSuccessLogData = {
|
|
||||||
path: writeResult.uri,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
logger.log(
|
|
||||||
"File write successful to app storage",
|
|
||||||
JSON.stringify(writeSuccessLogData, null, 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Then share the file to let user choose where to save it
|
|
||||||
try {
|
|
||||||
await Share.share({
|
|
||||||
title: "TimeSafari Backup",
|
|
||||||
text: "Here is your TimeSafari backup file.",
|
|
||||||
url: writeResult.uri,
|
|
||||||
dialogTitle: "Save your backup",
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.log(
|
|
||||||
"Share dialog shown for Android",
|
|
||||||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
|
|
||||||
);
|
|
||||||
} catch (shareError) {
|
|
||||||
// Log share error but don't fail the operation
|
|
||||||
logger.error(
|
|
||||||
"Share dialog failed for Android",
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
error: shareError,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const err = error as Error;
|
|
||||||
const finalErrorLogData = {
|
|
||||||
error: {
|
|
||||||
message: err.message,
|
|
||||||
name: err.name,
|
|
||||||
stack: err.stack,
|
|
||||||
},
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
logger.error(
|
|
||||||
"Error in writeFile operation:",
|
|
||||||
JSON.stringify(finalErrorLogData, null, 2),
|
|
||||||
);
|
|
||||||
throw new Error(`Failed to save file: ${err.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes content to a file in the device's app-private storage.
|
|
||||||
* Then shares the file using the system share dialog.
|
|
||||||
*
|
|
||||||
* Works on both Android and iOS without needing external storage permissions.
|
|
||||||
*
|
|
||||||
* @param fileName - The name of the file to create (e.g. "backup.json")
|
|
||||||
* @param content - The content to write to the file
|
|
||||||
*/
|
|
||||||
async writeAndShareFile(fileName: string, content: string): Promise<void> {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
const logData = {
|
|
||||||
action: 'writeAndShareFile',
|
|
||||||
fileName,
|
|
||||||
contentLength: content.length,
|
|
||||||
timestamp,
|
|
||||||
};
|
|
||||||
logger.log('[CapacitorPlatformService]', JSON.stringify(logData, null, 2));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { uri } = await Filesystem.writeFile({
|
|
||||||
path: fileName,
|
|
||||||
data: content,
|
|
||||||
directory: Directory.Data,
|
|
||||||
encoding: Encoding.UTF8,
|
|
||||||
recursive: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.log('[CapacitorPlatformService] File write successful:', { uri, timestamp: new Date().toISOString() });
|
|
||||||
|
|
||||||
await Share.share({
|
|
||||||
title: 'TimeSafari Backup',
|
|
||||||
text: 'Here is your backup file.',
|
|
||||||
url: uri,
|
|
||||||
dialogTitle: 'Share your backup file',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
const err = error as Error;
|
|
||||||
const errLog = {
|
|
||||||
message: err.message,
|
|
||||||
stack: err.stack,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
logger.error('[CapacitorPlatformService] Error writing or sharing file:', JSON.stringify(errLog, null, 2));
|
|
||||||
throw new Error(`Failed to write or share file: ${err.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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" });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import {
|
|
||||||
ImageResult,
|
|
||||||
PlatformService,
|
|
||||||
PlatformCapabilities,
|
|
||||||
} 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 {
|
|
||||||
/**
|
|
||||||
* Gets the capabilities of the Electron platform
|
|
||||||
* @returns Platform capabilities object
|
|
||||||
*/
|
|
||||||
getCapabilities(): PlatformCapabilities {
|
|
||||||
return {
|
|
||||||
hasFileSystem: false, // Not implemented yet
|
|
||||||
hasCamera: false, // Not implemented yet
|
|
||||||
isMobile: false,
|
|
||||||
isIOS: false,
|
|
||||||
hasFileDownload: false, // Not implemented yet
|
|
||||||
needsFileHandlingInstructions: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import {
|
|
||||||
ImageResult,
|
|
||||||
PlatformService,
|
|
||||||
PlatformCapabilities,
|
|
||||||
} 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 {
|
|
||||||
/**
|
|
||||||
* Gets the capabilities of the PyWebView platform
|
|
||||||
* @returns Platform capabilities object
|
|
||||||
*/
|
|
||||||
getCapabilities(): PlatformCapabilities {
|
|
||||||
return {
|
|
||||||
hasFileSystem: false, // Not implemented yet
|
|
||||||
hasCamera: false, // Not implemented yet
|
|
||||||
isMobile: false,
|
|
||||||
isIOS: false,
|
|
||||||
hasFileDownload: false, // Not implemented yet
|
|
||||||
needsFileHandlingInstructions: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
import {
|
|
||||||
ImageResult,
|
|
||||||
PlatformService,
|
|
||||||
PlatformCapabilities,
|
|
||||||
} 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 {
|
|
||||||
/**
|
|
||||||
* Gets the capabilities of the web platform
|
|
||||||
* @returns Platform capabilities object
|
|
||||||
*/
|
|
||||||
getCapabilities(): PlatformCapabilities {
|
|
||||||
return {
|
|
||||||
hasFileSystem: false,
|
|
||||||
hasCamera: true, // Through file input with capture
|
|
||||||
isMobile: /iPhone|iPad|iPod|Android/i.test(navigator.userAgent),
|
|
||||||
isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
|
|
||||||
hasFileDownload: true,
|
|
||||||
needsFileHandlingInstructions: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,10 +21,7 @@ function safeStringify(obj: unknown) {
|
|||||||
|
|
||||||
export const logger = {
|
export const logger = {
|
||||||
log: (message: string, ...args: unknown[]) => {
|
log: (message: string, ...args: unknown[]) => {
|
||||||
if (
|
if (process.env.NODE_ENV !== "production") {
|
||||||
process.env.NODE_ENV !== "production" ||
|
|
||||||
process.env.VITE_PLATFORM === "capacitor"
|
|
||||||
) {
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(message, ...args);
|
console.log(message, ...args);
|
||||||
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
||||||
@@ -32,10 +29,7 @@ export const logger = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
warn: (message: string, ...args: unknown[]) => {
|
warn: (message: string, ...args: unknown[]) => {
|
||||||
if (
|
if (process.env.NODE_ENV !== "production") {
|
||||||
process.env.NODE_ENV !== "production" ||
|
|
||||||
process.env.VITE_PLATFORM === "capacitor"
|
|
||||||
) {
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn(message, ...args);
|
console.warn(message, ...args);
|
||||||
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
||||||
|
|||||||
@@ -420,7 +420,53 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataExportSection :active-did="activeDid" />
|
<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>
|
||||||
|
|
||||||
<!-- id used by puppeteer test script -->
|
<!-- id used by puppeteer test script -->
|
||||||
<h3
|
<h3
|
||||||
@@ -900,7 +946,6 @@ import PushNotificationPermission from "../components/PushNotificationPermission
|
|||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import TopMessage from "../components/TopMessage.vue";
|
import TopMessage from "../components/TopMessage.vue";
|
||||||
import UserNameDialog from "../components/UserNameDialog.vue";
|
import UserNameDialog from "../components/UserNameDialog.vue";
|
||||||
import DataExportSection from "../components/DataExportSection.vue";
|
|
||||||
import {
|
import {
|
||||||
AppString,
|
AppString,
|
||||||
DEFAULT_IMAGE_API_SERVER,
|
DEFAULT_IMAGE_API_SERVER,
|
||||||
@@ -954,7 +999,6 @@ const inputImportFileNameRef = ref<Blob>();
|
|||||||
QuickNav,
|
QuickNav,
|
||||||
TopMessage,
|
TopMessage,
|
||||||
UserNameDialog,
|
UserNameDialog,
|
||||||
DataExportSection,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class AccountViewView extends Vue {
|
export default class AccountViewView extends Vue {
|
||||||
|
|||||||
@@ -350,47 +350,6 @@ import * as serverUtil from "../libs/endorserServer";
|
|||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { GiveRecordWithContactInfo } from "types";
|
import { GiveRecordWithContactInfo } from "types";
|
||||||
|
|
||||||
interface Claim {
|
|
||||||
claim?: Claim; // For nested claims in Verifiable Credentials
|
|
||||||
agent?: {
|
|
||||||
identifier?: string;
|
|
||||||
did?: string;
|
|
||||||
};
|
|
||||||
recipient?: {
|
|
||||||
identifier?: string;
|
|
||||||
did?: string;
|
|
||||||
};
|
|
||||||
provider?:
|
|
||||||
| {
|
|
||||||
identifier?: string;
|
|
||||||
}
|
|
||||||
| Array<{ identifier?: string }>;
|
|
||||||
object?: {
|
|
||||||
amountOfThisGood?: number;
|
|
||||||
unitCode?: string;
|
|
||||||
};
|
|
||||||
description?: string;
|
|
||||||
image?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FulfillsPlan {
|
|
||||||
locLat?: number;
|
|
||||||
locLon?: number;
|
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Provider {
|
|
||||||
identifier?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProvidedByPlan {
|
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FeedError {
|
|
||||||
userMessage?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HomeView Component
|
* HomeView Component
|
||||||
*
|
*
|
||||||
@@ -1028,7 +987,7 @@ export default class HomeView extends Vue {
|
|||||||
* @param claim The claim object containing giver information
|
* @param claim The claim object containing giver information
|
||||||
* @returns The giver's DID
|
* @returns The giver's DID
|
||||||
*/
|
*/
|
||||||
private extractGiverDid(claim: Claim) {
|
private extractGiverDid(claim: any) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
return claim.agent?.identifier || (claim.agent as any)?.did;
|
return claim.agent?.identifier || (claim.agent as any)?.did;
|
||||||
}
|
}
|
||||||
@@ -1039,7 +998,7 @@ export default class HomeView extends Vue {
|
|||||||
* @internal
|
* @internal
|
||||||
* Called by processRecord()
|
* Called by processRecord()
|
||||||
*/
|
*/
|
||||||
private extractRecipientDid(claim: Claim) {
|
private extractRecipientDid(claim: any) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
return claim.recipient?.identifier || (claim.recipient as any)?.did;
|
return claim.recipient?.identifier || (claim.recipient as any)?.did;
|
||||||
}
|
}
|
||||||
@@ -1097,7 +1056,7 @@ export default class HomeView extends Vue {
|
|||||||
*/
|
*/
|
||||||
private shouldIncludeRecord(
|
private shouldIncludeRecord(
|
||||||
record: GiveSummaryRecord,
|
record: GiveSummaryRecord,
|
||||||
fulfillsPlan?: FulfillsPlan,
|
fulfillsPlan: any,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!this.isAnyFeedFilterOn) {
|
if (!this.isAnyFeedFilterOn) {
|
||||||
return true;
|
return true;
|
||||||
@@ -1131,7 +1090,7 @@ export default class HomeView extends Vue {
|
|||||||
* @internal
|
* @internal
|
||||||
* Called by processRecord()
|
* Called by processRecord()
|
||||||
*/
|
*/
|
||||||
private extractProvider(claim: Claim): Provider | undefined {
|
private extractProvider(claim: any) {
|
||||||
return Array.isArray(claim.provider) ? claim.provider[0] : claim.provider;
|
return Array.isArray(claim.provider) ? claim.provider[0] : claim.provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1141,7 +1100,7 @@ export default class HomeView extends Vue {
|
|||||||
* @internal
|
* @internal
|
||||||
* Called by processRecord()
|
* Called by processRecord()
|
||||||
*/
|
*/
|
||||||
private async getProvidedByPlan(provider: Provider | undefined) {
|
private async getProvidedByPlan(provider: any) {
|
||||||
return await getPlanFromCache(
|
return await getPlanFromCache(
|
||||||
provider?.identifier as string,
|
provider?.identifier as string,
|
||||||
this.axios,
|
this.axios,
|
||||||
@@ -1179,12 +1138,12 @@ export default class HomeView extends Vue {
|
|||||||
*/
|
*/
|
||||||
private createFeedRecord(
|
private createFeedRecord(
|
||||||
record: GiveSummaryRecord,
|
record: GiveSummaryRecord,
|
||||||
claim: Claim,
|
claim: any,
|
||||||
giverDid: string,
|
giverDid: string,
|
||||||
recipientDid: string,
|
recipientDid: string,
|
||||||
provider: Provider | undefined,
|
provider: any,
|
||||||
fulfillsPlan?: FulfillsPlan,
|
fulfillsPlan: any,
|
||||||
providedByPlan?: ProvidedByPlan,
|
providedByPlan: any,
|
||||||
): GiveRecordWithContactInfo {
|
): GiveRecordWithContactInfo {
|
||||||
return {
|
return {
|
||||||
...record,
|
...record,
|
||||||
@@ -1243,16 +1202,14 @@ export default class HomeView extends Vue {
|
|||||||
* @internal
|
* @internal
|
||||||
* Called by updateAllFeed()
|
* Called by updateAllFeed()
|
||||||
*/
|
*/
|
||||||
private handleFeedError(e: unknown) {
|
private handleFeedError(e: any) {
|
||||||
logger.error("Error with feed load:", e);
|
logger.error("Error with feed load:", e);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Feed Error",
|
title: "Feed Error",
|
||||||
text:
|
text: e.userMessage || "There was an error retrieving feed data.",
|
||||||
(e as FeedError)?.userMessage ||
|
|
||||||
"There was an error retrieving feed data.",
|
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
// this might be wrong since "name" goes with params, but it works so test well when you change it
|
||||||
query: {
|
query: {
|
||||||
destinationPathAfter: "/",
|
destinationPathAfter: "/",
|
||||||
hideBackButton: "true",
|
hideBackButton: true,
|
||||||
imageUrl: url,
|
imageUrl: url,
|
||||||
recipientDid: this.activeDid,
|
recipientDid: this.activeDid,
|
||||||
},
|
},
|
||||||
@@ -221,63 +221,13 @@ export default class SharedPhotoView extends Vue {
|
|||||||
|
|
||||||
this.uploading = false;
|
this.uploading = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Log the raw error first
|
logger.error("Error uploading the image", error);
|
||||||
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(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: errorMessage,
|
text: "There was an error saving the picture.",
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,31 +1,35 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020", // Latest ECMAScript features that are widely supported by modern browsers
|
"target": "ES2020", // Latest ECMAScript features that are widely supported by modern browsers
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"module": "ESNext", // Use ES modules
|
"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
|
"strict": true, // Enable all strict type checking options
|
||||||
"noUnusedLocals": true,
|
"jsx": "preserve", // Preserves JSX to be transformed by Babel or another transpiler
|
||||||
"noUnusedParameters": true,
|
"moduleResolution": "node", // Use Node.js style module resolution
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"experimentalDecorators": true,
|
"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
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"]
|
"@/components/*": ["components/*"],
|
||||||
}
|
"@/views/*": ["views/*"],
|
||||||
|
"@/db/*": ["db/*"],
|
||||||
|
"@/libs/*": ["libs/*"],
|
||||||
|
"@/constants/*": ["constants/*"],
|
||||||
|
"@/store/*": ["store/*"]
|
||||||
|
},
|
||||||
|
"lib": ["ES2020", "dom", "dom.iterable"], // Include typings for ES2020 and DOM APIs
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*.ts",
|
"src/**/*.ts",
|
||||||
"src/**/*.d.ts",
|
|
||||||
"src/**/*.tsx",
|
"src/**/*.tsx",
|
||||||
"src/**/*.vue"
|
"src/**/*.vue",
|
||||||
|
"test-playwright/**/*.ts",
|
||||||
|
"test-playwright/**/*.tsx"
|
||||||
],
|
],
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowSyntheticDefaultImports": true
|
|
||||||
},
|
|
||||||
"include": ["vite.config.*"]
|
|
||||||
}
|
|
||||||