Compare commits
66 Commits
capacitor-
...
sql-absurd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
981920dd7a | ||
|
|
d189c39062 | ||
|
|
8edddb1a57 | ||
|
|
9eb07b3258 | ||
|
|
e5dffc30ff | ||
|
|
0b4e885edd | ||
|
|
b6d9b29720 | ||
|
|
b5348e42a7 | ||
|
|
a4fb3eea2d | ||
|
|
5d12c76693 | ||
|
|
d426f9c4ac | ||
| 340a574325 | |||
|
|
98b3a35e3c | ||
|
|
409de21fc4 | ||
|
|
17c9d32f49 | ||
|
|
25e4db395a | ||
|
|
b6ee30892f | ||
|
|
b01a450733 | ||
|
|
596f3355bf | ||
|
|
e1f9a6fa08 | ||
|
|
340e718199 | ||
|
|
5d97c98ae8 | ||
|
|
ec74fff892 | ||
|
|
1e88c0e26f | ||
|
|
3ec2364394 | ||
|
|
8b215c909d | ||
|
|
91a1c05473 | ||
|
|
66929d9b14 | ||
|
|
1e63ddcb6e | ||
|
|
51f5755f5c | ||
|
|
e5a3d622b6 | ||
|
|
a6edcd6269 | ||
|
|
b7b6be5831 | ||
|
|
cbaca0304d | ||
| 59d711bd90 | |||
|
|
c355de6e33 | ||
|
|
28c114a2c7 | ||
| dabfe33fbe | |||
| d8f2587d1c | |||
|
|
3946a8a27a | ||
| 4c40b80718 | |||
| 74989c2b64 | |||
| 7e17b41444 | |||
| 83acb028c7 | |||
|
|
786f07e067 | ||
|
|
710cc1683c | ||
|
|
ebef5d6c8d | ||
|
|
43ea7ee610 | ||
|
|
57191df416 | ||
| 644593a5f4 | |||
|
|
900c2521c7 | ||
|
|
182cff2b16 | ||
|
|
3b4ef908f3 | ||
|
|
a5a9e15ece | ||
|
|
a6d8f0eb8a | ||
|
|
3997a88b44 | ||
| 5eeeae32c6 | |||
|
|
d9895086e6 | ||
|
|
fb8d1cb8b2 | ||
|
|
70c0edbed0 | ||
|
|
55cc08d675 | ||
|
|
688a5be76e | ||
|
|
014341f320 | ||
|
|
1d5e062c76 | ||
|
|
2c5c15108a | ||
|
|
26df0fb671 |
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# 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
|
||||||
|
|||||||
6
.gitignore
vendored
@@ -21,7 +21,6 @@ npm-debug.log*
|
|||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
android/app/src/main/res/
|
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.idea
|
.idea
|
||||||
@@ -52,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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
69
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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,533 +0,0 @@
|
|||||||
# TimeSafari Contact Backup System
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The TimeSafari application implements a comprehensive contact backup and listing system that works across multiple platforms (Web, iOS, Android, Desktop). This document breaks down how contacts are saved, exported, and listed as backups.
|
|
||||||
|
|
||||||
## Architecture Components
|
|
||||||
|
|
||||||
### 1. Database Layer
|
|
||||||
|
|
||||||
#### Contact Data Structure
|
|
||||||
```typescript
|
|
||||||
interface Contact {
|
|
||||||
did: string; // Decentralized Identifier (primary key)
|
|
||||||
contactMethods?: ContactMethod[]; // Array of contact methods (EMAIL, SMS, etc.)
|
|
||||||
name?: string; // Display name
|
|
||||||
nextPubKeyHashB64?: string; // Base64 hash of next public key
|
|
||||||
notes?: string; // User notes
|
|
||||||
profileImageUrl?: string; // Profile image URL
|
|
||||||
publicKeyBase64?: string; // Base64 encoded public key
|
|
||||||
seesMe?: boolean; // Visibility setting
|
|
||||||
registered?: boolean; // Registration status
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContactMethod {
|
|
||||||
label: string; // Display label
|
|
||||||
type: string; // Type (EMAIL, SMS, WHATSAPP, etc.)
|
|
||||||
value: string; // Contact value
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Database Schema
|
|
||||||
```sql
|
|
||||||
CREATE TABLE contacts (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
did TEXT NOT NULL, -- Decentralized Identifier
|
|
||||||
name TEXT, -- Display name
|
|
||||||
contactMethods TEXT, -- JSON string of contact methods
|
|
||||||
nextPubKeyHashB64 TEXT, -- Next public key hash
|
|
||||||
notes TEXT, -- User notes
|
|
||||||
profileImageUrl TEXT, -- Profile image URL
|
|
||||||
publicKeyBase64 TEXT, -- Public key
|
|
||||||
seesMe BOOLEAN, -- Visibility flag
|
|
||||||
registered BOOLEAN -- Registration status
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_contacts_did ON contacts(did);
|
|
||||||
CREATE INDEX idx_contacts_name ON contacts(name);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Contact Saving Operations
|
|
||||||
|
|
||||||
#### A. Adding New Contacts
|
|
||||||
|
|
||||||
**1. QR Code Scanning (`ContactQRScanFullView.vue`)**
|
|
||||||
```typescript
|
|
||||||
async addNewContact(contact: Contact) {
|
|
||||||
// Check for existing contact
|
|
||||||
const existingContacts = await platformService.dbQuery(
|
|
||||||
"SELECT * FROM contacts WHERE did = ?", [contact.did]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingContact) {
|
|
||||||
// Handle duplicate
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert contactMethods to JSON string for storage
|
|
||||||
contact.contactMethods = JSON.stringify(
|
|
||||||
parseJsonField(contact.contactMethods, [])
|
|
||||||
);
|
|
||||||
|
|
||||||
// Insert into database
|
|
||||||
const { sql, params } = databaseUtil.generateInsertStatement(
|
|
||||||
contact as unknown as Record<string, unknown>, "contacts"
|
|
||||||
);
|
|
||||||
await platformService.dbExec(sql, params);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**2. Manual Contact Addition (`ContactsView.vue`)**
|
|
||||||
```typescript
|
|
||||||
private async addContact(newContact: Contact) {
|
|
||||||
// Validate DID format
|
|
||||||
if (!isDid(newContact.did)) {
|
|
||||||
throw new Error("Invalid DID format");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate and execute INSERT statement
|
|
||||||
const { sql, params } = databaseUtil.generateInsertStatement(
|
|
||||||
newContact as unknown as Record<string, unknown>, "contacts"
|
|
||||||
);
|
|
||||||
await platformService.dbExec(sql, params);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**3. Contact Import (`ContactImportView.vue`)**
|
|
||||||
```typescript
|
|
||||||
async importContacts() {
|
|
||||||
for (const contact of selectedContacts) {
|
|
||||||
const contactToStore = contactToDbRecord(contact);
|
|
||||||
|
|
||||||
if (existingContact) {
|
|
||||||
// Update existing contact
|
|
||||||
const { sql, params } = databaseUtil.generateUpdateStatement(
|
|
||||||
contactToStore, "contacts", "did = ?", [contact.did]
|
|
||||||
);
|
|
||||||
await platformService.dbExec(sql, params);
|
|
||||||
} else {
|
|
||||||
// Add new contact
|
|
||||||
const { sql, params } = databaseUtil.generateInsertStatement(
|
|
||||||
contactToStore, "contacts"
|
|
||||||
);
|
|
||||||
await platformService.dbExec(sql, params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B. Updating Existing Contacts
|
|
||||||
|
|
||||||
**Contact Editing (`ContactEditView.vue`)**
|
|
||||||
```typescript
|
|
||||||
async saveEdit() {
|
|
||||||
// Normalize contact methods
|
|
||||||
const contactMethods = this.contactMethods.map(method => ({
|
|
||||||
...method,
|
|
||||||
type: method.type.toUpperCase()
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Update database
|
|
||||||
const contactMethodsString = JSON.stringify(contactMethods);
|
|
||||||
await platformService.dbExec(
|
|
||||||
"UPDATE contacts SET name = ?, notes = ?, contactMethods = ? WHERE did = ?",
|
|
||||||
[this.contactName, this.contactNotes, contactMethodsString, this.contact?.did]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Contact Export/Backup System
|
|
||||||
|
|
||||||
#### A. Export Process (`DataExportSection.vue`)
|
|
||||||
|
|
||||||
#### 1. Data Retrieval
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
async exportDatabase() {
|
|
||||||
// Query all contacts from database
|
|
||||||
const result = await platformService.dbQuery("SELECT * FROM contacts");
|
|
||||||
const allContacts = databaseUtil.mapQueryResultToValues(result) as Contact[];
|
|
||||||
|
|
||||||
// Convert to export format
|
|
||||||
const exportData = contactsToExportJson(allContacts);
|
|
||||||
const jsonStr = JSON.stringify(exportData, null, 2);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Export Format Conversion (`libs/util.ts`)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
|
|
||||||
const rows = contacts.map((contact) => ({
|
|
||||||
did: contact.did,
|
|
||||||
name: contact.name || null,
|
|
||||||
contactMethods: contact.contactMethods
|
|
||||||
? JSON.stringify(parseJsonField(contact.contactMethods, []))
|
|
||||||
: null,
|
|
||||||
nextPubKeyHashB64: contact.nextPubKeyHashB64 || null,
|
|
||||||
notes: contact.notes || null,
|
|
||||||
profileImageUrl: contact.profileImageUrl || null,
|
|
||||||
publicKeyBase64: contact.publicKeyBase64 || null,
|
|
||||||
seesMe: contact.seesMe || false,
|
|
||||||
registered: contact.registered || false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: {
|
|
||||||
data: [{ tableName: "contacts", rows }]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. File Generation
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Create timestamped filename
|
|
||||||
const timestamp = getTimestampForFilename();
|
|
||||||
const fileName = `${AppString.APP_NAME_NO_SPACES}-backup-contacts-${timestamp}.json`;
|
|
||||||
|
|
||||||
// Create blob and save
|
|
||||||
const blob = new Blob([jsonStr], { type: "application/json" });
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B. Platform-Specific File Saving
|
|
||||||
|
|
||||||
##### 1. Web Platform (`WebPlatformService.ts`)**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Uses browser download API
|
|
||||||
const downloadUrl = URL.createObjectURL(blob);
|
|
||||||
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
|
|
||||||
downloadAnchor.href = downloadUrl;
|
|
||||||
downloadAnchor.download = fileName;
|
|
||||||
downloadAnchor.click();
|
|
||||||
```
|
|
||||||
|
|
||||||
##### 2. Mobile Platforms (`CapacitorPlatformService.ts`)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
async writeAndShareFile(fileName: string, content: string, options = {}) {
|
|
||||||
let fileUri: string;
|
|
||||||
|
|
||||||
if (options.allowLocationSelection) {
|
|
||||||
// User chooses location
|
|
||||||
fileUri = await this.saveWithUserChoice(fileName, content, options.mimeType);
|
|
||||||
} else if (options.saveToPrivateStorage) {
|
|
||||||
// Save to app-private storage
|
|
||||||
const result = await Filesystem.writeFile({
|
|
||||||
path: fileName,
|
|
||||||
data: content,
|
|
||||||
directory: Directory.Data,
|
|
||||||
encoding: Encoding.UTF8,
|
|
||||||
recursive: true,
|
|
||||||
});
|
|
||||||
fileUri = result.uri;
|
|
||||||
} else {
|
|
||||||
// Save to user-accessible location (Downloads/Documents)
|
|
||||||
fileUri = await this.saveToDownloads(fileName, content);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Share the file
|
|
||||||
return await this.shareFile(fileUri, fileName);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
##### 3. Desktop Platforms (`ElectronPlatformService.ts`, `PyWebViewPlatformService.ts`)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Not implemented - returns empty results
|
|
||||||
async listBackupFiles(): Promise<Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}>> {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Backup File Listing System
|
|
||||||
|
|
||||||
#### A. File Discovery (`CapacitorPlatformService.ts`)
|
|
||||||
|
|
||||||
##### 1. Enhanced File Discovery
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
async listUserAccessibleFilesEnhanced(): Promise<Array<{name: string, uri: string, size?: number, path?: string}>> {
|
|
||||||
const allFiles: Array<{name: string, uri: string, size?: number, path?: string}> = [];
|
|
||||||
|
|
||||||
if (this.getCapabilities().isIOS) {
|
|
||||||
// iOS: Documents directory
|
|
||||||
const result = await Filesystem.readdir({
|
|
||||||
path: ".",
|
|
||||||
directory: Directory.Documents,
|
|
||||||
});
|
|
||||||
const files = result.files.map((file) => ({
|
|
||||||
name: typeof file === "string" ? file : file.name,
|
|
||||||
uri: `file://${file.uri || file}`,
|
|
||||||
size: typeof file === "string" ? undefined : file.size,
|
|
||||||
path: "Documents"
|
|
||||||
}));
|
|
||||||
allFiles.push(...files);
|
|
||||||
} else {
|
|
||||||
// Android: Multiple locations
|
|
||||||
const commonPaths = ["Download", "Documents", "Backups", "TimeSafari", "Data"];
|
|
||||||
|
|
||||||
for (const path of commonPaths) {
|
|
||||||
try {
|
|
||||||
const result = await Filesystem.readdir({
|
|
||||||
path: path,
|
|
||||||
directory: Directory.ExternalStorage,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filter for TimeSafari-related files
|
|
||||||
const relevantFiles = result.files
|
|
||||||
.filter(file => {
|
|
||||||
const fileName = typeof file === "string" ? file : file.name;
|
|
||||||
const name = fileName.toLowerCase();
|
|
||||||
return name.includes('timesafari') ||
|
|
||||||
name.includes('backup') ||
|
|
||||||
name.includes('contacts') ||
|
|
||||||
name.endsWith('.json');
|
|
||||||
})
|
|
||||||
.map((file) => ({
|
|
||||||
name: typeof file === "string" ? file : file.name,
|
|
||||||
uri: `file://${file.uri || file}`,
|
|
||||||
size: typeof file === "string" ? undefined : file.size,
|
|
||||||
path: path
|
|
||||||
}));
|
|
||||||
|
|
||||||
allFiles.push(...relevantFiles);
|
|
||||||
} catch (error) {
|
|
||||||
// Silently skip inaccessible directories
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return allFiles;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**2. Backup File Filtering**
|
|
||||||
```typescript
|
|
||||||
async listBackupFiles(): Promise<Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}>> {
|
|
||||||
const allFiles = await this.listUserAccessibleFilesEnhanced();
|
|
||||||
|
|
||||||
const backupFiles = allFiles
|
|
||||||
.filter(file => {
|
|
||||||
const name = file.name.toLowerCase();
|
|
||||||
|
|
||||||
// Exclude directory-access notification files
|
|
||||||
if (name.startsWith('timesafari-directory-access-') && name.endsWith('.txt')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check backup criteria
|
|
||||||
const isJson = name.endsWith('.json');
|
|
||||||
const hasTimeSafari = name.includes('timesafari');
|
|
||||||
const hasBackup = name.includes('backup');
|
|
||||||
const hasContacts = name.includes('contacts');
|
|
||||||
const hasSeed = name.includes('seed');
|
|
||||||
const hasExport = name.includes('export');
|
|
||||||
const hasData = name.includes('data');
|
|
||||||
|
|
||||||
return isJson || hasTimeSafari || hasBackup || hasContacts || hasSeed || hasExport || hasData;
|
|
||||||
})
|
|
||||||
.map(file => {
|
|
||||||
const name = file.name.toLowerCase();
|
|
||||||
let type: 'contacts' | 'seed' | 'other' = 'other';
|
|
||||||
|
|
||||||
// Categorize files
|
|
||||||
if (name.includes('contacts') || (name.includes('timesafari') && name.includes('backup'))) {
|
|
||||||
type = 'contacts';
|
|
||||||
} else if (name.includes('seed') || name.includes('mnemonic') || name.includes('private')) {
|
|
||||||
type = 'seed';
|
|
||||||
} else if (name.endsWith('.json')) {
|
|
||||||
type = 'other';
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...file, type };
|
|
||||||
});
|
|
||||||
|
|
||||||
return backupFiles;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B. UI Components (`BackupFilesList.vue`)
|
|
||||||
|
|
||||||
**1. File Display**
|
|
||||||
```typescript
|
|
||||||
@Component
|
|
||||||
export default class BackupFilesList extends Vue {
|
|
||||||
backupFiles: Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}> = [];
|
|
||||||
selectedType: 'all' | 'contacts' | 'seed' | 'other' = 'all';
|
|
||||||
isLoading = false;
|
|
||||||
|
|
||||||
async refreshFiles() {
|
|
||||||
this.isLoading = true;
|
|
||||||
try {
|
|
||||||
this.backupFiles = await this.platformService.listBackupFiles();
|
|
||||||
|
|
||||||
// Log file type distribution
|
|
||||||
const typeCounts = {
|
|
||||||
contacts: this.backupFiles.filter(f => f.type === 'contacts').length,
|
|
||||||
seed: this.backupFiles.filter(f => f.type === 'seed').length,
|
|
||||||
other: this.backupFiles.filter(f => f.type === 'other').length,
|
|
||||||
total: this.backupFiles.length
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
// Handle error
|
|
||||||
} finally {
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**2. File Operations**
|
|
||||||
```typescript
|
|
||||||
async openFile(fileUri: string, fileName: string) {
|
|
||||||
const result = await this.platformService.openFile(fileUri, fileName);
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error || "Failed to open file");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async openBackupDirectory() {
|
|
||||||
const result = await this.platformService.openBackupDirectory();
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error || "Failed to open backup directory");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Platform-Specific Storage Locations
|
|
||||||
|
|
||||||
#### A. iOS Platform
|
|
||||||
- **Primary Location**: Documents folder (accessible via Files app)
|
|
||||||
- **Persistence**: Survives app installations
|
|
||||||
- **Access**: Through iOS Files app
|
|
||||||
- **File Format**: JSON with timestamped filenames
|
|
||||||
|
|
||||||
#### B. Android Platform
|
|
||||||
- **Primary Locations**:
|
|
||||||
- `Download/TimeSafari/` (external storage)
|
|
||||||
- `TimeSafari/` (external storage)
|
|
||||||
- User-chosen locations via file picker
|
|
||||||
- **Persistence**: Survives app installations
|
|
||||||
- **Access**: Through file managers
|
|
||||||
- **File Format**: JSON with timestamped filenames
|
|
||||||
|
|
||||||
#### C. Web Platform
|
|
||||||
- **Primary Location**: Browser downloads folder
|
|
||||||
- **Persistence**: Depends on browser settings
|
|
||||||
- **Access**: Through browser download manager
|
|
||||||
- **File Format**: JSON with timestamped filenames
|
|
||||||
|
|
||||||
#### D. Desktop Platforms (Electron/PyWebView)
|
|
||||||
- **Status**: Not implemented
|
|
||||||
- **Fallback**: Returns empty arrays for file operations
|
|
||||||
|
|
||||||
### 6. File Naming Convention
|
|
||||||
|
|
||||||
#### A. Contact Backup Files
|
|
||||||
```
|
|
||||||
TimeSafari-backup-contacts-YYYY-MM-DD-HH-MM-SS.json
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B. File Content Structure
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"tableName": "contacts",
|
|
||||||
"rows": [
|
|
||||||
{
|
|
||||||
"did": "did:ethr:0x...",
|
|
||||||
"name": "Contact Name",
|
|
||||||
"contactMethods": "[{\"type\":\"EMAIL\",\"value\":\"email@example.com\"}]",
|
|
||||||
"notes": "User notes",
|
|
||||||
"profileImageUrl": "https://...",
|
|
||||||
"publicKeyBase64": "base64...",
|
|
||||||
"seesMe": true,
|
|
||||||
"registered": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. Error Handling and Logging
|
|
||||||
|
|
||||||
#### A. Comprehensive Logging
|
|
||||||
```typescript
|
|
||||||
logger.log("[CapacitorPlatformService] File write successful:", {
|
|
||||||
uri: fileUri,
|
|
||||||
saved,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.log("[BackupFilesList] Refreshed backup files:", {
|
|
||||||
count: this.backupFiles.length,
|
|
||||||
files: this.backupFiles.map(f => ({
|
|
||||||
name: f.name,
|
|
||||||
type: f.type,
|
|
||||||
path: f.path,
|
|
||||||
size: f.size
|
|
||||||
})),
|
|
||||||
platform: this.platformCapabilities.isIOS ? "iOS" : "Android",
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B. Error Recovery
|
|
||||||
```typescript
|
|
||||||
try {
|
|
||||||
// File operations
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("[CapacitorPlatformService] Failed to list backup files:", error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8. Security Considerations
|
|
||||||
|
|
||||||
#### A. Data Privacy
|
|
||||||
- Contact data is stored locally on device
|
|
||||||
- No cloud synchronization of contact data
|
|
||||||
- User controls visibility settings per contact
|
|
||||||
- Backup files contain only user-authorized data
|
|
||||||
|
|
||||||
#### B. File Access
|
|
||||||
- Platform-specific permission handling
|
|
||||||
- User choice for file locations
|
|
||||||
- Secure storage options for sensitive data
|
|
||||||
- Proper error handling for access failures
|
|
||||||
|
|
||||||
### 9. Performance Optimizations
|
|
||||||
|
|
||||||
#### A. Database Operations
|
|
||||||
- Indexed queries on `did` and `name` fields
|
|
||||||
- Batch operations for multiple contacts
|
|
||||||
- Efficient JSON serialization/deserialization
|
|
||||||
- Connection pooling and reuse
|
|
||||||
|
|
||||||
#### B. File Operations
|
|
||||||
- Asynchronous file I/O
|
|
||||||
- Efficient file discovery algorithms
|
|
||||||
- Caching of file lists
|
|
||||||
- Background refresh operations
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
The TimeSafari contact backup system provides:
|
|
||||||
|
|
||||||
1. **Robust Data Storage**: SQLite-based contact storage with proper indexing
|
|
||||||
2. **Cross-Platform Compatibility**: Works on web, iOS, Android, and desktop
|
|
||||||
3. **Flexible Export Options**: Multiple file formats and storage locations
|
|
||||||
4. **Intelligent File Discovery**: Finds backup files regardless of user-chosen locations
|
|
||||||
5. **User-Friendly Interface**: Clear categorization and easy file management
|
|
||||||
6. **Comprehensive Logging**: Detailed tracking for debugging and monitoring
|
|
||||||
7. **Security-First Design**: Privacy-preserving with user-controlled data access
|
|
||||||
|
|
||||||
The system ensures that users can reliably backup and restore their contact data across different platforms while maintaining data integrity and user privacy.
|
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ios": {
|
"ios": {
|
||||||
"contentInset": "never",
|
"contentInset": "always",
|
||||||
"allowsLinkPreview": true,
|
"allowsLinkPreview": true,
|
||||||
"scrollEnabled": true,
|
"scrollEnabled": true,
|
||||||
"limitsNavigationsToAppBoundDomains": true,
|
"limitsNavigationsToAppBoundDomains": true,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package app.timesafari;
|
|||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import com.getcapacitor.BridgeActivity;
|
import com.getcapacitor.BridgeActivity;
|
||||||
//import com.getcapacitor.community.sqlite.SQLite;
|
import com.getcapacitor.community.sqlite.SQLite;
|
||||||
|
|
||||||
public class MainActivity extends BridgeActivity {
|
public class MainActivity extends BridgeActivity {
|
||||||
@Override
|
@Override
|
||||||
@@ -10,6 +10,6 @@ public class MainActivity extends BridgeActivity {
|
|||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
// Initialize SQLite
|
// Initialize SQLite
|
||||||
//registerPlugin(SQLite.class);
|
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 |
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 |
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"appId": "app.timesafari",
|
"appId": "com.timesafari.app",
|
||||||
"appName": "TimeSafari",
|
"appName": "TimeSafari",
|
||||||
"webDir": "dist",
|
"webDir": "dist",
|
||||||
"bundledWebRuntime": false,
|
"bundledWebRuntime": false,
|
||||||
"server": {
|
"server": {
|
||||||
"cleartext": true
|
"cleartext": true,
|
||||||
|
"androidScheme": "https"
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"App": {
|
"App": {
|
||||||
@@ -29,10 +30,16 @@
|
|||||||
"biometricAuth": true,
|
"biometricAuth": true,
|
||||||
"biometricTitle": "Biometric login for TimeSafari"
|
"biometricTitle": "Biometric login for TimeSafari"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"CapacitorSQLite": {
|
||||||
|
"electronIsEncryption": false,
|
||||||
|
"electronMacLocation": "~/Library/Application Support/TimeSafari",
|
||||||
|
"electronWindowsLocation": "C:\\ProgramData\\TimeSafari",
|
||||||
|
"electronLinuxLocation": "~/.local/share/TimeSafari"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ios": {
|
"ios": {
|
||||||
"contentInset": "never",
|
"contentInset": "always",
|
||||||
"allowsLinkPreview": true,
|
"allowsLinkPreview": true,
|
||||||
"scrollEnabled": true,
|
"scrollEnabled": true,
|
||||||
"limitsNavigationsToAppBoundDomains": true,
|
"limitsNavigationsToAppBoundDomains": true,
|
||||||
|
|||||||
270
doc/electron-migration.md
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
# Electron App Migration Strategy
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document outlines the migration strategy for the TimeSafari Electron app, focusing on the transition from web-based storage to native SQLite implementation while maintaining cross-platform compatibility.
|
||||||
|
|
||||||
|
## Current Architecture
|
||||||
|
|
||||||
|
### 1. Platform Services
|
||||||
|
- `ElectronPlatformService`: Implements platform-specific features for desktop
|
||||||
|
- Uses `@capacitor-community/sqlite` for database operations
|
||||||
|
- Maintains compatibility with web/mobile platforms through shared interfaces
|
||||||
|
|
||||||
|
### 2. Database Implementation
|
||||||
|
- SQLite with native Node.js backend
|
||||||
|
- WAL journal mode for better concurrency
|
||||||
|
- Connection pooling for performance
|
||||||
|
- Migration system for schema updates
|
||||||
|
- Secure file permissions (0o755)
|
||||||
|
|
||||||
|
### 3. Build Process
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
npm run dev:electron
|
||||||
|
|
||||||
|
# Production Build
|
||||||
|
npm run build:web
|
||||||
|
npm run build:electron
|
||||||
|
npm run electron:build-linux # or electron:build-mac
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Goals
|
||||||
|
|
||||||
|
1. **Data Integrity**
|
||||||
|
- Preserve existing data during migration
|
||||||
|
- Maintain data relationships
|
||||||
|
- Ensure ACID compliance
|
||||||
|
- Implement proper backup/restore
|
||||||
|
|
||||||
|
2. **Performance**
|
||||||
|
- Optimize SQLite configuration
|
||||||
|
- Implement connection pooling
|
||||||
|
- Use WAL journal mode
|
||||||
|
- Configure optimal PRAGMA settings
|
||||||
|
|
||||||
|
3. **Security**
|
||||||
|
- Secure file permissions
|
||||||
|
- Proper IPC communication
|
||||||
|
- Context isolation
|
||||||
|
- Safe preload scripts
|
||||||
|
|
||||||
|
4. **User Experience**
|
||||||
|
- Zero data loss
|
||||||
|
- Automatic migration
|
||||||
|
- Progress indicators
|
||||||
|
- Error recovery
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### 1. Database Initialization
|
||||||
|
```typescript
|
||||||
|
// electron/src/rt/sqlite-init.ts
|
||||||
|
export async function initializeSQLite() {
|
||||||
|
// Set up database path with proper permissions
|
||||||
|
const dbPath = path.join(app.getPath('userData'), 'timesafari.db');
|
||||||
|
|
||||||
|
// Initialize SQLite plugin
|
||||||
|
const sqlite = new CapacitorSQLite();
|
||||||
|
|
||||||
|
// Configure database
|
||||||
|
await sqlite.createConnection({
|
||||||
|
database: 'timesafari',
|
||||||
|
path: dbPath,
|
||||||
|
encrypted: false,
|
||||||
|
mode: 'no-encryption'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set optimal PRAGMA settings
|
||||||
|
await sqlite.execute({
|
||||||
|
database: 'timesafari',
|
||||||
|
statements: [
|
||||||
|
'PRAGMA journal_mode = WAL;',
|
||||||
|
'PRAGMA synchronous = NORMAL;',
|
||||||
|
'PRAGMA foreign_keys = ON;'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Migration System
|
||||||
|
```typescript
|
||||||
|
// electron/src/rt/sqlite-migrations.ts
|
||||||
|
interface Migration {
|
||||||
|
version: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
sql: string;
|
||||||
|
rollback?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runMigrations(plugin: any, database: string) {
|
||||||
|
// Track migration state
|
||||||
|
const state = await getMigrationState(plugin, database);
|
||||||
|
|
||||||
|
// Execute migrations in transaction
|
||||||
|
for (const migration of pendingMigrations) {
|
||||||
|
await executeMigration(plugin, database, migration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Platform Service Implementation
|
||||||
|
```typescript
|
||||||
|
// src/services/platforms/ElectronPlatformService.ts
|
||||||
|
export class ElectronPlatformService implements PlatformService {
|
||||||
|
private sqlite: any;
|
||||||
|
|
||||||
|
async dbQuery(sql: string, params: any[]): Promise<QueryExecResult> {
|
||||||
|
return await this.sqlite.execute({
|
||||||
|
database: 'timesafari',
|
||||||
|
statements: [{ statement: sql, values: params }]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Preload Script
|
||||||
|
```typescript
|
||||||
|
// electron/preload.ts
|
||||||
|
contextBridge.exposeInMainWorld('electron', {
|
||||||
|
sqlite: {
|
||||||
|
isAvailable: () => ipcRenderer.invoke('sqlite:isAvailable'),
|
||||||
|
execute: (method: string, ...args: unknown[]) =>
|
||||||
|
ipcRenderer.invoke('sqlite:execute', method, ...args)
|
||||||
|
},
|
||||||
|
getPath: (pathType: string) => ipcRenderer.invoke('get-path', pathType),
|
||||||
|
env: {
|
||||||
|
platform: 'electron'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build Configuration
|
||||||
|
|
||||||
|
### 1. Vite Configuration
|
||||||
|
```typescript
|
||||||
|
// vite.config.app.electron.mts
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
'process.env.VITE_PLATFORM': JSON.stringify('electron'),
|
||||||
|
'process.env.VITE_PWA_ENABLED': JSON.stringify(false)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Package Scripts
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev:electron": "vite build --watch --config vite.config.app.electron.mts",
|
||||||
|
"build:electron": "vite build --config vite.config.app.electron.mts",
|
||||||
|
"electron:build-linux": "electron-builder --linux",
|
||||||
|
"electron:build-mac": "electron-builder --mac"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
1. **Unit Tests**
|
||||||
|
- Database operations
|
||||||
|
- Migration system
|
||||||
|
- Platform service methods
|
||||||
|
- IPC communication
|
||||||
|
|
||||||
|
2. **Integration Tests**
|
||||||
|
- Full migration process
|
||||||
|
- Data integrity verification
|
||||||
|
- Cross-platform compatibility
|
||||||
|
- Error recovery
|
||||||
|
|
||||||
|
3. **End-to-End Tests**
|
||||||
|
- User workflows
|
||||||
|
- Data persistence
|
||||||
|
- UI interactions
|
||||||
|
- Platform-specific features
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
1. **Database Errors**
|
||||||
|
- Connection failures
|
||||||
|
- Migration errors
|
||||||
|
- Query execution errors
|
||||||
|
- Transaction failures
|
||||||
|
|
||||||
|
2. **Platform Errors**
|
||||||
|
- File system errors
|
||||||
|
- IPC communication errors
|
||||||
|
- Permission issues
|
||||||
|
- Resource constraints
|
||||||
|
|
||||||
|
3. **Recovery Mechanisms**
|
||||||
|
- Automatic retry logic
|
||||||
|
- Transaction rollback
|
||||||
|
- State verification
|
||||||
|
- User notifications
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **File System**
|
||||||
|
- Secure file permissions
|
||||||
|
- Path validation
|
||||||
|
- Access control
|
||||||
|
- Data encryption
|
||||||
|
|
||||||
|
2. **IPC Communication**
|
||||||
|
- Context isolation
|
||||||
|
- Channel validation
|
||||||
|
- Data sanitization
|
||||||
|
- Error handling
|
||||||
|
|
||||||
|
3. **Preload Scripts**
|
||||||
|
- Minimal API exposure
|
||||||
|
- Type safety
|
||||||
|
- Input validation
|
||||||
|
- Error boundaries
|
||||||
|
|
||||||
|
## Future Improvements
|
||||||
|
|
||||||
|
1. **Performance**
|
||||||
|
- Query optimization
|
||||||
|
- Index tuning
|
||||||
|
- Connection management
|
||||||
|
- Cache implementation
|
||||||
|
|
||||||
|
2. **Features**
|
||||||
|
- Offline support
|
||||||
|
- Sync capabilities
|
||||||
|
- Backup/restore
|
||||||
|
- Data export/import
|
||||||
|
|
||||||
|
3. **Security**
|
||||||
|
- Database encryption
|
||||||
|
- Secure storage
|
||||||
|
- Access control
|
||||||
|
- Audit logging
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
1. **Regular Tasks**
|
||||||
|
- Database optimization
|
||||||
|
- Log rotation
|
||||||
|
- Error monitoring
|
||||||
|
- Performance tracking
|
||||||
|
|
||||||
|
2. **Updates**
|
||||||
|
- Dependency updates
|
||||||
|
- Security patches
|
||||||
|
- Feature additions
|
||||||
|
- Bug fixes
|
||||||
|
|
||||||
|
3. **Documentation**
|
||||||
|
- API documentation
|
||||||
|
- Migration guides
|
||||||
|
- Troubleshooting
|
||||||
|
- Best practices
|
||||||
55
electron/.gitignore
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# NPM renames .gitignore to .npmignore
|
||||||
|
# In order to prevent that, we remove the initial "."
|
||||||
|
# And the CLI then renames it
|
||||||
|
app
|
||||||
|
node_modules
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
logs
|
||||||
|
# Node.js dependencies
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Capacitor build outputs
|
||||||
|
web/
|
||||||
|
ios/
|
||||||
|
android/
|
||||||
|
electron/app/
|
||||||
|
|
||||||
|
# Capacitor SQLite plugin data (important!)
|
||||||
|
capacitor-sqlite/
|
||||||
|
|
||||||
|
# TypeScript / build output
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Development / IDE files
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# JetBrains IDEs (IntelliJ, WebStorm, etc.)
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# macOS specific
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
*.tmp
|
||||||
|
|
||||||
|
# Windows specific
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
$RECYCLE.BIN/
|
||||||
BIN
electron/assets/appIcon.ico
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
electron/assets/appIcon.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
electron/assets/splash.gif
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
electron/assets/splash.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
62
electron/capacitor.config.json
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"appId": "com.timesafari.app",
|
||||||
|
"appName": "TimeSafari",
|
||||||
|
"webDir": "dist",
|
||||||
|
"bundledWebRuntime": false,
|
||||||
|
"server": {
|
||||||
|
"cleartext": true,
|
||||||
|
"androidScheme": "https"
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"App": {
|
||||||
|
"appUrlOpen": {
|
||||||
|
"handlers": [
|
||||||
|
{
|
||||||
|
"url": "timesafari://*",
|
||||||
|
"autoVerify": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SQLite": {
|
||||||
|
"iosDatabaseLocation": "Library/CapacitorDatabase",
|
||||||
|
"iosIsEncryption": true,
|
||||||
|
"iosBiometric": {
|
||||||
|
"biometricAuth": true,
|
||||||
|
"biometricTitle": "Biometric login for TimeSafari"
|
||||||
|
},
|
||||||
|
"androidIsEncryption": true,
|
||||||
|
"androidBiometric": {
|
||||||
|
"biometricAuth": true,
|
||||||
|
"biometricTitle": "Biometric login for TimeSafari"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CapacitorSQLite": {
|
||||||
|
"electronIsEncryption": false,
|
||||||
|
"electronMacLocation": "~/Library/Application Support/TimeSafari",
|
||||||
|
"electronWindowsLocation": "C:\\ProgramData\\TimeSafari"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ios": {
|
||||||
|
"contentInset": "always",
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
28
electron/electron-builder.config.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"appId": "com.yourdoamnin.yourapp",
|
||||||
|
"directories": {
|
||||||
|
"buildResources": "resources"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"assets/**/*",
|
||||||
|
"build/**/*",
|
||||||
|
"capacitor.config.*",
|
||||||
|
"app/**/*"
|
||||||
|
],
|
||||||
|
"publish": {
|
||||||
|
"provider": "github"
|
||||||
|
},
|
||||||
|
"nsis": {
|
||||||
|
"allowElevation": true,
|
||||||
|
"oneClick": false,
|
||||||
|
"allowToChangeInstallationDirectory": true
|
||||||
|
},
|
||||||
|
"win": {
|
||||||
|
"target": "nsis",
|
||||||
|
"icon": "assets/appIcon.ico"
|
||||||
|
},
|
||||||
|
"mac": {
|
||||||
|
"category": "your.app.category.type",
|
||||||
|
"target": "dmg"
|
||||||
|
}
|
||||||
|
}
|
||||||
75
electron/live-runner.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/* eslint-disable no-undef */
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const cp = require('child_process');
|
||||||
|
const chokidar = require('chokidar');
|
||||||
|
const electron = require('electron');
|
||||||
|
|
||||||
|
let child = null;
|
||||||
|
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||||
|
const reloadWatcher = {
|
||||||
|
debouncer: null,
|
||||||
|
ready: false,
|
||||||
|
watcher: null,
|
||||||
|
restarting: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
///*
|
||||||
|
function runBuild() {
|
||||||
|
return new Promise((resolve, _reject) => {
|
||||||
|
let tempChild = cp.spawn(npmCmd, ['run', 'build']);
|
||||||
|
tempChild.once('exit', () => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
tempChild.stdout.pipe(process.stdout);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//*/
|
||||||
|
|
||||||
|
async function spawnElectron() {
|
||||||
|
if (child !== null) {
|
||||||
|
child.stdin.pause();
|
||||||
|
child.kill();
|
||||||
|
child = null;
|
||||||
|
await runBuild();
|
||||||
|
}
|
||||||
|
child = cp.spawn(electron, ['--inspect=5858', './']);
|
||||||
|
child.on('exit', () => {
|
||||||
|
if (!reloadWatcher.restarting) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
child.stdout.pipe(process.stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupReloadWatcher() {
|
||||||
|
reloadWatcher.watcher = chokidar
|
||||||
|
.watch('./src/**/*', {
|
||||||
|
ignored: /[/\\]\./,
|
||||||
|
persistent: true,
|
||||||
|
})
|
||||||
|
.on('ready', () => {
|
||||||
|
reloadWatcher.ready = true;
|
||||||
|
})
|
||||||
|
.on('all', (_event, _path) => {
|
||||||
|
if (reloadWatcher.ready) {
|
||||||
|
clearTimeout(reloadWatcher.debouncer);
|
||||||
|
reloadWatcher.debouncer = setTimeout(async () => {
|
||||||
|
console.log('Restarting');
|
||||||
|
reloadWatcher.restarting = true;
|
||||||
|
await spawnElectron();
|
||||||
|
reloadWatcher.restarting = false;
|
||||||
|
reloadWatcher.ready = false;
|
||||||
|
clearTimeout(reloadWatcher.debouncer);
|
||||||
|
reloadWatcher.debouncer = null;
|
||||||
|
reloadWatcher.watcher = null;
|
||||||
|
setupReloadWatcher();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
await runBuild();
|
||||||
|
await spawnElectron();
|
||||||
|
setupReloadWatcher();
|
||||||
|
})();
|
||||||
5460
electron/package-lock.json
generated
Normal file
52
electron/package.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"name": "TimeSafari",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "TimeSafari Electron App",
|
||||||
|
"author": {
|
||||||
|
"name": "",
|
||||||
|
"email": ""
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": ""
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "build/src/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc && electron-rebuild",
|
||||||
|
"electron:start-live": "node ./live-runner.js",
|
||||||
|
"electron:start": "npm run build && electron --inspect=5858 ./",
|
||||||
|
"electron:pack": "npm run build && electron-builder build --dir -c ./electron-builder.config.json",
|
||||||
|
"electron:make": "npm run build && electron-builder build -c ./electron-builder.config.json -p always"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@capacitor-community/electron": "^5.0.0",
|
||||||
|
"@capacitor-community/sqlite": "^6.0.2",
|
||||||
|
"better-sqlite3-multiple-ciphers": "^11.10.0",
|
||||||
|
"chokidar": "~3.5.3",
|
||||||
|
"crypto": "^1.0.1",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"electron-is-dev": "~2.0.0",
|
||||||
|
"electron-json-storage": "^4.6.0",
|
||||||
|
"electron-serve": "~1.1.0",
|
||||||
|
"electron-unhandled": "~4.0.1",
|
||||||
|
"electron-updater": "^5.3.0",
|
||||||
|
"electron-window-state": "^5.0.3",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
|
"node-fetch": "^2.6.7",
|
||||||
|
"winston": "^3.17.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
|
"@types/crypto-js": "^4.2.2",
|
||||||
|
"@types/electron-json-storage": "^4.5.4",
|
||||||
|
"electron": "^26.2.2",
|
||||||
|
"electron-builder": "~23.6.0",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"typescript": "^5.0.4"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"capacitor",
|
||||||
|
"electron"
|
||||||
|
]
|
||||||
|
}
|
||||||
10
electron/resources/electron-publisher-custom.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/* eslint-disable no-undef */
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const electronPublish = require('electron-publish');
|
||||||
|
|
||||||
|
class Publisher extends electronPublish.Publisher {
|
||||||
|
async upload(task) {
|
||||||
|
console.log('electron-publisher-custom', task.file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = Publisher;
|
||||||
140
electron/src/index.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import type { CapacitorElectronConfig } from '@capacitor-community/electron';
|
||||||
|
import { getCapacitorElectronConfig, setupElectronDeepLinking } from '@capacitor-community/electron';
|
||||||
|
import type { MenuItemConstructorOptions } from 'electron';
|
||||||
|
import { app, MenuItem } from 'electron';
|
||||||
|
import electronIsDev from 'electron-is-dev';
|
||||||
|
import unhandled from 'electron-unhandled';
|
||||||
|
import { autoUpdater } from 'electron-updater';
|
||||||
|
|
||||||
|
import { ElectronCapacitorApp, setupContentSecurityPolicy, setupReloadWatcher } from './setup';
|
||||||
|
import { initializeSQLite, setupSQLiteHandlers } from './rt/sqlite-init';
|
||||||
|
|
||||||
|
// Graceful handling of unhandled errors.
|
||||||
|
unhandled();
|
||||||
|
|
||||||
|
// Define our menu templates (these are optional)
|
||||||
|
const trayMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [new MenuItem({ label: 'Quit App', role: 'quit' })];
|
||||||
|
const appMenuBarMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [
|
||||||
|
{ role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' },
|
||||||
|
{ role: 'viewMenu' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Get Config options from capacitor.config
|
||||||
|
const capacitorFileConfig: CapacitorElectronConfig = getCapacitorElectronConfig();
|
||||||
|
|
||||||
|
// Initialize our app. You can pass menu templates into the app here.
|
||||||
|
const myCapacitorApp = new ElectronCapacitorApp(capacitorFileConfig, trayMenuTemplate, appMenuBarMenuTemplate);
|
||||||
|
|
||||||
|
// If deeplinking is enabled then we will set it up here.
|
||||||
|
if (capacitorFileConfig.electron?.deepLinkingEnabled) {
|
||||||
|
setupElectronDeepLinking(myCapacitorApp, {
|
||||||
|
customProtocol: capacitorFileConfig.electron.deepLinkingCustomProtocol ?? 'mycapacitorapp',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are in Dev mode, use the file watcher components.
|
||||||
|
if (electronIsDev) {
|
||||||
|
setupReloadWatcher(myCapacitorApp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run Application
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
// Wait for electron app to be ready first
|
||||||
|
await app.whenReady();
|
||||||
|
console.log('[Electron Main Process] App is ready');
|
||||||
|
|
||||||
|
// Initialize SQLite plugin and handlers BEFORE creating any windows
|
||||||
|
console.log('[Electron Main Process] Initializing SQLite...');
|
||||||
|
setupSQLiteHandlers();
|
||||||
|
await initializeSQLite();
|
||||||
|
console.log('[Electron Main Process] SQLite initialization complete');
|
||||||
|
|
||||||
|
// Security - Set Content-Security-Policy
|
||||||
|
setupContentSecurityPolicy(myCapacitorApp.getCustomURLScheme());
|
||||||
|
|
||||||
|
// Initialize our app and create window
|
||||||
|
console.log('[Electron Main Process] Starting app initialization...');
|
||||||
|
await myCapacitorApp.init();
|
||||||
|
console.log('[Electron Main Process] App initialization complete');
|
||||||
|
|
||||||
|
// Get the main window
|
||||||
|
const mainWindow = myCapacitorApp.getMainWindow();
|
||||||
|
if (!mainWindow) {
|
||||||
|
throw new Error('Main window not available after app initialization');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for window to be ready and loaded
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
const handleReady = () => {
|
||||||
|
console.log('[Electron Main Process] Window ready to show');
|
||||||
|
mainWindow.show();
|
||||||
|
|
||||||
|
// Wait for window to finish loading
|
||||||
|
mainWindow.webContents.once('did-finish-load', () => {
|
||||||
|
console.log('[Electron Main Process] Window finished loading');
|
||||||
|
|
||||||
|
// Send SQLite ready signal after window is fully loaded
|
||||||
|
if (!mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('sqlite-ready');
|
||||||
|
console.log('[Electron Main Process] Sent SQLite ready signal to renderer');
|
||||||
|
} else {
|
||||||
|
console.warn('[Electron Main Process] Window was destroyed before sending SQLite ready signal');
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Always use the event since isReadyToShow is not reliable
|
||||||
|
mainWindow.once('ready-to-show', handleReady);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for updates if we are in a packaged app
|
||||||
|
if (!electronIsDev) {
|
||||||
|
console.log('[Electron Main Process] Checking for updates...');
|
||||||
|
autoUpdater.checkForUpdatesAndNotify();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle window close
|
||||||
|
mainWindow.on('closed', () => {
|
||||||
|
console.log('[Electron Main Process] Main window closed');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle window close request
|
||||||
|
mainWindow.on('close', (event) => {
|
||||||
|
console.log('[Electron Main Process] Window close requested');
|
||||||
|
if (mainWindow.webContents.isLoading()) {
|
||||||
|
event.preventDefault();
|
||||||
|
console.log('[Electron Main Process] Deferring window close due to loading state');
|
||||||
|
mainWindow.webContents.once('did-finish-load', () => {
|
||||||
|
mainWindow.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Electron Main Process] Fatal error during initialization:', error);
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Handle when all of our windows are close (platforms have their own expectations).
|
||||||
|
app.on('window-all-closed', function () {
|
||||||
|
// On OS X it is common for applications and their menu bar
|
||||||
|
// to stay active until the user quits explicitly with Cmd + Q
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// When the dock icon is clicked.
|
||||||
|
app.on('activate', async function () {
|
||||||
|
// On OS X it's common to re-create a window in the app when the
|
||||||
|
// dock icon is clicked and there are no other windows open.
|
||||||
|
if (myCapacitorApp.getMainWindow().isDestroyed()) {
|
||||||
|
await myCapacitorApp.init();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Place all ipc or other electron api calls and custom functionality under this line
|
||||||
303
electron/src/preload.ts
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
/**
|
||||||
|
* Preload script for Electron
|
||||||
|
* Sets up secure IPC communication between renderer and main process
|
||||||
|
*
|
||||||
|
* @author Matthew Raymer
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { contextBridge, ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
// Enhanced logger for preload script that forwards to main process
|
||||||
|
const logger = {
|
||||||
|
log: (...args: unknown[]) => {
|
||||||
|
console.log('[Preload]', ...args);
|
||||||
|
ipcRenderer.send('renderer-log', { level: 'log', args });
|
||||||
|
},
|
||||||
|
error: (...args: unknown[]) => {
|
||||||
|
console.error('[Preload]', ...args);
|
||||||
|
ipcRenderer.send('renderer-log', { level: 'error', args });
|
||||||
|
},
|
||||||
|
info: (...args: unknown[]) => {
|
||||||
|
console.info('[Preload]', ...args);
|
||||||
|
ipcRenderer.send('renderer-log', { level: 'info', args });
|
||||||
|
},
|
||||||
|
warn: (...args: unknown[]) => {
|
||||||
|
console.warn('[Preload]', ...args);
|
||||||
|
ipcRenderer.send('renderer-log', { level: 'warn', args });
|
||||||
|
},
|
||||||
|
debug: (...args: unknown[]) => {
|
||||||
|
console.debug('[Preload]', ...args);
|
||||||
|
ipcRenderer.send('renderer-log', { level: 'debug', args });
|
||||||
|
},
|
||||||
|
sqlite: {
|
||||||
|
log: (operation: string, ...args: unknown[]) => {
|
||||||
|
const message = ['[Preload][SQLite]', operation, ...args];
|
||||||
|
console.log(...message);
|
||||||
|
ipcRenderer.send('renderer-log', {
|
||||||
|
level: 'log',
|
||||||
|
args: message,
|
||||||
|
source: 'sqlite',
|
||||||
|
operation
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (operation: string, error: unknown) => {
|
||||||
|
const message = ['[Preload][SQLite]', operation, 'failed:', error];
|
||||||
|
console.error(...message);
|
||||||
|
ipcRenderer.send('renderer-log', {
|
||||||
|
level: 'error',
|
||||||
|
args: message,
|
||||||
|
source: 'sqlite',
|
||||||
|
operation,
|
||||||
|
error: error instanceof Error ? {
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
} : error
|
||||||
|
});
|
||||||
|
},
|
||||||
|
debug: (operation: string, ...args: unknown[]) => {
|
||||||
|
const message = ['[Preload][SQLite]', operation, ...args];
|
||||||
|
console.debug(...message);
|
||||||
|
ipcRenderer.send('renderer-log', {
|
||||||
|
level: 'debug',
|
||||||
|
args: message,
|
||||||
|
source: 'sqlite',
|
||||||
|
operation
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Types for SQLite connection options
|
||||||
|
interface SQLiteConnectionOptions {
|
||||||
|
database: string;
|
||||||
|
version?: number;
|
||||||
|
readOnly?: boolean;
|
||||||
|
readonly?: boolean; // Handle both cases
|
||||||
|
encryption?: string;
|
||||||
|
mode?: string;
|
||||||
|
useNative?: boolean;
|
||||||
|
[key: string]: unknown; // Allow other properties
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define valid channels for security
|
||||||
|
const VALID_CHANNELS = {
|
||||||
|
send: ['toMain'] as const,
|
||||||
|
receive: ['fromMain', 'sqlite-ready', 'database-status'] as const,
|
||||||
|
invoke: [
|
||||||
|
'sqlite-is-available',
|
||||||
|
'sqlite-echo',
|
||||||
|
'sqlite-create-connection',
|
||||||
|
'sqlite-execute',
|
||||||
|
'sqlite-query',
|
||||||
|
'sqlite-run',
|
||||||
|
'sqlite-close-connection',
|
||||||
|
'sqlite-open',
|
||||||
|
'sqlite-close',
|
||||||
|
'sqlite-is-db-open',
|
||||||
|
'sqlite-status',
|
||||||
|
'get-path',
|
||||||
|
'get-base-path'
|
||||||
|
] as const
|
||||||
|
};
|
||||||
|
|
||||||
|
type ValidSendChannel = typeof VALID_CHANNELS.send[number];
|
||||||
|
type ValidReceiveChannel = typeof VALID_CHANNELS.receive[number];
|
||||||
|
type ValidInvokeChannel = typeof VALID_CHANNELS.invoke[number];
|
||||||
|
|
||||||
|
// Create a secure IPC bridge
|
||||||
|
const createSecureIPCBridge = () => {
|
||||||
|
return {
|
||||||
|
send: (channel: string, data: unknown) => {
|
||||||
|
if (VALID_CHANNELS.send.includes(channel as ValidSendChannel)) {
|
||||||
|
logger.debug('IPC Send:', channel, data);
|
||||||
|
ipcRenderer.send(channel, data);
|
||||||
|
} else {
|
||||||
|
logger.warn(`[Preload] Attempted to send on invalid channel: ${channel}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
receive: (channel: string, func: (...args: unknown[]) => void) => {
|
||||||
|
if (VALID_CHANNELS.receive.includes(channel as ValidReceiveChannel)) {
|
||||||
|
logger.debug('IPC Receive:', channel);
|
||||||
|
ipcRenderer.on(channel, (_event, ...args) => {
|
||||||
|
logger.debug('IPC Received:', channel, args);
|
||||||
|
func(...args);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.warn(`[Preload] Attempted to receive on invalid channel: ${channel}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
once: (channel: string, func: (...args: unknown[]) => void) => {
|
||||||
|
if (VALID_CHANNELS.receive.includes(channel as ValidReceiveChannel)) {
|
||||||
|
logger.debug('IPC Once:', channel);
|
||||||
|
ipcRenderer.once(channel, (_event, ...args) => {
|
||||||
|
logger.debug('IPC Received Once:', channel, args);
|
||||||
|
func(...args);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.warn(`[Preload] Attempted to receive once on invalid channel: ${channel}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
invoke: async (channel: string, ...args: unknown[]) => {
|
||||||
|
if (VALID_CHANNELS.invoke.includes(channel as ValidInvokeChannel)) {
|
||||||
|
logger.debug('IPC Invoke:', channel, args);
|
||||||
|
try {
|
||||||
|
const result = await ipcRenderer.invoke(channel, ...args);
|
||||||
|
logger.debug('IPC Invoke Result:', channel, result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('IPC Invoke Error:', channel, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn(`[Preload] Attempted to invoke on invalid channel: ${channel}`);
|
||||||
|
throw new Error(`Invalid channel: ${channel}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create SQLite proxy with retry logic
|
||||||
|
const createSQLiteProxy = () => {
|
||||||
|
const MAX_RETRIES = 3;
|
||||||
|
const RETRY_DELAY = 1000;
|
||||||
|
|
||||||
|
const withRetry = async <T>(operation: string, ...args: unknown[]): Promise<T> => {
|
||||||
|
let lastError: Error | undefined;
|
||||||
|
const operationId = `${operation}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
logger.sqlite.debug(operation, 'starting with args:', {
|
||||||
|
operationId,
|
||||||
|
args,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||||
|
try {
|
||||||
|
logger.sqlite.debug(operation, `attempt ${attempt}/${MAX_RETRIES}`, {
|
||||||
|
operationId,
|
||||||
|
attempt,
|
||||||
|
args,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log the exact IPC call
|
||||||
|
logger.sqlite.debug(operation, 'invoking IPC', {
|
||||||
|
operationId,
|
||||||
|
channel: `sqlite-${operation}`,
|
||||||
|
args,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await ipcRenderer.invoke(`sqlite-${operation}`, ...args);
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.sqlite.log(operation, 'success', {
|
||||||
|
operationId,
|
||||||
|
attempt,
|
||||||
|
result,
|
||||||
|
duration: `${duration}ms`,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
return result as T;
|
||||||
|
} catch (error) {
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
lastError = error instanceof Error ? error : new Error(String(error));
|
||||||
|
|
||||||
|
logger.sqlite.error(operation, {
|
||||||
|
operationId,
|
||||||
|
attempt,
|
||||||
|
error: {
|
||||||
|
name: lastError.name,
|
||||||
|
message: lastError.message,
|
||||||
|
stack: lastError.stack
|
||||||
|
},
|
||||||
|
args,
|
||||||
|
duration: `${duration}ms`,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (attempt < MAX_RETRIES) {
|
||||||
|
const backoffDelay = RETRY_DELAY * Math.pow(2, attempt - 1);
|
||||||
|
logger.warn(`[Preload] SQLite ${operation} failed (attempt ${attempt}/${MAX_RETRIES}), retrying in ${backoffDelay}ms...`, {
|
||||||
|
operationId,
|
||||||
|
error: lastError,
|
||||||
|
args,
|
||||||
|
nextAttemptIn: `${backoffDelay}ms`,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
await new Promise(resolve => setTimeout(resolve, backoffDelay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalError = new Error(
|
||||||
|
`SQLite ${operation} failed after ${MAX_RETRIES} attempts: ${lastError?.message || "Unknown error"}`
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.error('[Preload] SQLite operation failed permanently:', {
|
||||||
|
operation,
|
||||||
|
operationId,
|
||||||
|
error: {
|
||||||
|
name: finalError.name,
|
||||||
|
message: finalError.message,
|
||||||
|
stack: finalError.stack,
|
||||||
|
originalError: lastError
|
||||||
|
},
|
||||||
|
args,
|
||||||
|
attempts: MAX_RETRIES,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
throw finalError;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAvailable: () => withRetry('is-available'),
|
||||||
|
echo: (value: string) => withRetry('echo', { value }),
|
||||||
|
createConnection: (options: SQLiteConnectionOptions) => withRetry('create-connection', options),
|
||||||
|
closeConnection: (options: { database: string }) => withRetry('close-connection', options),
|
||||||
|
query: (options: { statement: string; values?: unknown[] }) => withRetry('query', options),
|
||||||
|
run: (options: { statement: string; values?: unknown[] }) => withRetry('run', options),
|
||||||
|
execute: (options: { statements: { statement: string; values?: unknown[] }[] }) => withRetry('execute', options),
|
||||||
|
getPlatform: () => Promise.resolve('electron')
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Expose the secure IPC bridge and SQLite proxy
|
||||||
|
const electronAPI = {
|
||||||
|
ipcRenderer: createSecureIPCBridge(),
|
||||||
|
sqlite: createSQLiteProxy(),
|
||||||
|
env: {
|
||||||
|
platform: 'electron',
|
||||||
|
isDev: process.env.NODE_ENV === 'development'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Log the exposed API for debugging
|
||||||
|
logger.debug('Exposing Electron API:', {
|
||||||
|
hasIpcRenderer: !!electronAPI.ipcRenderer,
|
||||||
|
hasSqlite: !!electronAPI.sqlite,
|
||||||
|
sqliteMethods: Object.keys(electronAPI.sqlite),
|
||||||
|
env: electronAPI.env
|
||||||
|
});
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('electron', electronAPI);
|
||||||
|
logger.info('[Preload] IPC bridge and SQLite proxy initialized successfully');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[Preload] Failed to initialize IPC bridge:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log startup
|
||||||
|
logger.log('[CapacitorSQLite] Preload script starting...');
|
||||||
|
|
||||||
|
// Handle window load
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
logger.log('[CapacitorSQLite] Preload script complete');
|
||||||
|
});
|
||||||
6
electron/src/rt/electron-plugins.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const CapacitorCommunitySqlite = require('../../../node_modules/@capacitor-community/sqlite/electron/dist/plugin.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
CapacitorCommunitySqlite,
|
||||||
|
}
|
||||||
88
electron/src/rt/electron-rt.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { randomBytes } from 'crypto';
|
||||||
|
import { ipcRenderer, contextBridge } from 'electron';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const plugins = require('./electron-plugins');
|
||||||
|
|
||||||
|
const randomId = (length = 5) => randomBytes(length).toString('hex');
|
||||||
|
|
||||||
|
const contextApi: {
|
||||||
|
[plugin: string]: { [functionName: string]: () => Promise<any> };
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
Object.keys(plugins).forEach((pluginKey) => {
|
||||||
|
Object.keys(plugins[pluginKey])
|
||||||
|
.filter((className) => className !== 'default')
|
||||||
|
.forEach((classKey) => {
|
||||||
|
const functionList = Object.getOwnPropertyNames(plugins[pluginKey][classKey].prototype).filter(
|
||||||
|
(v) => v !== 'constructor'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!contextApi[classKey]) {
|
||||||
|
contextApi[classKey] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
functionList.forEach((functionName) => {
|
||||||
|
if (!contextApi[classKey][functionName]) {
|
||||||
|
contextApi[classKey][functionName] = (...args) => ipcRenderer.invoke(`${classKey}-${functionName}`, ...args);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Events
|
||||||
|
if (plugins[pluginKey][classKey].prototype instanceof EventEmitter) {
|
||||||
|
const listeners: { [key: string]: { type: string; listener: (...args: any[]) => void } } = {};
|
||||||
|
const listenersOfTypeExist = (type) =>
|
||||||
|
!!Object.values(listeners).find((listenerObj) => listenerObj.type === type);
|
||||||
|
|
||||||
|
Object.assign(contextApi[classKey], {
|
||||||
|
addListener(type: string, callback: (...args) => void) {
|
||||||
|
const id = randomId();
|
||||||
|
|
||||||
|
// Deduplicate events
|
||||||
|
if (!listenersOfTypeExist(type)) {
|
||||||
|
ipcRenderer.send(`event-add-${classKey}`, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventHandler = (_, ...args) => callback(...args);
|
||||||
|
|
||||||
|
ipcRenderer.addListener(`event-${classKey}-${type}`, eventHandler);
|
||||||
|
listeners[id] = { type, listener: eventHandler };
|
||||||
|
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
removeListener(id: string) {
|
||||||
|
if (!listeners[id]) {
|
||||||
|
throw new Error('Invalid id');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type, listener } = listeners[id];
|
||||||
|
|
||||||
|
ipcRenderer.removeListener(`event-${classKey}-${type}`, listener);
|
||||||
|
|
||||||
|
delete listeners[id];
|
||||||
|
|
||||||
|
if (!listenersOfTypeExist(type)) {
|
||||||
|
ipcRenderer.send(`event-remove-${classKey}-${type}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeAllListeners(type: string) {
|
||||||
|
Object.entries(listeners).forEach(([id, listenerObj]) => {
|
||||||
|
if (!type || listenerObj.type === type) {
|
||||||
|
ipcRenderer.removeListener(`event-${classKey}-${listenerObj.type}`, listenerObj.listener);
|
||||||
|
ipcRenderer.send(`event-remove-${classKey}-${listenerObj.type}`);
|
||||||
|
delete listeners[id];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('CapacitorCustomPlatform', {
|
||||||
|
name: 'electron',
|
||||||
|
plugins: contextApi,
|
||||||
|
});
|
||||||
|
////////////////////////////////////////////////////////
|
||||||
188
electron/src/rt/logger.ts
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
/**
|
||||||
|
* Enhanced logging system for TimeSafari Electron
|
||||||
|
* Provides structured logging with proper levels and formatting
|
||||||
|
* Supports both console and file output with different verbosity levels
|
||||||
|
*
|
||||||
|
* @author Matthew Raymer
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { app, ipcMain } from 'electron';
|
||||||
|
import winston from 'winston';
|
||||||
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
// Extend Winston Logger type with our custom loggers
|
||||||
|
declare module 'winston' {
|
||||||
|
interface Logger {
|
||||||
|
sqlite: {
|
||||||
|
debug: (message: string, ...args: unknown[]) => void;
|
||||||
|
info: (message: string, ...args: unknown[]) => void;
|
||||||
|
warn: (message: string, ...args: unknown[]) => void;
|
||||||
|
error: (message: string, ...args: unknown[]) => void;
|
||||||
|
};
|
||||||
|
migration: {
|
||||||
|
debug: (message: string, ...args: unknown[]) => void;
|
||||||
|
info: (message: string, ...args: unknown[]) => void;
|
||||||
|
warn: (message: string, ...args: unknown[]) => void;
|
||||||
|
error: (message: string, ...args: unknown[]) => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create logs directory if it doesn't exist
|
||||||
|
const logsDir = path.join(app.getPath('userData'), 'logs');
|
||||||
|
if (!fs.existsSync(logsDir)) {
|
||||||
|
fs.mkdirSync(logsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom format for console output with migration filtering
|
||||||
|
const consoleFormat = winston.format.combine(
|
||||||
|
winston.format.timestamp(),
|
||||||
|
winston.format.colorize(),
|
||||||
|
winston.format.printf(({ level, message, timestamp, ...metadata }) => {
|
||||||
|
// Skip migration logs unless DEBUG_MIGRATIONS is set
|
||||||
|
if (level === 'info' &&
|
||||||
|
typeof message === 'string' &&
|
||||||
|
message.includes('[Migration]') &&
|
||||||
|
!process.env.DEBUG_MIGRATIONS) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg = `${timestamp} [${level}] ${message}`;
|
||||||
|
if (Object.keys(metadata).length > 0) {
|
||||||
|
msg += ` ${JSON.stringify(metadata, null, 2)}`;
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Custom format for file output
|
||||||
|
const fileFormat = winston.format.combine(
|
||||||
|
winston.format.timestamp(),
|
||||||
|
winston.format.json()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create logger instance
|
||||||
|
const logger = winston.createLogger({
|
||||||
|
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
|
||||||
|
format: fileFormat,
|
||||||
|
defaultMeta: { service: 'timesafari-electron' },
|
||||||
|
transports: [
|
||||||
|
// Console transport with custom format and migration filtering
|
||||||
|
new winston.transports.Console({
|
||||||
|
format: consoleFormat,
|
||||||
|
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
|
||||||
|
silent: false // Ensure we can still see non-migration logs
|
||||||
|
}),
|
||||||
|
// File transport for all logs
|
||||||
|
new winston.transports.File({
|
||||||
|
filename: path.join(logsDir, 'error.log'),
|
||||||
|
level: 'error',
|
||||||
|
maxsize: 5242880, // 5MB
|
||||||
|
maxFiles: 5
|
||||||
|
}),
|
||||||
|
// File transport for all logs including debug
|
||||||
|
new winston.transports.File({
|
||||||
|
filename: path.join(logsDir, 'combined.log'),
|
||||||
|
maxsize: 5242880, // 5MB
|
||||||
|
maxFiles: 5
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}) as winston.Logger & {
|
||||||
|
sqlite: {
|
||||||
|
debug: (message: string, ...args: unknown[]) => void;
|
||||||
|
info: (message: string, ...args: unknown[]) => void;
|
||||||
|
warn: (message: string, ...args: unknown[]) => void;
|
||||||
|
error: (message: string, ...args: unknown[]) => void;
|
||||||
|
};
|
||||||
|
migration: {
|
||||||
|
debug: (message: string, ...args: unknown[]) => void;
|
||||||
|
info: (message: string, ...args: unknown[]) => void;
|
||||||
|
warn: (message: string, ...args: unknown[]) => void;
|
||||||
|
error: (message: string, ...args: unknown[]) => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add SQLite specific logger
|
||||||
|
logger.sqlite = {
|
||||||
|
debug: (message: string, ...args: unknown[]) => {
|
||||||
|
logger.debug(`[SQLite] ${message}`, ...args);
|
||||||
|
},
|
||||||
|
info: (message: string, ...args: unknown[]) => {
|
||||||
|
logger.info(`[SQLite] ${message}`, ...args);
|
||||||
|
},
|
||||||
|
warn: (message: string, ...args: unknown[]) => {
|
||||||
|
logger.warn(`[SQLite] ${message}`, ...args);
|
||||||
|
},
|
||||||
|
error: (message: string, ...args: unknown[]) => {
|
||||||
|
logger.error(`[SQLite] ${message}`, ...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add migration specific logger with debug filtering
|
||||||
|
logger.migration = {
|
||||||
|
debug: (message: string, ...args: unknown[]) => {
|
||||||
|
if (process.env.DEBUG_MIGRATIONS) {
|
||||||
|
//logger.debug(`[Migration] ${message}`, ...args);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
info: (message: string, ...args: unknown[]) => {
|
||||||
|
// Always log to file, but only log to console if DEBUG_MIGRATIONS is set
|
||||||
|
if (process.env.DEBUG_MIGRATIONS) {
|
||||||
|
//logger.info(`[Migration] ${message}`, ...args);
|
||||||
|
} else {
|
||||||
|
// Use a separate transport for migration logs to file only
|
||||||
|
const metadata = args[0] as Record<string, unknown>;
|
||||||
|
logger.write({
|
||||||
|
level: 'info',
|
||||||
|
message: `[Migration] ${message}`,
|
||||||
|
...(metadata || {})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
warn: (message: string, ...args: unknown[]) => {
|
||||||
|
// Always log warnings to both console and file
|
||||||
|
//logger.warn(`[Migration] ${message}`, ...args);
|
||||||
|
},
|
||||||
|
error: (message: string, ...args: unknown[]) => {
|
||||||
|
// Always log errors to both console and file
|
||||||
|
//logger.error(`[Migration] ${message}`, ...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add renderer log handler
|
||||||
|
ipcMain.on('renderer-log', (_event, { level, args, source, operation, error }) => {
|
||||||
|
const message = args.map((arg: unknown) =>
|
||||||
|
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
|
||||||
|
).join(' ');
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
source: source || 'renderer',
|
||||||
|
...(operation && { operation }),
|
||||||
|
...(error && { error })
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (level) {
|
||||||
|
case 'error':
|
||||||
|
logger.error(message, meta);
|
||||||
|
break;
|
||||||
|
case 'warn':
|
||||||
|
logger.warn(message, meta);
|
||||||
|
break;
|
||||||
|
case 'info':
|
||||||
|
logger.info(message, meta);
|
||||||
|
break;
|
||||||
|
case 'debug':
|
||||||
|
logger.debug(message, meta);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logger.log(level, message, meta);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export logger instance
|
||||||
|
export { logger };
|
||||||
|
|
||||||
|
// Export a function to get the logs directory
|
||||||
|
export const getLogsDirectory = () => logsDir;
|
||||||
14
electron/src/rt/sqlite-error.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Custom error class for SQLite operations
|
||||||
|
* Provides additional context and error tracking for SQLite operations
|
||||||
|
*/
|
||||||
|
export class SQLiteError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public operation: string,
|
||||||
|
public cause?: unknown
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'SQLiteError';
|
||||||
|
}
|
||||||
|
}
|
||||||
1147
electron/src/rt/sqlite-init.ts
Normal file
1261
electron/src/rt/sqlite-migrations.ts
Normal file
442
electron/src/setup.ts
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
import type { CapacitorElectronConfig } from '@capacitor-community/electron';
|
||||||
|
import {
|
||||||
|
CapElectronEventEmitter,
|
||||||
|
CapacitorSplashScreen,
|
||||||
|
setupCapacitorElectronPlugins,
|
||||||
|
} from '@capacitor-community/electron';
|
||||||
|
import chokidar from 'chokidar';
|
||||||
|
import type { MenuItemConstructorOptions } from 'electron';
|
||||||
|
import { app, BrowserWindow, Menu, MenuItem, nativeImage, Tray, session } from 'electron';
|
||||||
|
import electronIsDev from 'electron-is-dev';
|
||||||
|
import electronServe from 'electron-serve';
|
||||||
|
import windowStateKeeper from 'electron-window-state';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload watcher configuration and state management
|
||||||
|
* Prevents infinite reload loops and implements rate limiting
|
||||||
|
* Also prevents reloads during critical database operations
|
||||||
|
*
|
||||||
|
* @author Matthew Raymer
|
||||||
|
*/
|
||||||
|
const RELOAD_CONFIG = {
|
||||||
|
DEBOUNCE_MS: 1500,
|
||||||
|
COOLDOWN_MS: 5000,
|
||||||
|
MAX_RELOADS_PER_MINUTE: 10,
|
||||||
|
MAX_RELOADS_PER_SESSION: 100,
|
||||||
|
DATABASE_OPERATION_TIMEOUT_MS: 10000 // 10 second timeout for database operations
|
||||||
|
};
|
||||||
|
|
||||||
|
// Track database operation state
|
||||||
|
let isDatabaseOperationInProgress = false;
|
||||||
|
let lastDatabaseOperationTime = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a database operation is in progress or recently completed
|
||||||
|
* @returns {boolean} Whether a database operation is active
|
||||||
|
*/
|
||||||
|
const isDatabaseOperationActive = (): boolean => {
|
||||||
|
const now = Date.now();
|
||||||
|
return isDatabaseOperationInProgress ||
|
||||||
|
(now - lastDatabaseOperationTime < RELOAD_CONFIG.DATABASE_OPERATION_TIMEOUT_MS);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the start of a database operation
|
||||||
|
*/
|
||||||
|
export const startDatabaseOperation = (): void => {
|
||||||
|
isDatabaseOperationInProgress = true;
|
||||||
|
lastDatabaseOperationTime = Date.now();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the end of a database operation
|
||||||
|
*/
|
||||||
|
export const endDatabaseOperation = (): void => {
|
||||||
|
isDatabaseOperationInProgress = false;
|
||||||
|
lastDatabaseOperationTime = Date.now();
|
||||||
|
};
|
||||||
|
|
||||||
|
const reloadWatcher = {
|
||||||
|
debouncer: null as NodeJS.Timeout | null,
|
||||||
|
ready: false,
|
||||||
|
watcher: null as chokidar.FSWatcher | null,
|
||||||
|
lastReloadTime: 0,
|
||||||
|
reloadCount: 0,
|
||||||
|
sessionReloadCount: 0,
|
||||||
|
resetTimeout: null as NodeJS.Timeout | null,
|
||||||
|
isReloading: false
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the reload counter after one minute
|
||||||
|
*/
|
||||||
|
const resetReloadCounter = () => {
|
||||||
|
reloadWatcher.reloadCount = 0;
|
||||||
|
reloadWatcher.resetTimeout = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a reload is allowed based on rate limits, cooldown, and database state
|
||||||
|
* @returns {boolean} Whether a reload is allowed
|
||||||
|
*/
|
||||||
|
const canReload = (): boolean => {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Check if database operation is active
|
||||||
|
if (isDatabaseOperationActive()) {
|
||||||
|
console.warn('[Reload Watcher] Skipping reload - database operation in progress');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cooldown period
|
||||||
|
if (now - reloadWatcher.lastReloadTime < RELOAD_CONFIG.COOLDOWN_MS) {
|
||||||
|
console.warn('[Reload Watcher] Skipping reload - cooldown period active');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check per-minute limit
|
||||||
|
if (reloadWatcher.reloadCount >= RELOAD_CONFIG.MAX_RELOADS_PER_MINUTE) {
|
||||||
|
console.warn('[Reload Watcher] Skipping reload - maximum reloads per minute reached');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check session limit
|
||||||
|
if (reloadWatcher.sessionReloadCount >= RELOAD_CONFIG.MAX_RELOADS_PER_SESSION) {
|
||||||
|
console.error('[Reload Watcher] Maximum reloads per session reached. Please restart the application.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up the current watcher instance
|
||||||
|
*/
|
||||||
|
const cleanupWatcher = () => {
|
||||||
|
if (reloadWatcher.watcher) {
|
||||||
|
reloadWatcher.watcher.close();
|
||||||
|
reloadWatcher.watcher = null;
|
||||||
|
}
|
||||||
|
if (reloadWatcher.debouncer) {
|
||||||
|
clearTimeout(reloadWatcher.debouncer);
|
||||||
|
reloadWatcher.debouncer = null;
|
||||||
|
}
|
||||||
|
if (reloadWatcher.resetTimeout) {
|
||||||
|
clearTimeout(reloadWatcher.resetTimeout);
|
||||||
|
reloadWatcher.resetTimeout = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the file watcher for development mode reloading
|
||||||
|
* Implements rate limiting and prevents infinite reload loops
|
||||||
|
*
|
||||||
|
* @param electronCapacitorApp - The Electron Capacitor app instance
|
||||||
|
*/
|
||||||
|
export function setupReloadWatcher(electronCapacitorApp: ElectronCapacitorApp): void {
|
||||||
|
// Cleanup any existing watcher
|
||||||
|
cleanupWatcher();
|
||||||
|
|
||||||
|
// Reset state
|
||||||
|
reloadWatcher.ready = false;
|
||||||
|
reloadWatcher.isReloading = false;
|
||||||
|
|
||||||
|
reloadWatcher.watcher = chokidar
|
||||||
|
.watch(join(app.getAppPath(), 'app'), {
|
||||||
|
ignored: /[/\\]\./,
|
||||||
|
persistent: true,
|
||||||
|
awaitWriteFinish: {
|
||||||
|
stabilityThreshold: 1000,
|
||||||
|
pollInterval: 100
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('ready', () => {
|
||||||
|
reloadWatcher.ready = true;
|
||||||
|
console.log('[Reload Watcher] Ready to watch for changes');
|
||||||
|
})
|
||||||
|
.on('all', (_event, _path) => {
|
||||||
|
if (!reloadWatcher.ready || reloadWatcher.isReloading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear existing debouncer
|
||||||
|
if (reloadWatcher.debouncer) {
|
||||||
|
clearTimeout(reloadWatcher.debouncer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up new debouncer
|
||||||
|
reloadWatcher.debouncer = setTimeout(async () => {
|
||||||
|
if (!canReload()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
reloadWatcher.isReloading = true;
|
||||||
|
|
||||||
|
// Update reload counters
|
||||||
|
reloadWatcher.lastReloadTime = Date.now();
|
||||||
|
reloadWatcher.reloadCount++;
|
||||||
|
reloadWatcher.sessionReloadCount++;
|
||||||
|
|
||||||
|
// Set up reset timeout for per-minute counter
|
||||||
|
if (!reloadWatcher.resetTimeout) {
|
||||||
|
reloadWatcher.resetTimeout = setTimeout(resetReloadCounter, 60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform reload
|
||||||
|
console.log('[Reload Watcher] Reloading window...');
|
||||||
|
await electronCapacitorApp.getMainWindow().webContents.reload();
|
||||||
|
|
||||||
|
// Reset state after reload
|
||||||
|
reloadWatcher.ready = false;
|
||||||
|
reloadWatcher.isReloading = false;
|
||||||
|
|
||||||
|
// Re-setup watcher after successful reload
|
||||||
|
setupReloadWatcher(electronCapacitorApp);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Reload Watcher] Error during reload:', error);
|
||||||
|
reloadWatcher.isReloading = false;
|
||||||
|
reloadWatcher.ready = true;
|
||||||
|
}
|
||||||
|
}, RELOAD_CONFIG.DEBOUNCE_MS);
|
||||||
|
})
|
||||||
|
.on('error', (error) => {
|
||||||
|
console.error('[Reload Watcher] Error:', error);
|
||||||
|
cleanupWatcher();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define our class to manage our app.
|
||||||
|
export class ElectronCapacitorApp {
|
||||||
|
private MainWindow: BrowserWindow | null = null;
|
||||||
|
private SplashScreen: CapacitorSplashScreen | null = null;
|
||||||
|
private TrayIcon: Tray | null = null;
|
||||||
|
private CapacitorFileConfig: CapacitorElectronConfig;
|
||||||
|
private TrayMenuTemplate: (MenuItem | MenuItemConstructorOptions)[] = [
|
||||||
|
new MenuItem({ label: 'Quit App', role: 'quit' }),
|
||||||
|
];
|
||||||
|
private AppMenuBarMenuTemplate: (MenuItem | MenuItemConstructorOptions)[] = [
|
||||||
|
{ role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' },
|
||||||
|
{ role: 'viewMenu' },
|
||||||
|
];
|
||||||
|
private mainWindowState;
|
||||||
|
private loadWebApp;
|
||||||
|
private customScheme: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
capacitorFileConfig: CapacitorElectronConfig,
|
||||||
|
trayMenuTemplate?: (MenuItemConstructorOptions | MenuItem)[],
|
||||||
|
appMenuBarMenuTemplate?: (MenuItemConstructorOptions | MenuItem)[]
|
||||||
|
) {
|
||||||
|
this.CapacitorFileConfig = capacitorFileConfig;
|
||||||
|
|
||||||
|
this.customScheme = this.CapacitorFileConfig.electron?.customUrlScheme ?? 'capacitor-electron';
|
||||||
|
|
||||||
|
if (trayMenuTemplate) {
|
||||||
|
this.TrayMenuTemplate = trayMenuTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appMenuBarMenuTemplate) {
|
||||||
|
this.AppMenuBarMenuTemplate = appMenuBarMenuTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup our web app loader, this lets us load apps like react, vue, and angular without changing their build chains.
|
||||||
|
this.loadWebApp = electronServe({
|
||||||
|
directory: join(app.getAppPath(), 'app'),
|
||||||
|
scheme: this.customScheme,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to load in the app.
|
||||||
|
private async loadMainWindow(thisRef: any) {
|
||||||
|
await thisRef.loadWebApp(thisRef.MainWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose the mainWindow ref for use outside of the class.
|
||||||
|
getMainWindow(): BrowserWindow {
|
||||||
|
return this.MainWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCustomURLScheme(): string {
|
||||||
|
return this.customScheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(): Promise<void> {
|
||||||
|
const icon = nativeImage.createFromPath(
|
||||||
|
join(app.getAppPath(), 'assets', process.platform === 'win32' ? 'appIcon.ico' : 'appIcon.png')
|
||||||
|
);
|
||||||
|
this.mainWindowState = windowStateKeeper({
|
||||||
|
defaultWidth: 1000,
|
||||||
|
defaultHeight: 800,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup preload script path based on environment
|
||||||
|
const preloadPath = app.isPackaged
|
||||||
|
? join(process.resourcesPath, 'preload.js')
|
||||||
|
: join(__dirname, 'preload.js');
|
||||||
|
|
||||||
|
console.log('[Electron Main Process] Preload path:', preloadPath);
|
||||||
|
console.log('[Electron Main Process] Preload exists:', require('fs').existsSync(preloadPath));
|
||||||
|
|
||||||
|
this.MainWindow = new BrowserWindow({
|
||||||
|
icon,
|
||||||
|
show: false,
|
||||||
|
x: this.mainWindowState.x,
|
||||||
|
y: this.mainWindowState.y,
|
||||||
|
width: this.mainWindowState.width,
|
||||||
|
height: this.mainWindowState.height,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
sandbox: false,
|
||||||
|
preload: preloadPath,
|
||||||
|
webSecurity: true,
|
||||||
|
allowRunningInsecureContent: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.mainWindowState.manage(this.MainWindow);
|
||||||
|
|
||||||
|
if (this.CapacitorFileConfig.backgroundColor) {
|
||||||
|
this.MainWindow.setBackgroundColor(this.CapacitorFileConfig.electron.backgroundColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we close the main window with the splashscreen enabled we need to destory the ref.
|
||||||
|
this.MainWindow.on('closed', () => {
|
||||||
|
if (this.SplashScreen?.getSplashWindow() && !this.SplashScreen.getSplashWindow().isDestroyed()) {
|
||||||
|
this.SplashScreen.getSplashWindow().close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// When the tray icon is enabled, setup the options.
|
||||||
|
if (this.CapacitorFileConfig.electron?.trayIconAndMenuEnabled) {
|
||||||
|
this.TrayIcon = new Tray(icon);
|
||||||
|
this.TrayIcon.on('double-click', () => {
|
||||||
|
if (this.MainWindow) {
|
||||||
|
if (this.MainWindow.isVisible()) {
|
||||||
|
this.MainWindow.hide();
|
||||||
|
} else {
|
||||||
|
this.MainWindow.show();
|
||||||
|
this.MainWindow.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.TrayIcon.on('click', () => {
|
||||||
|
if (this.MainWindow) {
|
||||||
|
if (this.MainWindow.isVisible()) {
|
||||||
|
this.MainWindow.hide();
|
||||||
|
} else {
|
||||||
|
this.MainWindow.show();
|
||||||
|
this.MainWindow.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.TrayIcon.setToolTip(app.getName());
|
||||||
|
this.TrayIcon.setContextMenu(Menu.buildFromTemplate(this.TrayMenuTemplate));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the main manu bar at the top of our window.
|
||||||
|
Menu.setApplicationMenu(Menu.buildFromTemplate(this.AppMenuBarMenuTemplate));
|
||||||
|
|
||||||
|
// If the splashscreen is enabled, show it first while the main window loads then switch it out for the main window, or just load the main window from the start.
|
||||||
|
if (this.CapacitorFileConfig.electron?.splashScreenEnabled) {
|
||||||
|
this.SplashScreen = new CapacitorSplashScreen({
|
||||||
|
imageFilePath: join(
|
||||||
|
app.getAppPath(),
|
||||||
|
'assets',
|
||||||
|
this.CapacitorFileConfig.electron?.splashScreenImageName ?? 'splash.png'
|
||||||
|
),
|
||||||
|
windowWidth: 400,
|
||||||
|
windowHeight: 400,
|
||||||
|
});
|
||||||
|
this.SplashScreen.init(this.loadMainWindow, this);
|
||||||
|
} else {
|
||||||
|
this.loadMainWindow(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security
|
||||||
|
this.MainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
|
if (!details.url.includes(this.customScheme)) {
|
||||||
|
return { action: 'deny' };
|
||||||
|
} else {
|
||||||
|
return { action: 'allow' };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.MainWindow.webContents.on('will-navigate', (event, _newURL) => {
|
||||||
|
if (!this.MainWindow.webContents.getURL().includes(this.customScheme)) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Link electron plugins into the system.
|
||||||
|
setupCapacitorElectronPlugins();
|
||||||
|
|
||||||
|
// When the web app is loaded we hide the splashscreen if needed and show the mainwindow.
|
||||||
|
this.MainWindow.webContents.on('dom-ready', () => {
|
||||||
|
if (this.CapacitorFileConfig.electron?.splashScreenEnabled) {
|
||||||
|
this.SplashScreen.getSplashWindow().hide();
|
||||||
|
}
|
||||||
|
if (!this.CapacitorFileConfig.electron?.hideMainWindowOnLaunch) {
|
||||||
|
this.MainWindow.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-register SQLite handlers after reload
|
||||||
|
if (electronIsDev) {
|
||||||
|
console.log('[Electron Main Process] Re-registering SQLite handlers after reload');
|
||||||
|
const { setupSQLiteHandlers } = require('./rt/sqlite-init');
|
||||||
|
setupSQLiteHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (electronIsDev) {
|
||||||
|
this.MainWindow.webContents.openDevTools();
|
||||||
|
}
|
||||||
|
CapElectronEventEmitter.emit('CAPELECTRON_DeeplinkListenerInitialized', '');
|
||||||
|
}, 400);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a CSP up for our application based on the custom scheme
|
||||||
|
export function setupContentSecurityPolicy(customScheme: string): void {
|
||||||
|
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
||||||
|
callback({
|
||||||
|
responseHeaders: {
|
||||||
|
...details.responseHeaders,
|
||||||
|
'Content-Security-Policy': [
|
||||||
|
// Base CSP for both dev and prod
|
||||||
|
`default-src ${customScheme}://*;`,
|
||||||
|
// Script sources
|
||||||
|
`script-src ${customScheme}://* 'self' 'unsafe-inline'${electronIsDev ? " 'unsafe-eval'" : ''};`,
|
||||||
|
// Style sources
|
||||||
|
`style-src ${customScheme}://* 'self' 'unsafe-inline' https://fonts.googleapis.com;`,
|
||||||
|
// Font sources
|
||||||
|
`font-src ${customScheme}://* 'self' https://fonts.gstatic.com;`,
|
||||||
|
// Image sources
|
||||||
|
`img-src ${customScheme}://* 'self' data: https:;`,
|
||||||
|
// Connect sources (for API calls)
|
||||||
|
`connect-src ${customScheme}://* 'self' https:;`,
|
||||||
|
// Worker sources
|
||||||
|
`worker-src ${customScheme}://* 'self' blob:;`,
|
||||||
|
// Frame sources
|
||||||
|
`frame-src ${customScheme}://* 'self';`,
|
||||||
|
// Media sources
|
||||||
|
`media-src ${customScheme}://* 'self' data:;`,
|
||||||
|
// Object sources
|
||||||
|
`object-src 'none';`,
|
||||||
|
// Base URI
|
||||||
|
`base-uri 'self';`,
|
||||||
|
// Form action
|
||||||
|
`form-action ${customScheme}://* 'self';`,
|
||||||
|
// Frame ancestors
|
||||||
|
`frame-ancestors 'none';`,
|
||||||
|
// Upgrade insecure requests
|
||||||
|
'upgrade-insecure-requests;',
|
||||||
|
// Block mixed content
|
||||||
|
'block-all-mixed-content;'
|
||||||
|
].join(' ')
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
18
electron/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compileOnSave": true,
|
||||||
|
"include": ["./src/**/*", "./capacitor.config.ts", "./capacitor.config.js"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./build",
|
||||||
|
"importHelpers": true,
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"typeRoots": ["./node_modules/@types"],
|
||||||
|
"allowJs": true,
|
||||||
|
"rootDir": ".",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
155
experiment.sh
Executable file
@@ -0,0 +1,155 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# experiment.sh
|
||||||
|
# Author: Matthew Raymer
|
||||||
|
# Description: Build script for TimeSafari Electron application
|
||||||
|
# This script handles the complete build process for the TimeSafari Electron app,
|
||||||
|
# including web asset compilation and Capacitor sync.
|
||||||
|
#
|
||||||
|
# Build Process:
|
||||||
|
# 1. Environment setup and dependency checks
|
||||||
|
# 2. Web asset compilation (Vite)
|
||||||
|
# 3. Capacitor sync
|
||||||
|
# 4. Electron start
|
||||||
|
#
|
||||||
|
# Dependencies:
|
||||||
|
# - Node.js and npm
|
||||||
|
# - TypeScript
|
||||||
|
# - Vite
|
||||||
|
# - @capacitor-community/electron
|
||||||
|
#
|
||||||
|
# Usage: ./experiment.sh
|
||||||
|
#
|
||||||
|
# Exit Codes:
|
||||||
|
# 1 - Required command not found
|
||||||
|
# 2 - TypeScript installation failed
|
||||||
|
# 3 - Build process failed
|
||||||
|
# 4 - Capacitor sync failed
|
||||||
|
# 5 - Electron start failed
|
||||||
|
|
||||||
|
# Exit on any error
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# ANSI color codes for better output formatting
|
||||||
|
readonly RED='\033[0;31m'
|
||||||
|
readonly GREEN='\033[0;32m'
|
||||||
|
readonly YELLOW='\033[1;33m'
|
||||||
|
readonly BLUE='\033[0;34m'
|
||||||
|
readonly NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Logging functions
|
||||||
|
log_info() {
|
||||||
|
echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')] [INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success() {
|
||||||
|
echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] [SUCCESS]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warn() {
|
||||||
|
echo -e "${YELLOW}[$(date '+%Y-%m-%d %H:%M:%S')] [WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
echo -e "${RED}[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check if a command exists
|
||||||
|
check_command() {
|
||||||
|
if ! command -v "$1" &> /dev/null; then
|
||||||
|
log_error "$1 is required but not installed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_info "Found $1: $(command -v "$1")"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to measure and log execution time
|
||||||
|
measure_time() {
|
||||||
|
local start_time=$(date +%s)
|
||||||
|
"$@"
|
||||||
|
local end_time=$(date +%s)
|
||||||
|
local duration=$((end_time - start_time))
|
||||||
|
log_success "Completed in ${duration} seconds"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print build header
|
||||||
|
echo -e "\n${BLUE}=== TimeSafari Electron Build Process ===${NC}\n"
|
||||||
|
log_info "Starting build process at $(date)"
|
||||||
|
|
||||||
|
# Check required commands
|
||||||
|
log_info "Checking required dependencies..."
|
||||||
|
check_command node
|
||||||
|
check_command npm
|
||||||
|
check_command git
|
||||||
|
|
||||||
|
# Create application data directory
|
||||||
|
log_info "Setting up application directories..."
|
||||||
|
mkdir -p ~/.local/share/TimeSafari/timesafari
|
||||||
|
|
||||||
|
# Clean up previous builds
|
||||||
|
log_info "Cleaning previous builds..."
|
||||||
|
rm -rf dist* || log_warn "No previous builds to clean"
|
||||||
|
|
||||||
|
# Set environment variables for the build
|
||||||
|
log_info "Configuring build environment..."
|
||||||
|
export VITE_PLATFORM=electron
|
||||||
|
export VITE_PWA_ENABLED=false
|
||||||
|
export VITE_DISABLE_PWA=true
|
||||||
|
export DEBUG_MIGRATIONS=0
|
||||||
|
|
||||||
|
# Ensure TypeScript is installed
|
||||||
|
log_info "Verifying TypeScript installation..."
|
||||||
|
if [ ! -f "./node_modules/.bin/tsc" ]; then
|
||||||
|
log_info "Installing TypeScript..."
|
||||||
|
if ! npm install --save-dev typescript@~5.2.2; then
|
||||||
|
log_error "TypeScript installation failed!"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
# Verify installation
|
||||||
|
if [ ! -f "./node_modules/.bin/tsc" ]; then
|
||||||
|
log_error "TypeScript installation verification failed!"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
log_success "TypeScript installed successfully"
|
||||||
|
else
|
||||||
|
log_info "TypeScript already installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get git hash for versioning
|
||||||
|
GIT_HASH=$(git log -1 --pretty=format:%h)
|
||||||
|
log_info "Using git hash: ${GIT_HASH}"
|
||||||
|
|
||||||
|
# Build web assets
|
||||||
|
log_info "Building web assets with Vite..."
|
||||||
|
if ! measure_time env VITE_GIT_HASH="$GIT_HASH" npx vite build --config vite.config.app.electron.mts --mode electron; then
|
||||||
|
log_error "Web asset build failed!"
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sync with Capacitor
|
||||||
|
log_info "Syncing with Capacitor..."
|
||||||
|
if ! measure_time npx cap sync electron; then
|
||||||
|
log_error "Capacitor sync failed!"
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Restore capacitor config
|
||||||
|
log_info "Restoring capacitor config..."
|
||||||
|
if ! git checkout electron/capacitor.config.json; then
|
||||||
|
log_error "Failed to restore capacitor config!"
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start Electron
|
||||||
|
log_info "Starting Electron..."
|
||||||
|
cd electron/
|
||||||
|
if ! measure_time npm run electron:start; then
|
||||||
|
log_error "Electron start failed!"
|
||||||
|
exit 5
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Print build summary
|
||||||
|
log_success "Build and start completed successfully!"
|
||||||
|
echo -e "\n${GREEN}=== End of Build Process ===${NC}\n"
|
||||||
|
|
||||||
|
# Exit with success
|
||||||
|
exit 0
|
||||||
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 = "";
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
|
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
// Initialize SQLite
|
// Initialize SQLite
|
||||||
//let sqlite = SQLite()
|
let sqlite = SQLite()
|
||||||
//sqlite.initialize()
|
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 |
@@ -27,9 +27,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
|
||||||
|
|||||||
1151
package-lock.json
generated
53
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"
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"build": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.mts",
|
"build": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.mts",
|
||||||
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src",
|
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src",
|
||||||
"lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
|
"lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
|
||||||
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js",
|
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.cjs && node scripts/copy-wasm.cjs",
|
||||||
"test:all": "npm run test:prerequisites && npm run build && npm run test:web && npm run test:mobile",
|
"test:all": "npm run test:prerequisites && npm run build && npm run test:web && npm run test:mobile",
|
||||||
"test:prerequisites": "node scripts/check-prerequisites.js",
|
"test:prerequisites": "node scripts/check-prerequisites.js",
|
||||||
"test:web": "npx playwright test -c playwright.config-local.ts --trace on",
|
"test:web": "npx playwright test -c playwright.config-local.ts --trace on",
|
||||||
@@ -22,14 +22,15 @@
|
|||||||
"check:ios-device": "xcrun xctrace list devices 2>&1 | grep -w 'Booted' || (echo 'No iOS simulator running' && exit 1)",
|
"check:ios-device": "xcrun xctrace list devices 2>&1 | grep -w 'Booted' || (echo 'No iOS simulator running' && exit 1)",
|
||||||
"clean:electron": "rimraf dist-electron",
|
"clean:electron": "rimraf dist-electron",
|
||||||
"build:pywebview": "vite build --config vite.config.pywebview.mts",
|
"build:pywebview": "vite build --config vite.config.pywebview.mts",
|
||||||
"build:electron": "npm run clean:electron && tsc -p tsconfig.electron.json && vite build --config vite.config.electron.mts && node scripts/build-electron.js",
|
|
||||||
"build:capacitor": "vite build --mode capacitor --config vite.config.capacitor.mts",
|
|
||||||
"build:web": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.web.mts",
|
"build:web": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.web.mts",
|
||||||
|
"build:web:electron": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.web.mts && VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.electron.mts --mode electron",
|
||||||
|
"build:electron": "npm run clean:electron && npm run build:web:electron && tsc -p tsconfig.electron.json && vite build --config vite.config.electron.mts && node scripts/build-electron.cjs",
|
||||||
|
"build:capacitor": "vite build --mode capacitor --config vite.config.capacitor.mts",
|
||||||
"electron:dev": "npm run build && electron .",
|
"electron:dev": "npm run build && electron .",
|
||||||
"electron:start": "electron .",
|
"electron:start": "electron .",
|
||||||
"clean:android": "adb uninstall app.timesafari.app || true",
|
"clean:android": "adb uninstall app.timesafari.app || true",
|
||||||
"build:android": "npm run clean:android && rm -rf dist && npm run build:web && npm run build:capacitor && cd android && ./gradlew clean && ./gradlew assembleDebug && cd .. && npx cap sync android && npx capacitor-assets generate --android && npx cap open android",
|
"build:android": "npm run clean:android && rm -rf dist && npm run build:web && npm run build:capacitor && cd android && ./gradlew clean && ./gradlew assembleDebug && cd .. && npx cap sync android && npx capacitor-assets generate --android && npx cap open android",
|
||||||
"electron:build-linux": "npm run build:electron && electron-builder --linux AppImage",
|
"electron:build-linux": "electron-builder --linux AppImage",
|
||||||
"electron:build-linux-deb": "npm run build:electron && electron-builder --linux deb",
|
"electron:build-linux-deb": "npm run build:electron && electron-builder --linux deb",
|
||||||
"electron:build-linux-prod": "NODE_ENV=production npm run build:electron && electron-builder --linux AppImage",
|
"electron:build-linux-prod": "NODE_ENV=production npm run build:electron && electron-builder --linux AppImage",
|
||||||
"build:electron-prod": "NODE_ENV=production npm run build:electron",
|
"build:electron-prod": "NODE_ENV=production npm run build:electron",
|
||||||
@@ -46,7 +47,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.2",
|
||||||
"@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",
|
||||||
@@ -57,8 +58,8 @@
|
|||||||
"@capacitor/ios": "^6.2.0",
|
"@capacitor/ios": "^6.2.0",
|
||||||
"@capacitor/share": "^6.0.3",
|
"@capacitor/share": "^6.0.3",
|
||||||
"@capawesome/capacitor-file-picker": "^6.2.0",
|
"@capawesome/capacitor-file-picker": "^6.2.0",
|
||||||
"@dicebear/collection": "^5.4.1",
|
"@dicebear/collection": "^5.4.3",
|
||||||
"@dicebear/core": "^5.4.1",
|
"@dicebear/core": "^5.4.3",
|
||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
"@ethersproject/wallet": "^5.8.0",
|
"@ethersproject/wallet": "^5.8.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||||
@@ -69,7 +70,7 @@
|
|||||||
"@peculiar/asn1-schema": "^2.3.8",
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
||||||
"@simplewebauthn/browser": "^10.0.0",
|
"@simplewebauthn/browser": "^10.0.0",
|
||||||
"@simplewebauthn/server": "^10.0.0",
|
"@simplewebauthn/server": "^10.0.1",
|
||||||
"@tweenjs/tween.js": "^21.1.1",
|
"@tweenjs/tween.js": "^21.1.1",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@veramo/core": "^5.6.0",
|
"@veramo/core": "^5.6.0",
|
||||||
@@ -86,6 +87,7 @@
|
|||||||
"absurd-sql": "^0.0.54",
|
"absurd-sql": "^0.0.54",
|
||||||
"asn1-ber": "^1.2.2",
|
"asn1-ber": "^1.2.2",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
|
"better-sqlite3-multiple-ciphers": "^11.10.0",
|
||||||
"cbor-x": "^1.5.9",
|
"cbor-x": "^1.5.9",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"dexie": "^3.2.7",
|
"dexie": "^3.2.7",
|
||||||
@@ -93,22 +95,23 @@
|
|||||||
"did-jwt": "^7.4.7",
|
"did-jwt": "^7.4.7",
|
||||||
"did-resolver": "^4.1.0",
|
"did-resolver": "^4.1.0",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"ethereum-cryptography": "^2.1.3",
|
"electron-json-storage": "^4.6.0",
|
||||||
|
"ethereum-cryptography": "^2.2.1",
|
||||||
"ethereumjs-util": "^7.1.5",
|
"ethereumjs-util": "^7.1.5",
|
||||||
"jdenticon": "^3.2.0",
|
"jdenticon": "^3.3.0",
|
||||||
"js-generate-password": "^0.1.9",
|
"js-generate-password": "^0.1.9",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsqr": "^1.4.0",
|
"jsqr": "^1.4.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"localstorage-slim": "^2.7.0",
|
"localstorage-slim": "^2.7.0",
|
||||||
"lru-cache": "^10.2.0",
|
"lru-cache": "^10.4.3",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"merkletreejs": "^0.3.11",
|
"merkletreejs": "^0.3.11",
|
||||||
"nostr-tools": "^2.10.4",
|
"nostr-tools": "^2.13.1",
|
||||||
"notiwind": "^2.0.2",
|
"notiwind": "^2.0.2",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"pina": "^0.20.2204228",
|
"pina": "^0.20.2204228",
|
||||||
"pinia-plugin-persistedstate": "^3.2.1",
|
"pinia-plugin-persistedstate": "^3.2.3",
|
||||||
"qr-code-generator-vue3": "^1.4.21",
|
"qr-code-generator-vue3": "^1.4.21",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"ramda": "^0.29.1",
|
"ramda": "^0.29.1",
|
||||||
@@ -124,12 +127,13 @@
|
|||||||
"vue-axios": "^3.5.2",
|
"vue-axios": "^3.5.2",
|
||||||
"vue-facing-decorator": "^3.0.4",
|
"vue-facing-decorator": "^3.0.4",
|
||||||
"vue-picture-cropper": "^0.7.0",
|
"vue-picture-cropper": "^0.7.0",
|
||||||
"vue-qrcode-reader": "^5.5.3",
|
"vue-qrcode-reader": "^5.7.2",
|
||||||
"vue-router": "^4.5.0",
|
"vue-router": "^4.5.0",
|
||||||
"web-did-resolver": "^2.0.27",
|
"web-did-resolver": "^2.0.30",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@capacitor-community/electron": "^5.0.1",
|
||||||
"@capacitor/assets": "^3.0.5",
|
"@capacitor/assets": "^3.0.5",
|
||||||
"@playwright/test": "^1.45.2",
|
"@playwright/test": "^1.45.2",
|
||||||
"@types/dom-webcodecs": "^0.1.7",
|
"@types/dom-webcodecs": "^0.1.7",
|
||||||
@@ -144,7 +148,7 @@
|
|||||||
"@types/ua-parser-js": "^0.7.39",
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
"@typescript-eslint/parser": "^6.21.0",
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.4",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"browserify-fs": "^1.0.0",
|
"browserify-fs": "^1.0.0",
|
||||||
@@ -164,26 +168,32 @@
|
|||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
"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-pwa": "^1.0.0"
|
||||||
},
|
},
|
||||||
"main": "./dist-electron/main.js",
|
"main": "./dist-electron/main.mjs",
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "app.timesafari.app",
|
"appId": "app.timesafari",
|
||||||
"productName": "TimeSafari",
|
"productName": "TimeSafari",
|
||||||
"directories": {
|
"directories": {
|
||||||
"output": "dist-electron-packages"
|
"output": "dist-electron-packages"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist-electron/**/*",
|
"dist-electron/**/*",
|
||||||
"dist/**/*"
|
"dist/**/*",
|
||||||
|
"capacitor.config.json"
|
||||||
],
|
],
|
||||||
"extraResources": [
|
"extraResources": [
|
||||||
{
|
{
|
||||||
"from": "dist-electron/www",
|
"from": "dist-electron/www",
|
||||||
"to": "www"
|
"to": "www"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "dist-electron/resources/preload.js",
|
||||||
|
"to": "preload.js"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"linux": {
|
"linux": {
|
||||||
@@ -221,5 +231,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"type": "module"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
96
scripts/build-electron.cjs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const fse = require("fs-extra");
|
||||||
|
const path = require("path");
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
|
console.log("Starting Electron build finalization...");
|
||||||
|
|
||||||
|
// Define paths
|
||||||
|
const distPath = path.join(__dirname, "..", "dist");
|
||||||
|
const electronDistPath = path.join(__dirname, "..", "dist-electron");
|
||||||
|
const wwwPath = path.join(electronDistPath, "www");
|
||||||
|
const builtIndexPath = path.join(distPath, "index.html");
|
||||||
|
const finalIndexPath = path.join(wwwPath, "index.html");
|
||||||
|
|
||||||
|
// Ensure target directory exists
|
||||||
|
if (!fs.existsSync(wwwPath)) {
|
||||||
|
fs.mkdirSync(wwwPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy assets directory
|
||||||
|
const assetsSrc = path.join(distPath, "assets");
|
||||||
|
const assetsDest = path.join(wwwPath, "assets");
|
||||||
|
if (fs.existsSync(assetsSrc)) {
|
||||||
|
fse.copySync(assetsSrc, assetsDest, { overwrite: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy favicon.ico
|
||||||
|
const faviconSrc = path.join(distPath, "favicon.ico");
|
||||||
|
if (fs.existsSync(faviconSrc)) {
|
||||||
|
fs.copyFileSync(faviconSrc, path.join(wwwPath, "favicon.ico"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy manifest.webmanifest
|
||||||
|
const manifestSrc = path.join(distPath, "manifest.webmanifest");
|
||||||
|
if (fs.existsSync(manifestSrc)) {
|
||||||
|
fs.copyFileSync(manifestSrc, path.join(wwwPath, "manifest.webmanifest"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load and modify index.html from Vite output
|
||||||
|
let indexContent = fs.readFileSync(builtIndexPath, "utf-8");
|
||||||
|
|
||||||
|
// Inject the window.process shim after the first <script> block
|
||||||
|
indexContent = indexContent.replace(
|
||||||
|
/<script[^>]*type="module"[^>]*>/,
|
||||||
|
match => `${match}\n window.process = { env: { VITE_PLATFORM: 'electron' } };`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write the modified index.html to dist-electron/www
|
||||||
|
fs.writeFileSync(finalIndexPath, indexContent);
|
||||||
|
|
||||||
|
// Copy preload script to resources
|
||||||
|
const preloadSrc = path.join(electronDistPath, "preload.mjs");
|
||||||
|
const preloadDest = path.join(electronDistPath, "resources", "preload.js");
|
||||||
|
|
||||||
|
// Ensure resources directory exists
|
||||||
|
const resourcesDir = path.join(electronDistPath, "resources");
|
||||||
|
if (!fs.existsSync(resourcesDir)) {
|
||||||
|
fs.mkdirSync(resourcesDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(preloadSrc)) {
|
||||||
|
// Read the preload script
|
||||||
|
let preloadContent = fs.readFileSync(preloadSrc, 'utf-8');
|
||||||
|
|
||||||
|
// Convert ESM to CommonJS if needed
|
||||||
|
preloadContent = preloadContent
|
||||||
|
.replace(/import\s*{\s*([^}]+)\s*}\s*from\s*['"]electron['"];?/g, 'const { $1 } = require("electron");')
|
||||||
|
.replace(/export\s*{([^}]+)};?/g, '')
|
||||||
|
.replace(/export\s+default\s+([^;]+);?/g, 'module.exports = $1;');
|
||||||
|
|
||||||
|
// Write the modified preload script
|
||||||
|
fs.writeFileSync(preloadDest, preloadContent);
|
||||||
|
console.log("Preload script copied and converted to resources directory");
|
||||||
|
} else {
|
||||||
|
console.error("Preload script not found at:", preloadSrc);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy capacitor.config.json to dist-electron
|
||||||
|
try {
|
||||||
|
console.log("Copying capacitor.config.json to dist-electron...");
|
||||||
|
const configPath = path.join(process.cwd(), 'capacitor.config.json');
|
||||||
|
const targetPath = path.join(process.cwd(), 'dist-electron', 'capacitor.config.json');
|
||||||
|
|
||||||
|
if (!fs.existsSync(configPath)) {
|
||||||
|
throw new Error('capacitor.config.json not found in project root');
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.copyFileSync(configPath, targetPath);
|
||||||
|
console.log("Successfully copied capacitor.config.json");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to copy capacitor.config.json:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Electron index.html copied and patched for Electron context.");
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
console.log('Starting electron build process...');
|
|
||||||
|
|
||||||
// Define paths
|
|
||||||
const electronDistPath = path.join(__dirname, '..', 'dist-electron');
|
|
||||||
const wwwPath = path.join(electronDistPath, 'www');
|
|
||||||
|
|
||||||
// Create www directory if it doesn't exist
|
|
||||||
if (!fs.existsSync(wwwPath)) {
|
|
||||||
fs.mkdirSync(wwwPath, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a platform-specific index.html for Electron
|
|
||||||
const initialIndexContent = `<!DOCTYPE html>
|
|
||||||
<html lang="">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,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
|
|
||||||
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');
|
|
||||||
if (fs.existsSync(indexPath)) {
|
|
||||||
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
|
|
||||||
console.log('Fixing asset paths in index.html...');
|
|
||||||
let modifiedIndexContent = fs.readFileSync(indexPath, 'utf8');
|
|
||||||
modifiedIndexContent = modifiedIndexContent
|
|
||||||
.replace(/\/assets\//g, './assets/')
|
|
||||||
.replace(/href="\//g, 'href="./')
|
|
||||||
.replace(/src="\//g, 'src="./');
|
|
||||||
|
|
||||||
fs.writeFileSync(indexPath, modifiedIndexContent);
|
|
||||||
|
|
||||||
// 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
|
|
||||||
console.log('After path fixing, checking for remaining /assets/ paths:', finalContent.includes('/assets/'));
|
|
||||||
console.log('Sample of fixed content:', finalContent.substring(0, 500));
|
|
||||||
|
|
||||||
console.log('Copied and fixed web files in:', wwwPath);
|
|
||||||
|
|
||||||
// Copy main process files
|
|
||||||
console.log('Copying main process files...');
|
|
||||||
|
|
||||||
// Copy the main process file instead of creating a template
|
|
||||||
const mainSrcPath = path.join(__dirname, '..', 'dist-electron', 'main.js');
|
|
||||||
const mainDestPath = path.join(electronDistPath, 'main.js');
|
|
||||||
|
|
||||||
if (fs.existsSync(mainSrcPath)) {
|
|
||||||
fs.copyFileSync(mainSrcPath, mainDestPath);
|
|
||||||
console.log('Copied main process file successfully');
|
|
||||||
} else {
|
|
||||||
console.error('Main process file not found at:', mainSrcPath);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Electron build process 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`);
|
||||||
|
|||||||
19
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 }"
|
||||||
@@ -459,9 +459,10 @@ export default class App extends Vue {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverSubscription = {
|
const serverSubscription =
|
||||||
...subscription,
|
typeof subscription === "object" && subscription !== null
|
||||||
};
|
? { ...subscription }
|
||||||
|
: {};
|
||||||
if (!allGoingOff) {
|
if (!allGoingOff) {
|
||||||
serverSubscription["notifyType"] = notification.title;
|
serverSubscription["notifyType"] = notification.title;
|
||||||
logger.log(
|
logger.log(
|
||||||
@@ -548,13 +549,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,894 +0,0 @@
|
|||||||
/** * Backup Files List Component * * Displays a list of backup files saved by
|
|
||||||
the app and provides options to: * - View backup files by type (contacts, seed,
|
|
||||||
other) * - Open individual files in the device's file viewer * - Access the
|
|
||||||
backup directory in the device's file explorer * * @component * @displayName
|
|
||||||
BackupFilesList * @example * ```vue *
|
|
||||||
<BackupFilesList />
|
|
||||||
* ``` */
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="backup-files-list">
|
|
||||||
<div class="flex justify-between items-center mb-4">
|
|
||||||
<h3 class="text-lg font-semibold">Backup Files</h3>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<button
|
|
||||||
v-if="platformCapabilities.hasFileSystem"
|
|
||||||
class="text-sm bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded"
|
|
||||||
:disabled="isLoading"
|
|
||||||
@click="refreshFiles()"
|
|
||||||
>
|
|
||||||
<font-awesome
|
|
||||||
icon="refresh"
|
|
||||||
class="fa-fw"
|
|
||||||
:class="{ 'animate-spin': isLoading }"
|
|
||||||
/>
|
|
||||||
Refresh
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="platformCapabilities.hasFileSystem"
|
|
||||||
class="text-sm bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded"
|
|
||||||
:disabled="isLoading"
|
|
||||||
@click="openBackupDirectory()"
|
|
||||||
>
|
|
||||||
<font-awesome icon="folder-open" class="fa-fw" />
|
|
||||||
Open Directory
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="platformCapabilities.hasFileSystem && isDevelopment"
|
|
||||||
class="text-sm bg-yellow-500 hover:bg-yellow-600 text-white px-3 py-1 rounded"
|
|
||||||
:disabled="isLoading"
|
|
||||||
title="Debug file discovery (development only)"
|
|
||||||
@click="debugFileDiscovery()"
|
|
||||||
>
|
|
||||||
<font-awesome icon="bug" class="fa-fw" />
|
|
||||||
Debug
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
:disabled="isLoading"
|
|
||||||
class="px-3 py-1 bg-green-500 text-white rounded text-sm hover:bg-green-600 disabled:opacity-50"
|
|
||||||
@click="createTestBackup"
|
|
||||||
>
|
|
||||||
Create Test Backup
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
:disabled="isLoading"
|
|
||||||
class="px-3 py-1 bg-purple-500 text-white rounded text-sm hover:bg-purple-600 disabled:opacity-50"
|
|
||||||
@click="testDirectoryContexts"
|
|
||||||
>
|
|
||||||
Test Contexts
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="isLoading" class="text-center py-4">
|
|
||||||
<font-awesome icon="spinner" class="animate-spin fa-2x" />
|
|
||||||
<p class="mt-2">Loading backup files...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-else-if="backupFiles.length === 0"
|
|
||||||
class="text-center py-4 text-gray-500"
|
|
||||||
>
|
|
||||||
<font-awesome icon="folder-open" class="fa-2x mb-2" />
|
|
||||||
<p>No backup files found</p>
|
|
||||||
<p class="text-sm mt-1">
|
|
||||||
Create backups using the export functions above
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
class="mt-3 p-3 bg-blue-50 border border-blue-200 rounded-lg text-left"
|
|
||||||
>
|
|
||||||
<p class="text-sm font-medium text-blue-800 mb-2">
|
|
||||||
💡 How to create backup files:
|
|
||||||
</p>
|
|
||||||
<ul class="text-xs text-blue-700 space-y-1">
|
|
||||||
<li>
|
|
||||||
• Use the "Export Contacts" button above to create contact backups
|
|
||||||
</li>
|
|
||||||
<li>• Use the "Export Seed" button to backup your recovery phrase</li>
|
|
||||||
<li>
|
|
||||||
• Backup files are saved to persistent storage that survives app
|
|
||||||
installations
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
v-if="platformCapabilities.isMobile && !platformCapabilities.isIOS"
|
|
||||||
class="text-orange-700"
|
|
||||||
>
|
|
||||||
• On Android: Files are saved to Downloads/TimeSafari or app data
|
|
||||||
directory
|
|
||||||
</li>
|
|
||||||
<li v-if="platformCapabilities.isIOS" class="text-orange-700">
|
|
||||||
• On iOS: Files are saved to Documents folder (accessible via Files
|
|
||||||
app)
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="space-y-2">
|
|
||||||
<!-- File Type Filter -->
|
|
||||||
<div class="flex gap-2 mb-3">
|
|
||||||
<button
|
|
||||||
v-for="type in ['all', 'contacts', 'seed', 'other'] as const"
|
|
||||||
:key="type"
|
|
||||||
:class="[
|
|
||||||
'text-sm px-3 py-1 rounded border',
|
|
||||||
selectedType === type
|
|
||||||
? 'bg-blue-500 text-white border-blue-500'
|
|
||||||
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50',
|
|
||||||
]"
|
|
||||||
@click="selectedType = type"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
type === "all"
|
|
||||||
? "All"
|
|
||||||
: type.charAt(0).toUpperCase() + type.slice(1)
|
|
||||||
}}
|
|
||||||
<span class="ml-1 text-xs"> ({{ getFileCountByType(type) }}) </span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Files List -->
|
|
||||||
<div class="flex items-center gap-2 mb-2">
|
|
||||||
<span v-for="(crumb, idx) in breadcrumbs" :key="idx">
|
|
||||||
<span
|
|
||||||
v-if="idx < breadcrumbs.length - 1"
|
|
||||||
class="text-blue-600 cursor-pointer underline"
|
|
||||||
@click="goToBreadcrumb(idx)"
|
|
||||||
>
|
|
||||||
{{ crumb }}
|
|
||||||
</span>
|
|
||||||
<span v-else class="font-bold">{{ crumb }}</span>
|
|
||||||
<span v-if="idx < breadcrumbs.length - 1"> / </span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="currentPath.length > 1" class="mb-2">
|
|
||||||
<button class="text-xs text-blue-500 underline" @click="goUp">
|
|
||||||
⬅ Up
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="mb-2">
|
|
||||||
<label class="inline-flex items-center">
|
|
||||||
<input
|
|
||||||
v-model="debugShowAll"
|
|
||||||
type="checkbox"
|
|
||||||
class="mr-2"
|
|
||||||
@change="loadDirectory"
|
|
||||||
/>
|
|
||||||
<span class="text-xs">Debug: Show all entries as files</span>
|
|
||||||
</label>
|
|
||||||
<span v-if="debugShowAll" class="text-xs text-red-600 ml-2"
|
|
||||||
>[Debug mode: forcibly treating all entries as files]</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2 max-h-64 overflow-y-auto">
|
|
||||||
<div
|
|
||||||
v-for="entry in folders"
|
|
||||||
:key="'folder-' + entry.path"
|
|
||||||
class="flex items-center justify-between p-3 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer"
|
|
||||||
@click="openFolder(entry)"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<font-awesome icon="folder" class="fa-fw text-yellow-500" />
|
|
||||||
<span class="font-medium">{{ entry.name }}</span>
|
|
||||||
<span
|
|
||||||
class="text-xs bg-gray-200 text-gray-700 px-2 py-0.5 rounded-full ml-2"
|
|
||||||
>Folder</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-for="entry in files"
|
|
||||||
:key="'file-' + entry.path"
|
|
||||||
class="flex items-center justify-between p-3 bg-white border border-gray-200 rounded-lg hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<font-awesome icon="file-alt" class="fa-fw text-gray-500" />
|
|
||||||
<span class="font-medium truncate">{{ entry.name }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-500 mt-1">
|
|
||||||
<span v-if="entry.size">{{ formatFileSize(entry.size) }}</span>
|
|
||||||
<span v-else>Size unknown</span>
|
|
||||||
<span
|
|
||||||
v-if="entry.path && !platformCapabilities.isIOS"
|
|
||||||
class="ml-2 text-xs text-blue-600"
|
|
||||||
>📁 {{ entry.path }}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2 ml-3">
|
|
||||||
<button
|
|
||||||
class="text-blue-500 hover:text-blue-700 p-1"
|
|
||||||
title="Open file"
|
|
||||||
@click="openFile(entry.uri, entry.name)"
|
|
||||||
>
|
|
||||||
<font-awesome icon="external-link-alt" class="fa-fw" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Summary -->
|
|
||||||
<div class="text-sm text-gray-500 mt-3 pt-3 border-t">
|
|
||||||
Showing {{ filteredFiles.length }} of {{ backupFiles.length }} backup
|
|
||||||
files
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-sm text-gray-600 mb-2">
|
|
||||||
<p>
|
|
||||||
📁 Backup files are saved to persistent storage that survives app
|
|
||||||
installations:
|
|
||||||
</p>
|
|
||||||
<ul class="list-disc list-inside ml-2 mt-1 text-xs">
|
|
||||||
<li v-if="platformCapabilities.isIOS">
|
|
||||||
iOS: Documents folder (accessible via Files app)
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
v-if="platformCapabilities.isMobile && !platformCapabilities.isIOS"
|
|
||||||
>
|
|
||||||
Android: Downloads/TimeSafari or external storage (accessible via
|
|
||||||
file managers)
|
|
||||||
</li>
|
|
||||||
<li v-if="!platformCapabilities.isMobile">
|
|
||||||
Desktop: User's download directory
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Vue, Watch } from "vue-facing-decorator";
|
|
||||||
import { NotificationIface } from "../constants/app";
|
|
||||||
import { logger } from "../utils/logger";
|
|
||||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
|
||||||
import {
|
|
||||||
PlatformService,
|
|
||||||
PlatformCapabilities,
|
|
||||||
} from "../services/PlatformService";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @vue-component
|
|
||||||
* Backup Files List Component
|
|
||||||
* Displays and manages backup files with platform-specific functionality
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
export default class BackupFilesList extends Vue {
|
|
||||||
/**
|
|
||||||
* Notification function injected by Vue
|
|
||||||
* Used to show success/error messages to the user
|
|
||||||
*/
|
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Platform service instance for platform-specific operations
|
|
||||||
*/
|
|
||||||
private platformService: PlatformService =
|
|
||||||
PlatformServiceFactory.getInstance();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Platform capabilities for the current platform
|
|
||||||
*/
|
|
||||||
private get platformCapabilities(): PlatformCapabilities {
|
|
||||||
return this.platformService.getCapabilities();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of backup files found on the device
|
|
||||||
*/
|
|
||||||
backupFiles: Array<{
|
|
||||||
name: string;
|
|
||||||
uri: string;
|
|
||||||
size?: number;
|
|
||||||
type: "contacts" | "seed" | "other";
|
|
||||||
path?: string;
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Currently selected file type filter
|
|
||||||
*/
|
|
||||||
selectedType: "all" | "contacts" | "seed" | "other" = "all";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loading state for file operations
|
|
||||||
*/
|
|
||||||
isLoading = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interval for periodic refresh (5 minutes)
|
|
||||||
*/
|
|
||||||
private refreshInterval: number | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current path for folder navigation (array for breadcrumbs)
|
|
||||||
*/
|
|
||||||
currentPath: string[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of files/folders in the current directory
|
|
||||||
*/
|
|
||||||
directoryEntries: Array<{
|
|
||||||
name: string;
|
|
||||||
uri: string;
|
|
||||||
size?: number;
|
|
||||||
path: string;
|
|
||||||
type: "file" | "folder";
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Temporary debug mode to show all entries as files
|
|
||||||
*/
|
|
||||||
debugShowAll = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks and requests storage permissions if needed.
|
|
||||||
* Returns true if permission is granted, false otherwise.
|
|
||||||
*/
|
|
||||||
private async ensureStoragePermission(): Promise<boolean> {
|
|
||||||
logger.log(
|
|
||||||
"[BackupFilesList] ensureStoragePermission called. platformCapabilities:",
|
|
||||||
this.platformCapabilities,
|
|
||||||
);
|
|
||||||
if (!this.platformCapabilities.hasFileSystem) return true;
|
|
||||||
// Only relevant for native platforms (Android/iOS)
|
|
||||||
const platformService = this.platformService as any;
|
|
||||||
if (typeof platformService.checkStoragePermissions === "function") {
|
|
||||||
try {
|
|
||||||
await platformService.checkStoragePermissions();
|
|
||||||
logger.log("[BackupFilesList] Storage permission granted.");
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("[BackupFilesList] Storage permission denied:", error);
|
|
||||||
|
|
||||||
// Get specific guidance for the platform
|
|
||||||
let guidance =
|
|
||||||
"This app needs permission to access your files to list and restore backups.";
|
|
||||||
if (
|
|
||||||
typeof platformService.getStoragePermissionGuidance === "function"
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
guidance = await platformService.getStoragePermissionGuidance();
|
|
||||||
} catch (guidanceError) {
|
|
||||||
logger.warn(
|
|
||||||
"[BackupFilesList] Could not get permission guidance:",
|
|
||||||
guidanceError,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "warning",
|
|
||||||
title: "Storage Permission Required",
|
|
||||||
text: guidance,
|
|
||||||
},
|
|
||||||
10000, // Show for 10 seconds to give user time to read
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lifecycle hook to load backup files when component is mounted
|
|
||||||
*/
|
|
||||||
async mounted() {
|
|
||||||
logger.log(
|
|
||||||
"[BackupFilesList] mounted hook called. platformCapabilities:",
|
|
||||||
this.platformCapabilities,
|
|
||||||
);
|
|
||||||
if (this.platformCapabilities.hasFileSystem) {
|
|
||||||
// Check/request permission before loading
|
|
||||||
const hasPermission = await this.ensureStoragePermission();
|
|
||||||
if (hasPermission) {
|
|
||||||
// Set default root path
|
|
||||||
if (this.platformCapabilities.isIOS) {
|
|
||||||
this.currentPath = ["."];
|
|
||||||
} else {
|
|
||||||
this.currentPath = ["Download", "TimeSafari"];
|
|
||||||
}
|
|
||||||
await this.loadDirectory();
|
|
||||||
this.refreshInterval = window.setInterval(
|
|
||||||
() => {
|
|
||||||
this.loadDirectory();
|
|
||||||
},
|
|
||||||
5 * 60 * 1000,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lifecycle hook to clean up resources when component is unmounted
|
|
||||||
*/
|
|
||||||
beforeUnmount() {
|
|
||||||
if (this.refreshInterval) {
|
|
||||||
clearInterval(this.refreshInterval);
|
|
||||||
this.refreshInterval = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed property for filtered files based on selected type
|
|
||||||
* Note: The 'All' tab count is sometimes too small. Logging for debugging.
|
|
||||||
*/
|
|
||||||
get filteredFiles() {
|
|
||||||
if (this.selectedType === "all") {
|
|
||||||
logger.log("[BackupFilesList] filteredFiles (All):", this.backupFiles);
|
|
||||||
return this.backupFiles;
|
|
||||||
}
|
|
||||||
const filtered = this.backupFiles.filter(
|
|
||||||
(file) => file.type === this.selectedType,
|
|
||||||
);
|
|
||||||
logger.log(
|
|
||||||
`[BackupFilesList] filteredFiles (${this.selectedType}):`,
|
|
||||||
filtered,
|
|
||||||
);
|
|
||||||
return filtered;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed property to check if we're in development mode
|
|
||||||
*/
|
|
||||||
get isDevelopment(): boolean {
|
|
||||||
return import.meta.env.DEV;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load the current directory entries
|
|
||||||
*/
|
|
||||||
async loadDirectory() {
|
|
||||||
if (!this.platformCapabilities.hasFileSystem) return;
|
|
||||||
this.isLoading = true;
|
|
||||||
try {
|
|
||||||
const path =
|
|
||||||
this.currentPath.join("/") ||
|
|
||||||
(this.platformCapabilities.isIOS ? "." : "Download/TimeSafari");
|
|
||||||
this.directoryEntries = await (
|
|
||||||
this.platformService as PlatformService
|
|
||||||
).listFilesInDirectory(path, this.debugShowAll);
|
|
||||||
logger.log("[BackupFilesList] Loaded directory:", {
|
|
||||||
path,
|
|
||||||
entries: this.directoryEntries,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("[BackupFilesList] Failed to load directory:", error);
|
|
||||||
this.directoryEntries = [];
|
|
||||||
} finally {
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigate into a folder
|
|
||||||
*/
|
|
||||||
async openFolder(entry: { name: string; path: string }) {
|
|
||||||
this.currentPath.push(entry.name);
|
|
||||||
await this.loadDirectory();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigate to a breadcrumb
|
|
||||||
*/
|
|
||||||
async goToBreadcrumb(index: number) {
|
|
||||||
this.currentPath = this.currentPath.slice(0, index + 1);
|
|
||||||
await this.loadDirectory();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Go up one directory
|
|
||||||
*/
|
|
||||||
async goUp() {
|
|
||||||
if (this.currentPath.length > 1) {
|
|
||||||
this.currentPath.pop();
|
|
||||||
await this.loadDirectory();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed property for breadcrumbs
|
|
||||||
*/
|
|
||||||
get breadcrumbs() {
|
|
||||||
return this.currentPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed property for showing files and folders
|
|
||||||
*/
|
|
||||||
get folders() {
|
|
||||||
return this.directoryEntries.filter((e) => e.type === "folder");
|
|
||||||
}
|
|
||||||
get files() {
|
|
||||||
return this.directoryEntries.filter((e) => e.type === "file");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refreshes the list of backup files from the device
|
|
||||||
*/
|
|
||||||
async refreshFiles() {
|
|
||||||
logger.log("[BackupFilesList] refreshFiles called.");
|
|
||||||
if (!this.platformCapabilities.hasFileSystem) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Check/request permission before refreshing
|
|
||||||
const hasPermission = await this.ensureStoragePermission();
|
|
||||||
if (!hasPermission) {
|
|
||||||
this.backupFiles = [];
|
|
||||||
this.isLoading = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.isLoading = true;
|
|
||||||
try {
|
|
||||||
this.backupFiles = await this.platformService.listBackupFiles();
|
|
||||||
logger.log("[BackupFilesList] Refreshed backup files:", {
|
|
||||||
count: this.backupFiles.length,
|
|
||||||
files: this.backupFiles.map((f) => ({
|
|
||||||
name: f.name,
|
|
||||||
type: f.type,
|
|
||||||
path: f.path,
|
|
||||||
size: f.size,
|
|
||||||
})),
|
|
||||||
platform: this.platformCapabilities.isIOS ? "iOS" : "Android",
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
// Debug: Log file type distribution
|
|
||||||
const typeCounts = {
|
|
||||||
contacts: this.backupFiles.filter((f) => f.type === "contacts").length,
|
|
||||||
seed: this.backupFiles.filter((f) => f.type === "seed").length,
|
|
||||||
other: this.backupFiles.filter((f) => f.type === "other").length,
|
|
||||||
total: this.backupFiles.length,
|
|
||||||
};
|
|
||||||
logger.log("[BackupFilesList] File type distribution:", typeCounts);
|
|
||||||
// Log the full backupFiles array for debugging the 'All' tab count
|
|
||||||
logger.log(
|
|
||||||
"[BackupFilesList] backupFiles array for All tab:",
|
|
||||||
this.backupFiles,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("[BackupFilesList] Failed to refresh backup files:", error);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error Loading Files",
|
|
||||||
text: "Failed to load backup files from your device.",
|
|
||||||
},
|
|
||||||
5000,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a test backup file for debugging purposes
|
|
||||||
*/
|
|
||||||
async createTestBackup() {
|
|
||||||
try {
|
|
||||||
this.isLoading = true;
|
|
||||||
logger.log("[BackupFilesList] Creating test backup file");
|
|
||||||
|
|
||||||
const result = await this.platformService.createTestBackupFile();
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
logger.log("[BackupFilesList] Test backup file created successfully:", {
|
|
||||||
fileName: result.fileName,
|
|
||||||
uri: result.uri,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "success",
|
|
||||||
title: "Test Backup Created",
|
|
||||||
text: `Test backup file "${result.fileName}" created successfully. Refresh the list to see it.`,
|
|
||||||
},
|
|
||||||
5000,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Refresh the file list to show the new test file
|
|
||||||
await this.refreshFiles();
|
|
||||||
} else {
|
|
||||||
throw new Error(result.error || "Failed to create test backup file");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(
|
|
||||||
"[BackupFilesList] Failed to create test backup file:",
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Test Backup Failed",
|
|
||||||
text: "Failed to create test backup file. Check the console for details.",
|
|
||||||
},
|
|
||||||
5000,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests different directory contexts to debug file visibility issues
|
|
||||||
*/
|
|
||||||
async testDirectoryContexts() {
|
|
||||||
try {
|
|
||||||
this.isLoading = true;
|
|
||||||
logger.log("[BackupFilesList] Testing directory contexts");
|
|
||||||
|
|
||||||
const debugOutput = await this.platformService.testDirectoryContexts();
|
|
||||||
|
|
||||||
logger.log(
|
|
||||||
"[BackupFilesList] Directory context test results:",
|
|
||||||
debugOutput,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Show the debug output in a notification or alert
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "info",
|
|
||||||
title: "Directory Context Test",
|
|
||||||
text: "Directory context test completed. Check the console for detailed results.",
|
|
||||||
},
|
|
||||||
5000,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Also log the full output to console for easy access
|
|
||||||
logger.log("=== Directory Context Test Results ===");
|
|
||||||
logger.log(debugOutput);
|
|
||||||
logger.log("=== End Test Results ===");
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(
|
|
||||||
"[BackupFilesList] Failed to test directory contexts:",
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Context Test Failed",
|
|
||||||
text: "Failed to test directory contexts. Check the console for details.",
|
|
||||||
},
|
|
||||||
5000,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refreshes the file list after a backup is created
|
|
||||||
* This method can be called from parent components
|
|
||||||
*/
|
|
||||||
async refreshAfterSave() {
|
|
||||||
logger.log("[BackupFilesList] refreshAfterSave called");
|
|
||||||
await this.refreshFiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens a specific file in the device's file viewer
|
|
||||||
* @param fileUri - URI of the file to open
|
|
||||||
* @param fileName - Name of the file for display
|
|
||||||
*/
|
|
||||||
async openFile(fileUri: string, fileName: string) {
|
|
||||||
try {
|
|
||||||
const result = await this.platformService.openFile(fileUri, fileName);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
logger.log("[BackupFilesList] File opened successfully:", {
|
|
||||||
fileName,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new Error(result.error || "Failed to open file");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("[BackupFilesList] Failed to open file:", error);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error Opening File",
|
|
||||||
text: `Failed to open ${fileName}. ${error instanceof Error ? error.message : String(error)}`,
|
|
||||||
},
|
|
||||||
5000,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens the backup directory in the device's file explorer
|
|
||||||
*/
|
|
||||||
async openBackupDirectory() {
|
|
||||||
try {
|
|
||||||
const result = await this.platformService.openBackupDirectory();
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
logger.log("[BackupFilesList] Backup directory opened successfully:", {
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new Error(result.error || "Failed to open backup directory");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("[BackupFilesList] Failed to open backup directory:", error);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error Opening Directory",
|
|
||||||
text: `Failed to open backup directory. ${error instanceof Error ? error.message : String(error)}`,
|
|
||||||
},
|
|
||||||
5000,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the count of files for a specific type
|
|
||||||
* Note: The 'All' tab count is sometimes too small. Logging for debugging.
|
|
||||||
*/
|
|
||||||
getFileCountByType(type: "all" | "contacts" | "seed" | "other"): number {
|
|
||||||
let count;
|
|
||||||
if (type === "all") {
|
|
||||||
count = this.backupFiles.length;
|
|
||||||
logger.log(
|
|
||||||
"[BackupFilesList] getFileCountByType (All):",
|
|
||||||
count,
|
|
||||||
this.backupFiles,
|
|
||||||
);
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
count = this.backupFiles.filter((file) => file.type === type).length;
|
|
||||||
logger.log(`[BackupFilesList] getFileCountByType (${type}):`, count);
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the appropriate icon for a file type
|
|
||||||
* @param type - File type
|
|
||||||
* @returns FontAwesome icon name
|
|
||||||
*/
|
|
||||||
getFileIcon(type: "contacts" | "seed" | "other"): string {
|
|
||||||
switch (type) {
|
|
||||||
case "contacts":
|
|
||||||
return "address-book";
|
|
||||||
case "seed":
|
|
||||||
return "key";
|
|
||||||
default:
|
|
||||||
return "file-alt";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the appropriate icon color for a file type
|
|
||||||
* @param type - File type
|
|
||||||
* @returns CSS color class
|
|
||||||
*/
|
|
||||||
getFileIconColor(type: "contacts" | "seed" | "other"): string {
|
|
||||||
switch (type) {
|
|
||||||
case "contacts":
|
|
||||||
return "text-blue-500";
|
|
||||||
case "seed":
|
|
||||||
return "text-orange-500";
|
|
||||||
default:
|
|
||||||
return "text-gray-500";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the appropriate badge color for a file type
|
|
||||||
* @param type - File type
|
|
||||||
* @returns CSS color class
|
|
||||||
*/
|
|
||||||
getTypeBadgeColor(type: "contacts" | "seed" | "other"): string {
|
|
||||||
switch (type) {
|
|
||||||
case "contacts":
|
|
||||||
return "bg-blue-100 text-blue-800";
|
|
||||||
case "seed":
|
|
||||||
return "bg-orange-100 text-orange-800";
|
|
||||||
default:
|
|
||||||
return "bg-gray-100 text-gray-800";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats file size in human-readable format
|
|
||||||
* @param bytes - File size in bytes
|
|
||||||
* @returns Formatted file size string
|
|
||||||
*/
|
|
||||||
formatFileSize(bytes: number): string {
|
|
||||||
if (bytes === 0) return "0 Bytes";
|
|
||||||
|
|
||||||
const k = 1024;
|
|
||||||
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Debug method to test file discovery
|
|
||||||
* Can be called from browser console for troubleshooting
|
|
||||||
*/
|
|
||||||
public async debugFileDiscovery() {
|
|
||||||
try {
|
|
||||||
logger.log("[BackupFilesList] Starting debug file discovery...");
|
|
||||||
|
|
||||||
// Test the platform service's test methods
|
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
|
||||||
|
|
||||||
// Test listing all user files
|
|
||||||
const allFilesResult = await platformService.testListUserFiles();
|
|
||||||
logger.log(
|
|
||||||
"[BackupFilesList] All user files test result:",
|
|
||||||
allFilesResult,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test listing backup files specifically
|
|
||||||
const backupFilesResult = await platformService.testBackupFiles();
|
|
||||||
logger.log(
|
|
||||||
"[BackupFilesList] Backup files test result:",
|
|
||||||
backupFilesResult,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Note: testListAllBackupFiles method is not part of the PlatformService interface
|
|
||||||
// It exists only in CapacitorPlatformService implementation
|
|
||||||
// If needed, this could be added to the interface or called via type assertion
|
|
||||||
|
|
||||||
// Test debug listing all files without filtering (if available)
|
|
||||||
if ("debugListAllFiles" in platformService) {
|
|
||||||
const debugAllFiles = await (
|
|
||||||
platformService as any
|
|
||||||
).debugListAllFiles();
|
|
||||||
logger.log("[BackupFilesList] Debug all files (no filtering):", {
|
|
||||||
count: debugAllFiles.length,
|
|
||||||
files: debugAllFiles.map((f: any) => ({
|
|
||||||
name: f.name,
|
|
||||||
path: f.path,
|
|
||||||
size: f.size,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test comprehensive step-by-step debug (if available)
|
|
||||||
if ("debugFileDiscoveryStepByStep" in platformService) {
|
|
||||||
const stepByStepDebug = await (
|
|
||||||
platformService as any
|
|
||||||
).debugFileDiscoveryStepByStep();
|
|
||||||
logger.log(
|
|
||||||
"[BackupFilesList] Step-by-step debug output:",
|
|
||||||
stepByStepDebug,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
allFiles: allFilesResult,
|
|
||||||
backupFiles: backupFilesResult,
|
|
||||||
currentBackupFiles: this.backupFiles,
|
|
||||||
debugAllFiles:
|
|
||||||
"debugListAllFiles" in platformService
|
|
||||||
? await (platformService as any).debugListAllFiles()
|
|
||||||
: null,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("[BackupFilesList] Debug file discovery failed:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch("platformCapabilities.hasFileSystem", { immediate: true })
|
|
||||||
async onFileSystemCapabilityChanged(newVal: boolean) {
|
|
||||||
if (newVal) {
|
|
||||||
await this.refreshFiles();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
/** * Data Export Section Component * * Provides UI and functionality for
|
/** * Data Export Section Component * * Provides UI and functionality for
|
||||||
exporting user data and backing up identifier seeds. * Includes buttons for seed
|
exporting user data and backing up identifier seeds. * Includes buttons for seed
|
||||||
backup and database export, with platform-specific download instructions. * Also
|
backup and database export, with platform-specific download instructions. * *
|
||||||
displays a list of backup files with options to open them in the device's file
|
@component * @displayName DataExportSection * @example * ```vue *
|
||||||
explorer. * * @component * @displayName DataExportSection * @example * ```vue *
|
|
||||||
<DataExportSection :active-did="currentDid" />
|
<DataExportSection :active-did="currentDid" />
|
||||||
* ``` */
|
* ``` */
|
||||||
|
|
||||||
@@ -25,7 +24,9 @@ explorer. * * @component * @displayName DataExportSection * @example * ```vue *
|
|||||||
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"
|
||||||
@@ -44,52 +45,38 @@ explorer. * * @component * @displayName DataExportSection * @example * ```vue *
|
|||||||
v-if="platformCapabilities.isIOS"
|
v-if="platformCapabilities.isIOS"
|
||||||
class="list-disc list-outside ml-4"
|
class="list-disc list-outside ml-4"
|
||||||
>
|
>
|
||||||
On iOS: Files are saved to Documents folder (accessible via Files app)
|
On iOS: You will be prompted to choose a location to save your backup
|
||||||
and persist between app installations.
|
file.
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="platformCapabilities.isMobile && !platformCapabilities.isIOS"
|
v-if="platformCapabilities.isMobile && !platformCapabilities.isIOS"
|
||||||
class="list-disc list-outside ml-4"
|
class="list-disc list-outside ml-4"
|
||||||
>
|
>
|
||||||
On Android: Files are saved to Downloads/TimeSafari or external
|
On Android: You will be prompted to choose a location to save your
|
||||||
storage (accessible via file managers) and persist between app
|
backup file.
|
||||||
installations.
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Backup Files List -->
|
|
||||||
<div
|
|
||||||
v-if="platformCapabilities.hasFileSystem"
|
|
||||||
class="mt-6 pt-6 border-t border-gray-300"
|
|
||||||
>
|
|
||||||
<BackupFilesList ref="backupFilesList" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<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 { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||||
|
import { db } from "../db/index";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { logger } from "../utils/logger";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
|
||||||
|
|
||||||
import { logger, getTimestampForFilename } 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";
|
|
||||||
import BackupFilesList from "./BackupFilesList.vue";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @vue-component
|
* @vue-component
|
||||||
* Data Export Section Component
|
* Data Export Section Component
|
||||||
* Handles database export and seed backup functionality with platform-specific behavior
|
* Handles database export and seed backup functionality with platform-specific behavior
|
||||||
*/
|
*/
|
||||||
@Component({ components: { BackupFilesList } })
|
@Component
|
||||||
export default class DataExportSection extends Vue {
|
export default class DataExportSection extends Vue {
|
||||||
/**
|
/**
|
||||||
* Notification function injected by Vue
|
* Notification function injected by Vue
|
||||||
@@ -144,27 +131,24 @@ export default class DataExportSection extends Vue {
|
|||||||
*/
|
*/
|
||||||
public async exportDatabase() {
|
public async exportDatabase() {
|
||||||
try {
|
try {
|
||||||
let allContacts: Contact[] = [];
|
if (!USE_DEXIE_DB) {
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
throw new Error("Not implemented");
|
||||||
const result = await platformService.dbQuery(`SELECT * FROM contacts`);
|
|
||||||
if (result) {
|
|
||||||
allContacts = databaseUtil.mapQueryResultToValues(
|
|
||||||
result,
|
|
||||||
) as unknown as Contact[];
|
|
||||||
}
|
}
|
||||||
// if (USE_DEXIE_DB) {
|
const blob = await db.export({
|
||||||
// await db.open();
|
prettyJson: true,
|
||||||
// allContacts = await db.contacts.toArray();
|
transform: (table, value, key) => {
|
||||||
// }
|
if (table === "contacts") {
|
||||||
|
// Dexie inserts a number 0 when some are undefined, so we need to totally remove them.
|
||||||
// Convert contacts to export format
|
Object.keys(value).forEach((prop) => {
|
||||||
const exportData = contactsToExportJson(allContacts);
|
if (value[prop] === undefined) {
|
||||||
const jsonStr = JSON.stringify(exportData, null, 2);
|
delete value[prop];
|
||||||
const blob = new Blob([jsonStr], { type: "application/json" });
|
}
|
||||||
|
});
|
||||||
// Create timestamped filename
|
}
|
||||||
const timestamp = getTimestampForFilename();
|
return { value, key };
|
||||||
const fileName = `${AppString.APP_NAME_NO_SPACES}-backup-contacts-${timestamp}.json`;
|
},
|
||||||
|
});
|
||||||
|
const fileName = `${AppString.APP_NAME_NO_SPACES}-backup.json`;
|
||||||
|
|
||||||
if (this.platformCapabilities.hasFileDownload) {
|
if (this.platformCapabilities.hasFileDownload) {
|
||||||
// Web platform: Use download link
|
// Web platform: Use download link
|
||||||
@@ -175,21 +159,9 @@ export default class DataExportSection extends Vue {
|
|||||||
downloadAnchor.click();
|
downloadAnchor.click();
|
||||||
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 user-accessible location and share
|
// Native platform: Write to app directory
|
||||||
const result = await this.platformService.writeAndShareFile(
|
const content = await blob.text();
|
||||||
fileName,
|
await this.platformService.writeAndShareFile(fileName, content);
|
||||||
jsonStr,
|
|
||||||
{
|
|
||||||
allowLocationSelection: true,
|
|
||||||
showLocationSelectionDialog: true,
|
|
||||||
mimeType: "application/json",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle the result
|
|
||||||
if (!result.saved) {
|
|
||||||
throw new Error(result.error || "Failed to save file");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error("This platform does not support file downloads.");
|
throw new Error("This platform does not support file downloads.");
|
||||||
}
|
}
|
||||||
@@ -200,20 +172,11 @@ 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."
|
||||||
: "Backup saved to persistent storage that survives app installations. Use the share dialog to access your file and choose where to save it permanently.",
|
: "You should have been prompted to save your backup file.",
|
||||||
},
|
},
|
||||||
5000,
|
-1,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Refresh the backup files list
|
|
||||||
const backupFilesList = this.$refs.backupFilesList as any;
|
|
||||||
if (
|
|
||||||
backupFilesList &&
|
|
||||||
typeof backupFilesList.refreshAfterSave === "function"
|
|
||||||
) {
|
|
||||||
await backupFilesList.refreshAfterSave();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Export Error:", error);
|
logger.error("Export Error:", error);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -247,18 +210,5 @@ export default class DataExportSection extends Vue {
|
|||||||
hidden: !this.downloadUrl || !this.platformCapabilities.hasFileDownload,
|
hidden: !this.downloadUrl || !this.platformCapabilities.hasFileDownload,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async mounted() {
|
|
||||||
// Ensure permissions are requested and refresh backup files list on mount
|
|
||||||
if (this.platformCapabilities.hasFileSystem) {
|
|
||||||
const backupFilesList = this.$refs.backupFilesList as any;
|
|
||||||
if (
|
|
||||||
backupFilesList &&
|
|
||||||
typeof backupFilesList.refreshFiles === "function"
|
|
||||||
) {
|
|
||||||
await backupFilesList.refreshFiles();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ import { USE_DEXIE_DB } from "@/constants/app";
|
|||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
@@ -142,23 +143,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({
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
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 platformService = PlatformServiceFactory.getInstance();
|
||||||
filterFeedByNearby: this.isNearby,
|
await platformService.dbExec(
|
||||||
});
|
`UPDATE settings SET filterFeedByNearby = ? WHERE id = ?`,
|
||||||
|
[this.isNearby, MASTER_SETTINGS_KEY],
|
||||||
|
);
|
||||||
|
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
@@ -172,10 +169,11 @@ export default class FeedFilters extends Vue {
|
|||||||
this.settingChanged = true;
|
this.settingChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await databaseUtil.updateDefaultSettings({
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
filterFeedByNearby: false,
|
await platformService.dbExec(
|
||||||
filterFeedByVisible: false,
|
`UPDATE settings SET filterFeedByNearby = ? AND filterFeedByVisible = ? WHERE id = ?`,
|
||||||
});
|
[false, false, MASTER_SETTINGS_KEY],
|
||||||
|
);
|
||||||
|
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
@@ -193,10 +191,11 @@ export default class FeedFilters extends Vue {
|
|||||||
this.settingChanged = true;
|
this.settingChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await databaseUtil.updateDefaultSettings({
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
filterFeedByNearby: true,
|
await platformService.dbExec(
|
||||||
filterFeedByVisible: true,
|
`UPDATE settings SET filterFeedByNearby = ? AND filterFeedByVisible = ? WHERE id = ?`,
|
||||||
});
|
[true, true, MASTER_SETTINGS_KEY],
|
||||||
|
);
|
||||||
|
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
|||||||
@@ -227,7 +227,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 &&
|
||||||
@@ -246,8 +245,9 @@ export default class GivenPrompts extends Vue {
|
|||||||
[someContactDbIndex],
|
[someContactDbIndex],
|
||||||
);
|
);
|
||||||
if (result) {
|
if (result) {
|
||||||
const mappedContacts = databaseUtil.mapQueryResultToValues(result);
|
this.currentContact = databaseUtil.mapQueryResultToValues(result)[
|
||||||
this.currentContact = mappedContacts[0] as unknown as Contact;
|
someContactDbIndex
|
||||||
|
] as unknown as Contact;
|
||||||
}
|
}
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
await db.open();
|
await db.open();
|
||||||
|
|||||||
@@ -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,7 +247,6 @@ 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 {
|
import {
|
||||||
DEFAULT_IMAGE_API_SERVER,
|
DEFAULT_IMAGE_API_SERVER,
|
||||||
NotificationIface,
|
NotificationIface,
|
||||||
@@ -268,7 +254,7 @@ import {
|
|||||||
} from "../constants/app";
|
} 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, getTimestampForFilename } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
|
|
||||||
@@ -281,11 +267,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 +308,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;
|
||||||
|
|
||||||
@@ -391,16 +369,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 +446,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 +499,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 +525,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 +534,7 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
|
this.showCameraPreview = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -576,7 +564,7 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
(blob) => {
|
(blob) => {
|
||||||
if (blob) {
|
if (blob) {
|
||||||
this.blob = blob;
|
this.blob = blob;
|
||||||
this.fileName = `photo-${getTimestampForFilename()}.jpg`;
|
this.fileName = `photo_${Date.now()}.jpg`;
|
||||||
this.showRetry = true;
|
this.showRetry = true;
|
||||||
this.stopCameraPreview();
|
this.stopCameraPreview();
|
||||||
}
|
}
|
||||||
@@ -598,21 +586,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 +620,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 +674,6 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
);
|
);
|
||||||
this.uploading = false;
|
this.uploading = false;
|
||||||
this.blob = undefined;
|
this.blob = undefined;
|
||||||
this.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -710,14 +681,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>
|
||||||
|
|
||||||
|
|||||||
@@ -301,7 +301,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 +342,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 +352,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,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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">
|
||||||
@@ -259,7 +260,7 @@ export default class OnboardingDialog extends Vue {
|
|||||||
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 databaseUtil.updateAccountSettings(this.activeDid, {
|
||||||
finishedOnboarding: true,
|
finishedOnboarding: true,
|
||||||
});
|
});
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
@@ -273,7 +274,7 @@ export default class OnboardingDialog extends Vue {
|
|||||||
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 databaseUtil.updateAccountSettings(this.activeDid, {
|
||||||
finishedOnboarding: true,
|
finishedOnboarding: true,
|
||||||
});
|
});
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ import {
|
|||||||
import * as databaseUtil from "../db/databaseUtil";
|
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, getTimestampForFilename } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||||
|
|
||||||
@Component({ components: { VuePictureCropper } })
|
@Component({ components: { VuePictureCropper } })
|
||||||
@@ -393,7 +393,7 @@ export default class PhotoDialog extends Vue {
|
|||||||
(blob) => {
|
(blob) => {
|
||||||
if (blob) {
|
if (blob) {
|
||||||
this.blob = blob;
|
this.blob = blob;
|
||||||
this.fileName = `photo-${getTimestampForFilename()}.jpg`;
|
this.fileName = `photo_${Date.now()}.jpg`;
|
||||||
this.stopCameraPreview();
|
this.stopCameraPreview();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
|||||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class UserNameDialog extends Vue {
|
export default class UserNameDialog extends Vue {
|
||||||
@@ -72,11 +71,9 @@ export default class UserNameDialog extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onClickSaveChanges() {
|
async onClickSaveChanges() {
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
await databaseUtil.updateDefaultSettings({
|
||||||
await platformService.dbExec(
|
firstName: this.givenName,
|
||||||
"UPDATE settings SET firstName = ? WHERE id = ?",
|
});
|
||||||
[this.givenName, MASTER_SETTINGS_KEY],
|
|
||||||
);
|
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
firstName: this.givenName,
|
firstName: this.givenName,
|
||||||
|
|||||||