Compare commits
6 Commits
matthew-sc
...
sql-absurd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9ce884513 | ||
|
|
a1a1543ae1 | ||
|
|
93591a5815 | ||
|
|
b30c4c8b30 | ||
|
|
1f9db0ba94 | ||
|
|
bdc2d71d3c |
172
.cursor/rules/SQLITE.mdc
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
# @capacitor-community/sqlite MDC Ruleset
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
This ruleset is for the `@capacitor-community/sqlite` plugin, a Capacitor community plugin that provides native and Electron SQLite database functionality with encryption support.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
- Native SQLite database support for iOS, Android, and Electron
|
||||||
|
- Database encryption support using SQLCipher (Native) and better-sqlite3-multiple-ciphers (Electron)
|
||||||
|
- Biometric authentication support
|
||||||
|
- Cross-platform database operations
|
||||||
|
- JSON import/export capabilities
|
||||||
|
- Database migration support
|
||||||
|
- Sync table functionality
|
||||||
|
|
||||||
|
## Platform Support Matrix
|
||||||
|
|
||||||
|
### Core Database Operations
|
||||||
|
| Operation | Android | iOS | Electron | Web |
|
||||||
|
|-----------|---------|-----|----------|-----|
|
||||||
|
| Create Connection (RW) | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Create Connection (RO) | ✅ | ✅ | ✅ | ❌ |
|
||||||
|
| Open DB (non-encrypted) | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Open DB (encrypted) | ✅ | ✅ | ✅ | ❌ |
|
||||||
|
| Execute/Query | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Import/Export JSON | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
|
||||||
|
### Security Features
|
||||||
|
| Feature | Android | iOS | Electron | Web |
|
||||||
|
|---------|---------|-----|----------|-----|
|
||||||
|
| Encryption | ✅ | ✅ | ✅ | ❌ |
|
||||||
|
| Biometric Auth | ✅ | ✅ | ✅ | ❌ |
|
||||||
|
| Secret Management | ✅ | ✅ | ✅ | ❌ |
|
||||||
|
|
||||||
|
## Configuration Requirements
|
||||||
|
|
||||||
|
### Base Configuration
|
||||||
|
```typescript
|
||||||
|
// capacitor.config.ts
|
||||||
|
{
|
||||||
|
plugins: {
|
||||||
|
CapacitorSQLite: {
|
||||||
|
iosDatabaseLocation: 'Library/CapacitorDatabase',
|
||||||
|
iosIsEncryption: true,
|
||||||
|
iosKeychainPrefix: 'your-app-prefix',
|
||||||
|
androidIsEncryption: true,
|
||||||
|
electronIsEncryption: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Platform-Specific Requirements
|
||||||
|
|
||||||
|
#### Android
|
||||||
|
- Minimum SDK: 23
|
||||||
|
- Target SDK: 35
|
||||||
|
- Required Gradle JDK: 21
|
||||||
|
- Required Android Gradle Plugin: 8.7.2
|
||||||
|
- Required manifest settings for backup prevention
|
||||||
|
- Required data extraction rules
|
||||||
|
|
||||||
|
#### iOS
|
||||||
|
- No additional configuration needed beyond base setup
|
||||||
|
- Supports biometric authentication
|
||||||
|
- Uses keychain for encryption
|
||||||
|
|
||||||
|
#### Electron
|
||||||
|
Required dependencies:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"better-sqlite3-multiple-ciphers": "latest",
|
||||||
|
"electron-json-storage": "latest",
|
||||||
|
"jszip": "latest",
|
||||||
|
"node-fetch": "2.6.7",
|
||||||
|
"crypto": "latest",
|
||||||
|
"crypto-js": "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Web
|
||||||
|
- Requires `sql.js` and `jeep-sqlite`
|
||||||
|
- Manual copy of `sql-wasm.wasm` to assets folder
|
||||||
|
- Framework-specific asset placement:
|
||||||
|
- Angular: `src/assets/`
|
||||||
|
- Vue/React: `public/assets/`
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Database Operations
|
||||||
|
1. Always close connections after use
|
||||||
|
2. Use transactions for multiple operations
|
||||||
|
3. Implement proper error handling
|
||||||
|
4. Use prepared statements for queries
|
||||||
|
5. Implement proper database versioning
|
||||||
|
|
||||||
|
### Security
|
||||||
|
1. Always use encryption for sensitive data
|
||||||
|
2. Implement proper secret management
|
||||||
|
3. Use biometric authentication when available
|
||||||
|
4. Follow platform-specific security guidelines
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
1. Use appropriate indexes
|
||||||
|
2. Implement connection pooling
|
||||||
|
3. Use transactions for bulk operations
|
||||||
|
4. Implement proper database cleanup
|
||||||
|
|
||||||
|
## Common Issues and Solutions
|
||||||
|
|
||||||
|
### Android
|
||||||
|
- Build data properties conflict: Add to `app/build.gradle`:
|
||||||
|
```gradle
|
||||||
|
packagingOptions {
|
||||||
|
exclude 'build-data.properties'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Electron
|
||||||
|
- Node-fetch version must be ≤2.6.7
|
||||||
|
- For Capacitor Electron v5:
|
||||||
|
- Use Electron@25.8.4
|
||||||
|
- Add `"skipLibCheck": true` to tsconfig.json
|
||||||
|
|
||||||
|
### Web
|
||||||
|
- Ensure proper WASM file placement
|
||||||
|
- Handle browser compatibility
|
||||||
|
- Implement proper fallbacks
|
||||||
|
|
||||||
|
## Version Compatibility
|
||||||
|
- Requires Node.js ≥16.0.0
|
||||||
|
- Compatible with Capacitor ≥7.0.0
|
||||||
|
- Supports TypeScript 4.1.5+
|
||||||
|
|
||||||
|
## Testing Requirements
|
||||||
|
- Unit tests for database operations
|
||||||
|
- Platform-specific integration tests
|
||||||
|
- Encryption/decryption tests
|
||||||
|
- Biometric authentication tests
|
||||||
|
- Migration tests
|
||||||
|
- Sync functionality tests
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
- API Documentation: `/docs/API.md`
|
||||||
|
- Connection API: `/docs/APIConnection.md`
|
||||||
|
- DB Connection API: `/docs/APIDBConnection.md`
|
||||||
|
- Release Notes: `/docs/info_releases.md`
|
||||||
|
- Changelog: `CHANGELOG.md`
|
||||||
|
|
||||||
|
## Contributing Guidelines
|
||||||
|
- Follow Ionic coding standards
|
||||||
|
- Use provided linting and formatting tools
|
||||||
|
- Maintain platform compatibility
|
||||||
|
- Update documentation
|
||||||
|
- Add appropriate tests
|
||||||
|
- Follow semantic versioning
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
- Regular security updates
|
||||||
|
- Platform compatibility checks
|
||||||
|
- Performance optimization
|
||||||
|
- Documentation updates
|
||||||
|
- Dependency updates
|
||||||
|
|
||||||
|
## License
|
||||||
|
MIT License - See LICENSE file for details
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
description:
|
|
||||||
globs:
|
|
||||||
alwaysApply: true
|
|
||||||
---
|
|
||||||
use the system date function to understand the proper date and time for all interactions.
|
|
||||||
@@ -2,12 +2,11 @@
|
|||||||
|
|
||||||
# iOS doesn't like spaces in the app title.
|
# iOS doesn't like spaces in the app title.
|
||||||
TIME_SAFARI_APP_TITLE="TimeSafari_Dev"
|
TIME_SAFARI_APP_TITLE="TimeSafari_Dev"
|
||||||
VITE_APP_SERVER=http://localhost:8080
|
VITE_APP_SERVER=http://localhost:3000
|
||||||
# This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not production).
|
# This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not production).
|
||||||
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F
|
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F
|
||||||
VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000
|
VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000
|
||||||
# Using shared server by default to ease setup, which works for shared test users.
|
# Using shared server by default to ease setup, which works for shared test users.
|
||||||
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
|
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
|
||||||
VITE_DEFAULT_PARTNER_API_SERVER=http://localhost:3000
|
VITE_DEFAULT_PARTNER_API_SERVER=http://localhost:3000
|
||||||
#VITE_DEFAULT_PUSH_SERVER... can't be set up with localhost domain
|
|
||||||
VITE_PASSKEYS_ENABLED=true
|
VITE_PASSKEYS_ENABLED=true
|
||||||
|
|||||||
6
.env.example
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Admin DID credentials
|
||||||
|
ADMIN_DID=did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F
|
||||||
|
ADMIN_PRIVATE_KEY=2b6472c026ec2aa2c4235c994a63868fc9212d18b58f6cbfe861b52e71330f5b
|
||||||
|
|
||||||
|
# API Configuration
|
||||||
|
ENDORSER_API_URL=https://test-api.endorser.ch/api/v2/claim
|
||||||
@@ -9,4 +9,3 @@ VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
|
|||||||
|
|
||||||
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app
|
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app
|
||||||
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.endorser.ch
|
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.endorser.ch
|
||||||
VITE_DEFAULT_PUSH_SERVER=https://timesafari.app
|
|
||||||
|
|||||||
@@ -9,5 +9,4 @@ VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch
|
|||||||
|
|
||||||
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
|
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
|
||||||
VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch
|
VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch
|
||||||
VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app
|
|
||||||
VITE_PASSKEYS_ENABLED=true
|
VITE_PASSKEYS_ENABLED=true
|
||||||
|
|||||||
@@ -4,12 +4,6 @@ module.exports = {
|
|||||||
node: true,
|
node: true,
|
||||||
es2022: true,
|
es2022: true,
|
||||||
},
|
},
|
||||||
ignorePatterns: [
|
|
||||||
'node_modules/',
|
|
||||||
'dist/',
|
|
||||||
'dist-electron/',
|
|
||||||
'*.d.ts'
|
|
||||||
],
|
|
||||||
extends: [
|
extends: [
|
||||||
"plugin:vue/vue3-recommended",
|
"plugin:vue/vue3-recommended",
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
|
|||||||
5
.gitignore
vendored
@@ -51,7 +51,6 @@ vendor/
|
|||||||
# Build logs
|
# Build logs
|
||||||
build_logs/
|
build_logs/
|
||||||
|
|
||||||
# PWA icon files generated by capacitor-assets
|
android/app/src/main/assets/public
|
||||||
icons
|
android/app/src/main/res
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
71
BUILDING.md
@@ -9,6 +9,19 @@ For a quick dev environment setup, use [pkgx](https://pkgx.dev).
|
|||||||
- Node.js (LTS version recommended)
|
- Node.js (LTS version recommended)
|
||||||
- npm (comes with Node.js)
|
- npm (comes with Node.js)
|
||||||
- Git
|
- Git
|
||||||
|
- For Android builds: Android Studio with SDK installed
|
||||||
|
- For iOS builds: macOS with Xcode and ruby gems & bundle
|
||||||
|
- `pkgx +rubygems.org sh`
|
||||||
|
|
||||||
|
- ... and you may have to fix these, especially with pkgx
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gem_path=$(which gem)
|
||||||
|
shortened_path="${gem_path:h:h}"
|
||||||
|
export GEM_HOME=$shortened_path
|
||||||
|
export GEM_PATH=$shortened_path
|
||||||
|
```
|
||||||
|
|
||||||
- For desktop builds: Additional build tools based on your OS
|
- For desktop builds: Additional build tools based on your OS
|
||||||
|
|
||||||
## Forks
|
## Forks
|
||||||
@@ -71,7 +84,7 @@ Install dependencies:
|
|||||||
* For test, build the app (because test server is not yet set up to build):
|
* For test, build the app (because test server is not yet set up to build):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app VITE_PASSKEYS_ENABLED=true npm run build
|
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_PASSKEYS_ENABLED=true npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
... and transfer to the test server:
|
... and transfer to the test server:
|
||||||
@@ -313,32 +326,6 @@ npm run build:electron-prod && npm run electron:start
|
|||||||
|
|
||||||
Prerequisites: macOS with Xcode installed
|
Prerequisites: macOS with Xcode installed
|
||||||
|
|
||||||
#### First-time iOS Configuration
|
|
||||||
|
|
||||||
- Generate certificates inside XCode.
|
|
||||||
|
|
||||||
- Right-click on App and under Signing & Capabilities set the Team.
|
|
||||||
|
|
||||||
#### Each Release
|
|
||||||
|
|
||||||
0. First time (or if XCode dependencies change):
|
|
||||||
|
|
||||||
- `pkgx +rubygems.org sh`
|
|
||||||
|
|
||||||
- ... and you may have to fix these, especially with pkgx
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gem_path=$(which gem)
|
|
||||||
shortened_path="${gem_path:h:h}"
|
|
||||||
export GEM_HOME=$shortened_path
|
|
||||||
export GEM_PATH=$shortened_path
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ios/App
|
|
||||||
pod install
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Build the web assets:
|
1. Build the web assets:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -347,7 +334,6 @@ Prerequisites: macOS with Xcode installed
|
|||||||
npm run build:capacitor
|
npm run build:capacitor
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
2. Update iOS project with latest build:
|
2. Update iOS project with latest build:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -359,11 +345,7 @@ 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
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -371,10 +353,10 @@ Prerequisites: macOS with Xcode installed
|
|||||||
|
|
||||||
```
|
```
|
||||||
cd ios/App
|
cd ios/App
|
||||||
xcrun agvtool new-version 25
|
xcrun agvtool new-version 15
|
||||||
# Unfortunately this edits Info.plist directly.
|
# Unfortunately this edits Info.plist directly.
|
||||||
#xcrun agvtool new-marketing-version 0.4.5
|
#xcrun agvtool new-marketing-version 0.4.5
|
||||||
cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.5.1;/g" > temp
|
cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.4.5;/g" > temp
|
||||||
mv temp App.xcodeproj/project.pbxproj
|
mv temp App.xcodeproj/project.pbxproj
|
||||||
cd -
|
cd -
|
||||||
```
|
```
|
||||||
@@ -387,25 +369,28 @@ Prerequisites: macOS with Xcode installed
|
|||||||
|
|
||||||
6. Use Xcode to build and run on simulator or device.
|
6. Use Xcode to build and run on simulator or device.
|
||||||
|
|
||||||
* Select Product -> Destination with some Simulator version. Then click the run arrow.
|
|
||||||
|
|
||||||
7. Release
|
7. Release
|
||||||
|
|
||||||
* Someday: Under "General" we want to rename a bunch of things to "Time Safari"
|
* Under "General" renamed a bunch of things to "Time Safari"
|
||||||
* Choose Product -> Destination -> Any iOS Device
|
* Choose Product -> Destination -> Build Any iOS
|
||||||
* Choose Product -> Archive
|
* Choose Product -> Archive
|
||||||
* This will trigger a build and take time, needing user's "login" keychain password (user's login password), repeatedly.
|
* This will trigger a build and take time, needing user's "login" keychain password which is just their login password, repeatedly.
|
||||||
* If it fails with `building for 'iOS', but linking in dylib (.../.pkgx/zlib.net/v1.3.0/lib/libz.1.3.dylib) built for 'macOS'` then run XCode outside that terminal (ie. not with `npx cap open ios`).
|
* If it fails with `building for 'iOS', but linking in dylib (.../.pkgx/zlib.net/v1.3.0/lib/libz.1.3.dylib) built for 'macOS'` then run XCode outside that terminal (ie. not with `npx cap open ios`).
|
||||||
* Click Distribute -> App Store Connect
|
* Click Distribute -> App Store Connect
|
||||||
* In AppStoreConnect, add the build to the distribution: remove the current build with the "-" when you hover over it, then "Add Build" with the new build.
|
* In AppStoreConnect, add the build to the distribution: remove the current build with the "-" when you hover over it, then "Add Build" with the new build.
|
||||||
* May have to go to App Review, click Submission, then hover over the build and click "-".
|
|
||||||
* It can take 15 minutes for the build to show up in the list of builds.
|
* It can take 15 minutes for the build to show up in the list of builds.
|
||||||
* You'll probably have to "Manage" something about encryption, disallowed in France.
|
* You'll probably have to "Manage" something about encryption, disallowed in France.
|
||||||
* Then "Save" and "Add to Review" and "Resubmit to App Review".
|
* Then "Save" and "Add to Review" and "Resubmit to App Review".
|
||||||
|
|
||||||
|
#### First-time iOS Configuration
|
||||||
|
|
||||||
|
- Generate certificates inside XCode.
|
||||||
|
|
||||||
|
- Right-click on App and under Signing & Capabilities set the Team.
|
||||||
|
|
||||||
### Android Build
|
### Android Build
|
||||||
|
|
||||||
Prerequisites: Android Studio with Java SDK installed
|
Prerequisites: Android Studio with SDK installed
|
||||||
|
|
||||||
1. Build the web assets:
|
1. Build the web assets:
|
||||||
|
|
||||||
@@ -460,9 +445,7 @@ Prerequisites: Android Studio with Java SDK installed
|
|||||||
* Then `bundleRelease`:
|
* Then `bundleRelease`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd android
|
|
||||||
./gradlew bundleRelease -Dlint.baselines.continue=true
|
./gradlew bundleRelease -Dlint.baselines.continue=true
|
||||||
cd -
|
|
||||||
```
|
```
|
||||||
|
|
||||||
... and find your `aab` file at app/build/outputs/bundle/release
|
... and find your `aab` file at app/build/outputs/bundle/release
|
||||||
@@ -475,8 +458,6 @@ At play.google.com/console:
|
|||||||
- Hit "Next".
|
- Hit "Next".
|
||||||
- Save, go to "Publishing Overview" as prompted, and click "Send changes for review".
|
- Save, go to "Publishing Overview" as prompted, and click "Send changes for review".
|
||||||
|
|
||||||
- Note that if you add testers, you have to go to "Publishing Overview" and send those changes or your (closed) testers won't see it.
|
|
||||||
|
|
||||||
|
|
||||||
## First-time Android Configuration for deep links
|
## First-time Android Configuration for deep links
|
||||||
|
|
||||||
|
|||||||
@@ -1,538 +0,0 @@
|
|||||||
# CEFPython Implementation Survey for TimeSafari
|
|
||||||
|
|
||||||
**Author:** Matthew Raymer
|
|
||||||
**Date:** December 2025
|
|
||||||
**Project:** TimeSafari Cross-Platform Desktop Implementation
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
This survey evaluates implementing CEFPython as an additional desktop platform for TimeSafari, with full integration into the existing migration system used by Capacitor and native web platforms.
|
|
||||||
|
|
||||||
### Key Findings
|
|
||||||
|
|
||||||
**Feasibility:** ✅ **Highly Feasible** - CEFPython can integrate seamlessly with TimeSafari's existing architecture
|
|
||||||
|
|
||||||
**Migration System Compatibility:** ✅ **Full Compatibility** - Can use the exact same `migration.ts` system as Capacitor and web
|
|
||||||
|
|
||||||
**Performance:** ✅ **Excellent** - Native Python backend with Chromium rendering engine
|
|
||||||
|
|
||||||
**Security:** ✅ **Strong** - Chromium's security model with Python backend isolation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Architecture Overview
|
|
||||||
|
|
||||||
### 1.1 Current Platform Architecture
|
|
||||||
|
|
||||||
TimeSafari uses a sophisticated cross-platform architecture with shared codebase and platform-specific implementations:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
src/
|
|
||||||
├── main.common.ts # Shared initialization
|
|
||||||
├── main.web.ts # Web/PWA entry point
|
|
||||||
├── main.capacitor.ts # Mobile entry point
|
|
||||||
├── main.electron.ts # Electron entry point
|
|
||||||
├── main.pywebview.ts # PyWebView entry point
|
|
||||||
├── main.cefpython.ts # NEW: CEFPython entry point
|
|
||||||
├── services/
|
|
||||||
│ ├── PlatformService.ts # Platform abstraction interface
|
|
||||||
│ ├── PlatformServiceFactory.ts
|
|
||||||
│ └── platforms/
|
|
||||||
│ ├── WebPlatformService.ts
|
|
||||||
│ ├── CapacitorPlatformService.ts
|
|
||||||
│ ├── ElectronPlatformService.ts
|
|
||||||
│ ├── PyWebViewPlatformService.ts
|
|
||||||
│ └── CEFPythonPlatformService.ts # NEW
|
|
||||||
└── cefpython/ # NEW: CEFPython backend
|
|
||||||
├── main.py
|
|
||||||
├── handlers/
|
|
||||||
│ ├── database.py # SQLite with migration support
|
|
||||||
│ ├── crypto.py # Cryptographic operations
|
|
||||||
│ └── api.py # API server integration
|
|
||||||
└── bridge/
|
|
||||||
└── javascript_bridge.py # JS-Python communication
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.2 Migration System Integration
|
|
||||||
|
|
||||||
**Key Insight:** CEFPython can use the exact same migration system as Capacitor and web platforms:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// src/main.cefpython.ts - CEFPython entry point
|
|
||||||
import { initializeApp } from "./main.common";
|
|
||||||
import { runMigrations } from "./db-sql/migration";
|
|
||||||
import { CEFPythonPlatformService } from "./services/platforms/CEFPythonPlatformService";
|
|
||||||
|
|
||||||
const app = initializeApp();
|
|
||||||
|
|
||||||
// Initialize CEFPython platform service
|
|
||||||
const platformService = new CEFPythonPlatformService();
|
|
||||||
|
|
||||||
// Run migrations using the same system as Capacitor
|
|
||||||
async function initializeDatabase() {
|
|
||||||
const sqlExec = (sql: string) => platformService.dbExecute(sql);
|
|
||||||
const sqlQuery = (sql: string) => platformService.dbQuery(sql);
|
|
||||||
const extractMigrationNames = (result: any) => {
|
|
||||||
const names = result.values?.map((row: any) => row.name) || [];
|
|
||||||
return new Set(names);
|
|
||||||
};
|
|
||||||
|
|
||||||
await runMigrations(sqlExec, sqlQuery, extractMigrationNames);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize database before mounting app
|
|
||||||
initializeDatabase().then(() => {
|
|
||||||
app.mount("#app");
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Python Backend Implementation
|
|
||||||
|
|
||||||
### 2.1 Database Handler with Migration Support
|
|
||||||
|
|
||||||
```python
|
|
||||||
# src/cefpython/handlers/database.py
|
|
||||||
import sqlite3
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import List, Dict, Any
|
|
||||||
|
|
||||||
class DatabaseHandler:
|
|
||||||
def __init__(self):
|
|
||||||
self.db_path = self._get_db_path()
|
|
||||||
self.connection = sqlite3.connect(str(self.db_path))
|
|
||||||
self.connection.row_factory = sqlite3.Row
|
|
||||||
|
|
||||||
# Configure for better performance
|
|
||||||
self.connection.execute("PRAGMA journal_mode=WAL;")
|
|
||||||
self.connection.execute("PRAGMA synchronous=NORMAL;")
|
|
||||||
|
|
||||||
def query(self, sql: str, params: List[Any] = None) -> Dict[str, Any]:
|
|
||||||
"""Execute SQL query and return results in Capacitor-compatible format"""
|
|
||||||
cursor = self.connection.cursor()
|
|
||||||
|
|
||||||
if params:
|
|
||||||
cursor.execute(sql, params)
|
|
||||||
else:
|
|
||||||
cursor.execute(sql)
|
|
||||||
|
|
||||||
if sql.strip().upper().startswith('SELECT'):
|
|
||||||
columns = [description[0] for description in cursor.description]
|
|
||||||
rows = []
|
|
||||||
for row in cursor.fetchall():
|
|
||||||
rows.append(dict(zip(columns, row)))
|
|
||||||
return {'values': rows} # Match Capacitor format
|
|
||||||
else:
|
|
||||||
self.connection.commit()
|
|
||||||
return {'affected_rows': cursor.rowcount}
|
|
||||||
|
|
||||||
def execute(self, sql: string, params: List[Any] = None) -> Dict[str, Any]:
|
|
||||||
"""Execute SQL statement (for INSERT, UPDATE, DELETE, CREATE)"""
|
|
||||||
cursor = self.connection.cursor()
|
|
||||||
|
|
||||||
if params:
|
|
||||||
cursor.execute(sql, params)
|
|
||||||
else:
|
|
||||||
cursor.execute(sql)
|
|
||||||
|
|
||||||
self.connection.commit()
|
|
||||||
|
|
||||||
return {
|
|
||||||
'changes': {
|
|
||||||
'changes': cursor.rowcount,
|
|
||||||
'lastId': cursor.lastrowid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.2 Platform Service Implementation
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// src/services/platforms/CEFPythonPlatformService.ts
|
|
||||||
import { PlatformService } from '../PlatformService';
|
|
||||||
import { runMigrations } from '@/db-sql/migration';
|
|
||||||
|
|
||||||
export class CEFPythonPlatformService implements PlatformService {
|
|
||||||
private bridge: any;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.bridge = (window as any).cefBridge;
|
|
||||||
if (!this.bridge) {
|
|
||||||
throw new Error('CEFPython bridge not available');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Database operations using the same interface as Capacitor
|
|
||||||
async dbQuery(sql: string, params?: any[]): Promise<any> {
|
|
||||||
const result = await this.bridge.call('database', 'query', sql, params || []);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async dbExecute(sql: string, params?: any[]): Promise<any> {
|
|
||||||
const result = await this.bridge.call('database', 'execute', sql, params || []);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migration system integration
|
|
||||||
async runMigrations(): Promise<void> {
|
|
||||||
const sqlExec: (sql: string) => Promise<any> = this.dbExecute.bind(this);
|
|
||||||
const sqlQuery: (sql: string) => Promise<any> = this.dbQuery.bind(this);
|
|
||||||
const extractMigrationNames: (result: any) => Set<string> = (result) => {
|
|
||||||
const names = result.values?.map((row: any) => row.name) || [];
|
|
||||||
return new Set(names);
|
|
||||||
};
|
|
||||||
|
|
||||||
await runMigrations(sqlExec, sqlQuery, extractMigrationNames);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Platform detection
|
|
||||||
isCEFPython(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
getCapabilities(): PlatformCapabilities {
|
|
||||||
return {
|
|
||||||
hasCamera: true,
|
|
||||||
hasFileSystem: true,
|
|
||||||
hasNotifications: true,
|
|
||||||
hasSQLite: true,
|
|
||||||
hasCrypto: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Migration System Compatibility
|
|
||||||
|
|
||||||
### 3.1 Key Advantage
|
|
||||||
|
|
||||||
**CEFPython can use the exact same migration system as Capacitor:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Both Capacitor and CEFPython use the same migration.ts
|
|
||||||
import { runMigrations } from '@/db-sql/migration';
|
|
||||||
|
|
||||||
// Capacitor implementation
|
|
||||||
const sqlExec: (sql: string) => Promise<capSQLiteChanges> = this.db.execute.bind(this.db);
|
|
||||||
const sqlQuery: (sql: string) => Promise<DBSQLiteValues> = this.db.query.bind(this.db);
|
|
||||||
|
|
||||||
// CEFPython implementation
|
|
||||||
const sqlExec: (sql: string) => Promise<any> = this.dbExecute.bind(this);
|
|
||||||
const sqlQuery: (sql: string) => Promise<any> = this.dbQuery.bind(this);
|
|
||||||
|
|
||||||
// Both use the same migration runner
|
|
||||||
await runMigrations(sqlExec, sqlQuery, extractMigrationNames);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 Database Format Compatibility
|
|
||||||
|
|
||||||
The Python database handler returns data in the same format as Capacitor:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Python returns Capacitor-compatible format
|
|
||||||
def query(self, sql: str, params: List[Any] = None) -> Dict[str, Any]:
|
|
||||||
# ... execute query ...
|
|
||||||
return {
|
|
||||||
'values': [
|
|
||||||
{'name': '001_initial', 'executed_at': '2025-01-01'},
|
|
||||||
{'name': '002_add_contacts', 'executed_at': '2025-01-02'}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This matches the Capacitor format:
|
|
||||||
```typescript
|
|
||||||
// Capacitor returns same format
|
|
||||||
const result = await this.db.query("SELECT name FROM migrations");
|
|
||||||
// result = { values: [{ name: '001_initial' }, { name: '002_add_contacts' }] }
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Build Configuration
|
|
||||||
|
|
||||||
### 4.1 Vite Configuration
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// vite.config.cefpython.mts
|
|
||||||
import { defineConfig } from 'vite';
|
|
||||||
import { createBuildConfig } from './vite.config.common.mts';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
...createBuildConfig('cefpython'),
|
|
||||||
|
|
||||||
define: {
|
|
||||||
'process.env.VITE_PLATFORM': JSON.stringify('cefpython'),
|
|
||||||
'process.env.VITE_PWA_ENABLED': JSON.stringify(false),
|
|
||||||
__IS_MOBILE__: JSON.stringify(false),
|
|
||||||
__USE_QR_READER__: JSON.stringify(true)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 Package.json Scripts
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"scripts": {
|
|
||||||
"build:cefpython": "vite build --config vite.config.cefpython.mts",
|
|
||||||
"dev:cefpython": "concurrently \"npm run dev:web\" \"python src/cefpython/main.py --dev\"",
|
|
||||||
"test:cefpython": "python -m pytest tests/cefpython/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.3 Python Requirements
|
|
||||||
|
|
||||||
```txt
|
|
||||||
# requirements-cefpython.txt
|
|
||||||
cefpython3>=66.1
|
|
||||||
cryptography>=3.4.0
|
|
||||||
requests>=2.25.0
|
|
||||||
pyinstaller>=4.0
|
|
||||||
pytest>=6.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Platform Service Factory Integration
|
|
||||||
|
|
||||||
### 5.1 Updated Factory
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// src/services/PlatformServiceFactory.ts
|
|
||||||
import { CEFPythonPlatformService } from './platforms/CEFPythonPlatformService';
|
|
||||||
|
|
||||||
export function createPlatformService(platform: string): PlatformService {
|
|
||||||
switch (platform) {
|
|
||||||
case 'web':
|
|
||||||
return new WebPlatformService();
|
|
||||||
case 'capacitor':
|
|
||||||
return new CapacitorPlatformService();
|
|
||||||
case 'electron':
|
|
||||||
return new ElectronPlatformService();
|
|
||||||
case 'pywebview':
|
|
||||||
return new PyWebViewPlatformService();
|
|
||||||
case 'cefpython':
|
|
||||||
return new CEFPythonPlatformService(); // NEW
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported platform: ${platform}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Performance and Security Analysis
|
|
||||||
|
|
||||||
### 6.1 Performance Comparison
|
|
||||||
|
|
||||||
| Metric | Electron | PyWebView | CEFPython | Notes |
|
|
||||||
|--------|----------|-----------|-----------|-------|
|
|
||||||
| **Memory Usage** | 150-200MB | 80-120MB | 100-150MB | CEFPython more efficient than Electron |
|
|
||||||
| **Startup Time** | 3-5s | 2-3s | 2-4s | Similar to PyWebView |
|
|
||||||
| **Database Performance** | Good | Good | Excellent | Native SQLite |
|
|
||||||
| **Crypto Performance** | Good | Good | Excellent | Native Python crypto |
|
|
||||||
| **Bundle Size** | 120-150MB | 50-80MB | 80-120MB | Smaller than Electron |
|
|
||||||
|
|
||||||
### 6.2 Security Features
|
|
||||||
|
|
||||||
```python
|
|
||||||
# src/cefpython/utils/security.py
|
|
||||||
class SecurityManager:
|
|
||||||
def __init__(self):
|
|
||||||
self.blocked_domains = set(['malicious-site.com'])
|
|
||||||
self.allowed_schemes = {'https', 'http', 'file'}
|
|
||||||
|
|
||||||
def validate_network_access(self, url: str) -> bool:
|
|
||||||
"""Validate if network access is allowed"""
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
parsed = urlparse(url)
|
|
||||||
|
|
||||||
# Check blocked domains
|
|
||||||
if parsed.hostname in self.blocked_domains:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Allow HTTPS only for external domains
|
|
||||||
if parsed.scheme != 'https' and parsed.hostname != 'localhost':
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Migration Strategy
|
|
||||||
|
|
||||||
### 7.1 Phase 1: Foundation (Week 1-2)
|
|
||||||
|
|
||||||
**Objectives:**
|
|
||||||
- Set up CEFPython development environment
|
|
||||||
- Create basic application structure
|
|
||||||
- Implement database handler with migration support
|
|
||||||
- Establish JavaScript-Python bridge
|
|
||||||
|
|
||||||
**Deliverables:**
|
|
||||||
- [ ] Basic CEFPython application that loads TimeSafari web app
|
|
||||||
- [ ] Database handler with SQLite integration
|
|
||||||
- [ ] Migration system integration
|
|
||||||
- [ ] JavaScript bridge for communication
|
|
||||||
|
|
||||||
### 7.2 Phase 2: Platform Integration (Week 3-4)
|
|
||||||
|
|
||||||
**Objectives:**
|
|
||||||
- Implement CEFPython platform service
|
|
||||||
- Integrate with existing migration system
|
|
||||||
- Test database operations with real data
|
|
||||||
- Validate migration compatibility
|
|
||||||
|
|
||||||
**Deliverables:**
|
|
||||||
- [ ] CEFPython platform service implementation
|
|
||||||
- [ ] Migration system integration
|
|
||||||
- [ ] Database compatibility testing
|
|
||||||
- [ ] Performance benchmarking
|
|
||||||
|
|
||||||
### 7.3 Phase 3: Feature Integration (Week 5-6)
|
|
||||||
|
|
||||||
**Objectives:**
|
|
||||||
- Integrate with existing platform features
|
|
||||||
- Implement API server integration
|
|
||||||
- Add security features
|
|
||||||
- Test with real user workflows
|
|
||||||
|
|
||||||
**Deliverables:**
|
|
||||||
- [ ] Full feature compatibility
|
|
||||||
- [ ] API integration
|
|
||||||
- [ ] Security implementation
|
|
||||||
- [ ] User workflow testing
|
|
||||||
|
|
||||||
### 7.4 Phase 4: Polish and Distribution (Week 7-8)
|
|
||||||
|
|
||||||
**Objectives:**
|
|
||||||
- Optimize performance
|
|
||||||
- Add build and distribution scripts
|
|
||||||
- Create documentation
|
|
||||||
- Prepare for release
|
|
||||||
|
|
||||||
**Deliverables:**
|
|
||||||
- [ ] Performance optimization
|
|
||||||
- [ ] Build automation
|
|
||||||
- [ ] Documentation
|
|
||||||
- [ ] Release-ready application
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Risk Assessment
|
|
||||||
|
|
||||||
### 8.1 Technical Risks
|
|
||||||
|
|
||||||
| Risk | Probability | Impact | Mitigation |
|
|
||||||
|------|-------------|--------|------------|
|
|
||||||
| **CEFPython compatibility issues** | Medium | High | Use stable CEFPython version, test thoroughly |
|
|
||||||
| **Migration system integration** | Low | High | Follow existing patterns, extensive testing |
|
|
||||||
| **Performance issues** | Low | Medium | Benchmark early, optimize as needed |
|
|
||||||
| **Security vulnerabilities** | Low | High | Implement security manager, regular audits |
|
|
||||||
|
|
||||||
### 8.2 Development Risks
|
|
||||||
|
|
||||||
| Risk | Probability | Impact | Mitigation |
|
|
||||||
|------|-------------|--------|------------|
|
|
||||||
| **Python/CEF knowledge gap** | Medium | Medium | Training, documentation, pair programming |
|
|
||||||
| **Integration complexity** | Medium | Medium | Incremental development, extensive testing |
|
|
||||||
| **Build system complexity** | Low | Medium | Automated build scripts, CI/CD |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Success Metrics
|
|
||||||
|
|
||||||
### 9.1 Technical Metrics
|
|
||||||
|
|
||||||
- [ ] **Migration Compatibility:** 100% compatibility with existing migration system
|
|
||||||
- [ ] **Performance:** < 150MB memory usage, < 4s startup time
|
|
||||||
- [ ] **Security:** Pass security audit, no critical vulnerabilities
|
|
||||||
- [ ] **Reliability:** 99%+ uptime, < 1% error rate
|
|
||||||
|
|
||||||
### 9.2 Development Metrics
|
|
||||||
|
|
||||||
- [ ] **Code Quality:** 90%+ test coverage, < 5% code duplication
|
|
||||||
- [ ] **Documentation:** Complete API documentation, user guides
|
|
||||||
- [ ] **Build Automation:** Automated builds, CI/CD pipeline
|
|
||||||
- [ ] **Release Readiness:** Production-ready application
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Conclusion
|
|
||||||
|
|
||||||
### 10.1 Recommendation
|
|
||||||
|
|
||||||
**✅ PROCEED WITH IMPLEMENTATION**
|
|
||||||
|
|
||||||
CEFPython provides an excellent opportunity to add a robust desktop platform to TimeSafari with:
|
|
||||||
|
|
||||||
1. **Full Migration System Compatibility:** Can use the exact same migration system as Capacitor and web
|
|
||||||
2. **Native Performance:** Python backend with Chromium rendering
|
|
||||||
3. **Security:** Chromium's security model with Python backend isolation
|
|
||||||
4. **Development Efficiency:** Follows established patterns and architecture
|
|
||||||
|
|
||||||
### 10.2 Implementation Priority
|
|
||||||
|
|
||||||
**High Priority:**
|
|
||||||
- Database handler with migration support
|
|
||||||
- JavaScript-Python bridge
|
|
||||||
- Platform service integration
|
|
||||||
- Basic application structure
|
|
||||||
|
|
||||||
**Medium Priority:**
|
|
||||||
- Crypto handler integration
|
|
||||||
- API server integration
|
|
||||||
- Security features
|
|
||||||
- Performance optimization
|
|
||||||
|
|
||||||
**Low Priority:**
|
|
||||||
- Advanced features
|
|
||||||
- Build automation
|
|
||||||
- Documentation
|
|
||||||
- Distribution packaging
|
|
||||||
|
|
||||||
### 10.3 Timeline
|
|
||||||
|
|
||||||
**Total Duration:** 8 weeks (2 months)
|
|
||||||
**Team Size:** 1-2 developers
|
|
||||||
**Risk Level:** Medium
|
|
||||||
**Confidence:** 85%
|
|
||||||
|
|
||||||
The implementation leverages TimeSafari's existing architecture and migration system, making it a natural addition to the platform ecosystem while providing users with a high-performance desktop option.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Next Steps
|
|
||||||
|
|
||||||
1. **Immediate Actions:**
|
|
||||||
- Set up development environment
|
|
||||||
- Create basic CEFPython application structure
|
|
||||||
- Implement database handler with migration support
|
|
||||||
|
|
||||||
2. **Week 1-2:**
|
|
||||||
- Complete foundation implementation
|
|
||||||
- Test migration system integration
|
|
||||||
- Validate database operations
|
|
||||||
|
|
||||||
3. **Week 3-4:**
|
|
||||||
- Implement platform service
|
|
||||||
- Integrate with existing features
|
|
||||||
- Begin performance testing
|
|
||||||
|
|
||||||
4. **Week 5-6:**
|
|
||||||
- Complete feature integration
|
|
||||||
- Security implementation
|
|
||||||
- User workflow testing
|
|
||||||
|
|
||||||
5. **Week 7-8:**
|
|
||||||
- Performance optimization
|
|
||||||
- Build automation
|
|
||||||
- Documentation and release preparation
|
|
||||||
|
|
||||||
This implementation will provide TimeSafari users with a robust, secure, and high-performance desktop application that seamlessly integrates with the existing ecosystem.
|
|
||||||
@@ -7,13 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.4.7]
|
|
||||||
### Fixed
|
|
||||||
- Cameras everywhere
|
|
||||||
### Changed
|
|
||||||
- IndexedDB -> SQLite
|
|
||||||
|
|
||||||
|
|
||||||
## [0.4.5] - 2025.02.23
|
## [0.4.5] - 2025.02.23
|
||||||
### Added
|
### Added
|
||||||
- Total amounts of gives on project page
|
- Total amounts of gives on project page
|
||||||
|
|||||||
@@ -1,469 +0,0 @@
|
|||||||
# GiftedDialog Component Decomposition Plan
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This document outlines a comprehensive plan to refactor the GiftedDialog component by breaking it into smaller, more manageable sub-components. This approach will improve maintainability, testability, and reusability while preparing the codebase for future Pinia integration.
|
|
||||||
|
|
||||||
## Current State Analysis
|
|
||||||
|
|
||||||
The GiftedDialog component (1060 lines) is a complex Vue component that handles:
|
|
||||||
|
|
||||||
- **Two-step wizard UI**: Entity selection → Gift details
|
|
||||||
- **Multiple entity types**: Person/Project as giver/recipient
|
|
||||||
- **Complex conditional rendering**: Based on context and entity types
|
|
||||||
- **Form validation and submission**: Gift recording with API integration
|
|
||||||
- **State management**: UI flow, entity selection, form data
|
|
||||||
|
|
||||||
### Key Challenges
|
|
||||||
|
|
||||||
1. **Large single file**: Difficult to navigate and maintain
|
|
||||||
2. **Mixed concerns**: UI logic, business logic, and API calls in one place
|
|
||||||
3. **Complex state**: Multiple interconnected reactive properties
|
|
||||||
4. **Testing difficulty**: Hard to test individual features in isolation
|
|
||||||
5. **Reusability**: Components like entity grids could be reused elsewhere
|
|
||||||
|
|
||||||
## Decomposition Strategy
|
|
||||||
|
|
||||||
### Phase 1: Extract Display Components (✅ COMPLETED)
|
|
||||||
|
|
||||||
These components handle pure presentation with minimal business logic:
|
|
||||||
|
|
||||||
#### 1. PersonCard.vue ✅
|
|
||||||
|
|
||||||
- **Purpose**: Display individual person entities with selection capability
|
|
||||||
- **Features**:
|
|
||||||
- Person avatar using EntityIcon
|
|
||||||
- Selection states (selectable, conflicted, disabled)
|
|
||||||
- Time icon overlay for contacts
|
|
||||||
- Click event handling
|
|
||||||
- **Props**: `person`, `selectable`, `conflicted`, `showTimeIcon`
|
|
||||||
- **Emits**: `person-selected`
|
|
||||||
|
|
||||||
#### 2. ProjectCard.vue ✅
|
|
||||||
|
|
||||||
- **Purpose**: Display individual project entities with selection capability
|
|
||||||
- **Features**:
|
|
||||||
- Project icon using ProjectIcon
|
|
||||||
- Project name and issuer information
|
|
||||||
- Click event handling
|
|
||||||
- **Props**: `project`, `activeDid`, `allMyDids`, `allContacts`
|
|
||||||
- **Emits**: `project-selected`
|
|
||||||
|
|
||||||
#### 3. EntitySummaryButton.vue ✅
|
|
||||||
|
|
||||||
- **Purpose**: Display selected entity with edit capability in step 2
|
|
||||||
- **Features**:
|
|
||||||
- Entity avatar (person or project)
|
|
||||||
- Entity name and role label
|
|
||||||
- Editable vs locked states
|
|
||||||
- Edit button functionality
|
|
||||||
- **Props**: `entity`, `entityType`, `label`, `editable`
|
|
||||||
- **Emits**: `edit-requested`
|
|
||||||
|
|
||||||
#### 4. AmountInput.vue ✅
|
|
||||||
|
|
||||||
- **Purpose**: Specialized numeric input with increment/decrement controls
|
|
||||||
- **Features**:
|
|
||||||
- Increment/decrement buttons with validation
|
|
||||||
- Configurable min/max values and step size
|
|
||||||
- Input validation and formatting
|
|
||||||
- v-model compatibility
|
|
||||||
- **Props**: `value`, `min`, `max`, `step`, `inputId`
|
|
||||||
- **Emits**: `update:value`
|
|
||||||
|
|
||||||
### Phase 2: Extract Layout Components (✅ COMPLETED)
|
|
||||||
|
|
||||||
These components handle layout and entity organization:
|
|
||||||
|
|
||||||
#### 5. EntityGrid.vue ✅
|
|
||||||
|
|
||||||
- **Purpose**: Unified grid layout for displaying people or projects
|
|
||||||
- **Features**:
|
|
||||||
- Responsive grid layout for people/projects
|
|
||||||
- Special entity integration (You, Unnamed)
|
|
||||||
- Conflict detection integration
|
|
||||||
- Empty state messaging
|
|
||||||
- Show All navigation
|
|
||||||
- Event delegation for entity selection
|
|
||||||
- **Props**: `entityType`, `entities`, `maxItems`, `activeDid`, `allMyDids`, `allContacts`, `conflictChecker`, `showYouEntity`, `youSelectable`, `showAllRoute`, `showAllQueryParams`
|
|
||||||
- **Emits**: `entity-selected`
|
|
||||||
|
|
||||||
#### 6. SpecialEntityCard.vue ✅
|
|
||||||
|
|
||||||
- **Purpose**: Handle special entities like "You" and "Unnamed"
|
|
||||||
- **Features**:
|
|
||||||
- Special icon display (hand, question mark)
|
|
||||||
- Conflict state handling
|
|
||||||
- Configurable styling based on entity type
|
|
||||||
- Click event handling
|
|
||||||
- **Props**: `entityType`, `label`, `icon`, `selectable`, `conflicted`, `entityData`
|
|
||||||
- **Emits**: `entity-selected`
|
|
||||||
|
|
||||||
#### 7. ShowAllCard.vue ✅
|
|
||||||
|
|
||||||
- **Purpose**: Handle "Show All" navigation functionality
|
|
||||||
- **Features**:
|
|
||||||
- Router-link integration
|
|
||||||
- Query parameter passing
|
|
||||||
- Consistent visual styling
|
|
||||||
- Hover effects
|
|
||||||
- **Props**: `entityType`, `routeName`, `queryParams`
|
|
||||||
- **Emits**: None (uses router-link)
|
|
||||||
|
|
||||||
### Phase 3: Extract Step Components (✅ COMPLETED)
|
|
||||||
|
|
||||||
These components handle major UI sections:
|
|
||||||
|
|
||||||
#### 8. EntitySelectionStep.vue ✅
|
|
||||||
|
|
||||||
- **Purpose**: Complete step 1 entity selection interface
|
|
||||||
- **Features**:
|
|
||||||
- Dynamic step labeling based on context
|
|
||||||
- EntityGrid integration for unified entity display
|
|
||||||
- Conflict detection and prevention
|
|
||||||
- Special entity handling (You, Unnamed)
|
|
||||||
- Show All navigation with context preservation
|
|
||||||
- Cancel functionality
|
|
||||||
- Event delegation for entity selection
|
|
||||||
- **Props**: `stepType`, `giverEntityType`, `recipientEntityType`, `showProjects`, `isFromProjectView`, `projects`, `allContacts`, `activeDid`, `allMyDids`, `conflictChecker`, `fromProjectId`, `toProjectId`, `giver`, `receiver`
|
|
||||||
- **Emits**: `entity-selected`, `cancel`
|
|
||||||
|
|
||||||
#### 9. GiftDetailsStep.vue ✅
|
|
||||||
|
|
||||||
- **Purpose**: Complete step 2 gift details form interface
|
|
||||||
- **Features**:
|
|
||||||
- Entity summary display with edit capability
|
|
||||||
- Gift description input with placeholder support
|
|
||||||
- Amount input with increment/decrement controls
|
|
||||||
- Unit code selection (HUR, USD, BTC, etc.)
|
|
||||||
- Photo & more options navigation
|
|
||||||
- Conflict detection and warning display
|
|
||||||
- Form validation and submission
|
|
||||||
- Cancel functionality
|
|
||||||
- **Props**: `giver`, `receiver`, `giverEntityType`, `recipientEntityType`, `description`, `amount`, `unitCode`, `prompt`, `isFromProjectView`, `hasConflict`, `offerId`, `fromProjectId`, `toProjectId`
|
|
||||||
- **Emits**: `update:description`, `update:amount`, `update:unitCode`, `edit-entity`, `explain-data`, `submit`, `cancel`
|
|
||||||
|
|
||||||
### Phase 4: Refactor Main Component (FINAL)
|
|
||||||
|
|
||||||
#### 9. GiftedDialog.vue (PLANNED REFACTOR)
|
|
||||||
|
|
||||||
- **Purpose**: Orchestrate sub-components and manage overall state
|
|
||||||
- **Responsibilities**:
|
|
||||||
- Step navigation logic
|
|
||||||
- Entity conflict detection
|
|
||||||
- API integration for gift recording
|
|
||||||
- Success/error handling
|
|
||||||
- Dialog visibility management
|
|
||||||
|
|
||||||
## Implementation Progress
|
|
||||||
|
|
||||||
### ✅ Completed Components
|
|
||||||
|
|
||||||
**Phase 1: Display Components**
|
|
||||||
|
|
||||||
1. **PersonCard.vue** - Individual person display with selection
|
|
||||||
2. **ProjectCard.vue** - Individual project display with selection
|
|
||||||
3. **EntitySummaryButton.vue** - Selected entity display with edit capability
|
|
||||||
4. **AmountInput.vue** - Numeric input with increment/decrement controls
|
|
||||||
|
|
||||||
**Phase 2: Layout Components**
|
|
||||||
5. **EntityGrid.vue** - Unified grid layout for entity selection
|
|
||||||
6. **SpecialEntityCard.vue** - Special entities (You, Unnamed) with conflict handling
|
|
||||||
7. **ShowAllCard.vue** - Show All navigation with router integration
|
|
||||||
|
|
||||||
**Phase 3: Step Components**
|
|
||||||
8. **EntitySelectionStep.vue** - Complete step 1 entity selection interface
|
|
||||||
9. **GiftDetailsStep.vue** - Complete step 2 gift details form interface
|
|
||||||
|
|
||||||
### 🔄 Next Steps
|
|
||||||
|
|
||||||
1. **Update GiftedDialog.vue** - Integrate all Phase 1-3 components
|
|
||||||
2. **Test integration** - Ensure functionality remains intact
|
|
||||||
3. **Create unit tests** - For all new components
|
|
||||||
4. **Performance validation** - Ensure no regression
|
|
||||||
5. **Phase 4 planning** - Refactor main component to orchestration only
|
|
||||||
|
|
||||||
### 📋 Future Phases
|
|
||||||
|
|
||||||
1. **Extract EntitySelectionStep.vue** - Complete step 1 logic
|
|
||||||
2. **Extract GiftDetailsStep.vue** - Complete step 2 logic
|
|
||||||
3. **Refactor main component** - Minimal orchestration logic
|
|
||||||
4. **Add comprehensive tests** - Unit tests for each component
|
|
||||||
5. **Prepare for Pinia** - State management migration
|
|
||||||
|
|
||||||
## Benefits of This Approach
|
|
||||||
|
|
||||||
### 1. Incremental Refactoring
|
|
||||||
|
|
||||||
- Each phase can be implemented and tested independently
|
|
||||||
- Reduces risk of breaking existing functionality
|
|
||||||
- Allows for gradual improvement over time
|
|
||||||
|
|
||||||
### 2. Improved Maintainability
|
|
||||||
- Smaller, focused components are easier to understand
|
|
||||||
- Clear separation of concerns
|
|
||||||
- Easier to locate and fix bugs
|
|
||||||
|
|
||||||
### 3. Enhanced Testability
|
|
||||||
- Individual components can be unit tested in isolation
|
|
||||||
- Easier to mock dependencies
|
|
||||||
- Better test coverage possible
|
|
||||||
|
|
||||||
### 4. Better Reusability
|
|
||||||
- Components like EntityGrid can be used in other views
|
|
||||||
- PersonCard and ProjectCard can be used throughout the app
|
|
||||||
- AmountInput can be reused for other numeric inputs
|
|
||||||
|
|
||||||
### 5. Pinia Preparation
|
|
||||||
- Smaller components make state management migration easier
|
|
||||||
- Clear data flow patterns emerge
|
|
||||||
- Easier to identify what state should be global vs local
|
|
||||||
|
|
||||||
## Integration Examples
|
|
||||||
|
|
||||||
### Using PersonCard in EntityGrid
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<template>
|
|
||||||
<ul class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-2 gap-y-4">
|
|
||||||
<PersonCard
|
|
||||||
v-for="person in people"
|
|
||||||
:key="person.did"
|
|
||||||
:person="person"
|
|
||||||
:conflicted="wouldCreateConflict(person.did)"
|
|
||||||
@person-selected="handlePersonSelected"
|
|
||||||
/>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using AmountInput in GiftDetailsStep
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<template>
|
|
||||||
<AmountInput
|
|
||||||
:value="amount"
|
|
||||||
:min="0"
|
|
||||||
:max="1000"
|
|
||||||
input-id="gift-amount"
|
|
||||||
@update:value="amount = $event"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using EntitySummaryButton in GiftDetailsStep
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<template>
|
|
||||||
<div class="grid grid-cols-2 gap-2">
|
|
||||||
<EntitySummaryButton
|
|
||||||
:entity="giver"
|
|
||||||
entity-type="person"
|
|
||||||
label="Received from:"
|
|
||||||
:editable="canEditGiver"
|
|
||||||
@edit-requested="handleEditGiver"
|
|
||||||
/>
|
|
||||||
<EntitySummaryButton
|
|
||||||
:entity="receiver"
|
|
||||||
entity-type="person"
|
|
||||||
label="Given to:"
|
|
||||||
:editable="canEditReceiver"
|
|
||||||
@edit-requested="handleEditReceiver"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using EntityGrid in EntitySelectionStep
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<label class="block font-bold mb-4">
|
|
||||||
{{ stepLabel }}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<EntityGrid
|
|
||||||
:entity-type="shouldShowProjects ? 'projects' : 'people'"
|
|
||||||
:entities="shouldShowProjects ? projects : allContacts"
|
|
||||||
:max-items="10"
|
|
||||||
:active-did="activeDid"
|
|
||||||
:all-my-dids="allMyDids"
|
|
||||||
:all-contacts="allContacts"
|
|
||||||
:conflict-checker="wouldCreateConflict"
|
|
||||||
:show-you-entity="showYouEntity"
|
|
||||||
:you-selectable="youSelectable"
|
|
||||||
:show-all-route="showAllRoute"
|
|
||||||
:show-all-query-params="showAllQueryParams"
|
|
||||||
@entity-selected="handleEntitySelected"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using SpecialEntityCard Standalone
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<template>
|
|
||||||
<ul class="grid grid-cols-4 gap-2">
|
|
||||||
<SpecialEntityCard
|
|
||||||
entity-type="you"
|
|
||||||
label="You"
|
|
||||||
icon="hand"
|
|
||||||
:conflicted="wouldCreateConflict(activeDid)"
|
|
||||||
:entity-data="{ did: activeDid, name: 'You' }"
|
|
||||||
@entity-selected="handleYouSelected"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SpecialEntityCard
|
|
||||||
entity-type="unnamed"
|
|
||||||
label="Unnamed"
|
|
||||||
icon="circle-question"
|
|
||||||
:entity-data="{ did: '', name: 'Unnamed' }"
|
|
||||||
@entity-selected="handleUnnamedSelected"
|
|
||||||
/>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using EntitySelectionStep in GiftedDialog
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<template>
|
|
||||||
<div v-show="currentStep === 1">
|
|
||||||
<EntitySelectionStep
|
|
||||||
:step-type="stepType"
|
|
||||||
:giver-entity-type="giverEntityType"
|
|
||||||
:recipient-entity-type="recipientEntityType"
|
|
||||||
:show-projects="showProjects"
|
|
||||||
:is-from-project-view="isFromProjectView"
|
|
||||||
:projects="projects"
|
|
||||||
:all-contacts="allContacts"
|
|
||||||
:active-did="activeDid"
|
|
||||||
:all-my-dids="allMyDids"
|
|
||||||
:conflict-checker="wouldCreateConflict"
|
|
||||||
:from-project-id="fromProjectId"
|
|
||||||
:to-project-id="toProjectId"
|
|
||||||
:giver="giver"
|
|
||||||
:receiver="receiver"
|
|
||||||
@entity-selected="handleEntitySelected"
|
|
||||||
@cancel="cancel"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using GiftDetailsStep in GiftedDialog
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<template>
|
|
||||||
<div v-show="currentStep === 2">
|
|
||||||
<GiftDetailsStep
|
|
||||||
:giver="giver"
|
|
||||||
:receiver="receiver"
|
|
||||||
:giver-entity-type="giverEntityType"
|
|
||||||
:recipient-entity-type="recipientEntityType"
|
|
||||||
:description="description"
|
|
||||||
:amount="parseFloat(amountInput)"
|
|
||||||
:unit-code="unitCode"
|
|
||||||
:prompt="prompt"
|
|
||||||
:is-from-project-view="isFromProjectView"
|
|
||||||
:has-conflict="hasPersonConflict"
|
|
||||||
:offer-id="offerId"
|
|
||||||
:from-project-id="fromProjectId"
|
|
||||||
:to-project-id="toProjectId"
|
|
||||||
@update:description="description = $event"
|
|
||||||
@update:amount="amountInput = $event.toString()"
|
|
||||||
@update:unit-code="unitCode = $event"
|
|
||||||
@edit-entity="handleEditEntity"
|
|
||||||
@explain-data="explainData"
|
|
||||||
@submit="handleSubmit"
|
|
||||||
@cancel="cancel"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Migration Strategy
|
|
||||||
|
|
||||||
### Backward Compatibility
|
|
||||||
- Maintain existing API and prop interfaces
|
|
||||||
- Ensure all existing functionality works unchanged
|
|
||||||
- Preserve all event emissions and callbacks
|
|
||||||
|
|
||||||
### Testing Strategy
|
|
||||||
- Create unit tests for each new component
|
|
||||||
- Maintain existing integration tests
|
|
||||||
- Add visual regression tests for UI components
|
|
||||||
|
|
||||||
### Performance Considerations
|
|
||||||
- Monitor bundle size impact
|
|
||||||
- Ensure no performance regression
|
|
||||||
- Optimize component loading if needed
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
### Input Validation
|
|
||||||
- AmountInput includes proper numeric validation
|
|
||||||
- All user inputs are validated before processing
|
|
||||||
- XSS prevention through proper Vue templating
|
|
||||||
|
|
||||||
### Data Handling
|
|
||||||
- No sensitive data stored in component state
|
|
||||||
- Proper prop validation and type checking
|
|
||||||
- Secure API communication maintained
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
This decomposition plan provides a structured approach to refactoring the GiftedDialog component while maintaining functionality and preparing for future enhancements. The incremental approach reduces risk and allows for continuous improvement of the codebase.
|
|
||||||
|
|
||||||
The completed Phase 1 components (PersonCard, ProjectCard, EntitySummaryButton, AmountInput) provide a solid foundation for the remaining phases and demonstrate the benefits of component decomposition in terms of maintainability, testability, and reusability.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Final Integration Results
|
|
||||||
|
|
||||||
### ✅ **INTEGRATION COMPLETE**
|
|
||||||
|
|
||||||
**Completed on**: 2025-01-28
|
|
||||||
|
|
||||||
**Results:**
|
|
||||||
- **Main GiftedDialog template**: Reduced from ~200 lines to ~20 lines
|
|
||||||
- **Components created**: 9 focused, reusable components
|
|
||||||
- **Lines of code**: ~2,000 lines of well-structured component code
|
|
||||||
- **Backward compatibility**: 100% maintained
|
|
||||||
- **Build status**: ✅ Passing
|
|
||||||
- **Runtime status**: ✅ Working
|
|
||||||
|
|
||||||
**Components Successfully Integrated:**
|
|
||||||
|
|
||||||
1. **PersonCard.vue** - Individual person display with conflict detection
|
|
||||||
2. **ProjectCard.vue** - Individual project display with issuer info
|
|
||||||
3. **EntitySummaryButton.vue** - Selected entity display with edit capability
|
|
||||||
4. **AmountInput.vue** - Numeric input with validation and controls
|
|
||||||
5. **SpecialEntityCard.vue** - "You" and "Unnamed" entity handling
|
|
||||||
6. **ShowAllCard.vue** - Navigation with router integration
|
|
||||||
7. **EntityGrid.vue** - Unified grid layout orchestration
|
|
||||||
8. **EntitySelectionStep.vue** - Complete Step 1 interface
|
|
||||||
9. **GiftDetailsStep.vue** - Complete Step 2 interface
|
|
||||||
|
|
||||||
**Integration Benefits Achieved:**
|
|
||||||
- ✅ **Maintainability**: Each component has single responsibility
|
|
||||||
- ✅ **Testability**: Components can be unit tested in isolation
|
|
||||||
- ✅ **Reusability**: Components can be used across the application
|
|
||||||
- ✅ **Readability**: Clear separation of concerns and focused logic
|
|
||||||
- ✅ **Debugging**: Easier to identify and fix issues
|
|
||||||
- ✅ **Performance**: No performance regression, improved code splitting
|
|
||||||
|
|
||||||
**Next Steps:**
|
|
||||||
1. **Pinia State Management**: Ready for state management migration
|
|
||||||
2. **Component Testing**: Add comprehensive unit tests
|
|
||||||
3. **Visual Testing**: Add Playwright component tests
|
|
||||||
4. **Documentation**: Update component documentation
|
|
||||||
5. **Optimization**: Fine-tune performance if needed
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Author**: Matthew Raymer
|
|
||||||
**Last Updated**: 2025-01-28
|
|
||||||
**Status**: ✅ **INTEGRATION COMPLETE - READY FOR PRODUCTION**
|
|
||||||
@@ -31,8 +31,8 @@ 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 26
|
versionCode 18
|
||||||
versionName "0.5.1"
|
versionName "0.4.7"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
@@ -91,8 +91,6 @@ dependencies {
|
|||||||
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||||
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
||||||
implementation project(':capacitor-android')
|
implementation project(':capacitor-android')
|
||||||
implementation project(':capacitor-community-sqlite')
|
|
||||||
implementation "androidx.biometric:biometric:1.2.0-alpha05"
|
|
||||||
testImplementation "junit:junit:$junitVersion"
|
testImplementation "junit:junit:$junitVersion"
|
||||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||||
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||||
|
|||||||
@@ -9,7 +9,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-community-sqlite')
|
|
||||||
implementation project(':capacitor-mlkit-barcode-scanning')
|
implementation project(':capacitor-mlkit-barcode-scanning')
|
||||||
implementation project(':capacitor-app')
|
implementation project(':capacitor-app')
|
||||||
implementation project(':capacitor-camera')
|
implementation project(':capacitor-camera')
|
||||||
|
|||||||
@@ -16,41 +16,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"SQLite": {
|
|
||||||
"iosDatabaseLocation": "Library/CapacitorDatabase",
|
|
||||||
"iosIsEncryption": true,
|
|
||||||
"iosBiometric": {
|
|
||||||
"biometricAuth": true,
|
|
||||||
"biometricTitle": "Biometric login for TimeSafari"
|
|
||||||
},
|
|
||||||
"androidIsEncryption": true,
|
|
||||||
"androidBiometric": {
|
|
||||||
"biometricAuth": true,
|
|
||||||
"biometricTitle": "Biometric login for TimeSafari"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"ios": {
|
|
||||||
"contentInset": "never",
|
|
||||||
"allowsLinkPreview": true,
|
|
||||||
"scrollEnabled": true,
|
|
||||||
"limitsNavigationsToAppBoundDomains": true,
|
|
||||||
"backgroundColor": "#ffffff",
|
|
||||||
"allowNavigation": [
|
|
||||||
"*.timesafari.app",
|
|
||||||
"*.jsdelivr.net",
|
|
||||||
"api.endorser.ch"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"android": {
|
|
||||||
"allowMixedContent": false,
|
|
||||||
"captureInput": true,
|
|
||||||
"webContentsDebuggingEnabled": false,
|
|
||||||
"allowNavigation": [
|
|
||||||
"*.timesafari.app",
|
|
||||||
"*.jsdelivr.net",
|
|
||||||
"api.endorser.ch"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
[
|
[
|
||||||
{
|
|
||||||
"pkg": "@capacitor-community/sqlite",
|
|
||||||
"classpath": "com.getcapacitor.community.database.sqlite.CapacitorSQLitePlugin"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"pkg": "@capacitor-mlkit/barcode-scanning",
|
"pkg": "@capacitor-mlkit/barcode-scanning",
|
||||||
"classpath": "io.capawesome.capacitorjs.plugins.mlkit.barcodescanning.BarcodeScannerPlugin"
|
"classpath": "io.capawesome.capacitorjs.plugins.mlkit.barcodescanning.BarcodeScannerPlugin"
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
package app.timesafari;
|
package app.timesafari;
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import com.getcapacitor.BridgeActivity;
|
import com.getcapacitor.BridgeActivity;
|
||||||
//import com.getcapacitor.community.sqlite.SQLite;
|
|
||||||
|
|
||||||
public class MainActivity extends BridgeActivity {
|
public class MainActivity extends BridgeActivity {
|
||||||
@Override
|
// ... existing code ...
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
// Initialize SQLite
|
|
||||||
//registerPlugin(SQLite.class);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package timesafari.app;
|
||||||
|
|
||||||
|
import com.getcapacitor.BridgeActivity;
|
||||||
|
|
||||||
|
public class MainActivity extends BridgeActivity {}
|
||||||
BIN
android/app/src/main/res/drawable-land-hdpi/splash.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
android/app/src/main/res/drawable-land-mdpi/splash.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
android/app/src/main/res/drawable-land-xhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
android/app/src/main/res/drawable-land-xxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
android/app/src/main/res/drawable-land-xxxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
android/app/src/main/res/drawable-port-hdpi/splash.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
android/app/src/main/res/drawable-port-mdpi/splash.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
android/app/src/main/res/drawable-port-xhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
android/app/src/main/res/drawable-port-xxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
android/app/src/main/res/drawable-port-xxxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
android/app/src/main/res/drawable/splash.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
@@ -1,9 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
</background>
|
|
||||||
<foreground>
|
|
||||||
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
|
|
||||||
</foreground>
|
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
<?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>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
</background>
|
|
||||||
<foreground>
|
|
||||||
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
|
|
||||||
</foreground>
|
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 15 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
@@ -2,9 +2,6 @@
|
|||||||
include ':capacitor-android'
|
include ':capacitor-android'
|
||||||
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||||
|
|
||||||
include ':capacitor-community-sqlite'
|
|
||||||
project(':capacitor-community-sqlite').projectDir = new File('../node_modules/@capacitor-community/sqlite/android')
|
|
||||||
|
|
||||||
include ':capacitor-mlkit-barcode-scanning'
|
include ':capacitor-mlkit-barcode-scanning'
|
||||||
project(':capacitor-mlkit-barcode-scanning').projectDir = new File('../node_modules/@capacitor-mlkit/barcode-scanning/android')
|
project(':capacitor-mlkit-barcode-scanning').projectDir = new File('../node_modules/@capacitor-mlkit/barcode-scanning/android')
|
||||||
|
|
||||||
|
|||||||
BIN
assets/icon-only.jpg
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
assets/icon.png
|
Before Width: | Height: | Size: 279 KiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 1.9 MiB |
@@ -16,41 +16,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"SQLite": {
|
|
||||||
"iosDatabaseLocation": "Library/CapacitorDatabase",
|
|
||||||
"iosIsEncryption": true,
|
|
||||||
"iosBiometric": {
|
|
||||||
"biometricAuth": true,
|
|
||||||
"biometricTitle": "Biometric login for TimeSafari"
|
|
||||||
},
|
|
||||||
"androidIsEncryption": true,
|
|
||||||
"androidBiometric": {
|
|
||||||
"biometricAuth": true,
|
|
||||||
"biometricTitle": "Biometric login for TimeSafari"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"ios": {
|
|
||||||
"contentInset": "never",
|
|
||||||
"allowsLinkPreview": true,
|
|
||||||
"scrollEnabled": true,
|
|
||||||
"limitsNavigationsToAppBoundDomains": true,
|
|
||||||
"backgroundColor": "#ffffff",
|
|
||||||
"allowNavigation": [
|
|
||||||
"*.timesafari.app",
|
|
||||||
"*.jsdelivr.net",
|
|
||||||
"api.endorser.ch"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"android": {
|
|
||||||
"allowMixedContent": false,
|
|
||||||
"captureInput": true,
|
|
||||||
"webContentsDebuggingEnabled": false,
|
|
||||||
"allowNavigation": [
|
|
||||||
"*.timesafari.app",
|
|
||||||
"*.jsdelivr.net",
|
|
||||||
"api.endorser.ch"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,224 +0,0 @@
|
|||||||
# GiftedDialog Complete Documentation
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The GiftedDialog system is a sophisticated multi-step dialog for recording gifts between people and projects in the TimeSafari application. It consists of a main orchestrating component and 9 specialized child components that handle different aspects of the gift recording workflow.
|
|
||||||
|
|
||||||
## Key Features
|
|
||||||
|
|
||||||
- **Two-Step Workflow**: Entity selection → Gift details
|
|
||||||
- **Multi-Entity Support**: People, projects, and special entities
|
|
||||||
- **Conflict Detection**: Prevents invalid gift combinations
|
|
||||||
- **Responsive Design**: Works across all device sizes
|
|
||||||
- **Accessibility**: Full keyboard navigation and screen reader support
|
|
||||||
- **Validation**: Comprehensive form validation and error handling
|
|
||||||
- **Flexible Integration**: Can be embedded in any view with different contexts
|
|
||||||
|
|
||||||
## Component Architecture
|
|
||||||
|
|
||||||
### Main Component
|
|
||||||
- **GiftedDialog.vue** - Main orchestrating component that manages dialog state, step navigation, and API integration
|
|
||||||
|
|
||||||
### Step Components
|
|
||||||
- **EntitySelectionStep.vue** - Step 1 controller with dynamic labeling and context awareness
|
|
||||||
- **GiftDetailsStep.vue** - Step 2 controller with form validation and entity summaries
|
|
||||||
|
|
||||||
### Layout Components
|
|
||||||
- **EntityGrid.vue** - Unified entity grid layout with responsive design
|
|
||||||
- **EntitySummaryButton.vue** - Selected entity display with edit capability
|
|
||||||
|
|
||||||
### Display Components
|
|
||||||
- **PersonCard.vue** - Individual person display with avatar and selection states
|
|
||||||
- **ProjectCard.vue** - Individual project display with icons and issuer information
|
|
||||||
- **SpecialEntityCard.vue** - Special entities (You, Unnamed) with conflict detection
|
|
||||||
- **ShowAllCard.vue** - Navigation component with router integration
|
|
||||||
|
|
||||||
### Input Components
|
|
||||||
- **AmountInput.vue** - Numeric input with increment/decrement controls and validation
|
|
||||||
|
|
||||||
## Data Flow
|
|
||||||
|
|
||||||
### Step 1: Entity Selection
|
|
||||||
1. User opens dialog → GiftedDialog renders EntitySelectionStep
|
|
||||||
2. EntitySelectionStep renders EntityGrid with entities and configuration
|
|
||||||
3. EntityGrid renders PersonCard/ProjectCard/SpecialEntityCard components
|
|
||||||
4. User clicks entity → Card emits to Grid → Grid emits to Step → Step emits to Dialog
|
|
||||||
5. GiftedDialog updates state and advances to step 2
|
|
||||||
|
|
||||||
### Step 2: Gift Details
|
|
||||||
1. GiftedDialog renders GiftDetailsStep with selected entities
|
|
||||||
2. GiftDetailsStep renders EntitySummaryButton and AmountInput components
|
|
||||||
3. User fills form and clicks submit → Step emits to Dialog
|
|
||||||
4. GiftedDialog processes submission via API and handles success/error
|
|
||||||
|
|
||||||
## Key Props and Configuration
|
|
||||||
|
|
||||||
### GiftedDialog Props
|
|
||||||
```typescript
|
|
||||||
interface GiftedDialogProps {
|
|
||||||
fromProjectId?: string; // Project ID when project is giver
|
|
||||||
toProjectId?: string; // Project ID when project is recipient
|
|
||||||
showProjects?: boolean; // Whether to show projects
|
|
||||||
isFromProjectView?: boolean; // Context flag for project views
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Opening the Dialog
|
|
||||||
```typescript
|
|
||||||
// Basic usage
|
|
||||||
giftedDialog.open();
|
|
||||||
|
|
||||||
// With pre-selected entities and context
|
|
||||||
giftedDialog.open(
|
|
||||||
giverEntity, // Pre-selected giver
|
|
||||||
receiverEntity, // Pre-selected receiver
|
|
||||||
offerId, // Offer context
|
|
||||||
customTitle, // Custom dialog title
|
|
||||||
prompt, // Custom input prompt
|
|
||||||
successCallback // Success handler
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Integration Patterns
|
|
||||||
|
|
||||||
### Basic Integration
|
|
||||||
```vue
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<button @click="openGiftDialog">Record Gift</button>
|
|
||||||
<GiftedDialog ref="giftedDialog" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
export default class Example extends Vue {
|
|
||||||
openGiftDialog() {
|
|
||||||
const dialog = this.$refs.giftedDialog as GiftedDialog;
|
|
||||||
dialog.open();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Project Context Integration
|
|
||||||
```typescript
|
|
||||||
// Gift from project
|
|
||||||
dialog.open(
|
|
||||||
projectEntity, // Project as giver
|
|
||||||
undefined, // User selects receiver
|
|
||||||
undefined, // No offer
|
|
||||||
"Gift from Project",
|
|
||||||
"What did this project provide?"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Gift to project
|
|
||||||
dialog.open(
|
|
||||||
undefined, // User selects giver
|
|
||||||
projectEntity, // Project as receiver
|
|
||||||
undefined, // No offer
|
|
||||||
"Gift to Project",
|
|
||||||
"What was contributed to this project?"
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
## State Management
|
|
||||||
|
|
||||||
The dialog manages internal state through a reactive system:
|
|
||||||
|
|
||||||
- **Step Navigation**: Controls progression from entity selection to gift details
|
|
||||||
- **Entity Selection**: Tracks selected giver and receiver entities
|
|
||||||
- **Conflict Detection**: Prevents selecting same person for both roles
|
|
||||||
- **Form Validation**: Ensures required fields are completed
|
|
||||||
- **API Integration**: Handles gift submission and response processing
|
|
||||||
|
|
||||||
## Conflict Detection Logic
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
function wouldCreateConflict(selectedDid: string): boolean {
|
|
||||||
// Only applies to person-to-person gifts
|
|
||||||
if (giverEntityType !== "person" || recipientEntityType !== "person") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if selecting same person for both roles
|
|
||||||
if (stepType === "giver") {
|
|
||||||
return receiver?.did === selectedDid;
|
|
||||||
} else if (stepType === "recipient") {
|
|
||||||
return giver?.did === selectedDid;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## AmountInput Component Fix
|
|
||||||
|
|
||||||
The AmountInput component was recently fixed to resolve an issue where increment/decrement buttons weren't updating the displayed value:
|
|
||||||
|
|
||||||
### Problem
|
|
||||||
- Input field used `:value="displayValue"` (one-way binding)
|
|
||||||
- Programmatic updates to `displayValue` weren't reflected in DOM
|
|
||||||
|
|
||||||
### Solution
|
|
||||||
- Changed to `v-model="displayValue"` (two-way binding)
|
|
||||||
- Now properly synchronizes programmatic and user input changes
|
|
||||||
|
|
||||||
### Usage
|
|
||||||
```vue
|
|
||||||
<AmountInput
|
|
||||||
:value="amount"
|
|
||||||
:min="0"
|
|
||||||
:max="1000"
|
|
||||||
:step="1"
|
|
||||||
@update:value="handleAmountChange"
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
### Unit Testing
|
|
||||||
- Individual component behavior
|
|
||||||
- Props validation
|
|
||||||
- Event emission
|
|
||||||
- Computed property calculations
|
|
||||||
|
|
||||||
### Integration Testing
|
|
||||||
- Multi-component workflows
|
|
||||||
- State management
|
|
||||||
- API integration
|
|
||||||
- Error handling
|
|
||||||
|
|
||||||
### End-to-End Testing
|
|
||||||
- Complete user workflows
|
|
||||||
- Cross-browser compatibility
|
|
||||||
- Accessibility compliance
|
|
||||||
- Performance validation
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
- **Input Validation**: All user inputs are sanitized and validated
|
|
||||||
- **DID Privacy**: User identifiers only shared with authorized contacts
|
|
||||||
- **API Security**: Requests are cryptographically signed
|
|
||||||
- **XSS Prevention**: Template sanitization and CSP headers
|
|
||||||
|
|
||||||
## Performance Optimizations
|
|
||||||
|
|
||||||
- **Lazy Loading**: Components loaded only when needed
|
|
||||||
- **Virtual Scrolling**: For large entity lists
|
|
||||||
- **Debounced Input**: Prevents excessive API calls
|
|
||||||
- **Computed Properties**: Efficient reactive calculations
|
|
||||||
- **Memory Management**: Proper cleanup on component destruction
|
|
||||||
|
|
||||||
## Accessibility Features
|
|
||||||
|
|
||||||
- **Keyboard Navigation**: Full tab order and keyboard shortcuts
|
|
||||||
- **Screen Reader Support**: ARIA labels and semantic HTML
|
|
||||||
- **Focus Management**: Proper focus handling on open/close
|
|
||||||
- **High Contrast**: Supports high contrast themes
|
|
||||||
- **Responsive Design**: Works on all screen sizes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Author**: Matthew Raymer
|
|
||||||
**Last Updated**: 2025-06-30
|
|
||||||
**Version**: 1.0.0
|
|
||||||
@@ -2,338 +2,283 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This document outlines the implementation of secure storage for the TimeSafari app. The implementation focuses on:
|
This document outlines the implementation of secure storage for the TimeSafari app using a platform-agnostic approach with Capacitor and absurd-sql solutions. The implementation focuses on:
|
||||||
|
|
||||||
1. **Platform-Specific Storage Solutions**:
|
1. **Platform-Specific Storage Solutions**:
|
||||||
- Web: SQLite with IndexedDB backend (absurd-sql)
|
- Web: absurd-sql with IndexedDB backend and Web Worker support
|
||||||
- Electron: SQLite with Node.js backend
|
- iOS/Android: Capacitor SQLite with native SQLite implementation
|
||||||
- Native: (Planned) SQLCipher with platform-specific secure storage
|
- Electron: Node SQLite (planned, not implemented)
|
||||||
|
|
||||||
2. **Key Features**:
|
2. **Key Features**:
|
||||||
- SQLite-based storage using absurd-sql for web
|
- Platform-agnostic SQLite interface
|
||||||
- Platform-specific service factory pattern
|
- Web Worker support for web platform
|
||||||
- Consistent API across platforms
|
- Consistent API across platforms
|
||||||
- Migration support from Dexie.js
|
- Performance optimizations (WAL, mmap)
|
||||||
|
- Comprehensive error handling and logging
|
||||||
|
- Type-safe database operations
|
||||||
|
- Storage quota management
|
||||||
|
- Platform-specific security features
|
||||||
|
|
||||||
## Quick Start
|
## Architecture
|
||||||
|
|
||||||
### 1. Installation
|
The storage implementation follows a layered architecture:
|
||||||
|
|
||||||
```bash
|
1. **Platform Service Layer**
|
||||||
# Core dependencies
|
- `PlatformService` interface defines platform capabilities
|
||||||
npm install @jlongster/sql.js
|
- Platform-specific implementations:
|
||||||
npm install absurd-sql
|
- `WebPlatformService`: Web platform with absurd-sql
|
||||||
|
- `CapacitorPlatformService`: Mobile platforms with native SQLite
|
||||||
|
- `ElectronPlatformService`: Desktop platform (planned)
|
||||||
|
- Platform detection and capability reporting
|
||||||
|
- Storage quota and feature detection
|
||||||
|
|
||||||
# Platform-specific dependencies (for future native support)
|
2. **SQLite Service Layer**
|
||||||
npm install @capacitor/preferences
|
- `SQLiteOperations` interface for database operations
|
||||||
npm install @capacitor-community/biometric-auth
|
- Base implementation in `BaseSQLiteService`
|
||||||
|
- Platform-specific implementations:
|
||||||
|
- `AbsurdSQLService`: Web platform with Web Worker
|
||||||
|
- `CapacitorSQLiteService`: Mobile platforms with native SQLite
|
||||||
|
- `ElectronSQLiteService`: Desktop platform (planned)
|
||||||
|
- Common features:
|
||||||
|
- Transaction support
|
||||||
|
- Prepared statements
|
||||||
|
- Performance monitoring
|
||||||
|
- Error handling
|
||||||
|
- Database statistics
|
||||||
|
|
||||||
|
3. **Data Access Layer**
|
||||||
|
- Type-safe database operations
|
||||||
|
- Transaction support
|
||||||
|
- Prepared statements
|
||||||
|
- Performance monitoring
|
||||||
|
- Error recovery
|
||||||
|
- Data integrity verification
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Web Platform (absurd-sql)
|
||||||
|
|
||||||
|
The web implementation uses absurd-sql with the following features:
|
||||||
|
|
||||||
|
1. **Web Worker Support**
|
||||||
|
- SQLite operations run in a dedicated worker thread
|
||||||
|
- Main thread remains responsive
|
||||||
|
- SharedArrayBuffer support when available
|
||||||
|
- Worker initialization in `sqlite.worker.ts`
|
||||||
|
|
||||||
|
2. **IndexedDB Backend**
|
||||||
|
- Persistent storage using IndexedDB
|
||||||
|
- Automatic data synchronization
|
||||||
|
- Storage quota management (1GB limit)
|
||||||
|
- Virtual file system configuration
|
||||||
|
|
||||||
|
3. **Performance Optimizations**
|
||||||
|
- WAL mode for better concurrency
|
||||||
|
- Memory-mapped I/O (30GB when available)
|
||||||
|
- Prepared statement caching
|
||||||
|
- 2MB cache size
|
||||||
|
- Configurable performance settings
|
||||||
|
|
||||||
|
Example configuration:
|
||||||
|
```typescript
|
||||||
|
const webConfig: SQLiteConfig = {
|
||||||
|
name: 'timesafari',
|
||||||
|
useWAL: true,
|
||||||
|
useMMap: typeof SharedArrayBuffer !== 'undefined',
|
||||||
|
mmapSize: 30000000000,
|
||||||
|
usePreparedStatements: true,
|
||||||
|
maxPreparedStatements: 100
|
||||||
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Basic Usage
|
### Mobile Platform (Capacitor SQLite)
|
||||||
|
|
||||||
|
The mobile implementation uses Capacitor SQLite with:
|
||||||
|
|
||||||
|
1. **Native SQLite**
|
||||||
|
- Direct access to platform SQLite
|
||||||
|
- Native performance
|
||||||
|
- Platform-specific optimizations
|
||||||
|
- 2GB storage limit
|
||||||
|
|
||||||
|
2. **Platform Integration**
|
||||||
|
- iOS: Native SQLite with WAL support
|
||||||
|
- Android: Native SQLite with WAL support
|
||||||
|
- Platform-specific permissions handling
|
||||||
|
- Storage quota management
|
||||||
|
|
||||||
|
Example configuration:
|
||||||
```typescript
|
```typescript
|
||||||
// Using the platform service
|
const mobileConfig: SQLiteConfig = {
|
||||||
import { PlatformServiceFactory } from '../services/PlatformServiceFactory';
|
name: 'timesafari',
|
||||||
|
useWAL: true,
|
||||||
|
useMMap: false, // Not supported on mobile
|
||||||
|
usePreparedStatements: true
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
// Get platform-specific service instance
|
## Database Schema
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
|
||||||
|
|
||||||
// Example database operations
|
The implementation uses the following schema:
|
||||||
async function example() {
|
|
||||||
try {
|
|
||||||
// Query example
|
|
||||||
const result = await platformService.dbQuery(
|
|
||||||
"SELECT * FROM accounts WHERE did = ?",
|
|
||||||
[did]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Execute example
|
```sql
|
||||||
await platformService.dbExec(
|
-- Accounts table
|
||||||
"INSERT INTO accounts (did, public_key_hex) VALUES (?, ?)",
|
CREATE TABLE accounts (
|
||||||
[did, publicKeyHex]
|
did TEXT PRIMARY KEY,
|
||||||
);
|
public_key_hex TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
} catch (error) {
|
-- Settings table
|
||||||
console.error('Database operation failed:', error);
|
CREATE TABLE settings (
|
||||||
}
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Contacts table
|
||||||
|
CREATE TABLE contacts (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
did TEXT NOT NULL,
|
||||||
|
name TEXT,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (did) REFERENCES accounts(did)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Performance indexes
|
||||||
|
CREATE INDEX idx_accounts_created_at ON accounts(created_at);
|
||||||
|
CREATE INDEX idx_contacts_did ON contacts(did);
|
||||||
|
CREATE INDEX idx_settings_updated_at ON settings(updated_at);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The implementation includes comprehensive error handling:
|
||||||
|
|
||||||
|
1. **Error Types**
|
||||||
|
```typescript
|
||||||
|
export enum StorageErrorCodes {
|
||||||
|
INITIALIZATION_FAILED = 'STORAGE_INIT_FAILED',
|
||||||
|
QUERY_FAILED = 'STORAGE_QUERY_FAILED',
|
||||||
|
TRANSACTION_FAILED = 'STORAGE_TRANSACTION_FAILED',
|
||||||
|
PREPARED_STATEMENT_FAILED = 'STORAGE_PREPARED_STATEMENT_FAILED',
|
||||||
|
DATABASE_CORRUPTED = 'STORAGE_DB_CORRUPTED',
|
||||||
|
STORAGE_FULL = 'STORAGE_FULL',
|
||||||
|
CONCURRENT_ACCESS = 'STORAGE_CONCURRENT_ACCESS'
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Platform Detection
|
2. **Error Recovery**
|
||||||
|
- Automatic transaction rollback
|
||||||
|
- Connection recovery
|
||||||
|
- Data integrity verification
|
||||||
|
- Platform-specific error handling
|
||||||
|
- Comprehensive logging
|
||||||
|
|
||||||
|
## Performance Monitoring
|
||||||
|
|
||||||
|
The implementation includes built-in performance monitoring:
|
||||||
|
|
||||||
|
1. **Statistics**
|
||||||
```typescript
|
```typescript
|
||||||
// src/services/PlatformServiceFactory.ts
|
interface SQLiteStats {
|
||||||
export class PlatformServiceFactory {
|
totalQueries: number;
|
||||||
static getInstance(): PlatformService {
|
avgExecutionTime: number;
|
||||||
if (process.env.ELECTRON) {
|
preparedStatements: number;
|
||||||
// Electron platform
|
databaseSize: number;
|
||||||
return new ElectronPlatformService();
|
walMode: boolean;
|
||||||
} else {
|
mmapActive: boolean;
|
||||||
// Web platform (default)
|
|
||||||
return new AbsurdSqlDatabaseService();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Current Implementation Details
|
2. **Monitoring Features**
|
||||||
|
- Query execution time tracking
|
||||||
|
- Database size monitoring
|
||||||
|
- Prepared statement usage
|
||||||
|
- WAL and mmap status
|
||||||
|
- Platform-specific metrics
|
||||||
|
|
||||||
#### Web Platform (AbsurdSqlDatabaseService)
|
## Security Considerations
|
||||||
|
|
||||||
The web platform uses absurd-sql with IndexedDB backend:
|
1. **Web Platform**
|
||||||
|
- Worker thread isolation
|
||||||
|
- Storage quota monitoring
|
||||||
|
- Origin isolation
|
||||||
|
- Cross-origin protection
|
||||||
|
- SharedArrayBuffer availability check
|
||||||
|
|
||||||
```typescript
|
2. **Mobile Platform**
|
||||||
// src/services/AbsurdSqlDatabaseService.ts
|
- Platform-specific permissions
|
||||||
export class AbsurdSqlDatabaseService implements PlatformService {
|
- Storage access control
|
||||||
private static instance: AbsurdSqlDatabaseService | null = null;
|
- File system security
|
||||||
private db: AbsurdSqlDatabase | null = null;
|
- Platform sandboxing
|
||||||
private initialized: boolean = false;
|
|
||||||
|
|
||||||
// Singleton pattern
|
## Testing Strategy
|
||||||
static getInstance(): AbsurdSqlDatabaseService {
|
|
||||||
if (!AbsurdSqlDatabaseService.instance) {
|
|
||||||
AbsurdSqlDatabaseService.instance = new AbsurdSqlDatabaseService();
|
|
||||||
}
|
|
||||||
return AbsurdSqlDatabaseService.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Database operations
|
1. **Unit Tests**
|
||||||
async dbQuery(sql: string, params: unknown[] = []): Promise<QueryExecResult[]> {
|
- Platform service tests
|
||||||
await this.waitForInitialization();
|
- SQLite service tests
|
||||||
return this.queueOperation<QueryExecResult[]>("query", sql, params);
|
- Error handling tests
|
||||||
}
|
- Performance tests
|
||||||
|
|
||||||
async dbExec(sql: string, params: unknown[] = []): Promise<void> {
|
2. **Integration Tests**
|
||||||
await this.waitForInitialization();
|
- Cross-platform tests
|
||||||
await this.queueOperation<void>("run", sql, params);
|
- Migration tests
|
||||||
}
|
- Transaction tests
|
||||||
}
|
- Concurrency tests
|
||||||
```
|
|
||||||
|
|
||||||
Key features:
|
3. **E2E Tests**
|
||||||
- Uses absurd-sql for SQLite in the browser
|
- Platform-specific workflows
|
||||||
- Implements operation queuing for thread safety
|
- Error recovery scenarios
|
||||||
- Handles initialization and connection management
|
- Performance benchmarks
|
||||||
- Provides consistent API across platforms
|
- Data integrity verification
|
||||||
|
|
||||||
### 5. Migration from Dexie.js
|
|
||||||
|
|
||||||
The current implementation supports gradual migration from Dexie.js:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Example of dual-storage pattern
|
|
||||||
async function getAccount(did: string): Promise<Account | undefined> {
|
|
||||||
// Try SQLite first
|
|
||||||
const platform = PlatformServiceFactory.getInstance();
|
|
||||||
let account = await platform.dbQuery(
|
|
||||||
"SELECT * FROM accounts WHERE did = ?",
|
|
||||||
[did]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fallback to Dexie if needed
|
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
account = await db.accounts.get(did);
|
|
||||||
}
|
|
||||||
|
|
||||||
return account;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### A. Modifying Code
|
|
||||||
|
|
||||||
When converting from Dexie.js to SQL-based implementation, follow these patterns:
|
|
||||||
|
|
||||||
1. **Database Access Pattern**
|
|
||||||
```typescript
|
|
||||||
// Before (Dexie)
|
|
||||||
const result = await db.table.where("field").equals(value).first();
|
|
||||||
|
|
||||||
// After (SQL)
|
|
||||||
const platform = PlatformServiceFactory.getInstance();
|
|
||||||
let result = await platform.dbQuery(
|
|
||||||
"SELECT * FROM table WHERE field = ?",
|
|
||||||
[value]
|
|
||||||
);
|
|
||||||
result = databaseUtil.mapQueryResultToValues(result);
|
|
||||||
|
|
||||||
// Fallback to Dexie if needed
|
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
result = await db.table.where("field").equals(value).first();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Update Operations**
|
|
||||||
```typescript
|
|
||||||
// Before (Dexie)
|
|
||||||
await db.table.where("id").equals(id).modify(changes);
|
|
||||||
|
|
||||||
// After (SQL)
|
|
||||||
// For settings updates, use the utility methods:
|
|
||||||
await databaseUtil.updateDefaultSettings(changes);
|
|
||||||
// OR
|
|
||||||
await databaseUtil.updateAccountSettings(did, changes);
|
|
||||||
|
|
||||||
// For other tables, use direct SQL:
|
|
||||||
const platform = PlatformServiceFactory.getInstance();
|
|
||||||
await platform.dbExec(
|
|
||||||
"UPDATE table SET field1 = ?, field2 = ? WHERE id = ?",
|
|
||||||
[changes.field1, changes.field2, id]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fallback to Dexie if needed
|
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
await db.table.where("id").equals(id).modify(changes);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Insert Operations**
|
|
||||||
```typescript
|
|
||||||
// Before (Dexie)
|
|
||||||
await db.table.add(item);
|
|
||||||
|
|
||||||
// After (SQL)
|
|
||||||
const platform = PlatformServiceFactory.getInstance();
|
|
||||||
const columns = Object.keys(item);
|
|
||||||
const values = Object.values(item);
|
|
||||||
const placeholders = values.map(() => '?').join(', ');
|
|
||||||
const sql = `INSERT INTO table (${columns.join(', ')}) VALUES (${placeholders})`;
|
|
||||||
await platform.dbExec(sql, values);
|
|
||||||
|
|
||||||
// Fallback to Dexie if needed
|
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
await db.table.add(item);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Delete Operations**
|
|
||||||
```typescript
|
|
||||||
// Before (Dexie)
|
|
||||||
await db.table.where("id").equals(id).delete();
|
|
||||||
|
|
||||||
// After (SQL)
|
|
||||||
const platform = PlatformServiceFactory.getInstance();
|
|
||||||
await platform.dbExec("DELETE FROM table WHERE id = ?", [id]);
|
|
||||||
|
|
||||||
// Fallback to Dexie if needed
|
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
await db.table.where("id").equals(id).delete();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Result Processing**
|
|
||||||
```typescript
|
|
||||||
// Before (Dexie)
|
|
||||||
const items = await db.table.toArray();
|
|
||||||
|
|
||||||
// After (SQL)
|
|
||||||
const platform = PlatformServiceFactory.getInstance();
|
|
||||||
let items = await platform.dbQuery("SELECT * FROM table");
|
|
||||||
items = databaseUtil.mapQueryResultToValues(items);
|
|
||||||
|
|
||||||
// Fallback to Dexie if needed
|
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
items = await db.table.toArray();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
6. **Using Utility Methods**
|
|
||||||
|
|
||||||
When working with settings or other common operations, use the utility methods in `db/index.ts`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Settings operations
|
|
||||||
await databaseUtil.updateDefaultSettings(settings);
|
|
||||||
await databaseUtil.updateAccountSettings(did, settings);
|
|
||||||
const settings = await databaseUtil.retrieveSettingsForDefaultAccount();
|
|
||||||
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
|
||||||
|
|
||||||
// Logging operations
|
|
||||||
await databaseUtil.logToDb(message);
|
|
||||||
await databaseUtil.logConsoleAndDb(message, showInConsole);
|
|
||||||
```
|
|
||||||
|
|
||||||
Key Considerations:
|
|
||||||
- Always use `databaseUtil.mapQueryResultToValues()` to process SQL query results
|
|
||||||
- Use utility methods from `db/index.ts` when available instead of direct SQL
|
|
||||||
- Keep Dexie fallbacks wrapped in `if (USE_DEXIE_DB)` checks
|
|
||||||
- For queries that return results, use `let` variables to allow Dexie fallback to override
|
|
||||||
- For updates/inserts/deletes, execute both SQL and Dexie operations when `USE_DEXIE_DB` is true
|
|
||||||
|
|
||||||
Example Migration:
|
|
||||||
```typescript
|
|
||||||
// Before (Dexie)
|
|
||||||
export async function updateSettings(settings: Settings): Promise<void> {
|
|
||||||
await db.settings.put(settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
// After (SQL)
|
|
||||||
export async function updateSettings(settings: Settings): Promise<void> {
|
|
||||||
const platform = PlatformServiceFactory.getInstance();
|
|
||||||
const { sql, params } = generateUpdateStatement(
|
|
||||||
settings,
|
|
||||||
"settings",
|
|
||||||
"id = ?",
|
|
||||||
[settings.id]
|
|
||||||
);
|
|
||||||
await platform.dbExec(sql, params);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Remember to:
|
|
||||||
- Create database access code to use the platform service, putting it in front of the Dexie version
|
|
||||||
- Instead of removing Dexie-specific code, keep it.
|
|
||||||
|
|
||||||
- For creates & updates & deletes, the duplicate code is fine.
|
|
||||||
|
|
||||||
- For queries where we use the results, make the setting from SQL into a 'let' variable, then wrap the Dexie code in a check for USE_DEXIE_DB from app.ts and if
|
|
||||||
it's true then use that result instead of the SQL code's result.
|
|
||||||
|
|
||||||
- Consider data migration needs, and warn if there are any potential migration problems
|
|
||||||
|
|
||||||
## Success Criteria
|
## Success Criteria
|
||||||
|
|
||||||
1. **Functionality**
|
1. **Performance**
|
||||||
- [x] Basic CRUD operations work correctly
|
- Query response time < 100ms
|
||||||
- [x] Platform service factory pattern implemented
|
- Transaction completion < 500ms
|
||||||
- [x] Error handling in place
|
- Memory usage < 50MB
|
||||||
- [ ] Native platform support (planned)
|
- Database size < platform limits:
|
||||||
|
- Web: 1GB
|
||||||
|
- Mobile: 2GB
|
||||||
|
|
||||||
2. **Performance**
|
2. **Reliability**
|
||||||
- [x] Database operations complete within acceptable time
|
- 99.9% uptime
|
||||||
- [x] Operation queuing for thread safety
|
- Zero data loss
|
||||||
- [x] Proper initialization handling
|
- Automatic recovery
|
||||||
- [ ] Performance monitoring (planned)
|
- Transaction atomicity
|
||||||
|
|
||||||
3. **Security**
|
3. **Security**
|
||||||
- [x] Basic data integrity
|
- Platform-specific security features
|
||||||
- [ ] Encryption (planned for native platforms)
|
- Storage access control
|
||||||
- [ ] Secure key storage (planned)
|
- Data protection
|
||||||
- [ ] Platform-specific security features (planned)
|
- Audit logging
|
||||||
|
|
||||||
4. **Testing**
|
4. **User Experience**
|
||||||
- [x] Basic unit tests
|
- Smooth platform transitions
|
||||||
- [ ] Comprehensive integration tests (planned)
|
- Clear error messages
|
||||||
- [ ] Platform-specific tests (planned)
|
- Progress indicators
|
||||||
- [ ] Migration tests (planned)
|
- Recovery options
|
||||||
|
|
||||||
## Next Steps
|
## Future Improvements
|
||||||
|
|
||||||
1. **Native Platform Support**
|
1. **Planned Features**
|
||||||
- Implement SQLCipher for iOS/Android
|
- SQLCipher integration for mobile
|
||||||
- Add platform-specific secure storage
|
- Electron platform support
|
||||||
- Implement biometric authentication
|
- Advanced backup/restore
|
||||||
|
- Cross-platform sync
|
||||||
|
|
||||||
2. **Enhanced Security**
|
2. **Security Enhancements**
|
||||||
- Add encryption for sensitive data
|
- Biometric authentication
|
||||||
- Implement secure key storage
|
- Secure enclave usage
|
||||||
- Add platform-specific security features
|
- Advanced encryption
|
||||||
|
- Key management
|
||||||
|
|
||||||
3. **Testing and Monitoring**
|
3. **Performance Optimizations**
|
||||||
- Add comprehensive test coverage
|
- Advanced caching
|
||||||
- Implement performance monitoring
|
- Query optimization
|
||||||
- Add error tracking and analytics
|
- Memory management
|
||||||
|
- Storage efficiency
|
||||||
4. **Documentation**
|
|
||||||
- Add API documentation
|
|
||||||
- Create migration guides
|
|
||||||
- Document security measures
|
|
||||||
@@ -2,50 +2,289 @@
|
|||||||
|
|
||||||
## Core Services
|
## Core Services
|
||||||
|
|
||||||
### 1. Storage Service Layer
|
### 1. Platform Service Layer
|
||||||
- [x] Create base `PlatformService` interface
|
- [x] Create base `PlatformService` interface
|
||||||
- [x] Define common methods for all platforms
|
- [x] Define platform capabilities
|
||||||
- [x] Add platform-specific method signatures
|
- [x] File system access detection
|
||||||
- [x] Include error handling types
|
- [x] Camera availability
|
||||||
- [x] Add migration support methods
|
- [x] Mobile platform detection
|
||||||
|
- [x] iOS specific detection
|
||||||
|
- [x] File download capability
|
||||||
|
- [x] SQLite capabilities
|
||||||
|
- [x] Add SQLite operations interface
|
||||||
|
- [x] Database initialization
|
||||||
|
- [x] Query execution
|
||||||
|
- [x] Transaction management
|
||||||
|
- [x] Prepared statements
|
||||||
|
- [x] Database statistics
|
||||||
|
- [x] Include platform detection
|
||||||
|
- [x] Web platform detection
|
||||||
|
- [x] Mobile platform detection
|
||||||
|
- [x] Desktop platform detection
|
||||||
|
- [x] Add file system operations
|
||||||
|
- [x] File read operations
|
||||||
|
- [x] File write operations
|
||||||
|
- [x] File delete operations
|
||||||
|
- [x] Directory listing
|
||||||
|
|
||||||
- [x] Implement platform-specific services
|
- [x] Implement platform-specific services
|
||||||
- [x] `AbsurdSqlDatabaseService` (web)
|
- [x] `WebPlatformService`
|
||||||
- [x] Database initialization
|
- [x] AbsurdSQL integration
|
||||||
- [x] VFS setup with IndexedDB backend
|
- [x] SQL.js initialization
|
||||||
- [x] Connection management
|
- [x] IndexedDB backend setup
|
||||||
- [x] Operation queuing
|
- [x] Virtual file system configuration
|
||||||
- [ ] `NativeSQLiteService` (iOS/Android) (planned)
|
- [x] Web Worker support
|
||||||
- [ ] SQLCipher integration
|
- [x] Worker thread initialization
|
||||||
- [ ] Native bridge setup
|
- [x] Message passing
|
||||||
|
- [x] Error handling
|
||||||
|
- [x] IndexedDB backend
|
||||||
|
- [x] Database creation
|
||||||
|
- [x] Transaction handling
|
||||||
|
- [x] Storage quota management (1GB limit)
|
||||||
|
- [x] SharedArrayBuffer detection
|
||||||
|
- [x] Feature detection
|
||||||
|
- [x] Fallback handling
|
||||||
|
- [x] File system operations (intentionally not supported)
|
||||||
|
- [x] File read operations (not available in web)
|
||||||
|
- [x] File write operations (not available in web)
|
||||||
|
- [x] File delete operations (not available in web)
|
||||||
|
- [x] Directory operations (not available in web)
|
||||||
|
- [x] Settings implementation
|
||||||
|
- [x] AbsurdSQL settings operations
|
||||||
|
- [x] Worker-based settings updates
|
||||||
|
- [x] IndexedDB transaction handling
|
||||||
|
- [x] SharedArrayBuffer support
|
||||||
|
- [x] Web-specific settings features
|
||||||
|
- [x] Storage quota management
|
||||||
|
- [x] Worker thread isolation
|
||||||
|
- [x] Cross-origin settings
|
||||||
|
- [x] Web performance optimizations
|
||||||
|
- [x] Settings caching
|
||||||
|
- [x] Batch updates
|
||||||
|
- [x] Worker message optimization
|
||||||
|
- [x] Account implementation
|
||||||
|
- [x] Web-specific account handling
|
||||||
|
- [x] Browser storage persistence
|
||||||
|
- [x] Session management
|
||||||
|
- [x] Cross-tab synchronization
|
||||||
|
- [x] Web security features
|
||||||
|
- [x] Origin isolation
|
||||||
|
- [x] Worker thread security
|
||||||
|
- [x] Storage access control
|
||||||
|
- [x] `CapacitorPlatformService`
|
||||||
|
- [x] Native SQLite integration
|
||||||
|
- [x] Database connection
|
||||||
|
- [x] Query execution
|
||||||
|
- [x] Transaction handling
|
||||||
|
- [x] Platform capabilities
|
||||||
|
- [x] iOS detection
|
||||||
|
- [x] Android detection
|
||||||
|
- [x] Feature availability
|
||||||
|
- [x] File system operations
|
||||||
|
- [x] File read/write
|
||||||
|
- [x] Directory operations
|
||||||
|
- [x] Storage permissions
|
||||||
|
- [x] iOS permissions
|
||||||
|
- [x] Android permissions
|
||||||
|
- [x] Permission request handling
|
||||||
|
- [x] Settings implementation
|
||||||
|
- [x] Native SQLite settings operations
|
||||||
|
- [x] Platform-specific SQLite optimizations
|
||||||
|
- [x] Native transaction handling
|
||||||
|
- [x] Platform storage management
|
||||||
|
- [x] Mobile-specific settings features
|
||||||
|
- [x] Platform preferences sync
|
||||||
|
- [x] Background state handling
|
||||||
|
- [x] Mobile performance optimizations
|
||||||
|
- [x] Native caching
|
||||||
|
- [x] Battery-efficient updates
|
||||||
|
- [x] Memory management
|
||||||
|
- [x] Account implementation
|
||||||
|
- [x] Mobile-specific account handling
|
||||||
|
- [x] Platform storage integration
|
||||||
|
- [x] Background state handling
|
||||||
|
- [x] Mobile security features
|
||||||
|
- [x] Platform sandboxing
|
||||||
|
- [x] Storage access control
|
||||||
|
- [x] App sandboxing
|
||||||
|
- [ ] `ElectronPlatformService` (planned)
|
||||||
|
- [ ] Node SQLite integration
|
||||||
|
- [ ] Database connection
|
||||||
|
- [ ] Query execution
|
||||||
|
- [ ] Transaction handling
|
||||||
- [ ] File system access
|
- [ ] File system access
|
||||||
|
- [ ] File read operations
|
||||||
|
- [ ] File write operations
|
||||||
|
- [ ] File delete operations
|
||||||
|
- [ ] Directory operations
|
||||||
|
- [ ] IPC communication
|
||||||
|
- [ ] Main process communication
|
||||||
|
- [ ] Renderer process handling
|
||||||
|
- [ ] Message passing
|
||||||
|
- [ ] Native features implementation
|
||||||
|
- [ ] System dialogs
|
||||||
|
- [ ] Native menus
|
||||||
|
- [ ] System integration
|
||||||
|
- [ ] Settings implementation
|
||||||
|
- [ ] Node SQLite settings operations
|
||||||
|
- [ ] Main process SQLite handling
|
||||||
|
- [ ] IPC-based updates
|
||||||
|
- [ ] File system persistence
|
||||||
|
- [ ] Desktop-specific settings features
|
||||||
|
- [ ] System preferences integration
|
||||||
|
- [ ] Multi-window sync
|
||||||
|
- [ ] Offline state handling
|
||||||
|
- [ ] Desktop performance optimizations
|
||||||
|
- [ ] Process-based caching
|
||||||
|
- [ ] Window state management
|
||||||
|
- [ ] Resource optimization
|
||||||
|
- [ ] Account implementation
|
||||||
|
- [ ] Desktop-specific account handling
|
||||||
|
- [ ] System keychain integration
|
||||||
|
- [ ] Native authentication
|
||||||
|
- [ ] Process isolation
|
||||||
|
- [ ] Desktop security features
|
||||||
|
- [ ] Process sandboxing
|
||||||
|
- [ ] IPC security
|
||||||
|
- [ ] File system protection
|
||||||
|
|
||||||
|
### 2. SQLite Service Layer
|
||||||
|
- [x] Create base `BaseSQLiteService`
|
||||||
|
- [x] Common SQLite operations
|
||||||
|
- [x] Query execution
|
||||||
|
- [x] Transaction management
|
||||||
|
- [x] Prepared statements
|
||||||
|
- [x] Database statistics
|
||||||
|
- [x] Performance monitoring
|
||||||
|
- [x] Query timing
|
||||||
|
- [x] Memory usage
|
||||||
|
- [x] Database size
|
||||||
|
- [x] Statement caching
|
||||||
|
- [x] Error handling
|
||||||
|
- [x] Connection errors
|
||||||
|
- [x] Query errors
|
||||||
|
- [x] Transaction errors
|
||||||
|
- [x] Resource errors
|
||||||
|
- [x] Transaction support
|
||||||
|
- [x] Begin transaction
|
||||||
|
- [x] Commit transaction
|
||||||
|
- [x] Rollback transaction
|
||||||
|
- [x] Nested transactions
|
||||||
|
|
||||||
|
- [x] Implement platform-specific SQLite services
|
||||||
|
- [x] `AbsurdSQLService`
|
||||||
|
- [x] Web Worker initialization
|
||||||
|
- [x] Worker creation
|
||||||
|
- [x] Message handling
|
||||||
|
- [x] Error propagation
|
||||||
|
- [x] IndexedDB backend setup
|
||||||
|
- [x] Database creation
|
||||||
|
- [x] Transaction handling
|
||||||
|
- [x] Storage management
|
||||||
|
- [x] Prepared statements
|
||||||
|
- [x] Statement preparation
|
||||||
|
- [x] Parameter binding
|
||||||
|
- [x] Statement caching
|
||||||
|
- [x] Performance optimizations
|
||||||
|
- [x] WAL mode
|
||||||
|
- [x] Memory mapping
|
||||||
|
- [x] Cache configuration
|
||||||
|
- [x] WAL mode support
|
||||||
|
- [x] Journal mode configuration
|
||||||
|
- [x] Synchronization settings
|
||||||
|
- [x] Checkpoint handling
|
||||||
|
- [x] Memory-mapped I/O
|
||||||
|
- [x] MMAP size configuration (30GB)
|
||||||
|
- [x] Memory management
|
||||||
|
- [x] Performance monitoring
|
||||||
|
- [x] `CapacitorSQLiteService`
|
||||||
|
- [x] Native SQLite connection
|
||||||
|
- [x] Database initialization
|
||||||
|
- [x] Connection management
|
||||||
|
- [x] Error handling
|
||||||
|
- [x] Basic platform features
|
||||||
|
- [x] Query execution
|
||||||
|
- [x] Transaction handling
|
||||||
|
- [x] Statement management
|
||||||
|
- [x] Error handling
|
||||||
|
- [x] Connection errors
|
||||||
|
- [x] Query errors
|
||||||
|
- [x] Resource errors
|
||||||
|
- [x] WAL mode support
|
||||||
|
- [x] Journal mode
|
||||||
|
- [x] Synchronization
|
||||||
|
- [x] Checkpointing
|
||||||
|
- [ ] SQLCipher integration (planned)
|
||||||
|
- [ ] Encryption setup
|
||||||
|
- [ ] Key management
|
||||||
|
- [ ] Secure storage
|
||||||
- [ ] `ElectronSQLiteService` (planned)
|
- [ ] `ElectronSQLiteService` (planned)
|
||||||
- [ ] Node SQLite integration
|
- [ ] Node SQLite integration
|
||||||
|
- [ ] Database connection
|
||||||
|
- [ ] Query execution
|
||||||
|
- [ ] Transaction handling
|
||||||
- [ ] IPC communication
|
- [ ] IPC communication
|
||||||
|
- [ ] Process communication
|
||||||
|
- [ ] Error handling
|
||||||
|
- [ ] Resource management
|
||||||
- [ ] File system access
|
- [ ] File system access
|
||||||
|
- [ ] Native file operations
|
||||||
### 2. Migration Services
|
- [ ] Path handling
|
||||||
- [x] Implement basic migration support
|
- [ ] Permissions
|
||||||
- [x] Dual-storage pattern (SQLite + Dexie)
|
- [ ] Native features
|
||||||
- [x] Basic data verification
|
- [ ] System integration
|
||||||
- [ ] Rollback procedures (planned)
|
- [ ] Native dialogs
|
||||||
- [ ] Progress tracking (planned)
|
- [ ] Process management
|
||||||
- [ ] Create `MigrationUI` components (planned)
|
|
||||||
- [ ] Progress indicators
|
|
||||||
- [ ] Error handling
|
|
||||||
- [ ] User notifications
|
|
||||||
- [ ] Manual triggers
|
|
||||||
|
|
||||||
### 3. Security Layer
|
### 3. Security Layer
|
||||||
- [x] Basic data integrity
|
- [x] Implement platform-specific security
|
||||||
- [ ] Implement `EncryptionService` (planned)
|
- [x] Web platform
|
||||||
- [ ] Key management
|
- [x] Worker isolation
|
||||||
- [ ] Encryption/decryption
|
- [x] Thread separation
|
||||||
- [ ] Secure storage
|
- [x] Message security
|
||||||
- [ ] Add `BiometricService` (planned)
|
- [x] Resource isolation
|
||||||
- [ ] Platform detection
|
- [x] Storage quota management
|
||||||
- [ ] Authentication flow
|
- [x] Quota detection
|
||||||
- [ ] Fallback mechanisms
|
- [x] Usage monitoring
|
||||||
|
- [x] Error handling
|
||||||
|
- [x] Origin isolation
|
||||||
|
- [x] Cross-origin protection
|
||||||
|
- [x] Resource isolation
|
||||||
|
- [x] Security policy
|
||||||
|
- [x] Storage security
|
||||||
|
- [x] Access control
|
||||||
|
- [x] Data protection
|
||||||
|
- [x] Quota management
|
||||||
|
- [x] Mobile platform
|
||||||
|
- [x] Platform permissions
|
||||||
|
- [x] Storage access
|
||||||
|
- [x] File operations
|
||||||
|
- [x] System integration
|
||||||
|
- [x] Platform security
|
||||||
|
- [x] App sandboxing
|
||||||
|
- [x] Storage protection
|
||||||
|
- [x] Access control
|
||||||
|
- [ ] SQLCipher integration (planned)
|
||||||
|
- [ ] Encryption setup
|
||||||
|
- [ ] Key management
|
||||||
|
- [ ] Secure storage
|
||||||
|
- [ ] Electron platform (planned)
|
||||||
|
- [ ] IPC security
|
||||||
|
- [ ] Message validation
|
||||||
|
- [ ] Process isolation
|
||||||
|
- [ ] Resource protection
|
||||||
|
- [ ] File system security
|
||||||
|
- [ ] Access control
|
||||||
|
- [ ] Path validation
|
||||||
|
- [ ] Permission management
|
||||||
|
- [ ] Auto-update security
|
||||||
|
- [ ] Update verification
|
||||||
|
- [ ] Code signing
|
||||||
|
- [ ] Rollback protection
|
||||||
|
- [ ] Native security features
|
||||||
|
- [ ] System integration
|
||||||
|
- [ ] Security policies
|
||||||
|
- [ ] Resource protection
|
||||||
|
|
||||||
## Platform-Specific Implementation
|
## Platform-Specific Implementation
|
||||||
|
|
||||||
@@ -58,74 +297,125 @@
|
|||||||
"absurd-sql": "^1.8.0"
|
"absurd-sql": "^1.8.0"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
- [x] Configure VFS with IndexedDB backend
|
- [x] Configure Web Worker
|
||||||
- [x] Setup worker threads
|
- [x] Worker initialization
|
||||||
- [x] Implement operation queuing
|
- [x] Message handling
|
||||||
|
- [x] Error propagation
|
||||||
|
- [x] Setup IndexedDB backend
|
||||||
|
- [x] Database creation
|
||||||
|
- [x] Transaction handling
|
||||||
|
- [x] Storage management
|
||||||
- [x] Configure database pragmas
|
- [x] Configure database pragmas
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
PRAGMA journal_mode=MEMORY;
|
PRAGMA journal_mode = WAL;
|
||||||
PRAGMA synchronous=NORMAL;
|
PRAGMA synchronous = NORMAL;
|
||||||
PRAGMA foreign_keys=ON;
|
PRAGMA temp_store = MEMORY;
|
||||||
PRAGMA busy_timeout=5000;
|
PRAGMA cache_size = -2000;
|
||||||
|
PRAGMA mmap_size = 30000000000;
|
||||||
```
|
```
|
||||||
|
|
||||||
- [x] Update build configuration
|
- [x] Update build configuration
|
||||||
- [x] Modify `vite.config.ts`
|
- [x] Configure worker bundling
|
||||||
- [x] Add worker configuration
|
- [x] Worker file handling
|
||||||
- [x] Update chunk splitting
|
- [x] Asset management
|
||||||
- [x] Configure asset handling
|
- [x] Source maps
|
||||||
|
- [x] Setup asset handling
|
||||||
|
- [x] SQL.js WASM
|
||||||
|
- [x] Worker scripts
|
||||||
|
- [x] Static assets
|
||||||
|
- [x] Configure chunk splitting
|
||||||
|
- [x] Code splitting
|
||||||
|
- [x] Dynamic imports
|
||||||
|
- [x] Asset optimization
|
||||||
|
|
||||||
- [x] Implement IndexedDB backend
|
- [x] Implement fallback mechanisms
|
||||||
- [x] Create database service
|
- [x] SharedArrayBuffer detection
|
||||||
- [x] Add operation queuing
|
- [x] Feature detection
|
||||||
- [x] Handle initialization
|
- [x] Fallback handling
|
||||||
- [x] Implement atomic operations
|
- [x] Error reporting
|
||||||
|
- [x] Storage quota monitoring
|
||||||
|
- [x] Quota detection
|
||||||
|
- [x] Usage tracking
|
||||||
|
- [x] Error handling
|
||||||
|
- [x] Worker initialization fallback
|
||||||
|
- [x] Fallback detection
|
||||||
|
- [x] Alternative initialization
|
||||||
|
- [x] Error recovery
|
||||||
|
- [x] Error recovery
|
||||||
|
- [x] Connection recovery
|
||||||
|
- [x] Transaction rollback
|
||||||
|
- [x] State restoration
|
||||||
|
|
||||||
### iOS Platform (Planned)
|
### Mobile Platform
|
||||||
- [ ] Setup SQLCipher
|
- [x] Setup Capacitor SQLite
|
||||||
- [ ] Install pod dependencies
|
- [x] Install dependencies
|
||||||
- [ ] Configure encryption
|
- [x] Core SQLite plugin
|
||||||
- [ ] Setup keychain access
|
- [x] Platform plugins
|
||||||
- [ ] Implement secure storage
|
- [x] Native dependencies
|
||||||
|
- [x] Configure native SQLite
|
||||||
|
- [x] Database initialization
|
||||||
|
- [x] Connection management
|
||||||
|
- [x] Query handling
|
||||||
|
- [x] Configure basic permissions
|
||||||
|
- [x] Storage access
|
||||||
|
- [x] File operations
|
||||||
|
- [x] System integration
|
||||||
|
|
||||||
- [ ] Update Capacitor config
|
- [x] Update Capacitor config
|
||||||
- [ ] Modify `capacitor.config.ts`
|
- [x] Add basic platform permissions
|
||||||
- [ ] Add iOS permissions
|
- [x] iOS permissions
|
||||||
- [ ] Configure backup
|
- [x] Android permissions
|
||||||
- [ ] Setup app groups
|
- [x] Feature flags
|
||||||
|
- [x] Configure storage limits
|
||||||
|
- [x] iOS storage limits
|
||||||
|
- [x] Android storage limits
|
||||||
|
- [x] Quota management
|
||||||
|
- [x] Setup platform security
|
||||||
|
- [x] App sandboxing
|
||||||
|
- [x] Storage protection
|
||||||
|
- [x] Access control
|
||||||
|
|
||||||
### Android Platform (Planned)
|
### Electron Platform (planned)
|
||||||
- [ ] Setup SQLCipher
|
|
||||||
- [ ] Add Gradle dependencies
|
|
||||||
- [ ] Configure encryption
|
|
||||||
- [ ] Setup keystore
|
|
||||||
- [ ] Implement secure storage
|
|
||||||
|
|
||||||
- [ ] Update Capacitor config
|
|
||||||
- [ ] Modify `capacitor.config.ts`
|
|
||||||
- [ ] Add Android permissions
|
|
||||||
- [ ] Configure backup
|
|
||||||
- [ ] Setup file provider
|
|
||||||
|
|
||||||
### Electron Platform (Planned)
|
|
||||||
- [ ] Setup Node SQLite
|
- [ ] Setup Node SQLite
|
||||||
- [ ] Install dependencies
|
- [ ] Install dependencies
|
||||||
|
- [ ] SQLite3 module
|
||||||
|
- [ ] Native bindings
|
||||||
|
- [ ] Development tools
|
||||||
- [ ] Configure IPC
|
- [ ] Configure IPC
|
||||||
|
- [ ] Main process setup
|
||||||
|
- [ ] Renderer process handling
|
||||||
|
- [ ] Message passing
|
||||||
- [ ] Setup file system access
|
- [ ] Setup file system access
|
||||||
|
- [ ] Native file operations
|
||||||
|
- [ ] Path handling
|
||||||
|
- [ ] Permission management
|
||||||
- [ ] Implement secure storage
|
- [ ] Implement secure storage
|
||||||
|
- [ ] Encryption setup
|
||||||
|
- [ ] Key management
|
||||||
|
- [ ] Secure containers
|
||||||
|
|
||||||
- [ ] Update Electron config
|
- [ ] Update Electron config
|
||||||
- [ ] Modify `electron.config.ts`
|
|
||||||
- [ ] Add security policies
|
- [ ] Add security policies
|
||||||
|
- [ ] CSP configuration
|
||||||
|
- [ ] Process isolation
|
||||||
|
- [ ] Resource protection
|
||||||
- [ ] Configure file access
|
- [ ] Configure file access
|
||||||
|
- [ ] Access control
|
||||||
|
- [ ] Path validation
|
||||||
|
- [ ] Permission management
|
||||||
- [ ] Setup auto-updates
|
- [ ] Setup auto-updates
|
||||||
|
- [ ] Update server
|
||||||
|
- [ ] Code signing
|
||||||
|
- [ ] Rollback protection
|
||||||
|
- [ ] Configure IPC security
|
||||||
|
- [ ] Message validation
|
||||||
|
- [ ] Process isolation
|
||||||
|
- [ ] Resource protection
|
||||||
|
|
||||||
## Data Models and Types
|
## Data Models and Types
|
||||||
|
|
||||||
### 1. Database Schema
|
### 1. Database Schema
|
||||||
- [x] Define tables
|
- [x] Define tables
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- Accounts table
|
-- Accounts table
|
||||||
CREATE TABLE accounts (
|
CREATE TABLE accounts (
|
||||||
@@ -158,172 +448,312 @@
|
|||||||
CREATE INDEX idx_settings_updated_at ON settings(updated_at);
|
CREATE INDEX idx_settings_updated_at ON settings(updated_at);
|
||||||
```
|
```
|
||||||
|
|
||||||
- [x] Create indexes
|
|
||||||
- [x] Define constraints
|
|
||||||
- [ ] Add triggers (planned)
|
|
||||||
- [ ] Setup migrations (planned)
|
|
||||||
|
|
||||||
### 2. Type Definitions
|
### 2. Type Definitions
|
||||||
|
|
||||||
- [x] Create interfaces
|
- [x] Create interfaces
|
||||||
```typescript
|
```typescript
|
||||||
interface Account {
|
interface PlatformCapabilities {
|
||||||
did: string;
|
hasFileSystem: boolean;
|
||||||
publicKeyHex: string;
|
hasCamera: boolean;
|
||||||
createdAt: number;
|
isMobile: boolean;
|
||||||
updatedAt: number;
|
isIOS: boolean;
|
||||||
|
hasFileDownload: boolean;
|
||||||
|
needsFileHandlingInstructions: boolean;
|
||||||
|
sqlite: {
|
||||||
|
supported: boolean;
|
||||||
|
runsInWorker: boolean;
|
||||||
|
hasSharedArrayBuffer: boolean;
|
||||||
|
supportsWAL: boolean;
|
||||||
|
maxSize?: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Setting {
|
interface SQLiteConfig {
|
||||||
key: string;
|
name: string;
|
||||||
value: string;
|
useWAL?: boolean;
|
||||||
updatedAt: number;
|
useMMap?: boolean;
|
||||||
|
mmapSize?: number;
|
||||||
|
usePreparedStatements?: boolean;
|
||||||
|
maxPreparedStatements?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Contact {
|
interface SQLiteStats {
|
||||||
id: string;
|
totalQueries: number;
|
||||||
did: string;
|
avgExecutionTime: number;
|
||||||
name?: string;
|
preparedStatements: number;
|
||||||
createdAt: number;
|
databaseSize: number;
|
||||||
updatedAt: number;
|
walMode: boolean;
|
||||||
|
mmapActive: boolean;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- [x] Add validation
|
|
||||||
- [x] Create DTOs
|
|
||||||
- [x] Define enums
|
|
||||||
- [x] Add type guards
|
|
||||||
|
|
||||||
## UI Components
|
|
||||||
|
|
||||||
### 1. Migration UI (Planned)
|
|
||||||
- [ ] Create components
|
|
||||||
- [ ] `MigrationProgress.vue`
|
|
||||||
- [ ] `MigrationError.vue`
|
|
||||||
- [ ] `MigrationSettings.vue`
|
|
||||||
- [ ] `MigrationStatus.vue`
|
|
||||||
|
|
||||||
### 2. Settings UI (Planned)
|
|
||||||
- [ ] Update components
|
|
||||||
- [ ] Add storage settings
|
|
||||||
- [ ] Add migration controls
|
|
||||||
- [ ] Add backup options
|
|
||||||
- [ ] Add security settings
|
|
||||||
|
|
||||||
### 3. Error Handling UI (Planned)
|
|
||||||
- [ ] Create components
|
|
||||||
- [ ] `StorageError.vue`
|
|
||||||
- [ ] `QuotaExceeded.vue`
|
|
||||||
- [ ] `MigrationFailed.vue`
|
|
||||||
- [ ] `RecoveryOptions.vue`
|
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
### 1. Unit Tests
|
### 1. Unit Tests
|
||||||
- [x] Basic service tests
|
- [x] Test platform services
|
||||||
- [x] Platform service tests
|
- [x] Platform detection
|
||||||
- [x] Database operation tests
|
- [x] Web platform
|
||||||
- [ ] Security service tests (planned)
|
- [x] Mobile platform
|
||||||
- [ ] Platform detection tests (planned)
|
- [x] Desktop platform
|
||||||
|
- [x] Capability reporting
|
||||||
|
- [x] Feature detection
|
||||||
|
- [x] Platform specifics
|
||||||
|
- [x] Error cases
|
||||||
|
- [x] Basic SQLite operations
|
||||||
|
- [x] Query execution
|
||||||
|
- [x] Transaction handling
|
||||||
|
- [x] Error cases
|
||||||
|
- [x] Basic error handling
|
||||||
|
- [x] Connection errors
|
||||||
|
- [x] Query errors
|
||||||
|
- [x] Resource errors
|
||||||
|
|
||||||
### 2. Integration Tests (Planned)
|
### 2. Integration Tests
|
||||||
- [ ] Test migrations
|
- [x] Test SQLite services
|
||||||
- [ ] Web platform tests
|
- [x] Web platform tests
|
||||||
- [ ] iOS platform tests
|
- [x] Worker integration
|
||||||
- [ ] Android platform tests
|
- [x] IndexedDB backend
|
||||||
- [ ] Electron platform tests
|
- [x] Performance tests
|
||||||
|
- [x] Basic mobile platform tests
|
||||||
|
- [x] Native SQLite
|
||||||
|
- [x] Platform features
|
||||||
|
- [x] Error handling
|
||||||
|
- [ ] Electron platform tests (planned)
|
||||||
|
- [ ] Node SQLite
|
||||||
|
- [ ] IPC communication
|
||||||
|
- [ ] File system
|
||||||
|
- [x] Cross-platform tests
|
||||||
|
- [x] Feature parity
|
||||||
|
- [x] Data consistency
|
||||||
|
- [x] Performance comparison
|
||||||
|
|
||||||
### 3. E2E Tests (Planned)
|
### 3. E2E Tests
|
||||||
- [ ] Test workflows
|
- [x] Test workflows
|
||||||
- [ ] Account management
|
- [x] Basic database operations
|
||||||
- [ ] Settings management
|
- [x] CRUD operations
|
||||||
- [ ] Contact management
|
- [x] Transaction handling
|
||||||
- [ ] Migration process
|
- [x] Error recovery
|
||||||
|
- [x] Platform transitions
|
||||||
|
- [x] Web to mobile
|
||||||
|
- [x] Mobile to web
|
||||||
|
- [x] State preservation
|
||||||
|
- [x] Basic error recovery
|
||||||
|
- [x] Connection loss
|
||||||
|
- [x] Transaction failure
|
||||||
|
- [x] Resource errors
|
||||||
|
- [x] Performance benchmarks
|
||||||
|
- [x] Query performance
|
||||||
|
- [x] Transaction speed
|
||||||
|
- [x] Memory usage
|
||||||
|
- [x] Storage efficiency
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
### 1. Technical Documentation
|
### 1. Technical Documentation
|
||||||
- [x] Update architecture docs
|
- [x] Update architecture docs
|
||||||
- [x] Add API documentation
|
- [x] System overview
|
||||||
- [ ] Create migration guides (planned)
|
- [x] Component interaction
|
||||||
- [ ] Document security measures (planned)
|
- [x] Platform specifics
|
||||||
|
- [x] Add basic API documentation
|
||||||
|
- [x] Interface definitions
|
||||||
|
- [x] Method signatures
|
||||||
|
- [x] Usage examples
|
||||||
|
- [x] Document platform capabilities
|
||||||
|
- [x] Feature matrix
|
||||||
|
- [x] Platform support
|
||||||
|
- [x] Limitations
|
||||||
|
- [x] Document security measures
|
||||||
|
- [x] Platform security
|
||||||
|
- [x] Access control
|
||||||
|
- [x] Security policies
|
||||||
|
|
||||||
### 2. User Documentation (Planned)
|
### 2. User Documentation
|
||||||
- [ ] Update user guides
|
- [x] Update basic user guides
|
||||||
- [ ] Add troubleshooting guides
|
- [x] Installation
|
||||||
- [ ] Create FAQ
|
- [x] Configuration
|
||||||
- [ ] Document new features
|
- [x] Basic usage
|
||||||
|
- [x] Add basic troubleshooting guides
|
||||||
|
- [x] Common issues
|
||||||
|
- [x] Error messages
|
||||||
|
- [x] Recovery steps
|
||||||
|
- [x] Document implemented platform features
|
||||||
|
- [x] Web platform
|
||||||
|
- [x] Mobile platform
|
||||||
|
- [x] Desktop platform
|
||||||
|
- [x] Add basic performance tips
|
||||||
|
- [x] Optimization techniques
|
||||||
|
- [x] Best practices
|
||||||
|
- [x] Platform specifics
|
||||||
|
|
||||||
## Deployment
|
## Monitoring and Analytics
|
||||||
|
|
||||||
### 1. Build Process
|
### 1. Performance Monitoring
|
||||||
- [x] Update build scripts
|
- [x] Basic query execution time
|
||||||
- [x] Add platform-specific builds
|
- [x] Query timing
|
||||||
- [ ] Configure CI/CD (planned)
|
- [x] Transaction timing
|
||||||
- [ ] Setup automated testing (planned)
|
- [x] Statement timing
|
||||||
|
- [x] Database size monitoring
|
||||||
|
- [x] Size tracking
|
||||||
|
- [x] Growth patterns
|
||||||
|
- [x] Quota management
|
||||||
|
- [x] Basic memory usage
|
||||||
|
- [x] Heap usage
|
||||||
|
- [x] Cache usage
|
||||||
|
- [x] Worker memory
|
||||||
|
- [x] Worker performance
|
||||||
|
- [x] Message timing
|
||||||
|
- [x] Processing time
|
||||||
|
- [x] Resource usage
|
||||||
|
|
||||||
### 2. Release Process (Planned)
|
### 2. Error Tracking
|
||||||
- [ ] Create release checklist
|
- [x] Basic error logging
|
||||||
- [ ] Add version management
|
- [x] Error capture
|
||||||
- [ ] Setup rollback procedures
|
- [x] Stack traces
|
||||||
- [ ] Configure monitoring
|
- [x] Context data
|
||||||
|
- [x] Basic performance monitoring
|
||||||
|
- [x] Query metrics
|
||||||
|
- [x] Resource usage
|
||||||
|
- [x] Timing data
|
||||||
|
- [x] Platform-specific errors
|
||||||
|
- [x] Web platform
|
||||||
|
- [x] Mobile platform
|
||||||
|
- [x] Desktop platform
|
||||||
|
- [x] Basic recovery tracking
|
||||||
|
- [x] Recovery success
|
||||||
|
- [x] Failure patterns
|
||||||
|
- [x] User impact
|
||||||
|
|
||||||
## Monitoring and Analytics (Planned)
|
## Security Audit
|
||||||
|
|
||||||
### 1. Error Tracking
|
|
||||||
- [ ] Setup error logging
|
|
||||||
- [ ] Add performance monitoring
|
|
||||||
- [ ] Configure alerts
|
|
||||||
- [ ] Create dashboards
|
|
||||||
|
|
||||||
### 2. Usage Analytics
|
|
||||||
- [ ] Add storage metrics
|
|
||||||
- [ ] Track migration success
|
|
||||||
- [ ] Monitor performance
|
|
||||||
- [ ] Collect user feedback
|
|
||||||
|
|
||||||
## Security Audit (Planned)
|
|
||||||
|
|
||||||
### 1. Code Review
|
### 1. Code Review
|
||||||
- [ ] Review encryption
|
- [x] Review platform services
|
||||||
- [ ] Check access controls
|
- [x] Interface security
|
||||||
- [ ] Verify data handling
|
- [x] Data handling
|
||||||
- [ ] Audit dependencies
|
- [x] Error management
|
||||||
|
- [x] Check basic SQLite implementations
|
||||||
|
- [x] Query security
|
||||||
|
- [x] Transaction safety
|
||||||
|
- [x] Resource management
|
||||||
|
- [x] Verify basic error handling
|
||||||
|
- [x] Error propagation
|
||||||
|
- [x] Recovery procedures
|
||||||
|
- [x] User feedback
|
||||||
|
- [x] Complete dependency audit
|
||||||
|
- [x] Security vulnerabilities
|
||||||
|
- [x] License compliance
|
||||||
|
- [x] Update requirements
|
||||||
|
|
||||||
### 2. Penetration Testing
|
### 2. Platform Security
|
||||||
- [ ] Test data access
|
- [x] Web platform
|
||||||
- [ ] Verify encryption
|
- [x] Worker isolation
|
||||||
- [ ] Check authentication
|
- [x] Thread separation
|
||||||
- [ ] Review permissions
|
- [x] Message security
|
||||||
|
- [x] Resource isolation
|
||||||
|
- [x] Basic storage security
|
||||||
|
- [x] Access control
|
||||||
|
- [x] Data protection
|
||||||
|
- [x] Quota management
|
||||||
|
- [x] Origin isolation
|
||||||
|
- [x] Cross-origin protection
|
||||||
|
- [x] Resource isolation
|
||||||
|
- [x] Security policy
|
||||||
|
- [x] Mobile platform
|
||||||
|
- [x] Platform permissions
|
||||||
|
- [x] Storage access
|
||||||
|
- [x] File operations
|
||||||
|
- [x] System integration
|
||||||
|
- [x] Platform security
|
||||||
|
- [x] App sandboxing
|
||||||
|
- [x] Storage protection
|
||||||
|
- [x] Access control
|
||||||
|
- [ ] SQLCipher integration (planned)
|
||||||
|
- [ ] Encryption setup
|
||||||
|
- [ ] Key management
|
||||||
|
- [ ] Secure storage
|
||||||
|
- [ ] Electron platform (planned)
|
||||||
|
- [ ] IPC security
|
||||||
|
- [ ] Message validation
|
||||||
|
- [ ] Process isolation
|
||||||
|
- [ ] Resource protection
|
||||||
|
- [ ] File system security
|
||||||
|
- [ ] Access control
|
||||||
|
- [ ] Path validation
|
||||||
|
- [ ] Permission management
|
||||||
|
- [ ] Auto-update security
|
||||||
|
- [ ] Update verification
|
||||||
|
- [ ] Code signing
|
||||||
|
- [ ] Rollback protection
|
||||||
|
|
||||||
## Success Criteria
|
## Success Criteria
|
||||||
|
|
||||||
### 1. Performance
|
### 1. Performance
|
||||||
- [x] Query response time < 100ms
|
- [x] Basic query response time < 100ms
|
||||||
- [x] Operation queuing for thread safety
|
- [x] Simple queries
|
||||||
- [x] Proper initialization handling
|
- [x] Indexed queries
|
||||||
- [ ] Migration time < 5s per 1000 records (planned)
|
- [x] Prepared statements
|
||||||
- [ ] Storage overhead < 10% (planned)
|
- [x] Basic transaction completion < 500ms
|
||||||
- [ ] Memory usage < 50MB (planned)
|
- [x] Single operations
|
||||||
|
- [x] Batch operations
|
||||||
|
- [x] Complex transactions
|
||||||
|
- [x] Basic memory usage < 50MB
|
||||||
|
- [x] Normal operation
|
||||||
|
- [x] Peak usage
|
||||||
|
- [x] Background state
|
||||||
|
- [x] Database size < platform limits
|
||||||
|
- [x] Web platform (1GB)
|
||||||
|
- [x] Mobile platform (2GB)
|
||||||
|
- [ ] Desktop platform (10GB, planned)
|
||||||
|
|
||||||
### 2. Reliability
|
### 2. Reliability
|
||||||
|
- [x] Basic uptime
|
||||||
|
- [x] Service availability
|
||||||
|
- [x] Connection stability
|
||||||
|
- [x] Error recovery
|
||||||
- [x] Basic data integrity
|
- [x] Basic data integrity
|
||||||
- [x] Operation queuing
|
- [x] Transaction atomicity
|
||||||
- [ ] Automatic recovery (planned)
|
- [x] Data consistency
|
||||||
- [ ] Backup verification (planned)
|
- [x] Error handling
|
||||||
- [ ] Transaction atomicity (planned)
|
- [x] Basic recovery
|
||||||
- [ ] Data consistency (planned)
|
- [x] Connection recovery
|
||||||
|
- [x] Transaction rollback
|
||||||
|
- [x] State restoration
|
||||||
|
- [x] Basic transaction atomicity
|
||||||
|
- [x] Commit success
|
||||||
|
- [x] Rollback handling
|
||||||
|
- [x] Error recovery
|
||||||
|
|
||||||
### 3. Security
|
### 3. Security
|
||||||
- [x] Basic data integrity
|
- [x] Platform-specific security
|
||||||
- [ ] AES-256 encryption (planned)
|
- [x] Web platform security
|
||||||
- [ ] Secure key storage (planned)
|
- [x] Mobile platform security
|
||||||
- [ ] Access control (planned)
|
- [ ] Desktop platform security (planned)
|
||||||
- [ ] Audit logging (planned)
|
- [x] Basic access control
|
||||||
|
- [x] User permissions
|
||||||
|
- [x] Resource access
|
||||||
|
- [x] Operation limits
|
||||||
|
- [x] Basic audit logging
|
||||||
|
- [x] Access logs
|
||||||
|
- [x] Operation logs
|
||||||
|
- [x] Security events
|
||||||
|
- [ ] Advanced security features (planned)
|
||||||
|
- [ ] SQLCipher encryption
|
||||||
|
- [ ] Biometric authentication
|
||||||
|
- [ ] Secure enclave
|
||||||
|
- [ ] Key management
|
||||||
|
|
||||||
### 4. User Experience
|
### 4. User Experience
|
||||||
- [x] Basic database operations
|
- [x] Basic platform transitions
|
||||||
- [ ] Smooth migration (planned)
|
- [x] Web to mobile
|
||||||
- [ ] Clear error messages (planned)
|
- [x] Mobile to web
|
||||||
- [ ] Progress indicators (planned)
|
- [x] State preservation
|
||||||
- [ ] Recovery options (planned)
|
- [x] Basic error messages
|
||||||
|
- [x] User feedback
|
||||||
|
- [x] Recovery guidance
|
||||||
|
- [x] Error context
|
||||||
|
- [x] Basic progress indicators
|
||||||
|
- [x] Operation status
|
||||||
|
- [x] Loading states
|
||||||
|
- [x] Completion feedback
|
||||||
|
- [x] Basic recovery options
|
||||||
|
- [x] Automatic recovery
|
||||||
|
- [x] Manual intervention
|
||||||
|
- [x] Data restoration
|
||||||
13
ios/.gitignore
vendored
@@ -11,16 +11,3 @@ capacitor-cordova-ios-plugins
|
|||||||
# Generated Config files
|
# Generated Config files
|
||||||
App/App/capacitor.config.json
|
App/App/capacitor.config.json
|
||||||
App/App/config.xml
|
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
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
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 */; };
|
||||||
97EF2DC6FD76C3643D680B8D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90DCAFB4D8948F7A50C13800 /* Pods_App.framework */; };
|
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 */
|
||||||
@@ -27,9 +27,9 @@
|
|||||||
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>"; };
|
||||||
90DCAFB4D8948F7A50C13800 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "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>"; };
|
||||||
EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "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 */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -37,17 +37,17 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
97EF2DC6FD76C3643D680B8D /* Pods_App.framework in Frameworks */,
|
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
4B546315E668C7A13939F417 /* Frameworks */ = {
|
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
90DCAFB4D8948F7A50C13800 /* Pods_App.framework */,
|
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -57,8 +57,8 @@
|
|||||||
children = (
|
children = (
|
||||||
504EC3061FED79650016851F /* App */,
|
504EC3061FED79650016851F /* App */,
|
||||||
504EC3051FED79650016851F /* Products */,
|
504EC3051FED79650016851F /* Products */,
|
||||||
BA325FFCDCE8D334E5C7AEBE /* Pods */,
|
7F8756D8B27F46E3366F6CEA /* Pods */,
|
||||||
4B546315E668C7A13939F417 /* Frameworks */,
|
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -85,13 +85,13 @@
|
|||||||
path = App;
|
path = App;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
BA325FFCDCE8D334E5C7AEBE /* Pods */ = {
|
7F8756D8B27F46E3366F6CEA /* Pods */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */,
|
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
|
||||||
E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */,
|
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
|
||||||
);
|
);
|
||||||
path = Pods;
|
name = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
@@ -101,13 +101,12 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
|
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
92977BEA1068CC097A57FC77 /* [CP] Check Pods Manifest.lock */,
|
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
|
||||||
504EC3001FED79650016851F /* Sources */,
|
504EC3001FED79650016851F /* Sources */,
|
||||||
504EC3011FED79650016851F /* Frameworks */,
|
504EC3011FED79650016851F /* Frameworks */,
|
||||||
504EC3021FED79650016851F /* Resources */,
|
504EC3021FED79650016851F /* Resources */,
|
||||||
|
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
|
||||||
012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */,
|
012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */,
|
||||||
3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */,
|
|
||||||
96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */,
|
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -187,10 +186,28 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PROJECT_DIR}/app_privacy_manifest_fixer/fixer.sh\" \n";
|
shellScript = "\"${PROJECT_DIR}/app_privacy_manifest_fixer/fixer.sh\" ";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */ = {
|
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
@@ -205,47 +222,6 @@
|
|||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
92977BEA1068CC097A57FC77 /* [CP] Check Pods Manifest.lock */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
inputFileListPaths = (
|
|
||||||
);
|
|
||||||
inputPaths = (
|
|
||||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
|
||||||
"${PODS_ROOT}/Manifest.lock",
|
|
||||||
);
|
|
||||||
name = "[CP] Check Pods Manifest.lock";
|
|
||||||
outputFileListPaths = (
|
|
||||||
);
|
|
||||||
outputPaths = (
|
|
||||||
"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt",
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
|
||||||
showEnvVarsInLog = 0;
|
|
||||||
};
|
|
||||||
96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
alwaysOutOfDate = 1;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
inputFileListPaths = (
|
|
||||||
);
|
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
name = "Fix Privacy Manifest";
|
|
||||||
outputFileListPaths = (
|
|
||||||
);
|
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "$PROJECT_DIR/app_privacy_manifest_fixer/fixer.sh\n";
|
|
||||||
};
|
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
@@ -399,12 +375,11 @@
|
|||||||
};
|
};
|
||||||
504EC3171FED79650016851F /* Debug */ = {
|
504EC3171FED79650016851F /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */;
|
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 26;
|
CURRENT_PROJECT_VERSION = 18;
|
||||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
@@ -413,7 +388,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.5.1;
|
MARKETING_VERSION = 0.4.7;
|
||||||
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)";
|
||||||
@@ -426,12 +401,11 @@
|
|||||||
};
|
};
|
||||||
504EC3181FED79650016851F /* Release */ = {
|
504EC3181FED79650016851F /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */;
|
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 26;
|
CURRENT_PROJECT_VERSION = 18;
|
||||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
@@ -440,7 +414,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.5.1;
|
MARKETING_VERSION = 0.4.7;
|
||||||
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 = "";
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Capacitor
|
import Capacitor
|
||||||
import CapacitorCommunitySqlite
|
|
||||||
|
|
||||||
@UIApplicationMain
|
@UIApplicationMain
|
||||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
@@ -8,10 +7,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
var window: UIWindow?
|
var window: UIWindow?
|
||||||
|
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
// Initialize SQLite
|
|
||||||
//let sqlite = SQLite()
|
|
||||||
//sqlite.initialize()
|
|
||||||
|
|
||||||
// Override point for customization after application launch.
|
// Override point for customization after application launch.
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 116 KiB |
14
ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"size": "1024x1024",
|
||||||
|
"filename": "AppIcon-512@2x.png",
|
||||||
|
"platform": "ios"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info": {
|
||||||
|
"author": "xcode",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
23
ios/App/App/Assets.xcassets/Splash.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "splash-2732x2732-2.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "splash-2732x2732-1.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "splash-2732x2732.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png
vendored
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png
vendored
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png
vendored
Normal file
|
After Width: | Height: | Size: 40 KiB |
@@ -11,7 +11,6 @@ install! 'cocoapods', :disable_input_output_paths => true
|
|||||||
def capacitor_pods
|
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 'CapacitorCommunitySqlite', :path => '../../node_modules/@capacitor-community/sqlite'
|
|
||||||
pod 'CapacitorMlkitBarcodeScanning', :path => '../../node_modules/@capacitor-mlkit/barcode-scanning'
|
pod 'CapacitorMlkitBarcodeScanning', :path => '../../node_modules/@capacitor-mlkit/barcode-scanning'
|
||||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
||||||
pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
|
pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
|
||||||
@@ -27,9 +26,4 @@ end
|
|||||||
|
|
||||||
post_install do |installer|
|
post_install do |installer|
|
||||||
assertDeploymentTarget(installer)
|
assertDeploymentTarget(installer)
|
||||||
installer.pods_project.targets.each do |target|
|
|
||||||
target.build_configurations.each do |config|
|
|
||||||
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,10 +5,6 @@ PODS:
|
|||||||
- Capacitor
|
- Capacitor
|
||||||
- CapacitorCamera (6.1.2):
|
- CapacitorCamera (6.1.2):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- CapacitorCommunitySqlite (6.0.2):
|
|
||||||
- Capacitor
|
|
||||||
- SQLCipher
|
|
||||||
- ZIPFoundation
|
|
||||||
- CapacitorCordova (6.2.1)
|
- CapacitorCordova (6.2.1)
|
||||||
- CapacitorFilesystem (6.0.3):
|
- CapacitorFilesystem (6.0.3):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
@@ -77,18 +73,11 @@ PODS:
|
|||||||
- nanopb/decode (2.30910.0)
|
- nanopb/decode (2.30910.0)
|
||||||
- nanopb/encode (2.30910.0)
|
- nanopb/encode (2.30910.0)
|
||||||
- PromisesObjC (2.4.0)
|
- PromisesObjC (2.4.0)
|
||||||
- SQLCipher (4.9.0):
|
|
||||||
- SQLCipher/standard (= 4.9.0)
|
|
||||||
- SQLCipher/common (4.9.0)
|
|
||||||
- SQLCipher/standard (4.9.0):
|
|
||||||
- SQLCipher/common
|
|
||||||
- ZIPFoundation (0.9.19)
|
|
||||||
|
|
||||||
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`)"
|
- "CapacitorCamera (from `../../node_modules/@capacitor/camera`)"
|
||||||
- "CapacitorCommunitySqlite (from `../../node_modules/@capacitor-community/sqlite`)"
|
|
||||||
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
|
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
|
||||||
- "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)"
|
- "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)"
|
||||||
- "CapacitorMlkitBarcodeScanning (from `../../node_modules/@capacitor-mlkit/barcode-scanning`)"
|
- "CapacitorMlkitBarcodeScanning (from `../../node_modules/@capacitor-mlkit/barcode-scanning`)"
|
||||||
@@ -109,8 +98,6 @@ SPEC REPOS:
|
|||||||
- MLKitVision
|
- MLKitVision
|
||||||
- nanopb
|
- nanopb
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
- SQLCipher
|
|
||||||
- ZIPFoundation
|
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
Capacitor:
|
Capacitor:
|
||||||
@@ -119,8 +106,6 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../../node_modules/@capacitor/app"
|
:path: "../../node_modules/@capacitor/app"
|
||||||
CapacitorCamera:
|
CapacitorCamera:
|
||||||
:path: "../../node_modules/@capacitor/camera"
|
:path: "../../node_modules/@capacitor/camera"
|
||||||
CapacitorCommunitySqlite:
|
|
||||||
:path: "../../node_modules/@capacitor-community/sqlite"
|
|
||||||
CapacitorCordova:
|
CapacitorCordova:
|
||||||
:path: "../../node_modules/@capacitor/ios"
|
:path: "../../node_modules/@capacitor/ios"
|
||||||
CapacitorFilesystem:
|
CapacitorFilesystem:
|
||||||
@@ -136,7 +121,6 @@ SPEC CHECKSUMS:
|
|||||||
Capacitor: c95400d761e376be9da6be5a05f226c0e865cebf
|
Capacitor: c95400d761e376be9da6be5a05f226c0e865cebf
|
||||||
CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7
|
CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7
|
||||||
CapacitorCamera: 9bc7b005d0e6f1d5f525b8137045b60cffffce79
|
CapacitorCamera: 9bc7b005d0e6f1d5f525b8137045b60cffffce79
|
||||||
CapacitorCommunitySqlite: 0299d20f4b00c2e6aa485a1d8932656753937b9b
|
|
||||||
CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff
|
CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff
|
||||||
CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74
|
CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74
|
||||||
CapacitorMlkitBarcodeScanning: 7652be9c7922f39203a361de735d340ae37e134e
|
CapacitorMlkitBarcodeScanning: 7652be9c7922f39203a361de735d340ae37e134e
|
||||||
@@ -154,9 +138,7 @@ SPEC CHECKSUMS:
|
|||||||
MLKitVision: 90922bca854014a856f8b649d1f1f04f63fd9c79
|
MLKitVision: 90922bca854014a856f8b649d1f1f04f63fd9c79
|
||||||
nanopb: 438bc412db1928dac798aa6fd75726007be04262
|
nanopb: 438bc412db1928dac798aa6fd75726007be04262
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da
|
|
||||||
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
|
|
||||||
|
|
||||||
PODFILE CHECKSUM: f987510f7383b04a1b09ea8472bdadcd88b6c924
|
PODFILE CHECKSUM: 7e7e09e6937de7f015393aecf2cf7823645689b3
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
2111
package-lock.json
generated
12
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "timesafari",
|
"name": "timesafari",
|
||||||
"version": "0.5.1",
|
"version": "0.4.6",
|
||||||
"description": "Time Safari Application",
|
"description": "Time Safari Application",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Time Safari Team"
|
"name": "Time Safari Team"
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
"electron:build-mac-universal": "npm run build:electron-prod && electron-builder --mac --universal"
|
"electron:build-mac-universal": "npm run build:electron-prod && electron-builder --mac --universal"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor-community/sqlite": "6.0.2",
|
"@capacitor-community/sqlite": "6.0.0",
|
||||||
"@capacitor-mlkit/barcode-scanning": "^6.0.0",
|
"@capacitor-mlkit/barcode-scanning": "^6.0.0",
|
||||||
"@capacitor/android": "^6.2.0",
|
"@capacitor/android": "^6.2.0",
|
||||||
"@capacitor/app": "^6.0.0",
|
"@capacitor/app": "^6.0.0",
|
||||||
@@ -116,6 +116,7 @@
|
|||||||
"reflect-metadata": "^0.1.14",
|
"reflect-metadata": "^0.1.14",
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "^1.7.2",
|
||||||
"simple-vue-camera": "^1.1.3",
|
"simple-vue-camera": "^1.1.3",
|
||||||
|
"sqlite": "^5.1.1",
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "^5.1.7",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "^3.0.0",
|
||||||
"three": "^0.156.1",
|
"three": "^0.156.1",
|
||||||
@@ -167,11 +168,12 @@
|
|||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "~5.2.2",
|
"typescript": "~5.2.2",
|
||||||
"vite": "^5.2.0",
|
"vite": "^5.2.0",
|
||||||
"vite-plugin-pwa": "^1.0.0"
|
"vite-plugin-node-polyfills": "^0.23.0",
|
||||||
|
"vite-plugin-pwa": "^0.19.8"
|
||||||
},
|
},
|
||||||
"main": "./dist-electron/main.js",
|
"main": "./dist-electron/main.js",
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "app.timesafari.app",
|
"appId": "app.timesafari",
|
||||||
"productName": "TimeSafari",
|
"productName": "TimeSafari",
|
||||||
"directories": {
|
"directories": {
|
||||||
"output": "dist-electron-packages"
|
"output": "dist-electron-packages"
|
||||||
@@ -182,7 +184,7 @@
|
|||||||
],
|
],
|
||||||
"extraResources": [
|
"extraResources": [
|
||||||
{
|
{
|
||||||
"from": "dist-electron/www",
|
"from": "dist",
|
||||||
"to": "www"
|
"to": "www"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,6 +2,5 @@ dependencies:
|
|||||||
- gradle
|
- gradle
|
||||||
- java
|
- java
|
||||||
- pod
|
- pod
|
||||||
- rubygems.org
|
|
||||||
|
|
||||||
# other dependencies are discovered via package.json & requirements.txt & Gemfile (I'm guessing).
|
# other dependencies are discovered via package.json & requirements.txt & Gemfile (I'm guessing).
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
eth_keys
|
eth_keys
|
||||||
pywebview
|
pywebview
|
||||||
pyinstaller>=6.12.0
|
pyinstaller>=6.12.0
|
||||||
setuptools>=69.0.0 # Required for distutils for electron-builder on macOS
|
|
||||||
# For development
|
# For development
|
||||||
watchdog>=3.0.0 # For file watching support
|
watchdog>=3.0.0 # For file watching support
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
console.log('Starting electron build process...');
|
console.log('Starting electron build process...');
|
||||||
|
|
||||||
// Define paths
|
// Copy web files
|
||||||
|
const webDistPath = path.join(__dirname, '..', 'dist');
|
||||||
const electronDistPath = path.join(__dirname, '..', 'dist-electron');
|
const electronDistPath = path.join(__dirname, '..', 'dist-electron');
|
||||||
const wwwPath = path.join(electronDistPath, 'www');
|
const wwwPath = path.join(electronDistPath, 'www');
|
||||||
|
|
||||||
@@ -12,154 +13,231 @@ if (!fs.existsSync(wwwPath)) {
|
|||||||
fs.mkdirSync(wwwPath, { recursive: true });
|
fs.mkdirSync(wwwPath, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a platform-specific index.html for Electron
|
// Copy web files to www directory
|
||||||
const initialIndexContent = `<!DOCTYPE html>
|
fs.cpSync(webDistPath, wwwPath, { recursive: true });
|
||||||
<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,viewport-fit=cover">
|
|
||||||
<link rel="icon" href="/favicon.ico">
|
|
||||||
<title>TimeSafari</title>
|
|
||||||
</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>
|
|
||||||
<script type="module">
|
|
||||||
// Force electron platform
|
|
||||||
window.process = { env: { VITE_PLATFORM: 'electron' } };
|
|
||||||
import('./src/main.electron.ts');
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
|
|
||||||
// Write the Electron-specific index.html
|
// Fix asset paths in index.html
|
||||||
fs.writeFileSync(path.join(wwwPath, 'index.html'), initialIndexContent);
|
|
||||||
|
|
||||||
// Copy only necessary assets from web build
|
|
||||||
const webDistPath = path.join(__dirname, '..', 'dist');
|
|
||||||
if (fs.existsSync(webDistPath)) {
|
|
||||||
// Copy assets directory
|
|
||||||
const assetsSrc = path.join(webDistPath, 'assets');
|
|
||||||
const assetsDest = path.join(wwwPath, 'assets');
|
|
||||||
if (fs.existsSync(assetsSrc)) {
|
|
||||||
fs.cpSync(assetsSrc, assetsDest, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy favicon
|
|
||||||
const faviconSrc = path.join(webDistPath, 'favicon.ico');
|
|
||||||
if (fs.existsSync(faviconSrc)) {
|
|
||||||
fs.copyFileSync(faviconSrc, path.join(wwwPath, 'favicon.ico'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove service worker files
|
|
||||||
const swFilesToRemove = [
|
|
||||||
'sw.js',
|
|
||||||
'sw.js.map',
|
|
||||||
'workbox-*.js',
|
|
||||||
'workbox-*.js.map',
|
|
||||||
'registerSW.js',
|
|
||||||
'manifest.webmanifest',
|
|
||||||
'**/workbox-*.js',
|
|
||||||
'**/workbox-*.js.map',
|
|
||||||
'**/sw.js',
|
|
||||||
'**/sw.js.map',
|
|
||||||
'**/registerSW.js',
|
|
||||||
'**/manifest.webmanifest'
|
|
||||||
];
|
|
||||||
|
|
||||||
console.log('Removing service worker files...');
|
|
||||||
swFilesToRemove.forEach(pattern => {
|
|
||||||
const files = fs.readdirSync(wwwPath).filter(file =>
|
|
||||||
file.match(new RegExp(pattern.replace(/\*/g, '.*')))
|
|
||||||
);
|
|
||||||
files.forEach(file => {
|
|
||||||
const filePath = path.join(wwwPath, file);
|
|
||||||
console.log(`Removing ${filePath}`);
|
|
||||||
try {
|
|
||||||
fs.unlinkSync(filePath);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(`Could not remove ${filePath}:`, err.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Also check and remove from assets directory
|
|
||||||
const assetsPath = path.join(wwwPath, 'assets');
|
|
||||||
if (fs.existsSync(assetsPath)) {
|
|
||||||
swFilesToRemove.forEach(pattern => {
|
|
||||||
const files = fs.readdirSync(assetsPath).filter(file =>
|
|
||||||
file.match(new RegExp(pattern.replace(/\*/g, '.*')))
|
|
||||||
);
|
|
||||||
files.forEach(file => {
|
|
||||||
const filePath = path.join(assetsPath, file);
|
|
||||||
console.log(`Removing ${filePath}`);
|
|
||||||
try {
|
|
||||||
fs.unlinkSync(filePath);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(`Could not remove ${filePath}:`, err.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modify index.html to remove service worker registration
|
|
||||||
const indexPath = path.join(wwwPath, 'index.html');
|
const indexPath = path.join(wwwPath, 'index.html');
|
||||||
if (fs.existsSync(indexPath)) {
|
let indexContent = fs.readFileSync(indexPath, 'utf8');
|
||||||
console.log('Modifying index.html to remove service worker registration...');
|
|
||||||
let indexContent = fs.readFileSync(indexPath, 'utf8');
|
|
||||||
|
|
||||||
// Remove service worker registration script
|
|
||||||
indexContent = indexContent
|
|
||||||
.replace(/<script[^>]*id="vite-plugin-pwa:register-sw"[^>]*><\/script>/g, '')
|
|
||||||
.replace(/<script[^>]*registerServiceWorker[^>]*><\/script>/g, '')
|
|
||||||
.replace(/<link[^>]*rel="manifest"[^>]*>/g, '')
|
|
||||||
.replace(/<link[^>]*rel="serviceworker"[^>]*>/g, '')
|
|
||||||
.replace(/navigator\.serviceWorker\.register\([^)]*\)/g, '')
|
|
||||||
.replace(/if\s*\(\s*['"]serviceWorker['"]\s*in\s*navigator\s*\)\s*{[^}]*}/g, '');
|
|
||||||
|
|
||||||
fs.writeFileSync(indexPath, indexContent);
|
|
||||||
console.log('Successfully modified index.html');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fix asset paths
|
// Fix asset paths
|
||||||
console.log('Fixing asset paths in index.html...');
|
indexContent = indexContent
|
||||||
let modifiedIndexContent = fs.readFileSync(indexPath, 'utf8');
|
|
||||||
modifiedIndexContent = modifiedIndexContent
|
|
||||||
.replace(/\/assets\//g, './assets/')
|
.replace(/\/assets\//g, './assets/')
|
||||||
.replace(/href="\//g, 'href="./')
|
.replace(/href="\//g, 'href="./')
|
||||||
.replace(/src="\//g, 'src="./');
|
.replace(/src="\//g, 'src="./');
|
||||||
|
|
||||||
fs.writeFileSync(indexPath, modifiedIndexContent);
|
fs.writeFileSync(indexPath, indexContent);
|
||||||
|
|
||||||
// Verify no service worker references remain
|
|
||||||
const finalContent = fs.readFileSync(indexPath, 'utf8');
|
|
||||||
if (finalContent.includes('serviceWorker') || finalContent.includes('workbox')) {
|
|
||||||
console.warn('Warning: Service worker references may still exist in index.html');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for remaining /assets/ paths
|
// Check for remaining /assets/ paths
|
||||||
console.log('After path fixing, checking for remaining /assets/ paths:', finalContent.includes('/assets/'));
|
console.log('After path fixing, checking for remaining /assets/ paths:', indexContent.includes('/assets/'));
|
||||||
console.log('Sample of fixed content:', finalContent.substring(0, 500));
|
console.log('Sample of fixed content:', indexContent.substring(0, 500));
|
||||||
|
|
||||||
console.log('Copied and fixed web files in:', wwwPath);
|
console.log('Copied and fixed web files in:', wwwPath);
|
||||||
|
|
||||||
// Copy main process files
|
// Copy main process files
|
||||||
console.log('Copying main process files...');
|
console.log('Copying main process files...');
|
||||||
|
|
||||||
// Copy the main process file instead of creating a template
|
// Create the main process file with inlined logger
|
||||||
const mainSrcPath = path.join(__dirname, '..', 'dist-electron', 'main.js');
|
const mainContent = `const { app, BrowserWindow } = require("electron");
|
||||||
const mainDestPath = path.join(electronDistPath, 'main.js');
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
if (fs.existsSync(mainSrcPath)) {
|
// Inline logger implementation
|
||||||
fs.copyFileSync(mainSrcPath, mainDestPath);
|
const logger = {
|
||||||
console.log('Copied main process file successfully');
|
log: (...args) => console.log(...args),
|
||||||
} else {
|
error: (...args) => console.error(...args),
|
||||||
console.error('Main process file not found at:', mainSrcPath);
|
info: (...args) => console.info(...args),
|
||||||
process.exit(1);
|
warn: (...args) => console.warn(...args),
|
||||||
|
debug: (...args) => console.debug(...args),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if running in dev mode
|
||||||
|
const isDev = process.argv.includes("--inspect");
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
// Add before createWindow function
|
||||||
|
const preloadPath = path.join(__dirname, "preload.js");
|
||||||
|
logger.log("Checking preload path:", preloadPath);
|
||||||
|
logger.log("Preload exists:", fs.existsSync(preloadPath));
|
||||||
|
|
||||||
|
// Create the browser window.
|
||||||
|
const mainWindow = new BrowserWindow({
|
||||||
|
width: 1200,
|
||||||
|
height: 800,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
webSecurity: true,
|
||||||
|
allowRunningInsecureContent: false,
|
||||||
|
preload: path.join(__dirname, "preload.js"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Always open DevTools for now
|
||||||
|
mainWindow.webContents.openDevTools();
|
||||||
|
|
||||||
|
// Intercept requests to fix asset paths
|
||||||
|
mainWindow.webContents.session.webRequest.onBeforeRequest(
|
||||||
|
{
|
||||||
|
urls: [
|
||||||
|
"file://*/*/assets/*",
|
||||||
|
"file://*/assets/*",
|
||||||
|
"file:///assets/*", // Catch absolute paths
|
||||||
|
"<all_urls>", // Catch all URLs as a fallback
|
||||||
|
],
|
||||||
|
},
|
||||||
|
(details, callback) => {
|
||||||
|
let url = details.url;
|
||||||
|
|
||||||
|
// Handle paths that don't start with file://
|
||||||
|
if (!url.startsWith("file://") && url.includes("/assets/")) {
|
||||||
|
url = \`file://\${path.join(__dirname, "www", url)}\`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle absolute paths starting with /assets/
|
||||||
|
if (url.includes("/assets/") && !url.includes("/www/assets/")) {
|
||||||
|
const baseDir = url.includes("dist-electron")
|
||||||
|
? url.substring(
|
||||||
|
0,
|
||||||
|
url.indexOf("/dist-electron") + "/dist-electron".length,
|
||||||
|
)
|
||||||
|
: \`file://\${__dirname}\`;
|
||||||
|
const assetPath = url.split("/assets/")[1];
|
||||||
|
const newUrl = \`\${baseDir}/www/assets/\${assetPath}\`;
|
||||||
|
callback({ redirectURL: newUrl });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback({}); // No redirect for other URLs
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
// Debug info
|
||||||
|
logger.log("Debug Info:");
|
||||||
|
logger.log("Running in dev mode:", isDev);
|
||||||
|
logger.log("App is packaged:", app.isPackaged);
|
||||||
|
logger.log("Process resource path:", process.resourcesPath);
|
||||||
|
logger.log("App path:", app.getAppPath());
|
||||||
|
logger.log("__dirname:", __dirname);
|
||||||
|
logger.log("process.cwd():", process.cwd());
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexPath = path.join(__dirname, "www", "index.html");
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
logger.log("Loading index from:", indexPath);
|
||||||
|
logger.log("www path:", path.join(__dirname, "www"));
|
||||||
|
logger.log("www assets path:", path.join(__dirname, "www", "assets"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(indexPath)) {
|
||||||
|
logger.error(\`Index file not found at: \${indexPath}\`);
|
||||||
|
throw new Error("Index file not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add CSP headers to allow API connections, Google Fonts, and zxing-wasm
|
||||||
|
mainWindow.webContents.session.webRequest.onHeadersReceived(
|
||||||
|
(details, callback) => {
|
||||||
|
callback({
|
||||||
|
responseHeaders: {
|
||||||
|
...details.responseHeaders,
|
||||||
|
"Content-Security-Policy": [
|
||||||
|
"default-src 'self';" +
|
||||||
|
"connect-src 'self' https://api.endorser.ch https://*.timesafari.app https://*.jsdelivr.net;" +
|
||||||
|
"img-src 'self' data: https: blob:;" +
|
||||||
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.jsdelivr.net;" +
|
||||||
|
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;" +
|
||||||
|
"font-src 'self' data: https://fonts.gstatic.com;" +
|
||||||
|
"style-src-elem 'self' 'unsafe-inline' https://fonts.googleapis.com;" +
|
||||||
|
"worker-src 'self' blob:;",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load the index.html
|
||||||
|
mainWindow
|
||||||
|
.loadFile(indexPath)
|
||||||
|
.then(() => {
|
||||||
|
logger.log("Successfully loaded index.html");
|
||||||
|
if (isDev) {
|
||||||
|
mainWindow.webContents.openDevTools();
|
||||||
|
logger.log("DevTools opened - running in dev mode");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error("Failed to load index.html:", err);
|
||||||
|
logger.error("Attempted path:", indexPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for console messages from the renderer
|
||||||
|
mainWindow.webContents.on("console-message", (_event, _level, message) => {
|
||||||
|
logger.log("Renderer Console:", message);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add right after creating the BrowserWindow
|
||||||
|
mainWindow.webContents.on(
|
||||||
|
"did-fail-load",
|
||||||
|
(_event, errorCode, errorDescription) => {
|
||||||
|
logger.error("Page failed to load:", errorCode, errorDescription);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
mainWindow.webContents.on("preload-error", (_event, preloadPath, error) => {
|
||||||
|
logger.error("Preload script error:", preloadPath, error);
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.webContents.on(
|
||||||
|
"console-message",
|
||||||
|
(_event, _level, message, line, sourceId) => {
|
||||||
|
logger.log("Renderer Console:", line, sourceId, message);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Enable remote debugging when in dev mode
|
||||||
|
if (isDev) {
|
||||||
|
mainWindow.webContents.openDevTools();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Electron build process completed successfully');
|
// Handle app ready
|
||||||
|
app.whenReady().then(createWindow);
|
||||||
|
|
||||||
|
// Handle all windows closed
|
||||||
|
app.on("window-all-closed", () => {
|
||||||
|
if (process.platform !== "darwin") {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on("activate", () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
createWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle any errors
|
||||||
|
process.on("uncaughtException", (error) => {
|
||||||
|
logger.error("Uncaught Exception:", error);
|
||||||
|
});
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Write the main process file
|
||||||
|
const mainDest = path.join(electronDistPath, 'main.js');
|
||||||
|
fs.writeFileSync(mainDest, mainContent);
|
||||||
|
|
||||||
|
// Copy preload script if it exists
|
||||||
|
const preloadSrc = path.join(__dirname, '..', 'src', 'electron', 'preload.js');
|
||||||
|
const preloadDest = path.join(electronDistPath, 'preload.js');
|
||||||
|
if (fs.existsSync(preloadSrc)) {
|
||||||
|
console.log(`Copying ${preloadSrc} to ${preloadDest}`);
|
||||||
|
fs.copyFileSync(preloadSrc, preloadDest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify build structure
|
||||||
|
console.log('\nVerifying build structure:');
|
||||||
|
console.log('Files in dist-electron:', fs.readdirSync(electronDistPath));
|
||||||
|
|
||||||
|
console.log('Build completed successfully!');
|
||||||
@@ -51,7 +51,7 @@ const { existsSync } = require('fs');
|
|||||||
*/
|
*/
|
||||||
function checkCommand(command, errorMessage) {
|
function checkCommand(command, errorMessage) {
|
||||||
try {
|
try {
|
||||||
execSync(command, { stdio: 'ignore' });
|
execSync(command + ' --version', { stdio: 'ignore' });
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`❌ ${errorMessage}`);
|
console.error(`❌ ${errorMessage}`);
|
||||||
@@ -164,10 +164,10 @@ function main() {
|
|||||||
|
|
||||||
// Check required command line tools
|
// Check required command line tools
|
||||||
// These are essential for building and testing the application
|
// These are essential for building and testing the application
|
||||||
success &= checkCommand('node --version', 'Node.js is required');
|
success &= checkCommand('node', 'Node.js is required');
|
||||||
success &= checkCommand('npm --version', 'npm is required');
|
success &= checkCommand('npm', 'npm is required');
|
||||||
success &= checkCommand('gradle --version', 'Gradle is required for Android builds');
|
success &= checkCommand('gradle', 'Gradle is required for Android builds');
|
||||||
success &= checkCommand('xcodebuild --help', 'Xcode is required for iOS builds');
|
success &= checkCommand('xcodebuild', 'Xcode is required for iOS builds');
|
||||||
|
|
||||||
// Check platform-specific development environments
|
// Check platform-specific development environments
|
||||||
success &= checkAndroidSetup();
|
success &= checkAndroidSetup();
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ const executeDeeplink = async (url, description, log) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Stop the app before executing the deep link
|
// Stop the app before executing the deep link
|
||||||
execSync('adb shell am force-stop app.timesafari.app');
|
execSync('adb shell am force-stop app.timesafari');
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1s
|
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1s
|
||||||
|
|
||||||
execSync(`adb shell am start -W -a android.intent.action.VIEW -d "${url}" -c android.intent.category.BROWSABLE`);
|
execSync(`adb shell am start -W -a android.intent.action.VIEW -d "${url}" -c android.intent.category.BROWSABLE`);
|
||||||
|
|||||||
25
src/App.vue
@@ -4,7 +4,7 @@
|
|||||||
<!-- Messages in the upper-right - https://github.com/emmanuelsw/notiwind -->
|
<!-- Messages in the upper-right - https://github.com/emmanuelsw/notiwind -->
|
||||||
<NotificationGroup group="alert">
|
<NotificationGroup group="alert">
|
||||||
<div
|
<div
|
||||||
class="fixed z-[90] top-[max(1rem,env(safe-area-inset-top))] right-4 left-4 sm:left-auto sm:w-full sm:max-w-sm flex flex-col items-start justify-end"
|
class="fixed top-[calc(env(safe-area-inset-top)+1rem)] right-4 left-4 sm:left-auto sm:w-full sm:max-w-sm flex flex-col items-start justify-end"
|
||||||
>
|
>
|
||||||
<Notification
|
<Notification
|
||||||
v-slot="{ notifications, close }"
|
v-slot="{ notifications, close }"
|
||||||
@@ -330,11 +330,8 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Vue, Component } from "vue-facing-decorator";
|
import { Vue, Component } from "vue-facing-decorator";
|
||||||
|
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "./db/index";
|
||||||
import { NotificationIface, USE_DEXIE_DB } from "./constants/app";
|
import { NotificationIface } from "./constants/app";
|
||||||
import * as databaseUtil from "./db/databaseUtil";
|
|
||||||
import { retrieveSettingsForActiveAccount } from "./db/index";
|
|
||||||
import { logConsoleAndDb } from "./db/databaseUtil";
|
|
||||||
import { logger } from "./utils/logger";
|
import { logger } from "./utils/logger";
|
||||||
|
|
||||||
interface Settings {
|
interface Settings {
|
||||||
@@ -399,11 +396,7 @@ export default class App extends Vue {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
logger.log("Retrieving settings for the active account...");
|
logger.log("Retrieving settings for the active account...");
|
||||||
let settings: Settings =
|
const settings: Settings = await retrieveSettingsForActiveAccount();
|
||||||
await databaseUtil.retrieveSettingsForActiveAccount();
|
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
settings = await retrieveSettingsForActiveAccount();
|
|
||||||
}
|
|
||||||
logger.log("Retrieved settings:", settings);
|
logger.log("Retrieved settings:", settings);
|
||||||
|
|
||||||
const notifyingNewActivity = !!settings?.notifyingNewActivityTime;
|
const notifyingNewActivity = !!settings?.notifyingNewActivityTime;
|
||||||
@@ -548,13 +541,13 @@ export default class App extends Vue {
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
#Content {
|
#Content {
|
||||||
padding-left: max(1.5rem, env(safe-area-inset-left));
|
padding-left: 1.5rem;
|
||||||
padding-right: max(1.5rem, env(safe-area-inset-right));
|
padding-right: 1.5rem;
|
||||||
padding-top: max(1.5rem, env(safe-area-inset-top));
|
padding-top: calc(env(safe-area-inset-top) + 1.5rem);
|
||||||
padding-bottom: max(1.5rem, env(safe-area-inset-bottom));
|
padding-bottom: calc(env(safe-area-inset-bottom) + 1.5rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
#QuickNav ~ #Content {
|
#QuickNav ~ #Content {
|
||||||
padding-bottom: calc(env(safe-area-inset-bottom) + 6.333rem);
|
padding-bottom: calc(env(safe-area-inset-bottom) + 6rem);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -14,34 +14,22 @@
|
|||||||
class="flex items-center justify-between gap-2 text-lg bg-slate-200 border border-slate-300 border-b-0 rounded-t-md px-3 sm:px-4 py-1 sm:py-2"
|
class="flex items-center justify-between gap-2 text-lg bg-slate-200 border border-slate-300 border-b-0 rounded-t-md px-3 sm:px-4 py-1 sm:py-2"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<router-link
|
<div v-if="record.issuerDid">
|
||||||
v-if="record.issuerDid && !isHiddenDid(record.issuerDid)"
|
|
||||||
:to="{
|
|
||||||
path: '/did/' + encodeURIComponent(record.issuerDid),
|
|
||||||
}"
|
|
||||||
title="More details about this person"
|
|
||||||
>
|
|
||||||
<EntityIcon
|
<EntityIcon
|
||||||
:entity-id="record.issuerDid"
|
:entity-id="record.issuerDid"
|
||||||
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
|
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</div>
|
||||||
<font-awesome
|
<div v-else>
|
||||||
v-else-if="isHiddenDid(record.issuerDid)"
|
<font-awesome
|
||||||
icon="eye-slash"
|
icon="person-circle-question"
|
||||||
class="text-slate-400 !size-[2rem] cursor-pointer"
|
class="text-slate-300 text-[2rem]"
|
||||||
@click="notifyHiddenPerson"
|
/>
|
||||||
/>
|
</div>
|
||||||
<font-awesome
|
|
||||||
v-else
|
|
||||||
icon="person-circle-question"
|
|
||||||
class="text-slate-400 !size-[2rem] cursor-pointer"
|
|
||||||
@click="notifyUnknownPerson"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 v-if="record.issuer.known" class="font-semibold leading-tight">
|
<h3 class="font-semibold">
|
||||||
{{ record.issuer.displayName }}
|
{{ record.issuer.known ? record.issuer.displayName : "" }}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="ms-auto text-xs text-slate-500 italic">
|
<p class="ms-auto text-xs text-slate-500 italic">
|
||||||
{{ friendlyDate }}
|
{{ friendlyDate }}
|
||||||
@@ -49,11 +37,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a
|
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
|
||||||
class="cursor-pointer"
|
|
||||||
data-testid="circle-info-link"
|
|
||||||
@click="$emit('loadClaim', record.jwtId)"
|
|
||||||
>
|
|
||||||
<font-awesome icon="circle-info" class="fa-fw text-slate-500" />
|
<font-awesome icon="circle-info" class="fa-fw text-slate-500" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,7 +46,7 @@
|
|||||||
<!-- Record Image -->
|
<!-- Record Image -->
|
||||||
<div
|
<div
|
||||||
v-if="record.image"
|
v-if="record.image"
|
||||||
class="bg-cover mb-2 -mt-3 sm:-mt-4 -mx-3 sm:-mx-4"
|
class="bg-cover mb-6 -mt-3 sm:-mt-4 -mx-3 sm:-mx-4"
|
||||||
:style="`background-image: url(${record.image});`"
|
:style="`background-image: url(${record.image});`"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -78,59 +62,29 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Description -->
|
|
||||||
<p class="font-medium">
|
|
||||||
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
|
|
||||||
{{ description }}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="relative flex justify-between gap-4 max-w-[40rem] mx-auto mt-4"
|
class="relative flex justify-between gap-4 max-w-[40rem] mx-auto mb-5"
|
||||||
>
|
>
|
||||||
<!-- Source -->
|
<!-- Source -->
|
||||||
<div
|
<div
|
||||||
class="w-[7rem] sm:w-[12rem] text-center bg-white border border-slate-200 rounded p-2 sm:p-3"
|
class="w-[8rem] sm:w-[12rem] text-center bg-white border border-slate-200 rounded p-2 sm:p-3"
|
||||||
>
|
>
|
||||||
<div class="relative w-fit mx-auto">
|
<div class="relative w-fit mx-auto">
|
||||||
<div>
|
<div>
|
||||||
<!-- Project Icon -->
|
<!-- Project Icon -->
|
||||||
<div v-if="record.providerPlanName">
|
<div v-if="record.providerPlanName">
|
||||||
<router-link
|
<ProjectIcon
|
||||||
:to="{
|
:entity-id="record.providerPlanName"
|
||||||
path:
|
:icon-size="48"
|
||||||
'/project/' +
|
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
|
||||||
encodeURIComponent(record.providerPlanHandleId || ''),
|
/>
|
||||||
}"
|
|
||||||
title="View project details"
|
|
||||||
>
|
|
||||||
<ProjectIcon
|
|
||||||
:entity-id="record.providerPlanHandleId || ''"
|
|
||||||
:icon-size="48"
|
|
||||||
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
|
|
||||||
/>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- Identicon for DIDs -->
|
<!-- Identicon for DIDs -->
|
||||||
<div v-else-if="record.agentDid">
|
<div v-else-if="record.agentDid">
|
||||||
<router-link
|
<EntityIcon
|
||||||
v-if="!isHiddenDid(record.agentDid)"
|
:entity-id="record.agentDid"
|
||||||
:to="{
|
:profile-image-url="record.issuer.profileImageUrl"
|
||||||
path: '/did/' + encodeURIComponent(record.agentDid),
|
class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]"
|
||||||
}"
|
|
||||||
title="More details about this person"
|
|
||||||
>
|
|
||||||
<EntityIcon
|
|
||||||
:entity-id="record.agentDid"
|
|
||||||
:profile-image-url="record.issuer.profileImageUrl"
|
|
||||||
class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]"
|
|
||||||
/>
|
|
||||||
</router-link>
|
|
||||||
<font-awesome
|
|
||||||
v-else
|
|
||||||
icon="eye-slash"
|
|
||||||
class="text-slate-300 !size-[3rem] sm:!size-[4rem]"
|
|
||||||
@click="notifyHiddenPerson"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- Unknown Person -->
|
<!-- Unknown Person -->
|
||||||
@@ -138,7 +92,6 @@
|
|||||||
<font-awesome
|
<font-awesome
|
||||||
icon="person-circle-question"
|
icon="person-circle-question"
|
||||||
class="text-slate-300 text-[3rem] sm:text-[4rem]"
|
class="text-slate-300 text-[3rem] sm:text-[4rem]"
|
||||||
@click="notifyUnknownPerson"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -157,11 +110,9 @@
|
|||||||
|
|
||||||
<!-- Arrow -->
|
<!-- Arrow -->
|
||||||
<div
|
<div
|
||||||
class="absolute inset-x-[7rem] sm:inset-x-[12rem] mx-2 top-1/2 -translate-y-1/2"
|
class="absolute inset-x-[8rem] sm:inset-x-[12rem] mx-2 top-1/2 -translate-y-1/2"
|
||||||
>
|
>
|
||||||
<div
|
<div class="text-sm text-center leading-none font-semibold pe-[15px]">
|
||||||
class="text-sm text-center leading-none font-semibold pe-2 sm:pe-4"
|
|
||||||
>
|
|
||||||
{{ fetchAmount }}
|
{{ fetchAmount }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -178,47 +129,24 @@
|
|||||||
|
|
||||||
<!-- Destination -->
|
<!-- Destination -->
|
||||||
<div
|
<div
|
||||||
class="w-[7rem] sm:w-[12rem] text-center bg-white border border-slate-200 rounded p-2 sm:p-3"
|
class="w-[8rem] sm:w-[12rem] text-center bg-white border border-slate-200 rounded p-2 sm:p-3"
|
||||||
>
|
>
|
||||||
<div class="relative w-fit mx-auto">
|
<div class="relative w-fit mx-auto">
|
||||||
<div>
|
<div>
|
||||||
<!-- Project Icon -->
|
<!-- Project Icon -->
|
||||||
<div v-if="record.recipientProjectName">
|
<div v-if="record.recipientProjectName">
|
||||||
<router-link
|
<ProjectIcon
|
||||||
:to="{
|
:entity-id="record.recipientProjectName"
|
||||||
path:
|
:icon-size="48"
|
||||||
'/project/' +
|
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
|
||||||
encodeURIComponent(record.fulfillsPlanHandleId || ''),
|
/>
|
||||||
}"
|
|
||||||
title="View project details"
|
|
||||||
>
|
|
||||||
<ProjectIcon
|
|
||||||
:entity-id="record.fulfillsPlanHandleId || ''"
|
|
||||||
:icon-size="48"
|
|
||||||
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
|
|
||||||
/>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- Identicon for DIDs -->
|
<!-- Identicon for DIDs -->
|
||||||
<div v-else-if="record.recipientDid">
|
<div v-else-if="record.recipientDid">
|
||||||
<router-link
|
<EntityIcon
|
||||||
v-if="!isHiddenDid(record.recipientDid)"
|
:entity-id="record.recipientDid"
|
||||||
:to="{
|
:profile-image-url="record.receiver.profileImageUrl"
|
||||||
path: '/did/' + encodeURIComponent(record.recipientDid),
|
class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]"
|
||||||
}"
|
|
||||||
title="More details about this person"
|
|
||||||
>
|
|
||||||
<EntityIcon
|
|
||||||
:entity-id="record.recipientDid"
|
|
||||||
:profile-image-url="record.receiver.profileImageUrl"
|
|
||||||
class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]"
|
|
||||||
/>
|
|
||||||
</router-link>
|
|
||||||
<font-awesome
|
|
||||||
v-else
|
|
||||||
icon="eye-slash"
|
|
||||||
class="text-slate-300 !size-[3rem] sm:!size-[4rem]"
|
|
||||||
@click="notifyHiddenPerson"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- Unknown Person -->
|
<!-- Unknown Person -->
|
||||||
@@ -226,7 +154,6 @@
|
|||||||
<font-awesome
|
<font-awesome
|
||||||
icon="person-circle-question"
|
icon="person-circle-question"
|
||||||
class="text-slate-300 text-[3rem] sm:text-[4rem]"
|
class="text-slate-300 text-[3rem] sm:text-[4rem]"
|
||||||
@click="notifyUnknownPerson"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -243,6 +170,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<p class="font-medium">
|
||||||
|
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
|
||||||
|
{{ description }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
@@ -252,9 +186,8 @@ import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
|
|||||||
import { GiveRecordWithContactInfo } from "../types";
|
import { GiveRecordWithContactInfo } from "../types";
|
||||||
import EntityIcon from "./EntityIcon.vue";
|
import EntityIcon from "./EntityIcon.vue";
|
||||||
import { isGiveClaimType, notifyWhyCannotConfirm } from "../libs/util";
|
import { isGiveClaimType, notifyWhyCannotConfirm } from "../libs/util";
|
||||||
import { containsHiddenDid, isHiddenDid } from "../libs/endorserServer";
|
import { containsHiddenDid } from "../libs/endorserServer";
|
||||||
import ProjectIcon from "./ProjectIcon.vue";
|
import ProjectIcon from "./ProjectIcon.vue";
|
||||||
import { NotificationIface } from "../constants/app";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
@@ -269,33 +202,6 @@ export default class ActivityListItem extends Vue {
|
|||||||
@Prop() activeDid!: string;
|
@Prop() activeDid!: string;
|
||||||
@Prop() confirmerIdList?: string[];
|
@Prop() confirmerIdList?: string[];
|
||||||
|
|
||||||
isHiddenDid = isHiddenDid;
|
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
||||||
|
|
||||||
notifyHiddenPerson() {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "warning",
|
|
||||||
title: "Person Outside Your Network",
|
|
||||||
text: "This person is not visible to you.",
|
|
||||||
},
|
|
||||||
3000,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyUnknownPerson() {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "warning",
|
|
||||||
title: "Unidentified Person",
|
|
||||||
text: "Nobody specific was recognized.",
|
|
||||||
},
|
|
||||||
3000,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Emit()
|
@Emit()
|
||||||
cacheImage(image: string) {
|
cacheImage(image: string) {
|
||||||
return image;
|
return image;
|
||||||
@@ -316,7 +222,7 @@ export default class ActivityListItem extends Vue {
|
|||||||
const claim =
|
const claim =
|
||||||
(this.record.fullClaim as unknown).claim || this.record.fullClaim;
|
(this.record.fullClaim as unknown).claim || this.record.fullClaim;
|
||||||
|
|
||||||
return `${claim?.description || ""}`;
|
return `${claim.description}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private displayAmount(code: string, amt: number) {
|
private displayAmount(code: string, amt: number) {
|
||||||
|
|||||||
@@ -1,207 +0,0 @@
|
|||||||
/** * AmountInput.vue - Specialized amount input with increment/decrement
|
|
||||||
controls * * Extracted from GiftedDialog.vue to handle numeric amount input *
|
|
||||||
with increment/decrement buttons and validation. * * @author Matthew Raymer */
|
|
||||||
<template>
|
|
||||||
<div class="flex flex-grow">
|
|
||||||
<button
|
|
||||||
class="rounded-s border border-e-0 border-slate-400 bg-slate-200 px-4 py-2"
|
|
||||||
:disabled="isAtMinimum"
|
|
||||||
type="button"
|
|
||||||
@click.prevent="decrement"
|
|
||||||
>
|
|
||||||
<font-awesome icon="chevron-left" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<input
|
|
||||||
:id="inputId"
|
|
||||||
v-model="displayValue"
|
|
||||||
type="number"
|
|
||||||
:min="min"
|
|
||||||
:max="max"
|
|
||||||
:step="step"
|
|
||||||
class="flex-1 border border-e-0 border-slate-400 px-2 py-2 text-center w-[1px]"
|
|
||||||
@input="handleInput"
|
|
||||||
@blur="handleBlur"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="rounded-e border border-slate-400 bg-slate-200 px-4 py-2"
|
|
||||||
:disabled="isAtMaximum"
|
|
||||||
type="button"
|
|
||||||
@click.prevent="increment"
|
|
||||||
>
|
|
||||||
<font-awesome icon="chevron-right" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue, Watch, Emit } from "vue-facing-decorator";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AmountInput - Numeric input with increment/decrement controls
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Increment/decrement buttons with validation
|
|
||||||
* - Configurable min/max values and step size
|
|
||||||
* - Input validation and formatting
|
|
||||||
* - Disabled state handling for boundary values
|
|
||||||
* - Emits update events for v-model compatibility
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
export default class AmountInput extends Vue {
|
|
||||||
/** Current numeric value */
|
|
||||||
@Prop({ required: true })
|
|
||||||
value!: number;
|
|
||||||
|
|
||||||
/** Minimum allowed value */
|
|
||||||
@Prop({ default: 0 })
|
|
||||||
min!: number;
|
|
||||||
|
|
||||||
/** Maximum allowed value */
|
|
||||||
@Prop({ default: Number.MAX_SAFE_INTEGER })
|
|
||||||
max!: number;
|
|
||||||
|
|
||||||
/** Step size for increment/decrement */
|
|
||||||
@Prop({ default: 1 })
|
|
||||||
step!: number;
|
|
||||||
|
|
||||||
/** Input element ID for accessibility */
|
|
||||||
@Prop({ default: "amount-input" })
|
|
||||||
inputId!: string;
|
|
||||||
|
|
||||||
/** Internal display value for input field */
|
|
||||||
private displayValue: string = "0";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize display value from prop
|
|
||||||
*/
|
|
||||||
mounted(): void {
|
|
||||||
console.log(
|
|
||||||
`[AmountInput] mounted() - initial value: ${this.value}, min: ${this.min}, max: ${this.max}, step: ${this.step}`,
|
|
||||||
);
|
|
||||||
this.displayValue = this.value.toString();
|
|
||||||
console.log(
|
|
||||||
`[AmountInput] mounted() - displayValue set to: ${this.displayValue}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Watch for external value changes
|
|
||||||
*/
|
|
||||||
@Watch("value")
|
|
||||||
onValueChange(newValue: number): void {
|
|
||||||
this.displayValue = newValue.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if current value is at minimum
|
|
||||||
*/
|
|
||||||
get isAtMinimum(): boolean {
|
|
||||||
const result = this.value <= this.min;
|
|
||||||
console.log(
|
|
||||||
`[AmountInput] isAtMinimum - value: ${this.value}, min: ${this.min}, result: ${result}`,
|
|
||||||
);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if current value is at maximum
|
|
||||||
*/
|
|
||||||
get isAtMaximum(): boolean {
|
|
||||||
const result = this.value >= this.max;
|
|
||||||
console.log(
|
|
||||||
`[AmountInput] isAtMaximum - value: ${this.value}, max: ${this.max}, result: ${result}`,
|
|
||||||
);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Increment the value by step size
|
|
||||||
*/
|
|
||||||
increment(): void {
|
|
||||||
console.log(
|
|
||||||
`[AmountInput] increment() called - current value: ${this.value}, step: ${this.step}`,
|
|
||||||
);
|
|
||||||
const newValue = Math.min(this.value + this.step, this.max);
|
|
||||||
console.log(`[AmountInput] increment() calculated newValue: ${newValue}`);
|
|
||||||
this.updateValue(newValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decrement the value by step size
|
|
||||||
*/
|
|
||||||
decrement(): void {
|
|
||||||
console.log(
|
|
||||||
`[AmountInput] decrement() called - current value: ${this.value}, step: ${this.step}`,
|
|
||||||
);
|
|
||||||
const newValue = Math.max(this.value - this.step, this.min);
|
|
||||||
console.log(`[AmountInput] decrement() calculated newValue: ${newValue}`);
|
|
||||||
this.updateValue(newValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle direct input changes
|
|
||||||
*/
|
|
||||||
handleInput(): void {
|
|
||||||
const numericValue = parseFloat(this.displayValue);
|
|
||||||
if (!isNaN(numericValue)) {
|
|
||||||
const clampedValue = Math.max(this.min, Math.min(numericValue, this.max));
|
|
||||||
this.updateValue(clampedValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle input blur - ensure display value matches actual value
|
|
||||||
*/
|
|
||||||
handleBlur(): void {
|
|
||||||
this.displayValue = this.value.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the value and emit change event
|
|
||||||
*/
|
|
||||||
private updateValue(newValue: number): void {
|
|
||||||
console.log(
|
|
||||||
`[AmountInput] updateValue() called - oldValue: ${this.value}, newValue: ${newValue}`,
|
|
||||||
);
|
|
||||||
if (newValue !== this.value) {
|
|
||||||
console.log(
|
|
||||||
`[AmountInput] updateValue() - values different, updating and emitting`,
|
|
||||||
);
|
|
||||||
this.displayValue = newValue.toString();
|
|
||||||
this.emitUpdateValue(newValue);
|
|
||||||
} else {
|
|
||||||
console.log(`[AmountInput] updateValue() - values same, skipping update`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emit update:value event
|
|
||||||
*/
|
|
||||||
@Emit("update:value")
|
|
||||||
emitUpdateValue(value: number): number {
|
|
||||||
console.log(`[AmountInput] emitUpdateValue() - emitting value: ${value}`);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* Remove spinner arrows from number input */
|
|
||||||
input[type="number"]::-webkit-outer-spin-button,
|
|
||||||
input[type="number"]::-webkit-inner-spin-button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="number"] {
|
|
||||||
-moz-appearance: textfield;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Disabled button styles */
|
|
||||||
button:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -24,7 +24,9 @@ backup and database export, with platform-specific download instructions. * *
|
|||||||
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"
|
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()"
|
@click="exportDatabase()"
|
||||||
>
|
>
|
||||||
Download Contacts
|
Download Settings & Contacts
|
||||||
|
<br />
|
||||||
|
(excluding Identifier Data)
|
||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
ref="downloadLink"
|
ref="downloadLink"
|
||||||
@@ -60,18 +62,14 @@ backup and database export, with platform-specific download instructions. * *
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from "vue-facing-decorator";
|
import { Component, Prop, Vue } from "vue-facing-decorator";
|
||||||
import { AppString, NotificationIface } from "../constants/app";
|
import { NotificationIface } from "../constants/app";
|
||||||
|
import { db } from "../db/index";
|
||||||
import { Contact } from "../db/tables/contacts";
|
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
|
||||||
|
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||||
import {
|
import {
|
||||||
PlatformService,
|
PlatformService,
|
||||||
PlatformCapabilities,
|
PlatformCapabilities,
|
||||||
} from "../services/PlatformService";
|
} from "../services/PlatformService";
|
||||||
import { contactsToExportJson } from "../libs/util";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @vue-component
|
* @vue-component
|
||||||
@@ -133,25 +131,21 @@ export default class DataExportSection extends Vue {
|
|||||||
*/
|
*/
|
||||||
public async exportDatabase() {
|
public async exportDatabase() {
|
||||||
try {
|
try {
|
||||||
let allContacts: Contact[] = [];
|
const blob = await db.export({
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
prettyJson: true,
|
||||||
const result = await platformService.dbQuery(`SELECT * FROM contacts`);
|
transform: (table, value, key) => {
|
||||||
if (result) {
|
if (table === "contacts") {
|
||||||
allContacts = databaseUtil.mapQueryResultToValues(
|
// Dexie inserts a number 0 when some are undefined, so we need to totally remove them.
|
||||||
result,
|
Object.keys(value).forEach((prop) => {
|
||||||
) as unknown as Contact[];
|
if (value[prop] === undefined) {
|
||||||
}
|
delete value[prop];
|
||||||
// if (USE_DEXIE_DB) {
|
}
|
||||||
// await db.open();
|
});
|
||||||
// allContacts = await db.contacts.toArray();
|
}
|
||||||
// }
|
return { value, key };
|
||||||
|
},
|
||||||
// Convert contacts to export format
|
});
|
||||||
const exportData = contactsToExportJson(allContacts);
|
const fileName = `${db.name}-backup.json`;
|
||||||
const jsonStr = JSON.stringify(exportData, null, 2);
|
|
||||||
const blob = new Blob([jsonStr], { type: "application/json" });
|
|
||||||
|
|
||||||
const fileName = `${AppString.APP_NAME_NO_SPACES}-backup-contacts.json`;
|
|
||||||
|
|
||||||
if (this.platformCapabilities.hasFileDownload) {
|
if (this.platformCapabilities.hasFileDownload) {
|
||||||
// Web platform: Use download link
|
// Web platform: Use download link
|
||||||
@@ -163,9 +157,8 @@ export default class DataExportSection extends Vue {
|
|||||||
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
|
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
|
||||||
} else if (this.platformCapabilities.hasFileSystem) {
|
} else if (this.platformCapabilities.hasFileSystem) {
|
||||||
// Native platform: Write to app directory
|
// Native platform: Write to app directory
|
||||||
await this.platformService.writeAndShareFile(fileName, jsonStr);
|
const content = await blob.text();
|
||||||
} else {
|
await this.platformService.writeAndShareFile(fileName, content);
|
||||||
throw new Error("This platform does not support file downloads.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -174,10 +167,10 @@ export default class DataExportSection extends Vue {
|
|||||||
type: "success",
|
type: "success",
|
||||||
title: "Export Successful",
|
title: "Export Successful",
|
||||||
text: this.platformCapabilities.hasFileDownload
|
text: this.platformCapabilities.hasFileDownload
|
||||||
? "See your downloads directory for the backup."
|
? "See your downloads directory for the backup. It is in the Dexie format."
|
||||||
: "The backup file has been saved.",
|
: "You should have been prompted to save your backup file.",
|
||||||
},
|
},
|
||||||
3000,
|
-1,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Export Error:", error);
|
logger.error("Export Error:", error);
|
||||||
|
|||||||
@@ -1,264 +0,0 @@
|
|||||||
/** * EntityGrid.vue - Unified entity grid layout component * * Extracted from
|
|
||||||
GiftedDialog.vue to provide a reusable grid layout * for displaying people,
|
|
||||||
projects, and special entities with selection. * * @author Matthew Raymer */
|
|
||||||
<template>
|
|
||||||
<ul :class="gridClasses">
|
|
||||||
<!-- Special entities (You, Unnamed) for people grids -->
|
|
||||||
<template v-if="entityType === 'people'">
|
|
||||||
<!-- "You" entity -->
|
|
||||||
<SpecialEntityCard
|
|
||||||
v-if="showYouEntity"
|
|
||||||
entity-type="you"
|
|
||||||
label="You"
|
|
||||||
icon="hand"
|
|
||||||
:selectable="youSelectable"
|
|
||||||
:conflicted="youConflicted"
|
|
||||||
:entity-data="youEntityData"
|
|
||||||
@entity-selected="handleEntitySelected"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- "Unnamed" entity -->
|
|
||||||
<SpecialEntityCard
|
|
||||||
entity-type="unnamed"
|
|
||||||
label="Unnamed"
|
|
||||||
icon="circle-question"
|
|
||||||
:entity-data="unnamedEntityData"
|
|
||||||
@entity-selected="handleEntitySelected"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Empty state message -->
|
|
||||||
<li
|
|
||||||
v-if="entities.length === 0"
|
|
||||||
class="text-xs text-slate-500 italic col-span-full"
|
|
||||||
>
|
|
||||||
{{ emptyStateMessage }}
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<!-- Entity cards (people or projects) -->
|
|
||||||
<template v-if="entityType === 'people'">
|
|
||||||
<PersonCard
|
|
||||||
v-for="person in displayedEntities"
|
|
||||||
:key="person.did"
|
|
||||||
:person="person"
|
|
||||||
:conflicted="isPersonConflicted(person.did)"
|
|
||||||
:show-time-icon="true"
|
|
||||||
@person-selected="handlePersonSelected"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else-if="entityType === 'projects'">
|
|
||||||
<ProjectCard
|
|
||||||
v-for="project in displayedEntities"
|
|
||||||
:key="project.handleId"
|
|
||||||
:project="project"
|
|
||||||
:active-did="activeDid"
|
|
||||||
:all-my-dids="allMyDids"
|
|
||||||
:all-contacts="allContacts"
|
|
||||||
@project-selected="handleProjectSelected"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Show All navigation -->
|
|
||||||
<ShowAllCard
|
|
||||||
v-if="shouldShowAll"
|
|
||||||
:entity-type="entityType"
|
|
||||||
:route-name="showAllRoute"
|
|
||||||
:query-params="showAllQueryParams"
|
|
||||||
/>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
|
|
||||||
import PersonCard from "./PersonCard.vue";
|
|
||||||
import ProjectCard from "./ProjectCard.vue";
|
|
||||||
import SpecialEntityCard from "./SpecialEntityCard.vue";
|
|
||||||
import ShowAllCard from "./ShowAllCard.vue";
|
|
||||||
import { Contact } from "../db/tables/contacts";
|
|
||||||
import { PlanData } from "../interfaces/records";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* EntityGrid - Unified grid layout for displaying people or projects
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Responsive grid layout for people/projects
|
|
||||||
* - Special entity integration (You, Unnamed)
|
|
||||||
* - Conflict detection integration
|
|
||||||
* - Empty state messaging
|
|
||||||
* - Show All navigation
|
|
||||||
* - Event delegation for entity selection
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
PersonCard,
|
|
||||||
ProjectCard,
|
|
||||||
SpecialEntityCard,
|
|
||||||
ShowAllCard,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class EntityGrid extends Vue {
|
|
||||||
/** Type of entities to display */
|
|
||||||
@Prop({ required: true })
|
|
||||||
entityType!: "people" | "projects";
|
|
||||||
|
|
||||||
/** Array of entities to display */
|
|
||||||
@Prop({ required: true })
|
|
||||||
entities!: Contact[] | PlanData[];
|
|
||||||
|
|
||||||
/** Maximum number of entities to display */
|
|
||||||
@Prop({ default: 10 })
|
|
||||||
maxItems!: number;
|
|
||||||
|
|
||||||
/** Active user's DID */
|
|
||||||
@Prop({ required: true })
|
|
||||||
activeDid!: string;
|
|
||||||
|
|
||||||
/** All user's DIDs */
|
|
||||||
@Prop({ required: true })
|
|
||||||
allMyDids!: string[];
|
|
||||||
|
|
||||||
/** All contacts */
|
|
||||||
@Prop({ required: true })
|
|
||||||
allContacts!: Contact[];
|
|
||||||
|
|
||||||
/** Function to check if a person DID would create a conflict */
|
|
||||||
@Prop({ required: true })
|
|
||||||
conflictChecker!: (did: string) => boolean;
|
|
||||||
|
|
||||||
/** Whether to show the "You" entity for people grids */
|
|
||||||
@Prop({ default: true })
|
|
||||||
showYouEntity!: boolean;
|
|
||||||
|
|
||||||
/** Whether the "You" entity is selectable */
|
|
||||||
@Prop({ default: true })
|
|
||||||
youSelectable!: boolean;
|
|
||||||
|
|
||||||
/** Route name for "Show All" navigation */
|
|
||||||
@Prop({ default: "" })
|
|
||||||
showAllRoute!: string;
|
|
||||||
|
|
||||||
/** Query parameters for "Show All" navigation */
|
|
||||||
@Prop({ default: () => ({}) })
|
|
||||||
showAllQueryParams!: Record<string, any>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed CSS classes for the grid layout
|
|
||||||
*/
|
|
||||||
get gridClasses(): string {
|
|
||||||
const baseClasses = "grid gap-x-2 gap-y-4 text-center mb-4";
|
|
||||||
|
|
||||||
if (this.entityType === "projects") {
|
|
||||||
return `${baseClasses} grid-cols-3 md:grid-cols-4`;
|
|
||||||
} else {
|
|
||||||
return `${baseClasses} grid-cols-4 sm:grid-cols-5 md:grid-cols-6`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed entities to display (limited by maxItems)
|
|
||||||
*/
|
|
||||||
get displayedEntities(): Contact[] | PlanData[] {
|
|
||||||
const maxDisplay = this.entityType === "projects" ? 7 : this.maxItems;
|
|
||||||
return this.entities.slice(0, maxDisplay);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed empty state message based on entity type
|
|
||||||
*/
|
|
||||||
get emptyStateMessage(): string {
|
|
||||||
if (this.entityType === "projects") {
|
|
||||||
return "(No projects found.)";
|
|
||||||
} else {
|
|
||||||
return "(Add friends to see more people worthy of recognition.)";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to show the "Show All" navigation
|
|
||||||
*/
|
|
||||||
get shouldShowAll(): boolean {
|
|
||||||
return this.entities.length > 0 && this.showAllRoute !== "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the "You" entity is conflicted
|
|
||||||
*/
|
|
||||||
get youConflicted(): boolean {
|
|
||||||
return this.conflictChecker(this.activeDid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Entity data for the "You" special entity
|
|
||||||
*/
|
|
||||||
get youEntityData(): { did: string; name: string } {
|
|
||||||
return {
|
|
||||||
did: this.activeDid,
|
|
||||||
name: "You",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Entity data for the "Unnamed" special entity
|
|
||||||
*/
|
|
||||||
get unnamedEntityData(): { did: string; name: string } {
|
|
||||||
return {
|
|
||||||
did: "",
|
|
||||||
name: "Unnamed",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a person DID is conflicted
|
|
||||||
*/
|
|
||||||
isPersonConflicted(did: string): boolean {
|
|
||||||
return this.conflictChecker(did);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle person selection from PersonCard
|
|
||||||
*/
|
|
||||||
handlePersonSelected(person: Contact): void {
|
|
||||||
this.emitEntitySelected({
|
|
||||||
type: "person",
|
|
||||||
data: person,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle project selection from ProjectCard
|
|
||||||
*/
|
|
||||||
handleProjectSelected(project: PlanData): void {
|
|
||||||
this.emitEntitySelected({
|
|
||||||
type: "project",
|
|
||||||
data: project,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle special entity selection from SpecialEntityCard
|
|
||||||
*/
|
|
||||||
handleEntitySelected(event: {
|
|
||||||
type: string;
|
|
||||||
entityType: string;
|
|
||||||
data: any;
|
|
||||||
}): void {
|
|
||||||
this.emitEntitySelected({
|
|
||||||
type: "special",
|
|
||||||
entityType: event.entityType,
|
|
||||||
data: event.data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit methods using @Emit decorator
|
|
||||||
|
|
||||||
@Emit("entity-selected")
|
|
||||||
emitEntitySelected(data: any): any {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* Grid-specific styles if needed */
|
|
||||||
</style>
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
/** * EntitySelectionStep.vue - Entity selection step component * * Extracted
|
|
||||||
from GiftedDialog.vue to handle the complete step 1 * entity selection interface
|
|
||||||
with dynamic labeling and grid display. * * @author Matthew Raymer */
|
|
||||||
<template>
|
|
||||||
<div id="sectionGiftedGiver">
|
|
||||||
<label class="block font-bold mb-4">
|
|
||||||
{{ stepLabel }}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<EntityGrid
|
|
||||||
:entity-type="shouldShowProjects ? 'projects' : 'people'"
|
|
||||||
:entities="shouldShowProjects ? projects : allContacts"
|
|
||||||
:max-items="10"
|
|
||||||
:active-did="activeDid"
|
|
||||||
:all-my-dids="allMyDids"
|
|
||||||
:all-contacts="allContacts"
|
|
||||||
:conflict-checker="conflictChecker"
|
|
||||||
:show-you-entity="shouldShowYouEntity"
|
|
||||||
:you-selectable="youSelectable"
|
|
||||||
:show-all-route="showAllRoute"
|
|
||||||
:show-all-query-params="showAllQueryParams"
|
|
||||||
@entity-selected="handleEntitySelected"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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-1.5 py-2 rounded-lg"
|
|
||||||
@click="handleCancel"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
|
|
||||||
import EntityGrid from "./EntityGrid.vue";
|
|
||||||
import { Contact } from "../db/tables/contacts";
|
|
||||||
import { PlanData } from "../interfaces/records";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Entity selection event data structure
|
|
||||||
*/
|
|
||||||
interface EntitySelectionEvent {
|
|
||||||
type: "person" | "project" | "special";
|
|
||||||
entityType?: string;
|
|
||||||
data: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* EntitySelectionStep - Complete step 1 entity selection interface
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Dynamic step labeling based on context
|
|
||||||
* - EntityGrid integration for unified entity display
|
|
||||||
* - Conflict detection and prevention
|
|
||||||
* - Special entity handling (You, Unnamed)
|
|
||||||
* - Show All navigation with context preservation
|
|
||||||
* - Cancel functionality
|
|
||||||
* - Event delegation for entity selection
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
EntityGrid,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class EntitySelectionStep extends Vue {
|
|
||||||
/** Type of step: 'giver' or 'recipient' */
|
|
||||||
@Prop({ required: true })
|
|
||||||
stepType!: "giver" | "recipient";
|
|
||||||
|
|
||||||
/** Type of giver entity: 'person' or 'project' */
|
|
||||||
@Prop({ required: true })
|
|
||||||
giverEntityType!: "person" | "project";
|
|
||||||
|
|
||||||
/** Type of recipient entity: 'person' or 'project' */
|
|
||||||
@Prop({ required: true })
|
|
||||||
recipientEntityType!: "person" | "project";
|
|
||||||
|
|
||||||
/** Whether to show projects instead of people */
|
|
||||||
@Prop({ default: false })
|
|
||||||
showProjects!: boolean;
|
|
||||||
|
|
||||||
/** Whether this is from a project view */
|
|
||||||
@Prop({ default: false })
|
|
||||||
isFromProjectView!: boolean;
|
|
||||||
|
|
||||||
/** Array of available projects */
|
|
||||||
@Prop({ required: true })
|
|
||||||
projects!: PlanData[];
|
|
||||||
|
|
||||||
/** Array of available contacts */
|
|
||||||
@Prop({ required: true })
|
|
||||||
allContacts!: Contact[];
|
|
||||||
|
|
||||||
/** Active user's DID */
|
|
||||||
@Prop({ required: true })
|
|
||||||
activeDid!: string;
|
|
||||||
|
|
||||||
/** All user's DIDs */
|
|
||||||
@Prop({ required: true })
|
|
||||||
allMyDids!: string[];
|
|
||||||
|
|
||||||
/** Function to check if a DID would create a conflict */
|
|
||||||
@Prop({ required: true })
|
|
||||||
conflictChecker!: (did: string) => boolean;
|
|
||||||
|
|
||||||
/** Project ID for context (giver) */
|
|
||||||
@Prop({ default: "" })
|
|
||||||
fromProjectId!: string;
|
|
||||||
|
|
||||||
/** Project ID for context (recipient) */
|
|
||||||
@Prop({ default: "" })
|
|
||||||
toProjectId!: string;
|
|
||||||
|
|
||||||
/** Current giver entity for context */
|
|
||||||
@Prop()
|
|
||||||
giver?: any;
|
|
||||||
|
|
||||||
/** Current receiver entity for context */
|
|
||||||
@Prop()
|
|
||||||
receiver?: any;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed step label based on context
|
|
||||||
*/
|
|
||||||
get stepLabel(): string {
|
|
||||||
if (this.stepType === "recipient") {
|
|
||||||
return "Choose who received the gift:";
|
|
||||||
} else if (this.showProjects) {
|
|
||||||
return "Choose a project benefitted from:";
|
|
||||||
} else {
|
|
||||||
return "Choose a person received from:";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to show projects in the grid
|
|
||||||
*/
|
|
||||||
get shouldShowProjects(): boolean {
|
|
||||||
return (
|
|
||||||
(this.stepType === "giver" && this.giverEntityType === "project") ||
|
|
||||||
(this.stepType === "recipient" && this.recipientEntityType === "project")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to show the "You" entity
|
|
||||||
*/
|
|
||||||
get shouldShowYouEntity(): boolean {
|
|
||||||
return (
|
|
||||||
this.stepType === "recipient" ||
|
|
||||||
(this.stepType === "giver" && this.isFromProjectView)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the "You" entity is selectable
|
|
||||||
*/
|
|
||||||
get youSelectable(): boolean {
|
|
||||||
return !this.conflictChecker(this.activeDid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Route name for "Show All" navigation
|
|
||||||
*/
|
|
||||||
get showAllRoute(): string {
|
|
||||||
if (this.shouldShowProjects) {
|
|
||||||
return "discover";
|
|
||||||
} else if (this.allContacts.length > 0) {
|
|
||||||
return "contact-gift";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query parameters for "Show All" navigation
|
|
||||||
*/
|
|
||||||
get showAllQueryParams(): Record<string, any> {
|
|
||||||
if (this.shouldShowProjects) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
stepType: this.stepType,
|
|
||||||
giverEntityType: this.giverEntityType,
|
|
||||||
recipientEntityType: this.recipientEntityType,
|
|
||||||
...(this.stepType === "giver"
|
|
||||||
? {
|
|
||||||
recipientProjectId: this.toProjectId,
|
|
||||||
recipientProjectName: this.receiver?.name,
|
|
||||||
recipientProjectImage: this.receiver?.image,
|
|
||||||
recipientProjectHandleId: this.receiver?.handleId,
|
|
||||||
recipientDid: this.receiver?.did,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
giverProjectId: this.fromProjectId,
|
|
||||||
giverProjectName: this.giver?.name,
|
|
||||||
giverProjectImage: this.giver?.image,
|
|
||||||
giverProjectHandleId: this.giver?.handleId,
|
|
||||||
giverDid: this.giver?.did,
|
|
||||||
}),
|
|
||||||
fromProjectId: this.fromProjectId,
|
|
||||||
toProjectId: this.toProjectId,
|
|
||||||
showProjects: this.showProjects.toString(),
|
|
||||||
isFromProjectView: this.isFromProjectView.toString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle entity selection from EntityGrid
|
|
||||||
*/
|
|
||||||
handleEntitySelected(event: EntitySelectionEvent): void {
|
|
||||||
this.emitEntitySelected({
|
|
||||||
stepType: this.stepType,
|
|
||||||
...event,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle cancel button click
|
|
||||||
*/
|
|
||||||
handleCancel(): void {
|
|
||||||
this.emitCancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit methods using @Emit decorator
|
|
||||||
|
|
||||||
@Emit("entity-selected")
|
|
||||||
emitEntitySelected(data: any): any {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Emit("cancel")
|
|
||||||
emitCancel(): void {
|
|
||||||
// No return value needed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* Component-specific styles if needed */
|
|
||||||
</style>
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
/** * EntitySummaryButton.vue - Displays selected entity with edit capability *
|
|
||||||
* Extracted from GiftedDialog.vue to handle entity summary display * in the gift
|
|
||||||
details step with edit functionality. * * @author Matthew Raymer */
|
|
||||||
<template>
|
|
||||||
<component
|
|
||||||
:is="editable ? 'button' : 'div'"
|
|
||||||
class="flex-1 flex items-center gap-2 bg-slate-100 border border-slate-300 rounded-md p-2"
|
|
||||||
@click="handleClick"
|
|
||||||
>
|
|
||||||
<!-- Entity Icon/Avatar -->
|
|
||||||
<div>
|
|
||||||
<template v-if="entityType === 'project'">
|
|
||||||
<ProjectIcon
|
|
||||||
v-if="entity?.handleId"
|
|
||||||
:entity-id="entity.handleId"
|
|
||||||
:icon-size="32"
|
|
||||||
:image-url="entity.image"
|
|
||||||
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<EntityIcon
|
|
||||||
v-if="entity?.did"
|
|
||||||
:contact="entity"
|
|
||||||
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
|
|
||||||
/>
|
|
||||||
<font-awesome
|
|
||||||
v-else
|
|
||||||
icon="circle-question"
|
|
||||||
class="text-slate-400 text-3xl"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Entity Information -->
|
|
||||||
<div class="text-start min-w-0">
|
|
||||||
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
|
|
||||||
{{ label }}
|
|
||||||
</p>
|
|
||||||
<h3 class="font-semibold truncate">
|
|
||||||
{{ entity?.name || "Unnamed" }}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Edit/Lock Icon -->
|
|
||||||
<p class="ms-auto text-sm pe-1" :class="iconClasses">
|
|
||||||
<font-awesome
|
|
||||||
:icon="editable ? 'pen' : 'lock'"
|
|
||||||
:title="editable ? 'Change' : 'Can\'t be changed'"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</component>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
|
|
||||||
import EntityIcon from "./EntityIcon.vue";
|
|
||||||
import ProjectIcon from "./ProjectIcon.vue";
|
|
||||||
import { Contact } from "../db/tables/contacts";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Entity interface for both person and project entities
|
|
||||||
*/
|
|
||||||
interface EntityData {
|
|
||||||
did?: string;
|
|
||||||
handleId?: string;
|
|
||||||
name?: string;
|
|
||||||
image?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* EntitySummaryButton - Displays selected entity with optional edit capability
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Shows entity avatar (person or project)
|
|
||||||
* - Displays entity name and role label
|
|
||||||
* - Handles editable vs locked states
|
|
||||||
* - Emits edit events when clicked and editable
|
|
||||||
* - Supports both person and project entity types
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
EntityIcon,
|
|
||||||
ProjectIcon,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class EntitySummaryButton extends Vue {
|
|
||||||
/** Entity data to display */
|
|
||||||
@Prop({ required: true })
|
|
||||||
entity!: EntityData | Contact | null;
|
|
||||||
|
|
||||||
/** Type of entity: 'person' or 'project' */
|
|
||||||
@Prop({ required: true })
|
|
||||||
entityType!: "person" | "project";
|
|
||||||
|
|
||||||
/** Display label for the entity role */
|
|
||||||
@Prop({ required: true })
|
|
||||||
label!: string;
|
|
||||||
|
|
||||||
/** Whether the entity can be edited */
|
|
||||||
@Prop({ default: true })
|
|
||||||
editable!: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed CSS classes for the edit/lock icon
|
|
||||||
*/
|
|
||||||
get iconClasses(): string {
|
|
||||||
return this.editable ? "text-blue-500" : "text-slate-400";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle click event - only emit if editable
|
|
||||||
*/
|
|
||||||
handleClick(): void {
|
|
||||||
if (this.editable) {
|
|
||||||
this.emitEditRequested({
|
|
||||||
entityType: this.entityType,
|
|
||||||
entity: this.entity,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit methods using @Emit decorator
|
|
||||||
|
|
||||||
@Emit("edit-requested")
|
|
||||||
emitEditRequested(data: any): any {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* Ensure button styling is consistent */
|
|
||||||
button {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: #f1f5f9; /* hover:bg-slate-100 */
|
|
||||||
}
|
|
||||||
|
|
||||||
div {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -100,11 +100,6 @@ import {
|
|||||||
} from "@vue-leaflet/vue-leaflet";
|
} from "@vue-leaflet/vue-leaflet";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
|
|
||||||
import { USE_DEXIE_DB } from "@/constants/app";
|
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
|
||||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
|
||||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
LRectangle,
|
LRectangle,
|
||||||
@@ -125,10 +120,8 @@ export default class FeedFilters extends Vue {
|
|||||||
async open(onCloseIfChanged: () => void) {
|
async open(onCloseIfChanged: () => void) {
|
||||||
this.onCloseIfChanged = onCloseIfChanged;
|
this.onCloseIfChanged = onCloseIfChanged;
|
||||||
|
|
||||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
const platform = this.$platform;
|
||||||
if (USE_DEXIE_DB) {
|
const settings = await platform.getActiveAccountSettings();
|
||||||
settings = await retrieveSettingsForActiveAccount();
|
|
||||||
}
|
|
||||||
this.hasVisibleDid = !!settings.filterFeedByVisible;
|
this.hasVisibleDid = !!settings.filterFeedByVisible;
|
||||||
this.isNearby = !!settings.filterFeedByNearby;
|
this.isNearby = !!settings.filterFeedByNearby;
|
||||||
if (settings.searchBoxes && settings.searchBoxes.length > 0) {
|
if (settings.searchBoxes && settings.searchBoxes.length > 0) {
|
||||||
@@ -142,29 +135,19 @@ export default class FeedFilters extends Vue {
|
|||||||
async toggleHasVisibleDid() {
|
async toggleHasVisibleDid() {
|
||||||
this.settingChanged = true;
|
this.settingChanged = true;
|
||||||
this.hasVisibleDid = !this.hasVisibleDid;
|
this.hasVisibleDid = !this.hasVisibleDid;
|
||||||
await databaseUtil.updateDefaultSettings({
|
const platform = this.$platform;
|
||||||
|
await platform.updateMasterSettings({
|
||||||
filterFeedByVisible: this.hasVisibleDid,
|
filterFeedByVisible: this.hasVisibleDid,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
||||||
filterFeedByVisible: this.hasVisibleDid,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleNearby() {
|
async toggleNearby() {
|
||||||
this.settingChanged = true;
|
this.settingChanged = true;
|
||||||
this.isNearby = !this.isNearby;
|
this.isNearby = !this.isNearby;
|
||||||
await databaseUtil.updateDefaultSettings({
|
const platform = this.$platform;
|
||||||
|
await platform.updateMasterSettings({
|
||||||
filterFeedByNearby: this.isNearby,
|
filterFeedByNearby: this.isNearby,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
||||||
filterFeedByNearby: this.isNearby,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearAll() {
|
async clearAll() {
|
||||||
@@ -172,18 +155,12 @@ export default class FeedFilters extends Vue {
|
|||||||
this.settingChanged = true;
|
this.settingChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await databaseUtil.updateDefaultSettings({
|
const platform = this.$platform;
|
||||||
|
await platform.updateMasterSettings({
|
||||||
filterFeedByNearby: false,
|
filterFeedByNearby: false,
|
||||||
filterFeedByVisible: false,
|
filterFeedByVisible: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
||||||
filterFeedByNearby: false,
|
|
||||||
filterFeedByVisible: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.hasVisibleDid = false;
|
this.hasVisibleDid = false;
|
||||||
this.isNearby = false;
|
this.isNearby = false;
|
||||||
}
|
}
|
||||||
@@ -193,18 +170,12 @@ export default class FeedFilters extends Vue {
|
|||||||
this.settingChanged = true;
|
this.settingChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await databaseUtil.updateDefaultSettings({
|
const platform = this.$platform;
|
||||||
|
await platform.updateMasterSettings({
|
||||||
filterFeedByNearby: true,
|
filterFeedByNearby: true,
|
||||||
filterFeedByVisible: true,
|
filterFeedByVisible: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
||||||
filterFeedByNearby: true,
|
|
||||||
filterFeedByVisible: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.hasVisibleDid = true;
|
this.hasVisibleDid = true;
|
||||||
this.isNearby = true;
|
this.isNearby = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,414 +0,0 @@
|
|||||||
/** * GiftDetailsStep.vue - Gift details step component * * Extracted from
|
|
||||||
GiftedDialog.vue to handle the complete step 2 * gift details form interface
|
|
||||||
with entity summaries and validation. * * @author Matthew Raymer */
|
|
||||||
<template>
|
|
||||||
<div id="sectionGiftedGift">
|
|
||||||
<!-- Entity Summary Buttons -->
|
|
||||||
<div class="grid grid-cols-2 gap-2 mb-4">
|
|
||||||
<!-- Giver Button -->
|
|
||||||
<EntitySummaryButton
|
|
||||||
:entity="giver"
|
|
||||||
:entity-type="giverEntityType"
|
|
||||||
:label="giverLabel"
|
|
||||||
:editable="canEditGiver"
|
|
||||||
@edit-requested="handleEditGiver"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Recipient Button -->
|
|
||||||
<EntitySummaryButton
|
|
||||||
:entity="receiver"
|
|
||||||
:entity-type="recipientEntityType"
|
|
||||||
:label="recipientLabel"
|
|
||||||
:editable="canEditRecipient"
|
|
||||||
@edit-requested="handleEditRecipient"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Gift Description Input -->
|
|
||||||
<input
|
|
||||||
v-model="localDescription"
|
|
||||||
type="text"
|
|
||||||
class="block w-full rounded border border-slate-400 px-3 py-2 mb-4 placeholder:italic"
|
|
||||||
:placeholder="prompt || 'What was given?'"
|
|
||||||
@input="handleDescriptionChange"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Amount Input and Unit Selection -->
|
|
||||||
<div class="flex mb-4">
|
|
||||||
<AmountInput
|
|
||||||
:value="localAmount"
|
|
||||||
:min="0"
|
|
||||||
input-id="inputGivenAmount"
|
|
||||||
@update:value="handleAmountChange"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<select
|
|
||||||
v-model="localUnitCode"
|
|
||||||
class="flex-1 rounded border border-slate-400 ms-2 px-3 py-2"
|
|
||||||
@change="handleUnitCodeChange"
|
|
||||||
>
|
|
||||||
<option value="HUR">Hours</option>
|
|
||||||
<option value="USD">US $</option>
|
|
||||||
<option value="BTC">BTC</option>
|
|
||||||
<option value="BX">BX</option>
|
|
||||||
<option value="ETH">ETH</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Photo & More Options Link -->
|
|
||||||
<router-link
|
|
||||||
:to="photoOptionsRoute"
|
|
||||||
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-1.5 py-2 rounded-lg mb-4"
|
|
||||||
>
|
|
||||||
Photo & more options…
|
|
||||||
</router-link>
|
|
||||||
|
|
||||||
<!-- Sign & Send Info -->
|
|
||||||
<p class="text-center text-sm mb-4">
|
|
||||||
<b class="font-medium">Sign & Send</b> to publish to the world
|
|
||||||
<font-awesome
|
|
||||||
icon="circle-info"
|
|
||||||
class="fa-fw text-blue-500 text-base cursor-pointer"
|
|
||||||
@click="handleExplainData"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Conflict Warning -->
|
|
||||||
<div
|
|
||||||
v-if="hasConflict"
|
|
||||||
class="mb-4 p-3 bg-red-50 border border-red-200 rounded-md"
|
|
||||||
>
|
|
||||||
<p class="text-red-700 text-sm text-center">
|
|
||||||
<font-awesome icon="exclamation-triangle" class="fa-fw mr-1" />
|
|
||||||
Cannot record: Same person selected as both giver and recipient
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
||||||
<button
|
|
||||||
:disabled="hasConflict"
|
|
||||||
:class="submitButtonClasses"
|
|
||||||
@click="handleSubmit"
|
|
||||||
>
|
|
||||||
Sign & Send
|
|
||||||
</button>
|
|
||||||
<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-1.5 py-2 rounded-lg"
|
|
||||||
@click="handleCancel"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue, Watch, Emit } from "vue-facing-decorator";
|
|
||||||
import EntitySummaryButton from "./EntitySummaryButton.vue";
|
|
||||||
import AmountInput from "./AmountInput.vue";
|
|
||||||
import { RouteLocationRaw } from "vue-router";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Entity data interface for giver/receiver
|
|
||||||
*/
|
|
||||||
interface EntityData {
|
|
||||||
did?: string;
|
|
||||||
handleId?: string;
|
|
||||||
name?: string;
|
|
||||||
image?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GiftDetailsStep - Complete step 2 gift details form interface
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Entity summary display with edit capability
|
|
||||||
* - Gift description input with placeholder support
|
|
||||||
* - Amount input with increment/decrement controls
|
|
||||||
* - Unit code selection (HUR, USD, BTC, etc.)
|
|
||||||
* - Photo & more options navigation
|
|
||||||
* - Conflict detection and warning display
|
|
||||||
* - Form validation and submission
|
|
||||||
* - Cancel functionality
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
EntitySummaryButton,
|
|
||||||
AmountInput,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class GiftDetailsStep extends Vue {
|
|
||||||
/** Giver entity data */
|
|
||||||
@Prop({ required: true })
|
|
||||||
giver!: EntityData | null;
|
|
||||||
|
|
||||||
/** Receiver entity data */
|
|
||||||
@Prop({ required: true })
|
|
||||||
receiver!: EntityData | null;
|
|
||||||
|
|
||||||
/** Type of giver entity: 'person' or 'project' */
|
|
||||||
@Prop({ required: true })
|
|
||||||
giverEntityType!: "person" | "project";
|
|
||||||
|
|
||||||
/** Type of recipient entity: 'person' or 'project' */
|
|
||||||
@Prop({ required: true })
|
|
||||||
recipientEntityType!: "person" | "project";
|
|
||||||
|
|
||||||
/** Gift description */
|
|
||||||
@Prop({ default: "" })
|
|
||||||
description!: string;
|
|
||||||
|
|
||||||
/** Gift amount */
|
|
||||||
@Prop({ default: 0 })
|
|
||||||
amount!: number;
|
|
||||||
|
|
||||||
/** Unit code (HUR, USD, etc.) */
|
|
||||||
@Prop({ default: "HUR" })
|
|
||||||
unitCode!: string;
|
|
||||||
|
|
||||||
/** Input placeholder text */
|
|
||||||
@Prop({ default: "" })
|
|
||||||
prompt!: string;
|
|
||||||
|
|
||||||
/** Whether this is from a project view */
|
|
||||||
@Prop({ default: false })
|
|
||||||
isFromProjectView!: boolean;
|
|
||||||
|
|
||||||
/** Whether there's a conflict between giver and receiver */
|
|
||||||
@Prop({ default: false })
|
|
||||||
hasConflict!: boolean;
|
|
||||||
|
|
||||||
/** Offer ID for context */
|
|
||||||
@Prop({ default: "" })
|
|
||||||
offerId!: string;
|
|
||||||
|
|
||||||
/** Project ID for context (giver) */
|
|
||||||
@Prop({ default: "" })
|
|
||||||
fromProjectId!: string;
|
|
||||||
|
|
||||||
/** Project ID for context (recipient) */
|
|
||||||
@Prop({ default: "" })
|
|
||||||
toProjectId!: string;
|
|
||||||
|
|
||||||
/** Local reactive copies of props for v-model */
|
|
||||||
private localDescription: string = "";
|
|
||||||
private localAmount: number = 0;
|
|
||||||
private localUnitCode: string = "HUR";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize local values from props
|
|
||||||
*/
|
|
||||||
mounted(): void {
|
|
||||||
this.localDescription = this.description;
|
|
||||||
this.localAmount = this.amount;
|
|
||||||
this.localUnitCode = this.unitCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Watch for external prop changes
|
|
||||||
*/
|
|
||||||
@Watch("description")
|
|
||||||
onDescriptionChange(newValue: string): void {
|
|
||||||
this.localDescription = newValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch("amount")
|
|
||||||
onAmountChange(newValue: number): void {
|
|
||||||
this.localAmount = newValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch("unitCode")
|
|
||||||
onUnitCodeChange(newValue: string): void {
|
|
||||||
this.localUnitCode = newValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed label for giver entity
|
|
||||||
*/
|
|
||||||
get giverLabel(): string {
|
|
||||||
return this.giverEntityType === "project"
|
|
||||||
? "Benefited from:"
|
|
||||||
: "Received from:";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed label for recipient entity
|
|
||||||
*/
|
|
||||||
get recipientLabel(): string {
|
|
||||||
return this.recipientEntityType === "project"
|
|
||||||
? "Given to project:"
|
|
||||||
: "Given to:";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the giver can be edited
|
|
||||||
*/
|
|
||||||
get canEditGiver(): boolean {
|
|
||||||
return !(this.isFromProjectView && this.giverEntityType === "project");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the recipient can be edited
|
|
||||||
*/
|
|
||||||
get canEditRecipient(): boolean {
|
|
||||||
return this.recipientEntityType === "person";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed CSS classes for submit button
|
|
||||||
*/
|
|
||||||
get submitButtonClasses(): string {
|
|
||||||
if (this.hasConflict) {
|
|
||||||
return "block w-full text-center text-md uppercase font-bold bg-gradient-to-b from-slate-300 to-slate-500 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-slate-400 px-1.5 py-2 rounded-lg cursor-not-allowed";
|
|
||||||
}
|
|
||||||
return "block w-full text-center text-md uppercase font-bold 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-lg";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed route for photo & more options
|
|
||||||
*/
|
|
||||||
get photoOptionsRoute(): RouteLocationRaw {
|
|
||||||
return {
|
|
||||||
name: "gifted-details",
|
|
||||||
query: {
|
|
||||||
amountInput: this.localAmount.toString(),
|
|
||||||
description: this.localDescription,
|
|
||||||
giverDid:
|
|
||||||
this.giverEntityType === "person" ? this.giver?.did : undefined,
|
|
||||||
giverName: this.giver?.name,
|
|
||||||
offerId: this.offerId,
|
|
||||||
fulfillsProjectId:
|
|
||||||
this.giverEntityType === "person" &&
|
|
||||||
this.recipientEntityType === "project"
|
|
||||||
? this.toProjectId
|
|
||||||
: undefined,
|
|
||||||
providerProjectId:
|
|
||||||
this.giverEntityType === "project" &&
|
|
||||||
this.recipientEntityType === "person"
|
|
||||||
? this.giver?.handleId
|
|
||||||
: this.fromProjectId,
|
|
||||||
recipientDid: this.receiver?.did,
|
|
||||||
recipientName: this.receiver?.name,
|
|
||||||
unitCode: this.localUnitCode,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle description input changes
|
|
||||||
*/
|
|
||||||
handleDescriptionChange(): void {
|
|
||||||
this.emitUpdateDescription(this.localDescription);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle amount input changes
|
|
||||||
*/
|
|
||||||
handleAmountChange(newAmount: number): void {
|
|
||||||
console.log(
|
|
||||||
`[GiftDetailsStep] handleAmountChange() called - oldAmount: ${this.localAmount}, newAmount: ${newAmount}`,
|
|
||||||
);
|
|
||||||
this.localAmount = newAmount;
|
|
||||||
this.emitUpdateAmount(newAmount);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle unit code selection changes
|
|
||||||
*/
|
|
||||||
handleUnitCodeChange(): void {
|
|
||||||
this.emitUpdateUnitCode(this.localUnitCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle giver edit request
|
|
||||||
*/
|
|
||||||
handleEditGiver(): void {
|
|
||||||
this.emitEditEntity({
|
|
||||||
entityType: "giver",
|
|
||||||
currentEntity: this.giver,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle recipient edit request
|
|
||||||
*/
|
|
||||||
handleEditRecipient(): void {
|
|
||||||
this.emitEditEntity({
|
|
||||||
entityType: "recipient",
|
|
||||||
currentEntity: this.receiver,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle explain data info click
|
|
||||||
*/
|
|
||||||
handleExplainData(): void {
|
|
||||||
this.emitExplainData();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle form submission
|
|
||||||
*/
|
|
||||||
handleSubmit(): void {
|
|
||||||
if (!this.hasConflict) {
|
|
||||||
this.emitSubmit({
|
|
||||||
description: this.localDescription,
|
|
||||||
amount: this.localAmount,
|
|
||||||
unitCode: this.localUnitCode,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle cancel button click
|
|
||||||
*/
|
|
||||||
handleCancel(): void {
|
|
||||||
this.emitCancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit methods using @Emit decorator
|
|
||||||
|
|
||||||
@Emit("update:description")
|
|
||||||
emitUpdateDescription(description: string): string {
|
|
||||||
return description;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Emit("update:amount")
|
|
||||||
emitUpdateAmount(amount: number): number {
|
|
||||||
console.log(
|
|
||||||
`[GiftDetailsStep] emitUpdateAmount() - emitting amount: ${amount}`,
|
|
||||||
);
|
|
||||||
return amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Emit("update:unitCode")
|
|
||||||
emitUpdateUnitCode(unitCode: string): string {
|
|
||||||
return unitCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Emit("edit-entity")
|
|
||||||
emitEditEntity(data: any): any {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Emit("explain-data")
|
|
||||||
emitExplainData(): void {
|
|
||||||
// No return value needed
|
|
||||||
}
|
|
||||||
|
|
||||||
@Emit("submit")
|
|
||||||
emitSubmit(data: any): any {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Emit("cancel")
|
|
||||||
emitCancel(): void {
|
|
||||||
// No return value needed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* Component-specific styles if needed */
|
|
||||||
</style>
|
|
||||||
@@ -1,108 +1,112 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="visible" class="dialog-overlay">
|
<div v-if="visible" class="dialog-overlay">
|
||||||
<div class="dialog">
|
<div class="dialog">
|
||||||
<!-- Step 1: Entity Selection -->
|
<h1 class="text-xl font-bold text-center mb-4">
|
||||||
<EntitySelectionStep
|
{{ customTitle }}
|
||||||
v-show="currentStep === 1"
|
</h1>
|
||||||
:step-type="stepType"
|
<input
|
||||||
:giver-entity-type="giverEntityType"
|
v-model="description"
|
||||||
:recipient-entity-type="recipientEntityType"
|
type="text"
|
||||||
:show-projects="showProjects"
|
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
||||||
:is-from-project-view="isFromProjectView"
|
:placeholder="prompt || 'What was given?'"
|
||||||
:projects="projects"
|
|
||||||
:all-contacts="allContacts"
|
|
||||||
:active-did="activeDid"
|
|
||||||
:all-my-dids="allMyDids"
|
|
||||||
:conflict-checker="wouldCreateConflict"
|
|
||||||
:from-project-id="fromProjectId"
|
|
||||||
:to-project-id="toProjectId"
|
|
||||||
:giver="giver"
|
|
||||||
:receiver="receiver"
|
|
||||||
@entity-selected="handleEntitySelected"
|
|
||||||
@cancel="cancel"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Step 2: Gift Details -->
|
|
||||||
<GiftDetailsStep
|
|
||||||
v-show="currentStep === 2"
|
|
||||||
:giver="giver"
|
|
||||||
:receiver="receiver"
|
|
||||||
:giver-entity-type="giverEntityType"
|
|
||||||
:recipient-entity-type="recipientEntityType"
|
|
||||||
:description="description"
|
|
||||||
:amount="parseFloat(amountInput) || 0"
|
|
||||||
:unit-code="unitCode"
|
|
||||||
:prompt="prompt"
|
|
||||||
:is-from-project-view="isFromProjectView"
|
|
||||||
:has-conflict="hasPersonConflict"
|
|
||||||
:offer-id="offerId"
|
|
||||||
:from-project-id="fromProjectId"
|
|
||||||
:to-project-id="toProjectId"
|
|
||||||
@update:description="description = $event"
|
|
||||||
@update:amount="handleAmountUpdate"
|
|
||||||
@update:unit-code="unitCode = $event"
|
|
||||||
@edit-entity="handleEditEntity"
|
|
||||||
@explain-data="explainData"
|
|
||||||
@submit="handleSubmit"
|
|
||||||
@cancel="cancel"
|
|
||||||
/>
|
/>
|
||||||
|
<div class="flex flex-row justify-center">
|
||||||
|
<span
|
||||||
|
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20"
|
||||||
|
@click="changeUnitCode()"
|
||||||
|
>
|
||||||
|
{{ libsUtil.UNIT_SHORT[unitCode] || unitCode }}
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
|
||||||
|
@click="amountInput === '0' ? null : decrement()"
|
||||||
|
>
|
||||||
|
<font-awesome icon="chevron-left" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="inputGivenAmount"
|
||||||
|
v-model="amountInput"
|
||||||
|
type="number"
|
||||||
|
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
|
||||||
|
@click="increment()"
|
||||||
|
>
|
||||||
|
<font-awesome icon="chevron-right" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex justify-center">
|
||||||
|
<span>
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: 'gifted-details',
|
||||||
|
query: {
|
||||||
|
amountInput,
|
||||||
|
description,
|
||||||
|
giverDid: giver?.did,
|
||||||
|
giverName: giver?.name,
|
||||||
|
offerId,
|
||||||
|
fulfillsProjectId: toProjectId,
|
||||||
|
providerProjectId: fromProjectId,
|
||||||
|
recipientDid: receiver?.did,
|
||||||
|
recipientName: receiver?.name,
|
||||||
|
unitCode,
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
class="text-blue-500"
|
||||||
|
>
|
||||||
|
Photo & more options ...
|
||||||
|
</router-link>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-center mb-2 mt-6 italic">
|
||||||
|
Sign & Send to publish to the world
|
||||||
|
<font-awesome
|
||||||
|
icon="circle-info"
|
||||||
|
class="pl-2 text-blue-500 cursor-pointer"
|
||||||
|
@click="explainData()"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
|
<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"
|
||||||
|
@click="confirm"
|
||||||
|
>
|
||||||
|
Sign & Send
|
||||||
|
</button>
|
||||||
|
<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-1.5 py-2 rounded-md"
|
||||||
|
@click="cancel"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Vue, Component, Prop, Watch } from "vue-facing-decorator";
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||||
|
|
||||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
import { NotificationIface } from "../constants/app";
|
||||||
import {
|
import {
|
||||||
createAndSubmitGive,
|
createAndSubmitGive,
|
||||||
didInfo,
|
didInfo,
|
||||||
serverMessageForUser,
|
serverMessageForUser,
|
||||||
getHeaders,
|
|
||||||
} from "../libs/endorserServer";
|
} from "../libs/endorserServer";
|
||||||
import * as libsUtil from "../libs/util";
|
import * as libsUtil from "../libs/util";
|
||||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
|
||||||
import { retrieveAccountDids } from "../libs/util";
|
import { retrieveAccountDids } from "../libs/util";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
|
||||||
import EntityIcon from "../components/EntityIcon.vue";
|
|
||||||
import ProjectIcon from "../components/ProjectIcon.vue";
|
|
||||||
import EntitySelectionStep from "../components/EntitySelectionStep.vue";
|
|
||||||
import GiftDetailsStep from "../components/GiftDetailsStep.vue";
|
|
||||||
import { PlanData } from "../interfaces/records";
|
|
||||||
|
|
||||||
@Component({
|
@Component
|
||||||
components: {
|
|
||||||
EntityIcon,
|
|
||||||
ProjectIcon,
|
|
||||||
EntitySelectionStep,
|
|
||||||
GiftDetailsStep,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class GiftedDialog extends Vue {
|
export default class GiftedDialog extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
@Prop() fromProjectId = "";
|
@Prop() fromProjectId = "";
|
||||||
@Prop() toProjectId = "";
|
@Prop() toProjectId = "";
|
||||||
@Prop({ default: false }) showProjects = false;
|
|
||||||
@Prop() isFromProjectView = false;
|
|
||||||
|
|
||||||
@Watch("showProjects")
|
|
||||||
onShowProjectsChange() {
|
|
||||||
this.updateEntityTypes();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch("fromProjectId")
|
|
||||||
onFromProjectIdChange() {
|
|
||||||
this.updateEntityTypes();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch("toProjectId")
|
|
||||||
onToProjectIdChange() {
|
|
||||||
this.updateEntityTypes();
|
|
||||||
}
|
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
allContacts: Array<Contact> = [];
|
allContacts: Array<Contact> = [];
|
||||||
@@ -119,94 +123,9 @@ export default class GiftedDialog extends Vue {
|
|||||||
receiver?: libsUtil.GiverReceiverInputInfo;
|
receiver?: libsUtil.GiverReceiverInputInfo;
|
||||||
unitCode = "HUR";
|
unitCode = "HUR";
|
||||||
visible = false;
|
visible = false;
|
||||||
currentStep = 1;
|
|
||||||
|
|
||||||
libsUtil = libsUtil;
|
libsUtil = libsUtil;
|
||||||
|
|
||||||
projects: PlanData[] = [];
|
|
||||||
|
|
||||||
didInfo = didInfo;
|
|
||||||
|
|
||||||
// Computed property to help debug template logic
|
|
||||||
get shouldShowProjects() {
|
|
||||||
const result =
|
|
||||||
(this.stepType === "giver" && this.giverEntityType === "project") ||
|
|
||||||
(this.stepType === "recipient" && this.recipientEntityType === "project");
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Computed property to check if current selection would create a conflict
|
|
||||||
get hasPersonConflict() {
|
|
||||||
// Only check for conflicts when both entities are persons
|
|
||||||
if (
|
|
||||||
this.giverEntityType !== "person" ||
|
|
||||||
this.recipientEntityType !== "person"
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if giver and recipient are the same person
|
|
||||||
if (
|
|
||||||
this.giver?.did &&
|
|
||||||
this.receiver?.did &&
|
|
||||||
this.giver.did === this.receiver.did
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Computed property to check if a contact would create a conflict when selected
|
|
||||||
wouldCreateConflict(contactDid: string) {
|
|
||||||
// Only check for conflicts when both entities are persons
|
|
||||||
if (
|
|
||||||
this.giverEntityType !== "person" ||
|
|
||||||
this.recipientEntityType !== "person"
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.stepType === "giver") {
|
|
||||||
// If selecting as giver, check if it conflicts with current recipient
|
|
||||||
return this.receiver?.did === contactDid;
|
|
||||||
} else if (this.stepType === "recipient") {
|
|
||||||
// If selecting as recipient, check if it conflicts with current giver
|
|
||||||
return this.giver?.did === contactDid;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
stepType = "giver";
|
|
||||||
giverEntityType = "person" as "person" | "project";
|
|
||||||
recipientEntityType = "person" as "person" | "project";
|
|
||||||
|
|
||||||
updateEntityTypes() {
|
|
||||||
// Reset and set entity types based on current context
|
|
||||||
this.giverEntityType = "person";
|
|
||||||
this.recipientEntityType = "person";
|
|
||||||
|
|
||||||
// Determine entity types based on current context
|
|
||||||
if (this.showProjects) {
|
|
||||||
// HomeView "Project" button or ProjectViewView "Given by This"
|
|
||||||
this.giverEntityType = "project";
|
|
||||||
this.recipientEntityType = "person";
|
|
||||||
} else if (this.fromProjectId) {
|
|
||||||
// ProjectViewView "Given by This" button (project is giver)
|
|
||||||
this.giverEntityType = "project";
|
|
||||||
this.recipientEntityType = "person";
|
|
||||||
} else if (this.toProjectId) {
|
|
||||||
// ProjectViewView "Given to This" button (project is recipient)
|
|
||||||
this.giverEntityType = "person";
|
|
||||||
this.recipientEntityType = "project";
|
|
||||||
} else {
|
|
||||||
// HomeView "Person" button
|
|
||||||
this.giverEntityType = "person";
|
|
||||||
this.recipientEntityType = "person";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async open(
|
async open(
|
||||||
giver?: libsUtil.GiverReceiverInputInfo,
|
giver?: libsUtil.GiverReceiverInputInfo,
|
||||||
receiver?: libsUtil.GiverReceiverInputInfo,
|
receiver?: libsUtil.GiverReceiverInputInfo,
|
||||||
@@ -219,33 +138,17 @@ export default class GiftedDialog extends Vue {
|
|||||||
this.giver = giver;
|
this.giver = giver;
|
||||||
this.prompt = prompt || "";
|
this.prompt = prompt || "";
|
||||||
this.receiver = receiver;
|
this.receiver = receiver;
|
||||||
|
// if we show "given to user" selection, default checkbox to true
|
||||||
this.amountInput = "0";
|
this.amountInput = "0";
|
||||||
this.callbackOnSuccess = callbackOnSuccess;
|
this.callbackOnSuccess = callbackOnSuccess;
|
||||||
this.offerId = offerId || "";
|
this.offerId = offerId || "";
|
||||||
this.currentStep = giver ? 2 : 1;
|
|
||||||
this.stepType = "giver";
|
|
||||||
|
|
||||||
// Update entity types based on current props
|
|
||||||
this.updateEntityTypes();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount();
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
settings = await retrieveSettingsForActiveAccount();
|
|
||||||
}
|
|
||||||
this.apiServer = settings.apiServer || "";
|
this.apiServer = settings.apiServer || "";
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
|
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
this.allContacts = await db.contacts.toArray();
|
||||||
const result = await platformService.dbQuery(`SELECT * FROM contacts`);
|
|
||||||
if (result) {
|
|
||||||
this.allContacts = databaseUtil.mapQueryResultToValues(
|
|
||||||
result,
|
|
||||||
) as unknown as Contact[];
|
|
||||||
}
|
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
this.allContacts = await db.contacts.toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.allMyDids = await retrieveAccountDids();
|
this.allMyDids = await retrieveAccountDids();
|
||||||
|
|
||||||
@@ -257,16 +160,7 @@ export default class GiftedDialog extends Vue {
|
|||||||
this.allContacts,
|
this.allContacts,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
if (
|
|
||||||
this.giverEntityType === "project" ||
|
|
||||||
this.recipientEntityType === "project"
|
|
||||||
) {
|
|
||||||
await this.loadProjects();
|
|
||||||
} else {
|
|
||||||
// Clear projects array when not needed
|
|
||||||
this.projects = [];
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
logger.error("Error retrieving settings from database:", err);
|
logger.error("Error retrieving settings from database:", err);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -316,7 +210,6 @@ export default class GiftedDialog extends Vue {
|
|||||||
this.amountInput = "0";
|
this.amountInput = "0";
|
||||||
this.prompt = "";
|
this.prompt = "";
|
||||||
this.unitCode = "HUR";
|
this.unitCode = "HUR";
|
||||||
this.currentStep = 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirm() {
|
async confirm() {
|
||||||
@@ -359,20 +252,6 @@ export default class GiftedDialog extends Vue {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for person conflict
|
|
||||||
if (this.hasPersonConflict) {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: "You cannot select the same person as both giver and recipient.",
|
|
||||||
},
|
|
||||||
3000,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.close();
|
this.close();
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
@@ -411,55 +290,26 @@ export default class GiftedDialog extends Vue {
|
|||||||
unitCode: string = "HUR",
|
unitCode: string = "HUR",
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
// Determine the correct parameters based on entity types
|
|
||||||
let fromDid: string | undefined;
|
|
||||||
let toDid: string | undefined;
|
|
||||||
let fulfillsProjectHandleId: string | undefined;
|
|
||||||
let providerPlanHandleId: string | undefined;
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.giverEntityType === "project" &&
|
|
||||||
this.recipientEntityType === "person"
|
|
||||||
) {
|
|
||||||
// Project-to-person gift
|
|
||||||
fromDid = undefined; // No person giver
|
|
||||||
toDid = recipientDid as string; // Person recipient
|
|
||||||
fulfillsProjectHandleId = undefined; // No project recipient
|
|
||||||
providerPlanHandleId = this.giver?.handleId; // Project giver
|
|
||||||
} else if (
|
|
||||||
this.giverEntityType === "person" &&
|
|
||||||
this.recipientEntityType === "project"
|
|
||||||
) {
|
|
||||||
// Person-to-project gift
|
|
||||||
fromDid = giverDid as string; // Person giver
|
|
||||||
toDid = undefined; // No person recipient
|
|
||||||
fulfillsProjectHandleId = this.toProjectId; // Project recipient
|
|
||||||
providerPlanHandleId = undefined; // No project giver
|
|
||||||
} else {
|
|
||||||
// Person-to-person gift
|
|
||||||
fromDid = giverDid as string;
|
|
||||||
toDid = recipientDid as string;
|
|
||||||
fulfillsProjectHandleId = undefined;
|
|
||||||
providerPlanHandleId = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await createAndSubmitGive(
|
const result = await createAndSubmitGive(
|
||||||
this.axios,
|
this.axios,
|
||||||
this.apiServer,
|
this.apiServer,
|
||||||
this.activeDid,
|
this.activeDid,
|
||||||
fromDid,
|
giverDid as string,
|
||||||
toDid,
|
recipientDid as string,
|
||||||
description,
|
description,
|
||||||
amount,
|
amount,
|
||||||
unitCode,
|
unitCode,
|
||||||
fulfillsProjectHandleId,
|
this.toProjectId,
|
||||||
this.offerId,
|
this.offerId,
|
||||||
false,
|
false,
|
||||||
undefined,
|
undefined,
|
||||||
providerPlanHandleId,
|
this.fromProjectId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!result.success) {
|
if (
|
||||||
|
result.type === "error" ||
|
||||||
|
this.isGiveCreationError(result.response)
|
||||||
|
) {
|
||||||
const errorMessage = this.getGiveCreationErrorMessage(result);
|
const errorMessage = this.getGiveCreationErrorMessage(result);
|
||||||
logger.error("Error with give creation result:", result);
|
logger.error("Error with give creation result:", result);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -506,6 +356,15 @@ export default class GiftedDialog extends Vue {
|
|||||||
|
|
||||||
// Helper functions for readability
|
// Helper functions for readability
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param result response "data" from the server
|
||||||
|
* @returns true if the result indicates an error
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
isGiveCreationError(result: any) {
|
||||||
|
return result.status !== 201 || result.data?.error;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
||||||
* @returns best guess at an error message
|
* @returns best guess at an error message
|
||||||
@@ -530,173 +389,6 @@ export default class GiftedDialog extends Vue {
|
|||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectGiver(contact?: Contact) {
|
|
||||||
if (contact) {
|
|
||||||
this.giver = {
|
|
||||||
did: contact.did,
|
|
||||||
name: contact.name || contact.did,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
this.giver = {
|
|
||||||
did: "",
|
|
||||||
name: "Unnamed",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
this.currentStep = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
goBackToStep1(step: string) {
|
|
||||||
this.stepType = step;
|
|
||||||
this.currentStep = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadProjects() {
|
|
||||||
try {
|
|
||||||
const response = await fetch(this.apiServer + "/api/v2/report/plans", {
|
|
||||||
method: "GET",
|
|
||||||
headers: await getHeaders(this.activeDid),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
throw new Error("Failed to load projects");
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = await response.json();
|
|
||||||
if (results.data) {
|
|
||||||
this.projects = results.data;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error loading projects:", error);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: "Failed to load projects",
|
|
||||||
},
|
|
||||||
3000,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectProject(project: PlanData) {
|
|
||||||
this.giver = {
|
|
||||||
did: project.handleId,
|
|
||||||
name: project.name,
|
|
||||||
image: project.image,
|
|
||||||
handleId: project.handleId,
|
|
||||||
};
|
|
||||||
this.receiver = {
|
|
||||||
did: this.activeDid,
|
|
||||||
name: "You",
|
|
||||||
};
|
|
||||||
this.currentStep = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectRecipient(contact?: Contact) {
|
|
||||||
if (contact) {
|
|
||||||
this.receiver = {
|
|
||||||
did: contact.did,
|
|
||||||
name: contact.name || contact.did,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
this.receiver = {
|
|
||||||
did: "",
|
|
||||||
name: "Unnamed",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
this.currentStep = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectRecipientProject(project: PlanData) {
|
|
||||||
this.receiver = {
|
|
||||||
did: project.handleId,
|
|
||||||
name: project.name,
|
|
||||||
image: project.image,
|
|
||||||
handleId: project.handleId,
|
|
||||||
};
|
|
||||||
this.currentStep = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Computed property for the query parameters
|
|
||||||
get giftedDetailsQuery() {
|
|
||||||
return {
|
|
||||||
amountInput: this.amountInput,
|
|
||||||
description: this.description,
|
|
||||||
giverDid: this.giverEntityType === "person" ? this.giver?.did : undefined,
|
|
||||||
giverName: this.giver?.name,
|
|
||||||
offerId: this.offerId,
|
|
||||||
fulfillsProjectId:
|
|
||||||
this.giverEntityType === "person" &&
|
|
||||||
this.recipientEntityType === "project"
|
|
||||||
? this.toProjectId
|
|
||||||
: undefined,
|
|
||||||
providerProjectId:
|
|
||||||
this.giverEntityType === "project" &&
|
|
||||||
this.recipientEntityType === "person"
|
|
||||||
? this.giver?.handleId
|
|
||||||
: this.fromProjectId,
|
|
||||||
recipientDid: this.receiver?.did,
|
|
||||||
recipientName: this.receiver?.name,
|
|
||||||
unitCode: this.unitCode,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// New event handlers for component integration
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle entity selection from EntitySelectionStep
|
|
||||||
* @param entity - The selected entity (person or project)
|
|
||||||
*/
|
|
||||||
handleEntitySelected(entity: {
|
|
||||||
type: "person" | "project";
|
|
||||||
data: Contact | PlanData;
|
|
||||||
}) {
|
|
||||||
if (entity.type === "person") {
|
|
||||||
const contact = entity.data as Contact;
|
|
||||||
if (this.stepType === "giver") {
|
|
||||||
this.selectGiver(contact);
|
|
||||||
} else {
|
|
||||||
this.selectRecipient(contact);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const project = entity.data as PlanData;
|
|
||||||
if (this.stepType === "giver") {
|
|
||||||
this.selectProject(project);
|
|
||||||
} else {
|
|
||||||
this.selectRecipientProject(project);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle edit entity request from GiftDetailsStep
|
|
||||||
* @param entityType - 'giver' or 'recipient'
|
|
||||||
*/
|
|
||||||
handleEditEntity(entityType: "giver" | "recipient") {
|
|
||||||
this.goBackToStep1(entityType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle form submission from GiftDetailsStep
|
|
||||||
*/
|
|
||||||
handleSubmit() {
|
|
||||||
this.confirm();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle amount update from GiftDetailsStep
|
|
||||||
*/
|
|
||||||
handleAmountUpdate(newAmount: number) {
|
|
||||||
console.log(
|
|
||||||
`[GiftedDialog] handleAmountUpdate() called - oldAmount: ${this.amountInput}, newAmount: ${newAmount}`,
|
|
||||||
);
|
|
||||||
this.amountInput = newAmount.toString();
|
|
||||||
console.log(
|
|
||||||
`[GiftedDialog] handleAmountUpdate() - amountInput updated to: ${this.amountInput}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -74,12 +74,10 @@
|
|||||||
import { Vue, Component } from "vue-facing-decorator";
|
import { Vue, Component } from "vue-facing-decorator";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
|
|
||||||
import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
import { AppString, NotificationIface } from "../constants/app";
|
||||||
import { db } from "../db/index";
|
import { db } from "../db/index";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
|
||||||
import { GiverReceiverInputInfo } from "../libs/util";
|
import { GiverReceiverInputInfo } from "../libs/util";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class GivenPrompts extends Vue {
|
export default class GivenPrompts extends Vue {
|
||||||
@@ -129,16 +127,8 @@ export default class GivenPrompts extends Vue {
|
|||||||
this.visible = true;
|
this.visible = true;
|
||||||
this.callbackOnFullGiftInfo = callbackOnFullGiftInfo;
|
this.callbackOnFullGiftInfo = callbackOnFullGiftInfo;
|
||||||
|
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
await db.open();
|
||||||
const result = await platformService.dbQuery(
|
this.numContacts = await db.contacts.count();
|
||||||
"SELECT COUNT(*) FROM contacts",
|
|
||||||
);
|
|
||||||
if (result) {
|
|
||||||
this.numContacts = result.values[0][0] as number;
|
|
||||||
}
|
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
this.numContacts = await db.contacts.count();
|
|
||||||
}
|
|
||||||
this.shownContactDbIndices = new Array<boolean>(this.numContacts); // all undefined to start
|
this.shownContactDbIndices = new Array<boolean>(this.numContacts); // all undefined to start
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +217,6 @@ export default class GivenPrompts extends Vue {
|
|||||||
|
|
||||||
let someContactDbIndex = Math.floor(Math.random() * this.numContacts);
|
let someContactDbIndex = Math.floor(Math.random() * this.numContacts);
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
// as long as the index has an entry, loop
|
// as long as the index has an entry, loop
|
||||||
while (
|
while (
|
||||||
this.shownContactDbIndices[someContactDbIndex] != null &&
|
this.shownContactDbIndices[someContactDbIndex] != null &&
|
||||||
@@ -240,21 +229,10 @@ export default class GivenPrompts extends Vue {
|
|||||||
this.nextIdeaPastContacts();
|
this.nextIdeaPastContacts();
|
||||||
} else {
|
} else {
|
||||||
// get the contact at that offset
|
// get the contact at that offset
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
await db.open();
|
||||||
const result = await platformService.dbQuery(
|
this.currentContact = await db.contacts
|
||||||
"SELECT * FROM contacts LIMIT 1 OFFSET ?",
|
.offset(someContactDbIndex)
|
||||||
[someContactDbIndex],
|
.first();
|
||||||
);
|
|
||||||
if (result) {
|
|
||||||
const mappedContacts = databaseUtil.mapQueryResultToValues(result);
|
|
||||||
this.currentContact = mappedContacts[0] as unknown as Contact;
|
|
||||||
}
|
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
await db.open();
|
|
||||||
this.currentContact = await db.contacts
|
|
||||||
.offset(someContactDbIndex)
|
|
||||||
.first();
|
|
||||||
}
|
|
||||||
this.shownContactDbIndices[someContactDbIndex] = true;
|
this.shownContactDbIndices[someContactDbIndex] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,11 @@
|
|||||||
<span>
|
<span>
|
||||||
{{ didInfo(visDid) }}
|
{{ didInfo(visDid) }}
|
||||||
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
|
||||||
<a :href="`/did/${visDid}`" class="text-blue-500">
|
<a
|
||||||
|
:href="`/did/${visDid}`"
|
||||||
|
target="_blank"
|
||||||
|
class="text-blue-500"
|
||||||
|
>
|
||||||
<font-awesome
|
<font-awesome
|
||||||
icon="arrow-up-right-from-square"
|
icon="arrow-up-right-from-square"
|
||||||
class="fa-fw"
|
class="fa-fw"
|
||||||
|
|||||||
@@ -4,9 +4,7 @@
|
|||||||
<div class="text-lg text-center font-bold relative">
|
<div class="text-lg text-center font-bold relative">
|
||||||
<h1 id="ViewHeading" class="text-center font-bold">
|
<h1 id="ViewHeading" class="text-center font-bold">
|
||||||
<span v-if="uploading">Uploading Image…</span>
|
<span v-if="uploading">Uploading Image…</span>
|
||||||
<span v-else-if="blob">{{
|
<span v-else-if="blob">Crop Image</span>
|
||||||
crop ? "Crop Image" : "Preview Image"
|
|
||||||
}}</span>
|
|
||||||
<span v-else-if="showCameraPreview">Upload Image</span>
|
<span v-else-if="showCameraPreview">Upload Image</span>
|
||||||
<span v-else>Add Photo</span>
|
<span v-else>Add Photo</span>
|
||||||
</h1>
|
</h1>
|
||||||
@@ -121,23 +119,12 @@
|
|||||||
playsinline
|
playsinline
|
||||||
muted
|
muted
|
||||||
></video>
|
></video>
|
||||||
<div
|
<button
|
||||||
class="absolute bottom-4 inset-x-0 flex items-center justify-center gap-4"
|
class="absolute bottom-4 left-1/2 -translate-x-1/2 bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
|
||||||
|
@click="capturePhoto"
|
||||||
>
|
>
|
||||||
<button
|
<font-awesome icon="camera" class="w-[1em]" />
|
||||||
class="bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
|
</button>
|
||||||
@click="capturePhoto"
|
|
||||||
>
|
|
||||||
<font-awesome icon="camera" class="w-[1em]" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="platformCapabilities.isMobile"
|
|
||||||
class="bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
|
|
||||||
@click="rotateCamera"
|
|
||||||
>
|
|
||||||
<font-awesome icon="rotate" class="w-[1em]" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -242,12 +229,12 @@
|
|||||||
<p class="mb-2">
|
<p class="mb-2">
|
||||||
Before you can upload a photo, a friend needs to register you.
|
Before you can upload a photo, a friend needs to register you.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<router-link
|
||||||
|
:to="{ name: 'contact-qr' }"
|
||||||
class="inline-block text-md 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-4 py-2 rounded-md"
|
class="inline-block text-md 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-4 py-2 rounded-md"
|
||||||
@click="handleQRCodeClick"
|
|
||||||
>
|
>
|
||||||
Share Your Info
|
Share Your Info
|
||||||
</button>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -260,17 +247,11 @@ import axios from "axios";
|
|||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
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 { Capacitor } from "@capacitor/core";
|
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
|
||||||
import {
|
|
||||||
DEFAULT_IMAGE_API_SERVER,
|
|
||||||
NotificationIface,
|
|
||||||
USE_DEXIE_DB,
|
|
||||||
} 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";
|
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
|
||||||
|
|
||||||
const inputImageFileNameRef = ref<Blob>();
|
const inputImageFileNameRef = ref<Blob>();
|
||||||
|
|
||||||
@@ -281,11 +262,6 @@ const inputImageFileNameRef = ref<Blob>();
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
defaultCameraMode: {
|
|
||||||
type: String,
|
|
||||||
default: "environment",
|
|
||||||
validator: (value: string) => ["environment", "user"].includes(value),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class ImageMethodDialog extends Vue {
|
export default class ImageMethodDialog extends Vue {
|
||||||
@@ -327,9 +303,6 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
/** Camera stream reference */
|
/** Camera stream reference */
|
||||||
private cameraStream: MediaStream | null = null;
|
private cameraStream: MediaStream | null = null;
|
||||||
|
|
||||||
/** Current camera facing mode */
|
|
||||||
private currentFacingMode: "environment" | "user" = "environment";
|
|
||||||
|
|
||||||
private platformService = PlatformServiceFactory.getInstance();
|
private platformService = PlatformServiceFactory.getInstance();
|
||||||
URL = window.URL || window.webkitURL;
|
URL = window.URL || window.webkitURL;
|
||||||
|
|
||||||
@@ -361,10 +334,7 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
*/
|
*/
|
||||||
async mounted() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount();
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
settings = await retrieveSettingsForActiveAccount();
|
|
||||||
}
|
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error("Error retrieving settings from database:", error);
|
logger.error("Error retrieving settings from database:", error);
|
||||||
@@ -391,16 +361,15 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
open(setImageFn: (arg: string) => void, claimType: string, crop?: boolean) {
|
open(setImageFn: (arg: string) => void, claimType: string, crop?: boolean) {
|
||||||
logger.debug("ImageMethodDialog.open called");
|
|
||||||
this.claimType = claimType;
|
this.claimType = claimType;
|
||||||
this.crop = !!crop;
|
this.crop = !!crop;
|
||||||
this.imageCallback = setImageFn;
|
this.imageCallback = setImageFn;
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
this.currentFacingMode = this.defaultCameraMode as "environment" | "user";
|
|
||||||
|
|
||||||
// Start camera preview immediately
|
// Start camera preview immediately if not on mobile
|
||||||
logger.debug("Starting camera preview from open()");
|
if (!this.platformCapabilities.isNativeApp) {
|
||||||
this.startCameraPreview();
|
this.startCameraPreview();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadImageFile(event: Event) {
|
async uploadImageFile(event: Event) {
|
||||||
@@ -469,24 +438,46 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
logger.debug("startCameraPreview called");
|
logger.debug("startCameraPreview called");
|
||||||
logger.debug("Current showCameraPreview state:", this.showCameraPreview);
|
logger.debug("Current showCameraPreview state:", this.showCameraPreview);
|
||||||
logger.debug("Platform capabilities:", this.platformCapabilities);
|
logger.debug("Platform capabilities:", this.platformCapabilities);
|
||||||
logger.debug("MediaDevices available:", !!navigator.mediaDevices);
|
|
||||||
logger.debug(
|
|
||||||
"getUserMedia available:",
|
|
||||||
!!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
|
|
||||||
);
|
|
||||||
|
|
||||||
|
if (this.platformCapabilities.isNativeApp) {
|
||||||
|
logger.debug("Using platform service for mobile device");
|
||||||
|
this.cameraState = "initializing";
|
||||||
|
this.cameraStateMessage = "Using platform camera service...";
|
||||||
|
try {
|
||||||
|
const result = await this.platformService.takePicture();
|
||||||
|
this.blob = result.blob;
|
||||||
|
this.fileName = result.fileName;
|
||||||
|
this.cameraState = "ready";
|
||||||
|
this.cameraStateMessage = "Photo captured successfully";
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error taking picture:", error);
|
||||||
|
this.cameraState = "error";
|
||||||
|
this.cameraStateMessage =
|
||||||
|
error instanceof Error ? error.message : "Failed to take picture";
|
||||||
|
this.error =
|
||||||
|
error instanceof Error ? error.message : "Failed to take picture";
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Failed to take picture. Please try again.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Starting camera preview for desktop browser");
|
||||||
try {
|
try {
|
||||||
this.cameraState = "initializing";
|
this.cameraState = "initializing";
|
||||||
this.cameraStateMessage = "Requesting camera access...";
|
this.cameraStateMessage = "Requesting camera access...";
|
||||||
this.showCameraPreview = true;
|
this.showCameraPreview = true;
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
|
|
||||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
||||||
throw new Error("Camera API not available in this browser");
|
|
||||||
}
|
|
||||||
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
video: { facingMode: this.currentFacingMode },
|
video: { facingMode: "environment" },
|
||||||
});
|
});
|
||||||
logger.debug("Camera access granted");
|
logger.debug("Camera access granted");
|
||||||
this.cameraStream = stream;
|
this.cameraStream = stream;
|
||||||
@@ -500,36 +491,25 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
videoElement.srcObject = stream;
|
videoElement.srcObject = stream;
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
videoElement.onloadedmetadata = () => {
|
videoElement.onloadedmetadata = () => {
|
||||||
videoElement
|
videoElement.play().then(() => {
|
||||||
.play()
|
resolve(true);
|
||||||
.then(() => {
|
});
|
||||||
logger.debug("Video element started playing");
|
|
||||||
resolve(true);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
logger.error("Error playing video:", error);
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
logger.error("Video element not found");
|
|
||||||
throw new Error("Video element not found");
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error starting camera preview:", error);
|
logger.error("Error starting camera preview:", error);
|
||||||
let errorMessage =
|
let errorMessage =
|
||||||
error instanceof Error ? error.message : "Failed to access camera";
|
error instanceof Error ? error.message : "Failed to access camera";
|
||||||
if (
|
if (
|
||||||
error instanceof Error &&
|
error.name === "NotReadableError" ||
|
||||||
(error.name === "NotReadableError" || error.name === "TrackStartError")
|
error.name === "TrackStartError"
|
||||||
) {
|
) {
|
||||||
errorMessage =
|
errorMessage =
|
||||||
"Camera is in use by another application. Please close any other apps or browser tabs using the camera and try again.";
|
"Camera is in use by another application. Please close any other apps or browser tabs using the camera and try again.";
|
||||||
} else if (
|
} else if (
|
||||||
error instanceof Error &&
|
error.name === "NotAllowedError" ||
|
||||||
(error.name === "NotAllowedError" ||
|
error.name === "PermissionDeniedError"
|
||||||
error.name === "PermissionDeniedError")
|
|
||||||
) {
|
) {
|
||||||
errorMessage =
|
errorMessage =
|
||||||
"Camera access was denied. Please allow camera access in your browser settings.";
|
"Camera access was denied. Please allow camera access in your browser settings.";
|
||||||
@@ -537,7 +517,6 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
this.cameraState = "error";
|
this.cameraState = "error";
|
||||||
this.cameraStateMessage = errorMessage;
|
this.cameraStateMessage = errorMessage;
|
||||||
this.error = errorMessage;
|
this.error = errorMessage;
|
||||||
this.showCameraPreview = false;
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -547,6 +526,7 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
|
this.showCameraPreview = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -598,21 +578,6 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async rotateCamera() {
|
|
||||||
// Toggle between front and back cameras
|
|
||||||
this.currentFacingMode =
|
|
||||||
this.currentFacingMode === "environment" ? "user" : "environment";
|
|
||||||
|
|
||||||
// Stop current stream
|
|
||||||
if (this.cameraStream) {
|
|
||||||
this.cameraStream.getTracks().forEach((track) => track.stop());
|
|
||||||
this.cameraStream = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start new stream with updated facing mode
|
|
||||||
await this.startCameraPreview();
|
|
||||||
}
|
|
||||||
|
|
||||||
private createBlobURL(blob: Blob): string {
|
private createBlobURL(blob: Blob): string {
|
||||||
return URL.createObjectURL(blob);
|
return URL.createObjectURL(blob);
|
||||||
}
|
}
|
||||||
@@ -647,7 +612,6 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
this.uploading = false;
|
this.uploading = false;
|
||||||
this.close();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
formData.append("image", this.blob, this.fileName || "photo.jpg");
|
formData.append("image", this.blob, this.fileName || "photo.jpg");
|
||||||
@@ -702,7 +666,6 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
);
|
);
|
||||||
this.uploading = false;
|
this.uploading = false;
|
||||||
this.blob = undefined;
|
this.blob = undefined;
|
||||||
this.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -710,14 +673,6 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
toggleDiagnostics() {
|
toggleDiagnostics() {
|
||||||
this.showDiagnostics = !this.showDiagnostics;
|
this.showDiagnostics = !this.showDiagnostics;
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleQRCodeClick() {
|
|
||||||
if (Capacitor.isNativePlatform()) {
|
|
||||||
this.$router.push({ name: "contact-qr-scan-full" });
|
|
||||||
} else {
|
|
||||||
this.$router.push({ name: "contact-qr" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -172,10 +172,8 @@ import {
|
|||||||
} from "../libs/endorserServer";
|
} from "../libs/endorserServer";
|
||||||
import { decryptMessage } from "../libs/crypto";
|
import { decryptMessage } from "../libs/crypto";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
|
||||||
import * as libsUtil from "../libs/util";
|
import * as libsUtil from "../libs/util";
|
||||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
import { NotificationIface } from "../constants/app";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
|
||||||
|
|
||||||
interface Member {
|
interface Member {
|
||||||
admitted: boolean;
|
admitted: boolean;
|
||||||
@@ -211,10 +209,7 @@ export default class MembersList extends Vue {
|
|||||||
contacts: Array<Contact> = [];
|
contacts: Array<Contact> = [];
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount();
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
settings = await retrieveSettingsForActiveAccount();
|
|
||||||
}
|
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
this.apiServer = settings.apiServer || "";
|
this.apiServer = settings.apiServer || "";
|
||||||
this.firstName = settings.firstName || "";
|
this.firstName = settings.firstName || "";
|
||||||
@@ -301,7 +296,7 @@ export default class MembersList extends Vue {
|
|||||||
this.decryptedMembers.length === 0 ||
|
this.decryptedMembers.length === 0 ||
|
||||||
this.decryptedMembers[0].member.memberId !== this.members[0].memberId
|
this.decryptedMembers[0].member.memberId !== this.members[0].memberId
|
||||||
) {
|
) {
|
||||||
return "Your password is not the same as the organizer. Retry or have them check their password.";
|
return "Your password is not the same as the organizer. Reload or have them check their password.";
|
||||||
} else {
|
} else {
|
||||||
// the first (organizer) member was decrypted OK
|
// the first (organizer) member was decrypted OK
|
||||||
return "";
|
return "";
|
||||||
@@ -342,7 +337,7 @@ export default class MembersList extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "info",
|
type: "info",
|
||||||
title: "Contact Exists",
|
title: "Contact Exists",
|
||||||
text: "They are in your contacts. To remove them, use the contacts page.",
|
text: "They are in your contacts. If you want to remove them, you must do that from the contacts screen.",
|
||||||
},
|
},
|
||||||
10000,
|
10000,
|
||||||
);
|
);
|
||||||
@@ -352,7 +347,7 @@ export default class MembersList extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "info",
|
type: "info",
|
||||||
title: "Contact Available",
|
title: "Contact Available",
|
||||||
text: "This is to add them to your contacts. To remove them later, use the contacts page.",
|
text: "This is to add them to your contacts. If you want to remove them later, you must do that from the contacts screen.",
|
||||||
},
|
},
|
||||||
10000,
|
10000,
|
||||||
);
|
);
|
||||||
@@ -360,16 +355,7 @@ export default class MembersList extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadContacts() {
|
async loadContacts() {
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
this.contacts = await db.contacts.toArray();
|
||||||
const result = await platformService.dbQuery("SELECT * FROM contacts");
|
|
||||||
if (result) {
|
|
||||||
this.contacts = databaseUtil.mapQueryResultToValues(
|
|
||||||
result,
|
|
||||||
) as unknown as Contact[];
|
|
||||||
}
|
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
this.contacts = await db.contacts.toArray();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getContactFor(did: string): Contact | undefined {
|
getContactFor(did: string): Contact | undefined {
|
||||||
@@ -453,14 +439,7 @@ export default class MembersList extends Vue {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
decrMember.isRegistered = true;
|
decrMember.isRegistered = true;
|
||||||
if (oldContact) {
|
if (oldContact) {
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
await db.contacts.update(decrMember.did, { registered: true });
|
||||||
await platformService.dbExec(
|
|
||||||
"UPDATE contacts SET registered = ? WHERE did = ?",
|
|
||||||
[true, decrMember.did],
|
|
||||||
);
|
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
await db.contacts.update(decrMember.did, { registered: true });
|
|
||||||
}
|
|
||||||
oldContact.registered = true;
|
oldContact.registered = true;
|
||||||
}
|
}
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -513,14 +492,7 @@ export default class MembersList extends Vue {
|
|||||||
name: member.name,
|
name: member.name,
|
||||||
};
|
};
|
||||||
|
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
await db.contacts.add(newContact);
|
||||||
await platformService.dbExec(
|
|
||||||
"INSERT INTO contacts (did, name) VALUES (?, ?)",
|
|
||||||
[member.did, member.name],
|
|
||||||
);
|
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
await db.contacts.add(newContact);
|
|
||||||
}
|
|
||||||
this.contacts.push(newContact);
|
this.contacts.push(newContact);
|
||||||
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
|
|||||||
@@ -82,13 +82,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||||
|
|
||||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
import { NotificationIface } from "../constants/app";
|
||||||
import {
|
import {
|
||||||
createAndSubmitOffer,
|
createAndSubmitOffer,
|
||||||
serverMessageForUser,
|
serverMessageForUser,
|
||||||
} from "../libs/endorserServer";
|
} from "../libs/endorserServer";
|
||||||
import * as libsUtil from "../libs/util";
|
import * as libsUtil from "../libs/util";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
|
||||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
@@ -117,10 +116,7 @@ export default class OfferDialog extends Vue {
|
|||||||
this.recipientDid = recipientDid;
|
this.recipientDid = recipientDid;
|
||||||
this.recipientName = recipientName;
|
this.recipientName = recipientName;
|
||||||
|
|
||||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount();
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
settings = await retrieveSettingsForActiveAccount();
|
|
||||||
}
|
|
||||||
this.apiServer = settings.apiServer || "";
|
this.apiServer = settings.apiServer || "";
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
|
|
||||||
@@ -249,7 +245,10 @@ export default class OfferDialog extends Vue {
|
|||||||
this.projectId,
|
this.projectId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!result.success) {
|
if (
|
||||||
|
result.type === "error" ||
|
||||||
|
this.isOfferCreationError(result.response)
|
||||||
|
) {
|
||||||
const errorMessage = this.getOfferCreationErrorMessage(result);
|
const errorMessage = this.getOfferCreationErrorMessage(result);
|
||||||
logger.error("Error with offer creation result:", result);
|
logger.error("Error with offer creation result:", result);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -293,6 +292,15 @@ export default class OfferDialog extends Vue {
|
|||||||
|
|
||||||
// Helper functions for readability
|
// Helper functions for readability
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param result response "data" from the server
|
||||||
|
* @returns true if the result indicates an error
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
isOfferCreationError(result: any) {
|
||||||
|
return result.status !== 201 || result.data?.error;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
||||||
* @returns best guess at an error message
|
* @returns best guess at an error message
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<h1 class="text-xl font-bold text-center mb-4 relative">
|
<h1 class="text-xl font-bold text-center mb-4 relative">
|
||||||
Welcome to Time Safari
|
Welcome to Time Safari
|
||||||
<br />
|
<br />
|
||||||
- Showcase Impact & Magnify Time
|
- Showcasing Gratitude & Magnifying Time
|
||||||
<div
|
<div
|
||||||
class="text-lg text-center leading-none absolute right-0 -top-1"
|
class="text-lg text-center leading-none absolute right-0 -top-1"
|
||||||
@click="onClickClose(true)"
|
@click="onClickClose(true)"
|
||||||
@@ -14,9 +14,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
The feed underneath this pop-up shows the latest contributions, some from
|
|
||||||
people and some from projects.
|
|
||||||
|
|
||||||
<p v-if="isRegistered" class="mt-4">
|
<p v-if="isRegistered" class="mt-4">
|
||||||
You can now log things that you've seen:
|
You can now log things that you've seen:
|
||||||
<span v-if="numContacts > 0">
|
<span v-if="numContacts > 0">
|
||||||
@@ -26,10 +23,14 @@
|
|||||||
<span class="bg-green-600 text-white rounded-full">
|
<span class="bg-green-600 text-white rounded-full">
|
||||||
<font-awesome icon="plus" class="fa-fw" />
|
<font-awesome icon="plus" class="fa-fw" />
|
||||||
</span>
|
</span>
|
||||||
button to express your appreciation for... whatever.
|
button to express your appreciation for... whatever -- maybe thanks for
|
||||||
|
showing you all these fascinating stories of
|
||||||
|
<em>gratitude</em>.
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-4">
|
<p v-else class="mt-4">
|
||||||
Once someone registers you, you can log your appreciation, too.
|
The feed underneath this pop-up shows the latest gifts that others have
|
||||||
|
recognized. Once someone registers you, you can log your appreciation,
|
||||||
|
too.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="mt-4">
|
<p class="mt-4">
|
||||||
@@ -200,16 +201,13 @@
|
|||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
|
|
||||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
import { NotificationIface } from "../constants/app";
|
||||||
import {
|
import {
|
||||||
db,
|
db,
|
||||||
retrieveSettingsForActiveAccount,
|
retrieveSettingsForActiveAccount,
|
||||||
updateAccountSettings,
|
updateAccountSettings,
|
||||||
} from "../db/index";
|
} from "../db/index";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
|
||||||
import { OnboardPage } from "../libs/util";
|
import { OnboardPage } from "../libs/util";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
|
||||||
import { Contact } from "@/db/tables/contacts";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
computed: {
|
computed: {
|
||||||
@@ -224,7 +222,7 @@ export default class OnboardingDialog extends Vue {
|
|||||||
$router!: Router;
|
$router!: Router;
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
firstContactName = "";
|
firstContactName = null;
|
||||||
givenName = "";
|
givenName = "";
|
||||||
isRegistered = false;
|
isRegistered = false;
|
||||||
numContacts = 0;
|
numContacts = 0;
|
||||||
@@ -233,54 +231,29 @@ export default class OnboardingDialog extends Vue {
|
|||||||
|
|
||||||
async open(page: OnboardPage) {
|
async open(page: OnboardPage) {
|
||||||
this.page = page;
|
this.page = page;
|
||||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount();
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
settings = await retrieveSettingsForActiveAccount();
|
|
||||||
}
|
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
this.isRegistered = !!settings.isRegistered;
|
this.isRegistered = !!settings.isRegistered;
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
const contacts = await db.contacts.toArray();
|
||||||
const dbContacts = await platformService.dbQuery("SELECT * FROM contacts");
|
this.numContacts = contacts.length;
|
||||||
if (dbContacts) {
|
if (this.numContacts > 0) {
|
||||||
this.numContacts = dbContacts.values.length;
|
this.firstContactName = contacts[0].name;
|
||||||
const firstContact = dbContacts.values[0];
|
|
||||||
const fullContact = databaseUtil.mapColumnsToValues(dbContacts.columns, [
|
|
||||||
firstContact,
|
|
||||||
]) as unknown as Contact;
|
|
||||||
this.firstContactName = fullContact.name || "";
|
|
||||||
}
|
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
const contacts = await db.contacts.toArray();
|
|
||||||
this.numContacts = contacts.length;
|
|
||||||
if (this.numContacts > 0) {
|
|
||||||
this.firstContactName = contacts[0].name || "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
if (this.page === OnboardPage.Create) {
|
if (this.page === OnboardPage.Create) {
|
||||||
// we'll assume that they've been through all the other pages
|
// we'll assume that they've been through all the other pages
|
||||||
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
await updateAccountSettings(this.activeDid, {
|
||||||
finishedOnboarding: true,
|
finishedOnboarding: true,
|
||||||
});
|
});
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
await updateAccountSettings(this.activeDid, {
|
|
||||||
finishedOnboarding: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickClose(done?: boolean, goHome?: boolean) {
|
async onClickClose(done?: boolean, goHome?: boolean) {
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
if (done) {
|
if (done) {
|
||||||
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
await updateAccountSettings(this.activeDid, {
|
||||||
finishedOnboarding: true,
|
finishedOnboarding: true,
|
||||||
});
|
});
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
await updateAccountSettings(this.activeDid, {
|
|
||||||
finishedOnboarding: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (goHome) {
|
if (goHome) {
|
||||||
this.$router.push({ name: "home" });
|
this.$router.push({ name: "home" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
/** * PersonCard.vue - Individual person display component * * Extracted from
|
|
||||||
GiftedDialog.vue to handle person entity display * with selection states and
|
|
||||||
conflict detection. * * @author Matthew Raymer */
|
|
||||||
<template>
|
|
||||||
<li :class="cardClasses" @click="handleClick">
|
|
||||||
<div class="relative w-fit mx-auto">
|
|
||||||
<EntityIcon
|
|
||||||
v-if="person.did"
|
|
||||||
:contact="person"
|
|
||||||
class="!size-[3rem] mx-auto border border-slate-300 bg-white overflow-hidden rounded-full mb-1"
|
|
||||||
/>
|
|
||||||
<font-awesome
|
|
||||||
v-else
|
|
||||||
icon="circle-question"
|
|
||||||
class="text-slate-400 text-5xl mb-1"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Time icon overlay for contacts -->
|
|
||||||
<div
|
|
||||||
v-if="person.did && showTimeIcon"
|
|
||||||
class="rounded-full bg-slate-400 absolute bottom-0 right-0 p-1 translate-x-1/3"
|
|
||||||
>
|
|
||||||
<font-awesome icon="clock" class="block text-white text-xs w-[1em]" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 :class="nameClasses">
|
|
||||||
{{ person.name || person.did || "Unnamed" }}
|
|
||||||
</h3>
|
|
||||||
</li>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
|
|
||||||
import EntityIcon from "./EntityIcon.vue";
|
|
||||||
import { Contact } from "../db/tables/contacts";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PersonCard - Individual person display with selection capability
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Person avatar using EntityIcon
|
|
||||||
* - Selection states (selectable, conflicted, disabled)
|
|
||||||
* - Time icon overlay for contacts
|
|
||||||
* - Click event handling
|
|
||||||
* - Emits click events for parent handling
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
EntityIcon,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class PersonCard extends Vue {
|
|
||||||
/** Contact data to display */
|
|
||||||
@Prop({ required: true })
|
|
||||||
person!: Contact;
|
|
||||||
|
|
||||||
/** Whether this person can be selected */
|
|
||||||
@Prop({ default: true })
|
|
||||||
selectable!: boolean;
|
|
||||||
|
|
||||||
/** Whether this person would create a conflict if selected */
|
|
||||||
@Prop({ default: false })
|
|
||||||
conflicted!: boolean;
|
|
||||||
|
|
||||||
/** Whether to show time icon overlay */
|
|
||||||
@Prop({ default: false })
|
|
||||||
showTimeIcon!: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed CSS classes for the card
|
|
||||||
*/
|
|
||||||
get cardClasses(): string {
|
|
||||||
if (!this.selectable || this.conflicted) {
|
|
||||||
return "opacity-50 cursor-not-allowed";
|
|
||||||
}
|
|
||||||
return "cursor-pointer hover:bg-slate-50";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed CSS classes for the person name
|
|
||||||
*/
|
|
||||||
get nameClasses(): string {
|
|
||||||
const baseClasses =
|
|
||||||
"text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden";
|
|
||||||
|
|
||||||
if (this.conflicted) {
|
|
||||||
return `${baseClasses} text-slate-400`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseClasses;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle card click - only emit if selectable and not conflicted
|
|
||||||
*/
|
|
||||||
handleClick(): void {
|
|
||||||
if (this.selectable && !this.conflicted) {
|
|
||||||
this.emitPersonSelected(this.person);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit methods using @Emit decorator
|
|
||||||
|
|
||||||
@Emit("person-selected")
|
|
||||||
emitPersonSelected(person: Contact): Contact {
|
|
||||||
return person;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* Component-specific styles if needed */
|
|
||||||
</style>
|
|
||||||
@@ -119,12 +119,7 @@ PhotoDialog.vue */
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
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 {
|
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
|
||||||
DEFAULT_IMAGE_API_SERVER,
|
|
||||||
NotificationIface,
|
|
||||||
USE_DEXIE_DB,
|
|
||||||
} from "../constants/app";
|
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
|
||||||
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";
|
||||||
@@ -178,12 +173,9 @@ export default class PhotoDialog extends Vue {
|
|||||||
* @throws {Error} When settings retrieval fails
|
* @throws {Error} When settings retrieval fails
|
||||||
*/
|
*/
|
||||||
async mounted() {
|
async mounted() {
|
||||||
// logger.log("PhotoDialog mounted");
|
logger.log("PhotoDialog mounted");
|
||||||
try {
|
try {
|
||||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount();
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
settings = await retrieveSettingsForActiveAccount();
|
|
||||||
}
|
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
this.isRegistered = !!settings.isRegistered;
|
this.isRegistered = !!settings.isRegistered;
|
||||||
logger.log("isRegistered:", this.isRegistered);
|
logger.log("isRegistered:", this.isRegistered);
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
/** * ProjectCard.vue - Individual project display component * * Extracted from
|
|
||||||
GiftedDialog.vue to handle project entity display * with selection states and
|
|
||||||
issuer information. * * @author Matthew Raymer */
|
|
||||||
<template>
|
|
||||||
<li class="cursor-pointer" @click="handleClick">
|
|
||||||
<div class="relative w-fit mx-auto">
|
|
||||||
<ProjectIcon
|
|
||||||
:entity-id="project.handleId"
|
|
||||||
:icon-size="48"
|
|
||||||
:image-url="project.image"
|
|
||||||
class="!size-[3rem] mx-auto border border-slate-300 bg-white overflow-hidden rounded-full mb-1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3
|
|
||||||
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
|
||||||
>
|
|
||||||
{{ project.name }}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="text-xs text-slate-500 truncate">
|
|
||||||
<font-awesome icon="user" class="fa-fw text-slate-400" />
|
|
||||||
{{ issuerDisplayName }}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
|
|
||||||
import ProjectIcon from "./ProjectIcon.vue";
|
|
||||||
import { PlanData } from "../interfaces/records";
|
|
||||||
import { Contact } from "../db/tables/contacts";
|
|
||||||
import { didInfo } from "../libs/endorserServer";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ProjectCard - Displays a project entity with selection capability
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Shows project icon using ProjectIcon
|
|
||||||
* - Displays project name and issuer information
|
|
||||||
* - Handles click events for selection
|
|
||||||
* - Shows issuer name using didInfo utility
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
ProjectIcon,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class ProjectCard extends Vue {
|
|
||||||
/** Project entity to display */
|
|
||||||
@Prop({ required: true })
|
|
||||||
project!: PlanData;
|
|
||||||
|
|
||||||
/** Active user's DID for issuer display */
|
|
||||||
@Prop({ required: true })
|
|
||||||
activeDid!: string;
|
|
||||||
|
|
||||||
/** All user's DIDs for issuer display */
|
|
||||||
@Prop({ required: true })
|
|
||||||
allMyDids!: string[];
|
|
||||||
|
|
||||||
/** All contacts for issuer display */
|
|
||||||
@Prop({ required: true })
|
|
||||||
allContacts!: Contact[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed display name for the project issuer
|
|
||||||
*/
|
|
||||||
get issuerDisplayName(): string {
|
|
||||||
return didInfo(
|
|
||||||
this.project.issuerDid,
|
|
||||||
this.activeDid,
|
|
||||||
this.allMyDids,
|
|
||||||
this.allContacts,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle card click - emit project selection
|
|
||||||
*/
|
|
||||||
handleClick(): void {
|
|
||||||
this.emitProjectSelected(this.project);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit methods using @Emit decorator
|
|
||||||
|
|
||||||
@Emit("project-selected")
|
|
||||||
emitProjectSelected(project: PlanData): PlanData {
|
|
||||||
return project;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* Component-specific styles if needed */
|
|
||||||
</style>
|
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
<template>
|
<template>
|
||||||
<a
|
<a
|
||||||
v-if="linkToFullImage && imageUrl"
|
v-if="linkToFull && imageUrl"
|
||||||
:href="imageUrl"
|
:href="imageUrl"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="h-full w-full object-contain"
|
class="h-full w-full object-contain"
|
||||||
>
|
>
|
||||||
<div class="h-full w-full object-contain" v-html="generateIcon()" />
|
<div class="h-full w-full object-contain" v-html="generateIdenticon()" />
|
||||||
</a>
|
</a>
|
||||||
<div v-else class="h-full w-full object-contain" v-html="generateIcon()" />
|
<div
|
||||||
|
v-else
|
||||||
|
class="h-full w-full object-contain"
|
||||||
|
v-html="generateIdenticon()"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { toSvg } from "jdenticon";
|
import { toSvg } from "jdenticon";
|
||||||
@@ -31,9 +35,9 @@ export default class ProjectIcon extends Vue {
|
|||||||
@Prop entityId = "";
|
@Prop entityId = "";
|
||||||
@Prop iconSize = 0;
|
@Prop iconSize = 0;
|
||||||
@Prop imageUrl = "";
|
@Prop imageUrl = "";
|
||||||
@Prop linkToFullImage = false;
|
@Prop linkToFull = false;
|
||||||
|
|
||||||
generateIcon() {
|
generateIdenticon() {
|
||||||
if (this.imageUrl) {
|
if (this.imageUrl) {
|
||||||
return `<img src="${this.imageUrl}" class="w-full h-full object-contain" />`;
|
return `<img src="${this.imageUrl}" class="w-full h-full object-contain" />`;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -102,12 +102,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
import {
|
import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
|
||||||
DEFAULT_PUSH_SERVER,
|
|
||||||
NotificationIface,
|
|
||||||
USE_DEXIE_DB,
|
|
||||||
} from "../constants/app";
|
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
|
||||||
import {
|
import {
|
||||||
logConsoleAndDb,
|
logConsoleAndDb,
|
||||||
retrieveSettingsForActiveAccount,
|
retrieveSettingsForActiveAccount,
|
||||||
@@ -174,10 +169,7 @@ export default class PushNotificationPermission extends Vue {
|
|||||||
this.isVisible = true;
|
this.isVisible = true;
|
||||||
this.pushType = pushType;
|
this.pushType = pushType;
|
||||||
try {
|
try {
|
||||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount();
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
settings = await retrieveSettingsForActiveAccount();
|
|
||||||
}
|
|
||||||
let pushUrl = DEFAULT_PUSH_SERVER;
|
let pushUrl = DEFAULT_PUSH_SERVER;
|
||||||
if (settings?.webPushServer) {
|
if (settings?.webPushServer) {
|
||||||
pushUrl = settings.webPushServer;
|
pushUrl = settings.webPushServer;
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
/** * ShowAllCard.vue - Show All navigation card component * * Extracted from
|
|
||||||
GiftedDialog.vue to handle "Show All" navigation * for both people and projects
|
|
||||||
entity types. * * @author Matthew Raymer */
|
|
||||||
<template>
|
|
||||||
<li class="cursor-pointer">
|
|
||||||
<router-link :to="navigationRoute" class="block text-center">
|
|
||||||
<font-awesome icon="circle-right" class="text-blue-500 text-5xl mb-1" />
|
|
||||||
<h3
|
|
||||||
class="text-xs text-slate-500 font-medium italic text-ellipsis whitespace-nowrap overflow-hidden"
|
|
||||||
>
|
|
||||||
Show All
|
|
||||||
</h3>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue } from "vue-facing-decorator";
|
|
||||||
import { RouteLocationRaw } from "vue-router";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ShowAllCard - Displays "Show All" navigation for entity grids
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Provides navigation to full entity listings
|
|
||||||
* - Supports different routes based on entity type
|
|
||||||
* - Maintains context through query parameters
|
|
||||||
* - Consistent visual styling with other cards
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
export default class ShowAllCard extends Vue {
|
|
||||||
/** Type of entities being shown */
|
|
||||||
@Prop({ required: true })
|
|
||||||
entityType!: "people" | "projects";
|
|
||||||
|
|
||||||
/** Route name to navigate to */
|
|
||||||
@Prop({ required: true })
|
|
||||||
routeName!: string;
|
|
||||||
|
|
||||||
/** Query parameters to pass to the route */
|
|
||||||
@Prop({ default: () => ({}) })
|
|
||||||
queryParams!: Record<string, any>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed navigation route with query parameters
|
|
||||||
*/
|
|
||||||
get navigationRoute(): RouteLocationRaw {
|
|
||||||
return {
|
|
||||||
name: this.routeName,
|
|
||||||
query: this.queryParams,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* Ensure router-link styling is consistent */
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover .fa-circle-right {
|
|
||||||
transform: scale(1.1);
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
/** * SpecialEntityCard.vue - Special entity display component * * Extracted
|
|
||||||
from GiftedDialog.vue to handle special entities like "You" * and "Unnamed" with
|
|
||||||
conflict detection and selection capability. * * @author Matthew Raymer */
|
|
||||||
<template>
|
|
||||||
<li :class="cardClasses" @click="handleClick">
|
|
||||||
<font-awesome :icon="icon" :class="iconClasses" />
|
|
||||||
<h3 :class="nameClasses">
|
|
||||||
{{ label }}
|
|
||||||
</h3>
|
|
||||||
</li>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue } from "vue-facing-decorator";
|
|
||||||
import { Emit } from "vue-facing-decorator";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SpecialEntityCard - Displays special entities with selection capability
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Displays special entities like "You" and "Unnamed"
|
|
||||||
* - Shows appropriate FontAwesome icons
|
|
||||||
* - Handles conflict states and selection
|
|
||||||
* - Emits selection events with entity data
|
|
||||||
* - Configurable styling based on entity type
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
emits: ["entity-selected"],
|
|
||||||
})
|
|
||||||
export default class SpecialEntityCard extends Vue {
|
|
||||||
/** Type of special entity */
|
|
||||||
@Prop({ required: true })
|
|
||||||
entityType!: "you" | "unnamed";
|
|
||||||
|
|
||||||
/** Display label for the entity */
|
|
||||||
@Prop({ required: true })
|
|
||||||
label!: string;
|
|
||||||
|
|
||||||
/** FontAwesome icon name */
|
|
||||||
@Prop({ required: true })
|
|
||||||
icon!: string;
|
|
||||||
|
|
||||||
/** Whether this entity can be selected */
|
|
||||||
@Prop({ default: true })
|
|
||||||
selectable!: boolean;
|
|
||||||
|
|
||||||
/** Whether selecting this entity would create a conflict */
|
|
||||||
@Prop({ default: false })
|
|
||||||
conflicted!: boolean;
|
|
||||||
|
|
||||||
/** Entity data to emit when selected */
|
|
||||||
@Prop({ required: true })
|
|
||||||
entityData!: { did?: string; name: string };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed CSS classes for the card container
|
|
||||||
*/
|
|
||||||
get cardClasses(): string {
|
|
||||||
const baseClasses = "block";
|
|
||||||
|
|
||||||
if (!this.selectable || this.conflicted) {
|
|
||||||
return `${baseClasses} cursor-not-allowed opacity-50`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${baseClasses} cursor-pointer`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed CSS classes for the icon
|
|
||||||
*/
|
|
||||||
get iconClasses(): string {
|
|
||||||
const baseClasses = "text-5xl mb-1";
|
|
||||||
|
|
||||||
if (this.conflicted) {
|
|
||||||
return `${baseClasses} text-slate-400`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Different colors for different entity types
|
|
||||||
switch (this.entityType) {
|
|
||||||
case "you":
|
|
||||||
return `${baseClasses} text-blue-500`;
|
|
||||||
case "unnamed":
|
|
||||||
return `${baseClasses} text-slate-400`;
|
|
||||||
default:
|
|
||||||
return `${baseClasses} text-slate-400`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed CSS classes for the entity name/label
|
|
||||||
*/
|
|
||||||
get nameClasses(): string {
|
|
||||||
const baseClasses =
|
|
||||||
"text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden";
|
|
||||||
|
|
||||||
if (this.conflicted) {
|
|
||||||
return `${baseClasses} text-slate-400`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Different colors for different entity types
|
|
||||||
switch (this.entityType) {
|
|
||||||
case "you":
|
|
||||||
return `${baseClasses} text-blue-500`;
|
|
||||||
case "unnamed":
|
|
||||||
return `${baseClasses} text-slate-500 italic`;
|
|
||||||
default:
|
|
||||||
return `${baseClasses} text-slate-500`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle card click - only emit if selectable and not conflicted
|
|
||||||
*/
|
|
||||||
handleClick(): void {
|
|
||||||
if (this.selectable && !this.conflicted) {
|
|
||||||
this.emitEntitySelected({
|
|
||||||
type: "special",
|
|
||||||
entityType: this.entityType,
|
|
||||||
data: this.entityData,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit methods using @Emit decorator
|
|
||||||
|
|
||||||
@Emit("entity-selected")
|
|
||||||
emitEntitySelected(data: any): any {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* Component-specific styles if needed */
|
|
||||||
</style>
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="absolute right-5 top-[max(0.75rem,env(safe-area-inset-top))]">
|
<div class="absolute right-5 top-[calc(env(safe-area-inset-top)+0.75rem)]">
|
||||||
<span class="align-center text-red-500 mr-2">{{ message }}</span>
|
<span class="align-center text-red-500 mr-2">{{ message }}</span>
|
||||||
<span class="ml-2">
|
<span class="ml-2">
|
||||||
<router-link
|
<router-link
|
||||||
@@ -15,8 +15,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Prop } from "vue-facing-decorator";
|
import { Component, Vue, Prop } from "vue-facing-decorator";
|
||||||
|
|
||||||
import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
import { AppString, NotificationIface } from "../constants/app";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
|
||||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -29,10 +28,7 @@ export default class TopMessage extends Vue {
|
|||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount();
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
settings = await retrieveSettingsForActiveAccount();
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
settings.warnIfTestServer &&
|
settings.warnIfTestServer &&
|
||||||
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
|
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
|
||||||
|
|||||||