Compare commits
66 Commits
master
...
sql-absurd
Author | SHA1 | Date |
---|---|---|
|
981920dd7a | 4 months ago |
|
d189c39062 | 4 months ago |
|
8edddb1a57 | 4 months ago |
|
9eb07b3258 | 4 months ago |
|
e5dffc30ff | 4 months ago |
|
0b4e885edd | 4 months ago |
|
b6d9b29720 | 4 months ago |
|
b5348e42a7 | 4 months ago |
|
a4fb3eea2d | 4 months ago |
|
5d12c76693 | 4 months ago |
|
d426f9c4ac | 4 months ago |
|
340a574325 | 4 months ago |
|
98b3a35e3c | 4 months ago |
|
409de21fc4 | 4 months ago |
|
17c9d32f49 | 4 months ago |
|
25e4db395a | 4 months ago |
|
b6ee30892f | 4 months ago |
|
b01a450733 | 4 months ago |
|
596f3355bf | 4 months ago |
|
e1f9a6fa08 | 4 months ago |
|
340e718199 | 4 months ago |
|
5d97c98ae8 | 4 months ago |
|
ec74fff892 | 4 months ago |
|
1e88c0e26f | 4 months ago |
|
3ec2364394 | 4 months ago |
|
8b215c909d | 4 months ago |
|
91a1c05473 | 4 months ago |
|
66929d9b14 | 4 months ago |
|
1e63ddcb6e | 4 months ago |
|
51f5755f5c | 4 months ago |
|
e5a3d622b6 | 4 months ago |
|
a6edcd6269 | 4 months ago |
|
b7b6be5831 | 4 months ago |
|
cbaca0304d | 4 months ago |
|
59d711bd90 | 4 months ago |
|
c355de6e33 | 4 months ago |
|
28c114a2c7 | 4 months ago |
|
dabfe33fbe | 4 months ago |
|
d8f2587d1c | 4 months ago |
|
3946a8a27a | 4 months ago |
|
4c40b80718 | 4 months ago |
|
74989c2b64 | 4 months ago |
|
7e17b41444 | 4 months ago |
|
83acb028c7 | 4 months ago |
|
786f07e067 | 4 months ago |
|
710cc1683c | 4 months ago |
|
ebef5d6c8d | 4 months ago |
|
43ea7ee610 | 4 months ago |
|
57191df416 | 4 months ago |
|
644593a5f4 | 4 months ago |
|
900c2521c7 | 4 months ago |
|
182cff2b16 | 4 months ago |
|
3b4ef908f3 | 4 months ago |
|
a5a9e15ece | 4 months ago |
|
a6d8f0eb8a | 4 months ago |
|
3997a88b44 | 4 months ago |
|
5eeeae32c6 | 4 months ago |
|
d9895086e6 | 4 months ago |
|
fb8d1cb8b2 | 4 months ago |
|
70c0edbed0 | 4 months ago |
|
55cc08d675 | 4 months ago |
|
688a5be76e | 4 months ago |
|
014341f320 | 4 months ago |
|
1d5e062c76 | 4 months ago |
|
2c5c15108a | 4 months ago |
|
26df0fb671 | 4 months ago |
@ -1,101 +0,0 @@ |
|||||
VM5:29 [Preload] Preload script starting... |
|
||||
VM5:29 [Preload] Preload script completed successfully |
|
||||
main.common-DiOUyXe7.js:27 Platform Object |
|
||||
error @ main.common-DiOUyXe7.js:27 |
|
||||
main.common-DiOUyXe7.js:27 PWA enabled Object |
|
||||
error @ main.common-DiOUyXe7.js:27 |
|
||||
main.common-DiOUyXe7.js:27 [Web] PWA enabled Object |
|
||||
error @ main.common-DiOUyXe7.js:27 |
|
||||
main.common-DiOUyXe7.js:27 [Web] Platform Object |
|
||||
error @ main.common-DiOUyXe7.js:27 |
|
||||
main.common-DiOUyXe7.js:29 Opened! |
|
||||
main.common-DiOUyXe7.js:2552 Failed to log to database: Error: no such column: value |
|
||||
at E.handleError (main.common-DiOUyXe7.js:27:21133) |
|
||||
at E.exec (main.common-DiOUyXe7.js:27:19785) |
|
||||
at Rc.processQueue (main.common-DiOUyXe7.js:2379:2368) |
|
||||
F7 @ main.common-DiOUyXe7.js:2552 |
|
||||
main.common-DiOUyXe7.js:2552 Original message: PWA enabled - [{"pwa_enabled":false}] |
|
||||
F7 @ main.common-DiOUyXe7.js:2552 |
|
||||
main.common-DiOUyXe7.js:2552 Failed to log to database: Error: no such column: value |
|
||||
at E.handleError (main.common-DiOUyXe7.js:27:21133) |
|
||||
at E.exec (main.common-DiOUyXe7.js:27:19785) |
|
||||
at Rc.processQueue (main.common-DiOUyXe7.js:2379:2368) |
|
||||
at main.common-DiOUyXe7.js:2379:2816 |
|
||||
at new Promise (<anonymous>) |
|
||||
at Rc.queueOperation (main.common-DiOUyXe7.js:2379:2685) |
|
||||
at Rc.query (main.common-DiOUyXe7.js:2379:3378) |
|
||||
at async F7 (main.common-DiOUyXe7.js:2552:117) |
|
||||
F7 @ main.common-DiOUyXe7.js:2552 |
|
||||
main.common-DiOUyXe7.js:2552 Original message: [Web] PWA enabled - [{"pwa_enabled":false}] |
|
||||
F7 @ main.common-DiOUyXe7.js:2552 |
|
||||
main.common-DiOUyXe7.js:2552 Failed to log to database: Error: no such column: value |
|
||||
at E.handleError (main.common-DiOUyXe7.js:27:21133) |
|
||||
at E.exec (main.common-DiOUyXe7.js:27:19785) |
|
||||
at Rc.processQueue (main.common-DiOUyXe7.js:2379:2368) |
|
||||
at main.common-DiOUyXe7.js:2379:2816 |
|
||||
at new Promise (<anonymous>) |
|
||||
at Rc.queueOperation (main.common-DiOUyXe7.js:2379:2685) |
|
||||
at Rc.query (main.common-DiOUyXe7.js:2379:3378) |
|
||||
at async F7 (main.common-DiOUyXe7.js:2552:117) |
|
||||
F7 @ main.common-DiOUyXe7.js:2552 |
|
||||
main.common-DiOUyXe7.js:2552 Original message: [Web] Platform - [{"platform":"web"}] |
|
||||
F7 @ main.common-DiOUyXe7.js:2552 |
|
||||
main.common-DiOUyXe7.js:2552 Failed to log to database: Error: no such column: value |
|
||||
at E.handleError (main.common-DiOUyXe7.js:27:21133) |
|
||||
at E.exec (main.common-DiOUyXe7.js:27:19785) |
|
||||
at Rc.processQueue (main.common-DiOUyXe7.js:2379:2368) |
|
||||
at main.common-DiOUyXe7.js:2379:2816 |
|
||||
at new Promise (<anonymous>) |
|
||||
at Rc.queueOperation (main.common-DiOUyXe7.js:2379:2685) |
|
||||
at Rc.query (main.common-DiOUyXe7.js:2379:3378) |
|
||||
at async F7 (main.common-DiOUyXe7.js:2552:117) |
|
||||
F7 @ main.common-DiOUyXe7.js:2552 |
|
||||
main.common-DiOUyXe7.js:2552 Original message: Platform - [{"platform":"web"}] |
|
||||
F7 @ main.common-DiOUyXe7.js:2552 |
|
||||
main.common-DiOUyXe7.js:2100 |
|
||||
|
|
||||
|
|
||||
GET https://api.endorser.ch/api/report/rateLimits 400 (Bad Request) |
|
||||
(anonymous) @ main.common-DiOUyXe7.js:2100 |
|
||||
xhr @ main.common-DiOUyXe7.js:2100 |
|
||||
p6 @ main.common-DiOUyXe7.js:2102 |
|
||||
_request @ main.common-DiOUyXe7.js:2103 |
|
||||
request @ main.common-DiOUyXe7.js:2102 |
|
||||
Yc.<computed> @ main.common-DiOUyXe7.js:2103 |
|
||||
(anonymous) @ main.common-DiOUyXe7.js:2098 |
|
||||
dJ @ main.common-DiOUyXe7.js:2295 |
|
||||
main.common-DiOUyXe7.js:2100 |
|
||||
|
|
||||
|
|
||||
GET https://api.endorser.ch/api/report/rateLimits 400 (Bad Request) |
|
||||
(anonymous) @ main.common-DiOUyXe7.js:2100 |
|
||||
xhr @ main.common-DiOUyXe7.js:2100 |
|
||||
p6 @ main.common-DiOUyXe7.js:2102 |
|
||||
_request @ main.common-DiOUyXe7.js:2103 |
|
||||
request @ main.common-DiOUyXe7.js:2102 |
|
||||
Yc.<computed> @ main.common-DiOUyXe7.js:2103 |
|
||||
(anonymous) @ main.common-DiOUyXe7.js:2098 |
|
||||
dJ @ main.common-DiOUyXe7.js:2295 |
|
||||
await in dJ |
|
||||
checkRegistrationStatus @ HomeView-DJMSCuMg.js:1 |
|
||||
mounted @ HomeView-DJMSCuMg.js:1 |
|
||||
XMLHttpRequest.send |
|
||||
(anonymous) @ main.common-DiOUyXe7.js:2100 |
|
||||
xhr @ main.common-DiOUyXe7.js:2100 |
|
||||
p6 @ main.common-DiOUyXe7.js:2102 |
|
||||
_request @ main.common-DiOUyXe7.js:2103 |
|
||||
request @ main.common-DiOUyXe7.js:2102 |
|
||||
Yc.<computed> @ main.common-DiOUyXe7.js:2103 |
|
||||
(anonymous) @ main.common-DiOUyXe7.js:2098 |
|
||||
ZG @ main.common-DiOUyXe7.js:2295 |
|
||||
await in ZG |
|
||||
initializeIdentity @ HomeView-DJMSCuMg.js:1 |
|
||||
XMLHttpRequest.send |
|
||||
(anonymous) @ main.common-DiOUyXe7.js:2100 |
|
||||
xhr @ main.common-DiOUyXe7.js:2100 |
|
||||
p6 @ main.common-DiOUyXe7.js:2102 |
|
||||
_request @ main.common-DiOUyXe7.js:2103 |
|
||||
request @ main.common-DiOUyXe7.js:2102 |
|
||||
Yc.<computed> @ main.common-DiOUyXe7.js:2103 |
|
||||
(anonymous) @ main.common-DiOUyXe7.js:2098 |
|
||||
dJ @ main.common-DiOUyXe7.js:2295 |
|
@ -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 |
@ -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/ |
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 142 KiB |
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 121 KiB |
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 159 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@ -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" |
||||
|
] |
||||
|
} |
||||
|
} |
@ -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" |
||||
|
} |
||||
|
} |
@ -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(); |
||||
|
})(); |
@ -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" |
||||
|
] |
||||
|
} |
@ -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; |
@ -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
|
@ -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'); |
||||
|
}); |
@ -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, |
||||
|
} |
@ -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, |
||||
|
}); |
||||
|
////////////////////////////////////////////////////////
|
@ -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; |
@ -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'; |
||||
|
} |
||||
|
} |
@ -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(' ') |
||||
|
}, |
||||
|
}); |
||||
|
}); |
||||
|
} |
@ -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 |
||||
|
} |
||||
|
} |
||||
|
|
@ -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 |
@ -1,5 +1,6 @@ |
|||||
eth_keys |
eth_keys |
||||
pywebview |
pywebview |
||||
pyinstaller>=6.12.0 |
pyinstaller>=6.12.0 |
||||
|
setuptools>=69.0.0 # Required for distutils for electron-builder on macOS |
||||
# For development |
# For development |
||||
watchdog>=3.0.0 # For file watching support |
watchdog>=3.0.0 # For file watching support |
@ -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'); |
|
@ -1,174 +0,0 @@ |
|||||
const { app, BrowserWindow } = require("electron"); |
|
||||
const path = require("path"); |
|
||||
const fs = require("fs"); |
|
||||
const logger = require("../utils/logger"); |
|
||||
|
|
||||
// Check if running in dev mode
|
|
||||
const isDev = process.argv.includes("--inspect"); |
|
||||
|
|
||||
function createWindow() { |
|
||||
// Add before createWindow function
|
|
||||
const preloadPath = path.join(__dirname, "preload.js"); |
|
||||
logger.log("Checking preload path:", preloadPath); |
|
||||
logger.log("Preload exists:", fs.existsSync(preloadPath)); |
|
||||
|
|
||||
// Create the browser window.
|
|
||||
const mainWindow = new BrowserWindow({ |
|
||||
width: 1200, |
|
||||
height: 800, |
|
||||
webPreferences: { |
|
||||
nodeIntegration: false, |
|
||||
contextIsolation: true, |
|
||||
webSecurity: true, |
|
||||
allowRunningInsecureContent: false, |
|
||||
preload: path.join(__dirname, "preload.js"), |
|
||||
}, |
|
||||
}); |
|
||||
|
|
||||
// Always open DevTools for now
|
|
||||
mainWindow.webContents.openDevTools(); |
|
||||
|
|
||||
// Intercept requests to fix asset paths
|
|
||||
mainWindow.webContents.session.webRequest.onBeforeRequest( |
|
||||
{ |
|
||||
urls: [ |
|
||||
"file://*/*/assets/*", |
|
||||
"file://*/assets/*", |
|
||||
"file:///assets/*", // Catch absolute paths
|
|
||||
"<all_urls>", // Catch all URLs as a fallback
|
|
||||
], |
|
||||
}, |
|
||||
(details, callback) => { |
|
||||
let url = details.url; |
|
||||
|
|
||||
// Handle paths that don't start with file://
|
|
||||
if (!url.startsWith("file://") && url.includes("/assets/")) { |
|
||||
url = `file://${path.join(__dirname, "www", url)}`; |
|
||||
} |
|
||||
|
|
||||
// Handle absolute paths starting with /assets/
|
|
||||
if (url.includes("/assets/") && !url.includes("/www/assets/")) { |
|
||||
const baseDir = url.includes("dist-electron") |
|
||||
? url.substring( |
|
||||
0, |
|
||||
url.indexOf("/dist-electron") + "/dist-electron".length, |
|
||||
) |
|
||||
: `file://${__dirname}`; |
|
||||
const assetPath = url.split("/assets/")[1]; |
|
||||
const newUrl = `${baseDir}/www/assets/${assetPath}`; |
|
||||
callback({ redirectURL: newUrl }); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
callback({}); // No redirect for other URLs
|
|
||||
}, |
|
||||
); |
|
||||
|
|
||||
if (isDev) { |
|
||||
// Debug info
|
|
||||
logger.log("Debug Info:"); |
|
||||
logger.log("Running in dev mode:", isDev); |
|
||||
logger.log("App is packaged:", app.isPackaged); |
|
||||
logger.log("Process resource path:", process.resourcesPath); |
|
||||
logger.log("App path:", app.getAppPath()); |
|
||||
logger.log("__dirname:", __dirname); |
|
||||
logger.log("process.cwd():", process.cwd()); |
|
||||
} |
|
||||
|
|
||||
const indexPath = path.join(__dirname, "www", "index.html"); |
|
||||
|
|
||||
if (isDev) { |
|
||||
logger.log("Loading index from:", indexPath); |
|
||||
logger.log("www path:", path.join(__dirname, "www")); |
|
||||
logger.log("www assets path:", path.join(__dirname, "www", "assets")); |
|
||||
} |
|
||||
|
|
||||
if (!fs.existsSync(indexPath)) { |
|
||||
logger.error(`Index file not found at: ${indexPath}`); |
|
||||
throw new Error("Index file not found"); |
|
||||
} |
|
||||
|
|
||||
// Add CSP headers to allow API connections
|
|
||||
mainWindow.webContents.session.webRequest.onHeadersReceived( |
|
||||
(details, callback) => { |
|
||||
callback({ |
|
||||
responseHeaders: { |
|
||||
...details.responseHeaders, |
|
||||
"Content-Security-Policy": [ |
|
||||
"default-src 'self';" + |
|
||||
"connect-src 'self' https://api.endorser.ch https://*.timesafari.app;" + |
|
||||
"img-src 'self' data: https: blob:;" + |
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval';" + |
|
||||
"style-src 'self' 'unsafe-inline';" + |
|
||||
"font-src 'self' data:;", |
|
||||
], |
|
||||
}, |
|
||||
}); |
|
||||
}, |
|
||||
); |
|
||||
|
|
||||
// Load the index.html
|
|
||||
mainWindow |
|
||||
.loadFile(indexPath) |
|
||||
.then(() => { |
|
||||
logger.log("Successfully loaded index.html"); |
|
||||
if (isDev) { |
|
||||
mainWindow.webContents.openDevTools(); |
|
||||
logger.log("DevTools opened - running in dev mode"); |
|
||||
} |
|
||||
}) |
|
||||
.catch((err) => { |
|
||||
logger.error("Failed to load index.html:", err); |
|
||||
logger.error("Attempted path:", indexPath); |
|
||||
}); |
|
||||
|
|
||||
// Listen for console messages from the renderer
|
|
||||
mainWindow.webContents.on("console-message", (_event, level, message) => { |
|
||||
logger.log("Renderer Console:", message); |
|
||||
}); |
|
||||
|
|
||||
// Add right after creating the BrowserWindow
|
|
||||
mainWindow.webContents.on( |
|
||||
"did-fail-load", |
|
||||
(event, errorCode, errorDescription) => { |
|
||||
logger.error("Page failed to load:", errorCode, errorDescription); |
|
||||
}, |
|
||||
); |
|
||||
|
|
||||
mainWindow.webContents.on("preload-error", (event, preloadPath, error) => { |
|
||||
logger.error("Preload script error:", preloadPath, error); |
|
||||
}); |
|
||||
|
|
||||
mainWindow.webContents.on( |
|
||||
"console-message", |
|
||||
(event, level, message, line, sourceId) => { |
|
||||
logger.log("Renderer Console:", line, sourceId, message); |
|
||||
}, |
|
||||
); |
|
||||
|
|
||||
// Enable remote debugging when in dev mode
|
|
||||
if (isDev) { |
|
||||
mainWindow.webContents.openDevTools(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// Handle app ready
|
|
||||
app.whenReady().then(createWindow); |
|
||||
|
|
||||
// Handle all windows closed
|
|
||||
app.on("window-all-closed", () => { |
|
||||
if (process.platform !== "darwin") { |
|
||||
app.quit(); |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
app.on("activate", () => { |
|
||||
if (BrowserWindow.getAllWindows().length === 0) { |
|
||||
createWindow(); |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
// Handle any errors
|
|
||||
process.on("uncaughtException", (error) => { |
|
||||
logger.error("Uncaught Exception:", error); |
|
||||
}); |
|
@ -1,215 +0,0 @@ |
|||||
import { app, BrowserWindow } from "electron"; |
|
||||
import path from "path"; |
|
||||
import fs from "fs"; |
|
||||
|
|
||||
// Simple logger implementation
|
|
||||
const logger = { |
|
||||
// eslint-disable-next-line no-console
|
|
||||
log: (...args: unknown[]) => console.log(...args), |
|
||||
// eslint-disable-next-line no-console
|
|
||||
error: (...args: unknown[]) => console.error(...args), |
|
||||
// eslint-disable-next-line no-console
|
|
||||
info: (...args: unknown[]) => console.info(...args), |
|
||||
// eslint-disable-next-line no-console
|
|
||||
warn: (...args: unknown[]) => console.warn(...args), |
|
||||
// eslint-disable-next-line no-console
|
|
||||
debug: (...args: unknown[]) => console.debug(...args), |
|
||||
}; |
|
||||
|
|
||||
// Check if running in dev mode
|
|
||||
const isDev = process.argv.includes("--inspect"); |
|
||||
|
|
||||
function createWindow(): void { |
|
||||
// Add before createWindow function
|
|
||||
const preloadPath = path.join(__dirname, "preload.js"); |
|
||||
logger.log("Checking preload path:", preloadPath); |
|
||||
logger.log("Preload exists:", fs.existsSync(preloadPath)); |
|
||||
|
|
||||
// Log environment and paths
|
|
||||
logger.log("process.cwd():", process.cwd()); |
|
||||
logger.log("__dirname:", __dirname); |
|
||||
logger.log("app.getAppPath():", app.getAppPath()); |
|
||||
logger.log("app.isPackaged:", app.isPackaged); |
|
||||
|
|
||||
// List files in __dirname and __dirname/www
|
|
||||
try { |
|
||||
logger.log("Files in __dirname:", fs.readdirSync(__dirname)); |
|
||||
const wwwDir = path.join(__dirname, "www"); |
|
||||
if (fs.existsSync(wwwDir)) { |
|
||||
logger.log("Files in www:", fs.readdirSync(wwwDir)); |
|
||||
} else { |
|
||||
logger.log("www directory does not exist in __dirname"); |
|
||||
} |
|
||||
} catch (e) { |
|
||||
logger.error("Error reading directories:", e); |
|
||||
} |
|
||||
|
|
||||
// Create the browser window.
|
|
||||
const mainWindow = new BrowserWindow({ |
|
||||
width: 1200, |
|
||||
height: 800, |
|
||||
webPreferences: { |
|
||||
nodeIntegration: false, |
|
||||
contextIsolation: true, |
|
||||
webSecurity: true, |
|
||||
allowRunningInsecureContent: false, |
|
||||
preload: path.join(__dirname, "preload.js"), |
|
||||
}, |
|
||||
}); |
|
||||
|
|
||||
// Always open DevTools for now
|
|
||||
mainWindow.webContents.openDevTools(); |
|
||||
|
|
||||
// Intercept requests to fix asset paths
|
|
||||
mainWindow.webContents.session.webRequest.onBeforeRequest( |
|
||||
{ |
|
||||
urls: [ |
|
||||
"file://*/*/assets/*", |
|
||||
"file://*/assets/*", |
|
||||
"file:///assets/*", // Catch absolute paths
|
|
||||
"<all_urls>", // Catch all URLs as a fallback
|
|
||||
], |
|
||||
}, |
|
||||
(details, callback) => { |
|
||||
let url = details.url; |
|
||||
|
|
||||
// Handle paths that don't start with file://
|
|
||||
if (!url.startsWith("file://") && url.includes("/assets/")) { |
|
||||
url = `file://${path.join(__dirname, "www", url)}`; |
|
||||
} |
|
||||
|
|
||||
// Handle absolute paths starting with /assets/
|
|
||||
if (url.includes("/assets/") && !url.includes("/www/assets/")) { |
|
||||
const baseDir = url.includes("dist-electron") |
|
||||
? url.substring( |
|
||||
0, |
|
||||
url.indexOf("/dist-electron") + "/dist-electron".length, |
|
||||
) |
|
||||
: `file://${__dirname}`; |
|
||||
const assetPath = url.split("/assets/")[1]; |
|
||||
const newUrl = `${baseDir}/www/assets/${assetPath}`; |
|
||||
callback({ redirectURL: newUrl }); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
callback({}); // No redirect for other URLs
|
|
||||
}, |
|
||||
); |
|
||||
|
|
||||
if (isDev) { |
|
||||
// Debug info
|
|
||||
logger.log("Debug Info:"); |
|
||||
logger.log("Running in dev mode:", isDev); |
|
||||
logger.log("App is packaged:", app.isPackaged); |
|
||||
logger.log("Process resource path:", process.resourcesPath); |
|
||||
logger.log("App path:", app.getAppPath()); |
|
||||
logger.log("__dirname:", __dirname); |
|
||||
logger.log("process.cwd():", process.cwd()); |
|
||||
} |
|
||||
|
|
||||
let indexPath = path.resolve(__dirname, "dist-electron", "www", "index.html"); |
|
||||
if (!fs.existsSync(indexPath)) { |
|
||||
// Fallback for dev mode
|
|
||||
indexPath = path.resolve( |
|
||||
process.cwd(), |
|
||||
"dist-electron", |
|
||||
"www", |
|
||||
"index.html", |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
if (isDev) { |
|
||||
logger.log("Loading index from:", indexPath); |
|
||||
logger.log("www path:", path.join(__dirname, "www")); |
|
||||
logger.log("www assets path:", path.join(__dirname, "www", "assets")); |
|
||||
} |
|
||||
|
|
||||
if (!fs.existsSync(indexPath)) { |
|
||||
logger.error(`Index file not found at: ${indexPath}`); |
|
||||
throw new Error("Index file not found"); |
|
||||
} |
|
||||
|
|
||||
// Add CSP headers to allow API connections
|
|
||||
mainWindow.webContents.session.webRequest.onHeadersReceived( |
|
||||
(details, callback) => { |
|
||||
callback({ |
|
||||
responseHeaders: { |
|
||||
...details.responseHeaders, |
|
||||
"Content-Security-Policy": [ |
|
||||
"default-src 'self';" + |
|
||||
"connect-src 'self' https://api.endorser.ch https://*.timesafari.app;" + |
|
||||
"img-src 'self' data: https: blob:;" + |
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval';" + |
|
||||
"style-src 'self' 'unsafe-inline';" + |
|
||||
"font-src 'self' data:;", |
|
||||
], |
|
||||
}, |
|
||||
}); |
|
||||
}, |
|
||||
); |
|
||||
|
|
||||
// Load the index.html
|
|
||||
mainWindow |
|
||||
.loadFile(indexPath) |
|
||||
.then(() => { |
|
||||
logger.log("Successfully loaded index.html"); |
|
||||
if (isDev) { |
|
||||
mainWindow.webContents.openDevTools(); |
|
||||
logger.log("DevTools opened - running in dev mode"); |
|
||||
} |
|
||||
}) |
|
||||
.catch((err) => { |
|
||||
logger.error("Failed to load index.html:", err); |
|
||||
logger.error("Attempted path:", indexPath); |
|
||||
}); |
|
||||
|
|
||||
// Listen for console messages from the renderer
|
|
||||
mainWindow.webContents.on("console-message", (_event, _level, message) => { |
|
||||
logger.log("Renderer Console:", message); |
|
||||
}); |
|
||||
|
|
||||
// Add right after creating the BrowserWindow
|
|
||||
mainWindow.webContents.on( |
|
||||
"did-fail-load", |
|
||||
(_event, errorCode, errorDescription) => { |
|
||||
logger.error("Page failed to load:", errorCode, errorDescription); |
|
||||
}, |
|
||||
); |
|
||||
|
|
||||
mainWindow.webContents.on("preload-error", (_event, preloadPath, error) => { |
|
||||
logger.error("Preload script error:", preloadPath, error); |
|
||||
}); |
|
||||
|
|
||||
mainWindow.webContents.on( |
|
||||
"console-message", |
|
||||
(_event, _level, message, line, sourceId) => { |
|
||||
logger.log("Renderer Console:", line, sourceId, message); |
|
||||
}, |
|
||||
); |
|
||||
|
|
||||
// Enable remote debugging when in dev mode
|
|
||||
if (isDev) { |
|
||||
mainWindow.webContents.openDevTools(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// Handle app ready
|
|
||||
app.whenReady().then(createWindow); |
|
||||
|
|
||||
// Handle all windows closed
|
|
||||
app.on("window-all-closed", () => { |
|
||||
if (process.platform !== "darwin") { |
|
||||
app.quit(); |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
app.on("activate", () => { |
|
||||
if (BrowserWindow.getAllWindows().length === 0) { |
|
||||
createWindow(); |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
// Handle any errors
|
|
||||
process.on("uncaughtException", (error) => { |
|
||||
logger.error("Uncaught Exception:", error); |
|
||||
}); |
|
@ -1,91 +0,0 @@ |
|||||
const { contextBridge, ipcRenderer } = require("electron"); |
|
||||
|
|
||||
const logger = { |
|
||||
log: (message, ...args) => { |
|
||||
// Always log in development, log with context in production
|
|
||||
if (process.env.NODE_ENV !== "production") { |
|
||||
/* eslint-disable no-console */ |
|
||||
console.log(`[Preload] ${message}`, ...args); |
|
||||
/* eslint-enable no-console */ |
|
||||
} |
|
||||
}, |
|
||||
warn: (message, ...args) => { |
|
||||
// Always log warnings
|
|
||||
/* eslint-disable no-console */ |
|
||||
console.warn(`[Preload] ${message}`, ...args); |
|
||||
/* eslint-enable no-console */ |
|
||||
}, |
|
||||
error: (message, ...args) => { |
|
||||
// Always log errors
|
|
||||
/* eslint-disable no-console */ |
|
||||
console.error(`[Preload] ${message}`, ...args); |
|
||||
/* eslint-enable no-console */ |
|
||||
}, |
|
||||
info: (message, ...args) => { |
|
||||
// Always log info in development, log with context in production
|
|
||||
if (process.env.NODE_ENV !== "production") { |
|
||||
/* eslint-disable no-console */ |
|
||||
console.info(`[Preload] ${message}`, ...args); |
|
||||
/* eslint-enable no-console */ |
|
||||
} |
|
||||
}, |
|
||||
}; |
|
||||
|
|
||||
// Use a more direct path resolution approach
|
|
||||
const getPath = (pathType) => { |
|
||||
switch (pathType) { |
|
||||
case "userData": |
|
||||
return ( |
|
||||
process.env.APPDATA || |
|
||||
(process.platform === "darwin" |
|
||||
? `${process.env.HOME}/Library/Application Support` |
|
||||
: `${process.env.HOME}/.local/share`) |
|
||||
); |
|
||||
case "home": |
|
||||
return process.env.HOME; |
|
||||
case "appPath": |
|
||||
return process.resourcesPath; |
|
||||
default: |
|
||||
return ""; |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
logger.info("Preload script starting..."); |
|
||||
|
|
||||
// Force electron platform in the renderer process
|
|
||||
window.process = { env: { VITE_PLATFORM: "electron" } }; |
|
||||
|
|
||||
try { |
|
||||
contextBridge.exposeInMainWorld("electronAPI", { |
|
||||
// Path utilities
|
|
||||
getPath, |
|
||||
|
|
||||
// IPC functions
|
|
||||
send: (channel, data) => { |
|
||||
const validChannels = ["toMain"]; |
|
||||
if (validChannels.includes(channel)) { |
|
||||
ipcRenderer.send(channel, data); |
|
||||
} |
|
||||
}, |
|
||||
receive: (channel, func) => { |
|
||||
const validChannels = ["fromMain"]; |
|
||||
if (validChannels.includes(channel)) { |
|
||||
ipcRenderer.on(channel, (event, ...args) => func(...args)); |
|
||||
} |
|
||||
}, |
|
||||
// Environment info
|
|
||||
env: { |
|
||||
isElectron: true, |
|
||||
isDev: process.env.NODE_ENV === "development", |
|
||||
platform: "electron", // Explicitly set platform
|
|
||||
}, |
|
||||
// Path utilities
|
|
||||
getBasePath: () => { |
|
||||
return process.env.NODE_ENV === "development" ? "/" : "./"; |
|
||||
}, |
|
||||
}); |
|
||||
|
|
||||
logger.info("Preload script completed successfully"); |
|
||||
} catch (error) { |
|
||||
logger.error("Error in preload script:", error); |
|
||||
} |
|
@ -1,16 +1,301 @@ |
|||||
import { initializeApp } from "./main.common"; |
import { initializeApp } from "./main.common"; |
||||
import { logger } from "./utils/logger"; |
import { logger } from "./utils/logger"; |
||||
|
import { SQLiteQueryResult } from "./services/platforms/ElectronPlatformService"; |
||||
|
|
||||
const platform = process.env.VITE_PLATFORM; |
const platform = process.env.VITE_PLATFORM; |
||||
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true"; |
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true"; |
||||
|
|
||||
logger.info("[Electron] Initializing app"); |
logger.info("[Main Electron] Initializing app"); |
||||
logger.info("[Electron] Platform:", { platform }); |
logger.info("[Main Electron] Platform:", { platform }); |
||||
logger.info("[Electron] PWA enabled:", { pwa_enabled }); |
logger.info("[Main Electron] PWA enabled:", { pwa_enabled }); |
||||
|
|
||||
if (pwa_enabled) { |
if (pwa_enabled) { |
||||
logger.warn("[Electron] PWA is enabled, but not supported in electron"); |
logger.warn("[Main Electron] PWA is enabled, but not supported in electron"); |
||||
} |
} |
||||
|
|
||||
|
// Initialize app and SQLite
|
||||
const app = initializeApp(); |
const app = initializeApp(); |
||||
app.mount("#app"); |
|
||||
|
// Create a promise that resolves when SQLite is ready
|
||||
|
const sqliteReady = new Promise<void>((resolve, reject) => { |
||||
|
let retryCount = 0; |
||||
|
let initializationTimeout: NodeJS.Timeout; |
||||
|
|
||||
|
const attemptInitialization = () => { |
||||
|
// Clear any existing timeout
|
||||
|
if (initializationTimeout) { |
||||
|
clearTimeout(initializationTimeout); |
||||
|
} |
||||
|
|
||||
|
// Set timeout for this attempt
|
||||
|
initializationTimeout = setTimeout(() => { |
||||
|
if (retryCount < 3) { |
||||
|
// Use same retry count as ElectronPlatformService
|
||||
|
retryCount++; |
||||
|
logger.warn( |
||||
|
`[Main Electron] SQLite initialization attempt ${retryCount} timed out, retrying...`, |
||||
|
); |
||||
|
setTimeout(attemptInitialization, 1000); // Use same delay as ElectronPlatformService
|
||||
|
} else { |
||||
|
logger.error( |
||||
|
"[Main Electron] SQLite initialization failed after all retries", |
||||
|
); |
||||
|
reject(new Error("SQLite initialization timeout after all retries")); |
||||
|
} |
||||
|
}, 10000); // Use same timeout as ElectronPlatformService
|
||||
|
|
||||
|
// Wait for electron bridge to be available
|
||||
|
const checkElectronBridge = () => { |
||||
|
if (!window.electron?.ipcRenderer) { |
||||
|
// Check again in 100ms if bridge isn't ready
|
||||
|
setTimeout(checkElectronBridge, 100); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// At this point we know ipcRenderer exists
|
||||
|
const ipcRenderer = window.electron.ipcRenderer; |
||||
|
|
||||
|
logger.info("[Main Electron] [IPC:bridge] IPC renderer bridge available"); |
||||
|
|
||||
|
// Listen for SQLite ready signal
|
||||
|
logger.debug( |
||||
|
"[Main Electron] [IPC:sqlite-ready] Registering listener for SQLite ready signal", |
||||
|
); |
||||
|
ipcRenderer.once("sqlite-ready", () => { |
||||
|
clearTimeout(initializationTimeout); |
||||
|
logger.info( |
||||
|
"[Main Electron] [IPC:sqlite-ready] Received SQLite ready signal", |
||||
|
); |
||||
|
resolve(); |
||||
|
}); |
||||
|
|
||||
|
// Also listen for database errors
|
||||
|
logger.debug( |
||||
|
"[Main Electron] [IPC:database-status] Registering listener for database status", |
||||
|
); |
||||
|
ipcRenderer.once("database-status", (...args: unknown[]) => { |
||||
|
clearTimeout(initializationTimeout); |
||||
|
const status = args[0] as { status: string; error?: string }; |
||||
|
if (status.status === "error") { |
||||
|
logger.error( |
||||
|
"[Main Electron] [IPC:database-status] Database error:", |
||||
|
{ |
||||
|
error: status.error, |
||||
|
channel: "database-status", |
||||
|
}, |
||||
|
); |
||||
|
reject(new Error(status.error || "Database initialization failed")); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// Check if SQLite is already available
|
||||
|
logger.debug( |
||||
|
"[Main Electron] [IPC:sqlite-is-available] Checking SQLite availability", |
||||
|
); |
||||
|
ipcRenderer |
||||
|
.invoke("sqlite-is-available") |
||||
|
.then(async (result: unknown) => { |
||||
|
const isAvailable = Boolean(result); |
||||
|
if (isAvailable) { |
||||
|
logger.info( |
||||
|
"[Main Electron] [IPC:sqlite-is-available] SQLite is available", |
||||
|
); |
||||
|
|
||||
|
try { |
||||
|
// First create a database connection
|
||||
|
logger.debug( |
||||
|
"[Main Electron] [IPC:get-path] Requesting database path", |
||||
|
); |
||||
|
const dbPath = await ipcRenderer.invoke("get-path"); |
||||
|
logger.info( |
||||
|
"[Main Electron] [IPC:get-path] Database path received:", |
||||
|
{ dbPath }, |
||||
|
); |
||||
|
|
||||
|
// Create the database connection
|
||||
|
logger.debug( |
||||
|
"[Main Electron] [IPC:sqlite-create-connection] Creating database connection", |
||||
|
); |
||||
|
await ipcRenderer.invoke("sqlite-create-connection", { |
||||
|
database: "timesafari", |
||||
|
version: 1, |
||||
|
}); |
||||
|
logger.info( |
||||
|
"[Main Electron] [IPC:sqlite-create-connection] Database connection created", |
||||
|
); |
||||
|
|
||||
|
// Explicitly open the database
|
||||
|
logger.debug( |
||||
|
"[Main Electron] [IPC:sqlite-open] Opening database", |
||||
|
); |
||||
|
await ipcRenderer.invoke("sqlite-open", { |
||||
|
database: "timesafari", |
||||
|
}); |
||||
|
logger.info( |
||||
|
"[Main Electron] [IPC:sqlite-open] Database opened successfully", |
||||
|
); |
||||
|
|
||||
|
// Verify the database is open
|
||||
|
logger.debug( |
||||
|
"[Main Electron] [IPC:sqlite-is-db-open] Verifying database is open", |
||||
|
); |
||||
|
const isOpen = await ipcRenderer.invoke("sqlite-is-db-open", { |
||||
|
database: "timesafari", |
||||
|
}); |
||||
|
logger.info( |
||||
|
"[Main Electron] [IPC:sqlite-is-db-open] Database open status:", |
||||
|
{ isOpen }, |
||||
|
); |
||||
|
|
||||
|
if (!isOpen) { |
||||
|
throw new Error("Database failed to open"); |
||||
|
} |
||||
|
|
||||
|
// Now execute the test query
|
||||
|
logger.debug( |
||||
|
"[Main Electron] [IPC:sqlite-query] Executing test query", |
||||
|
); |
||||
|
const testQuery = (await ipcRenderer.invoke("sqlite-query", { |
||||
|
database: "timesafari", |
||||
|
statement: "SELECT 1 as test;", // Safe test query
|
||||
|
})) as SQLiteQueryResult; |
||||
|
logger.info( |
||||
|
"[Main Electron] [IPC:sqlite-query] Test query successful:", |
||||
|
{ |
||||
|
hasResults: Boolean(testQuery?.values), |
||||
|
resultCount: testQuery?.values?.length, |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
// Signal that SQLite is ready - database stays open
|
||||
|
logger.debug( |
||||
|
"[Main Electron] [IPC:sqlite-status] Sending SQLite ready status", |
||||
|
); |
||||
|
await ipcRenderer.invoke("sqlite-status", { |
||||
|
status: "ready", |
||||
|
database: "timesafari", |
||||
|
timestamp: Date.now(), |
||||
|
}); |
||||
|
logger.info( |
||||
|
"[Main Electron] SQLite ready status sent, database connection maintained", |
||||
|
); |
||||
|
|
||||
|
// Remove the close operations - database stays open for component use
|
||||
|
// Database will be closed during app shutdown
|
||||
|
} catch (error) { |
||||
|
logger.error( |
||||
|
"[Main Electron] [IPC:*] SQLite test operation failed:", |
||||
|
{ |
||||
|
error, |
||||
|
lastOperation: "sqlite-test-query", |
||||
|
database: "timesafari", |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
// Try to close everything if anything was opened
|
||||
|
try { |
||||
|
logger.debug( |
||||
|
"[Main Electron] [IPC:cleanup] Attempting database cleanup after error", |
||||
|
); |
||||
|
await ipcRenderer |
||||
|
.invoke("sqlite-close", { |
||||
|
database: "timesafari", |
||||
|
}) |
||||
|
.catch((closeError) => { |
||||
|
logger.warn( |
||||
|
"[Main Electron] [IPC:sqlite-close] Failed to close database during cleanup:", |
||||
|
closeError, |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
await ipcRenderer |
||||
|
.invoke("sqlite-close-connection", { |
||||
|
database: "timesafari", |
||||
|
}) |
||||
|
.catch((closeError) => { |
||||
|
logger.warn( |
||||
|
"[Main Electron] [IPC:sqlite-close-connection] Failed to close connection during cleanup:", |
||||
|
closeError, |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
logger.info( |
||||
|
"[Main Electron] [IPC:cleanup] Database cleanup completed after error", |
||||
|
); |
||||
|
} catch (closeError) { |
||||
|
logger.error( |
||||
|
"[Main Electron] [IPC:cleanup] Failed to cleanup database:", |
||||
|
{ |
||||
|
error: closeError, |
||||
|
database: "timesafari", |
||||
|
}, |
||||
|
); |
||||
|
} |
||||
|
// Don't reject here - we still want to wait for the ready signal
|
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
.catch((error: Error) => { |
||||
|
logger.error( |
||||
|
"[Main Electron] [IPC:sqlite-is-available] Failed to check SQLite availability:", |
||||
|
{ |
||||
|
error, |
||||
|
channel: "sqlite-is-available", |
||||
|
}, |
||||
|
); |
||||
|
// Don't reject here - wait for either ready signal or timeout
|
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
// Start checking for bridge
|
||||
|
checkElectronBridge(); |
||||
|
}; |
||||
|
|
||||
|
// Start first initialization attempt
|
||||
|
attemptInitialization(); |
||||
|
}); |
||||
|
|
||||
|
// Wait for SQLite to be ready before initializing router and mounting app
|
||||
|
sqliteReady |
||||
|
.then(async () => { |
||||
|
logger.info("[Main Electron] SQLite ready, initializing router..."); |
||||
|
|
||||
|
// Initialize router after SQLite is ready
|
||||
|
const router = await import("./router").then((m) => m.default); |
||||
|
app.use(router); |
||||
|
logger.info("[Main Electron] Router initialized"); |
||||
|
|
||||
|
// Now mount the app
|
||||
|
logger.info("[Main Electron] Mounting app..."); |
||||
|
app.mount("#app"); |
||||
|
logger.info("[Main Electron] App mounted successfully"); |
||||
|
}) |
||||
|
.catch((error) => { |
||||
|
logger.error( |
||||
|
"[Main Electron] Failed to initialize SQLite:", |
||||
|
error instanceof Error ? error.message : "Unknown error", |
||||
|
); |
||||
|
// Show error to user with retry option
|
||||
|
const errorDiv = document.createElement("div"); |
||||
|
errorDiv.style.cssText = |
||||
|
"position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #ffebee; color: #c62828; padding: 20px; border-radius: 4px; text-align: center; max-width: 80%; z-index: 9999;"; |
||||
|
errorDiv.innerHTML = ` |
||||
|
<h2>Failed to Initialize Application</h2> |
||||
|
<p>There was an error initializing the database. This could be due to:</p> |
||||
|
<ul style="text-align: left; margin: 10px 0;"> |
||||
|
<li>Database file is locked by another process</li> |
||||
|
<li>Insufficient permissions to access the database</li> |
||||
|
<li>Database file is corrupted</li> |
||||
|
</ul> |
||||
|
<p>Error details: ${error instanceof Error ? error.message : "Unknown error"}</p> |
||||
|
<div style="margin-top: 15px;"> |
||||
|
<button onclick="window.location.reload()" style="margin: 0 5px; padding: 8px 16px; background: #c62828; color: white; border: none; border-radius: 4px; cursor: pointer;"> |
||||
|
Retry |
||||
|
</button> |
||||
|
<button onclick="window.electron.ipcRenderer.send('sqlite-status', { action: 'reset' })" style="margin: 0 5px; padding: 8px 16px; background: #f57c00; color: white; border: none; border-radius: 4px; cursor: pointer;"> |
||||
|
Reset Database |
||||
|
</button> |
||||
|
</div> |
||||
|
`;
|
||||
|
document.body.appendChild(errorDiv); |
||||
|
}); |
||||
|
@ -0,0 +1,132 @@ |
|||||
|
import { logger } from "../../utils/logger"; |
||||
|
import { SQLiteDBConnection } from "@capacitor-community/sqlite"; |
||||
|
|
||||
|
interface ConnectionState { |
||||
|
connection: SQLiteDBConnection; |
||||
|
lastUsed: number; |
||||
|
inUse: boolean; |
||||
|
} |
||||
|
|
||||
|
export class DatabaseConnectionPool { |
||||
|
private static instance: DatabaseConnectionPool | null = null; |
||||
|
private connections: Map<string, ConnectionState> = new Map(); |
||||
|
private readonly MAX_CONNECTIONS = 1; // We only need one connection for SQLite
|
||||
|
private readonly MAX_IDLE_TIME = 5 * 60 * 1000; // 5 minutes
|
||||
|
private readonly CLEANUP_INTERVAL = 60 * 1000; // 1 minute
|
||||
|
private cleanupInterval: NodeJS.Timeout | null = null; |
||||
|
|
||||
|
private constructor() { |
||||
|
// Start cleanup interval
|
||||
|
this.cleanupInterval = setInterval( |
||||
|
() => this.cleanup(), |
||||
|
this.CLEANUP_INTERVAL, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
public static getInstance(): DatabaseConnectionPool { |
||||
|
if (!DatabaseConnectionPool.instance) { |
||||
|
DatabaseConnectionPool.instance = new DatabaseConnectionPool(); |
||||
|
} |
||||
|
return DatabaseConnectionPool.instance; |
||||
|
} |
||||
|
|
||||
|
public async getConnection( |
||||
|
dbName: string, |
||||
|
createConnection: () => Promise<SQLiteDBConnection>, |
||||
|
): Promise<SQLiteDBConnection> { |
||||
|
// Check if we have an existing connection
|
||||
|
const existing = this.connections.get(dbName); |
||||
|
if (existing && !existing.inUse) { |
||||
|
existing.inUse = true; |
||||
|
existing.lastUsed = Date.now(); |
||||
|
logger.debug( |
||||
|
`[ConnectionPool] Reusing existing connection for ${dbName}`, |
||||
|
); |
||||
|
return existing.connection; |
||||
|
} |
||||
|
|
||||
|
// If we have too many connections, wait for one to be released
|
||||
|
if (this.connections.size >= this.MAX_CONNECTIONS) { |
||||
|
logger.debug(`[ConnectionPool] Waiting for connection to be released...`); |
||||
|
await this.waitForConnection(); |
||||
|
} |
||||
|
|
||||
|
// Create new connection
|
||||
|
try { |
||||
|
const connection = await createConnection(); |
||||
|
this.connections.set(dbName, { |
||||
|
connection, |
||||
|
lastUsed: Date.now(), |
||||
|
inUse: true, |
||||
|
}); |
||||
|
logger.debug(`[ConnectionPool] Created new connection for ${dbName}`); |
||||
|
return connection; |
||||
|
} catch (error) { |
||||
|
logger.error( |
||||
|
`[ConnectionPool] Failed to create connection for ${dbName}:`, |
||||
|
error, |
||||
|
); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async releaseConnection(dbName: string): Promise<void> { |
||||
|
const connection = this.connections.get(dbName); |
||||
|
if (connection) { |
||||
|
connection.inUse = false; |
||||
|
connection.lastUsed = Date.now(); |
||||
|
logger.debug(`[ConnectionPool] Released connection for ${dbName}`); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async waitForConnection(): Promise<void> { |
||||
|
return new Promise((resolve) => { |
||||
|
const checkInterval = setInterval(() => { |
||||
|
if (this.connections.size < this.MAX_CONNECTIONS) { |
||||
|
clearInterval(checkInterval); |
||||
|
resolve(); |
||||
|
} |
||||
|
}, 100); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private async cleanup(): Promise<void> { |
||||
|
const now = Date.now(); |
||||
|
for (const [dbName, state] of this.connections.entries()) { |
||||
|
if (!state.inUse && now - state.lastUsed > this.MAX_IDLE_TIME) { |
||||
|
try { |
||||
|
await state.connection.close(); |
||||
|
this.connections.delete(dbName); |
||||
|
logger.debug( |
||||
|
`[ConnectionPool] Cleaned up idle connection for ${dbName}`, |
||||
|
); |
||||
|
} catch (error) { |
||||
|
logger.warn( |
||||
|
`[ConnectionPool] Error closing idle connection for ${dbName}:`, |
||||
|
error, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async closeAll(): Promise<void> { |
||||
|
if (this.cleanupInterval) { |
||||
|
clearInterval(this.cleanupInterval); |
||||
|
this.cleanupInterval = null; |
||||
|
} |
||||
|
|
||||
|
for (const [dbName, state] of this.connections.entries()) { |
||||
|
try { |
||||
|
await state.connection.close(); |
||||
|
logger.debug(`[ConnectionPool] Closed connection for ${dbName}`); |
||||
|
} catch (error) { |
||||
|
logger.warn( |
||||
|
`[ConnectionPool] Error closing connection for ${dbName}:`, |
||||
|
error, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
this.connections.clear(); |
||||
|
} |
||||
|
} |
@ -0,0 +1,46 @@ |
|||||
|
declare module '@capacitor-community/sqlite/electron/dist/plugin.js' { |
||||
|
export class CapacitorSQLite { |
||||
|
constructor(); |
||||
|
handle(event: Electron.IpcMainInvokeEvent, ...args: any[]): Promise<any>; |
||||
|
createConnection(options: any): Promise<any>; |
||||
|
closeConnection(options: any): Promise<any>; |
||||
|
echo(options: any): Promise<any>; |
||||
|
open(options: any): Promise<any>; |
||||
|
close(options: any): Promise<any>; |
||||
|
beginTransaction(options: any): Promise<any>; |
||||
|
commitTransaction(options: any): Promise<any>; |
||||
|
rollbackTransaction(options: any): Promise<any>; |
||||
|
isTransactionActive(options: any): Promise<any>; |
||||
|
getVersion(options: any): Promise<any>; |
||||
|
getTableList(options: any): Promise<any>; |
||||
|
execute(options: any): Promise<any>; |
||||
|
executeSet(options: any): Promise<any>; |
||||
|
run(options: any): Promise<any>; |
||||
|
query(options: any): Promise<any>; |
||||
|
isDBExists(options: any): Promise<any>; |
||||
|
isDBOpen(options: any): Promise<any>; |
||||
|
isDatabase(options: any): Promise<any>; |
||||
|
isTableExists(options: any): Promise<any>; |
||||
|
deleteDatabase(options: any): Promise<any>; |
||||
|
isJsonValid(options: any): Promise<any>; |
||||
|
importFromJson(options: any): Promise<any>; |
||||
|
exportToJson(options: any): Promise<any>; |
||||
|
createSyncTable(options: any): Promise<any>; |
||||
|
setSyncDate(options: any): Promise<any>; |
||||
|
getSyncDate(options: any): Promise<any>; |
||||
|
deleteExportedRows(options: any): Promise<any>; |
||||
|
addUpgradeStatement(options: any): Promise<any>; |
||||
|
copyFromAssets(options: any): Promise<any>; |
||||
|
getFromHTTPRequest(options: any): Promise<any>; |
||||
|
getDatabaseList(): Promise<any>; |
||||
|
checkConnectionsConsistency(options: any): Promise<any>; |
||||
|
isSecretStored(): Promise<any>; |
||||
|
isPassphraseValid(options: any): Promise<any>; |
||||
|
setEncryptionSecret(options: any): Promise<any>; |
||||
|
changeEncryptionSecret(options: any): Promise<any>; |
||||
|
clearEncryptionSecret(): Promise<any>; |
||||
|
isInConfigEncryption(): Promise<any>; |
||||
|
isDatabaseEncrypted(options: any): Promise<any>; |
||||
|
checkEncryptionSecret(options: any): Promise<any>; |
||||
|
} |
||||
|
} |
@ -0,0 +1,87 @@ |
|||||
|
interface ElectronAPI { |
||||
|
sqlite: { |
||||
|
isAvailable: () => Promise<boolean>; |
||||
|
echo: (value: string) => Promise<{ value: string }>; |
||||
|
createConnection: (options: { |
||||
|
database: string; |
||||
|
version?: number; |
||||
|
readOnly?: boolean; |
||||
|
readonly?: boolean; |
||||
|
encryption?: string; |
||||
|
mode?: string; |
||||
|
useNative?: boolean; |
||||
|
[key: string]: unknown; |
||||
|
}) => Promise<void>; |
||||
|
closeConnection: (options: { database: string }) => Promise<void>; |
||||
|
query: (options: { statement: string; values?: unknown[] }) => Promise<{ |
||||
|
values?: Record<string, unknown>[]; |
||||
|
changes?: { changes: number; lastId?: number }; |
||||
|
}>; |
||||
|
run: (options: { statement: string; values?: unknown[] }) => Promise<{ |
||||
|
changes?: { changes: number; lastId?: number }; |
||||
|
}>; |
||||
|
execute: (options: { statements: { statement: string; values?: unknown[] }[] }) => Promise<{ |
||||
|
changes?: { changes: number; lastId?: number }[]; |
||||
|
}>; |
||||
|
getPlatform: () => Promise<string>; |
||||
|
}; |
||||
|
ipcRenderer: { |
||||
|
on: (channel: string, func: (...args: unknown[]) => void) => void; |
||||
|
once: (channel: string, func: (...args: unknown[]) => void) => void; |
||||
|
send: (channel: string, data: unknown) => void; |
||||
|
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>; |
||||
|
}; |
||||
|
env: { |
||||
|
platform: string; |
||||
|
isDev: boolean; |
||||
|
}; |
||||
|
getPath: (pathType: string) => Promise<string>; |
||||
|
getBasePath: () => Promise<string>; |
||||
|
} |
||||
|
|
||||
|
declare global { |
||||
|
interface Window { |
||||
|
electron: { |
||||
|
ipcRenderer: { |
||||
|
on: (channel: string, func: (...args: unknown[]) => void) => void; |
||||
|
once: (channel: string, func: (...args: unknown[]) => void) => void; |
||||
|
send: (channel: string, data: unknown) => void; |
||||
|
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>; |
||||
|
}; |
||||
|
sqlite: { |
||||
|
isAvailable: () => Promise<boolean>; |
||||
|
echo: (value: string) => Promise<{ value: string }>; |
||||
|
createConnection: (options: { |
||||
|
database: string; |
||||
|
version?: number; |
||||
|
readOnly?: boolean; |
||||
|
readonly?: boolean; |
||||
|
encryption?: string; |
||||
|
mode?: string; |
||||
|
useNative?: boolean; |
||||
|
[key: string]: unknown; |
||||
|
}) => Promise<void>; |
||||
|
closeConnection: (options: { database: string }) => Promise<void>; |
||||
|
query: (options: { statement: string; values?: unknown[] }) => Promise<{ |
||||
|
values?: Record<string, unknown>[]; |
||||
|
changes?: { changes: number; lastId?: number }; |
||||
|
}>; |
||||
|
run: (options: { statement: string; values?: unknown[] }) => Promise<{ |
||||
|
changes?: { changes: number; lastId?: number }; |
||||
|
}>; |
||||
|
execute: (options: { statements: { statement: string; values?: unknown[] }[] }) => Promise<{ |
||||
|
changes?: { changes: number; lastId?: number }[]; |
||||
|
}>; |
||||
|
getPlatform: () => Promise<string>; |
||||
|
}; |
||||
|
env: { |
||||
|
platform: string; |
||||
|
isDev: boolean; |
||||
|
}; |
||||
|
getPath: (pathType: string) => Promise<string>; |
||||
|
getBasePath: () => Promise<string>; |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export {}; |
@ -0,0 +1 @@ |
|||||
|
declare module '@jeepq/sqlite/loader'; |
@ -0,0 +1,169 @@ |
|||||
|
/** |
||||
|
* Debug utilities for Electron integration |
||||
|
* Helps verify the context bridge and SQLite functionality |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
*/ |
||||
|
|
||||
|
import { logger } from "./logger"; |
||||
|
|
||||
|
// Define the SQLite interface
|
||||
|
export interface SQLiteAPI { |
||||
|
isAvailable: () => Promise<boolean>; |
||||
|
echo: (value: string) => Promise<string>; |
||||
|
createConnection: (options: { |
||||
|
database: string; |
||||
|
version: number; |
||||
|
readOnly: boolean; |
||||
|
}) => Promise<void>; |
||||
|
closeConnection: (options: { database: string }) => Promise<void>; |
||||
|
query: (options: { statement: string }) => Promise<unknown>; |
||||
|
run: (options: { statement: string }) => Promise<unknown>; |
||||
|
execute: (options: { |
||||
|
statements: Array<{ statement: string }>; |
||||
|
}) => Promise<unknown>; |
||||
|
getPlatform: () => Promise<string>; |
||||
|
} |
||||
|
|
||||
|
// Define the IPC renderer interface
|
||||
|
export interface IPCRenderer { |
||||
|
on: (channel: string, func: (...args: unknown[]) => void) => void; |
||||
|
once: (channel: string, func: (...args: unknown[]) => void) => void; |
||||
|
send: (channel: string, ...args: unknown[]) => void; |
||||
|
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>; |
||||
|
} |
||||
|
|
||||
|
// Define the environment interface
|
||||
|
export interface ElectronEnv { |
||||
|
platform: string; |
||||
|
isDev: boolean; |
||||
|
} |
||||
|
|
||||
|
// Define the complete electron interface
|
||||
|
export interface ElectronAPI { |
||||
|
sqlite: SQLiteAPI; |
||||
|
ipcRenderer: IPCRenderer; |
||||
|
env: ElectronEnv; |
||||
|
getPath: (pathType: string) => Promise<string>; |
||||
|
getBasePath: () => Promise<string>; |
||||
|
} |
||||
|
|
||||
|
// Define the window.electron interface
|
||||
|
declare global { |
||||
|
interface Window { |
||||
|
electron: ElectronAPI; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export async function verifyElectronAPI(): Promise<void> { |
||||
|
logger.info("[Debug] Verifying Electron API exposure..."); |
||||
|
|
||||
|
// Check if window.electron exists
|
||||
|
if (!window.electron) { |
||||
|
throw new Error("window.electron is not defined"); |
||||
|
} |
||||
|
logger.info("[Debug] window.electron is available"); |
||||
|
|
||||
|
// Verify IPC renderer
|
||||
|
if (!window.electron.ipcRenderer) { |
||||
|
throw new Error("IPC renderer is not available"); |
||||
|
} |
||||
|
logger.info("[Debug] IPC renderer is available with methods:", { |
||||
|
hasOn: typeof window.electron.ipcRenderer.on === "function", |
||||
|
hasOnce: typeof window.electron.ipcRenderer.once === "function", |
||||
|
hasSend: typeof window.electron.ipcRenderer.send === "function", |
||||
|
hasInvoke: typeof window.electron.ipcRenderer.invoke === "function", |
||||
|
}); |
||||
|
|
||||
|
// Verify SQLite API
|
||||
|
if (!window.electron.sqlite) { |
||||
|
throw new Error("SQLite API is not available"); |
||||
|
} |
||||
|
logger.info("[Debug] SQLite API is available with methods:", { |
||||
|
hasIsAvailable: typeof window.electron.sqlite.isAvailable === "function", |
||||
|
hasEcho: typeof window.electron.sqlite.echo === "function", |
||||
|
hasCreateConnection: |
||||
|
typeof window.electron.sqlite.createConnection === "function", |
||||
|
hasCloseConnection: |
||||
|
typeof window.electron.sqlite.closeConnection === "function", |
||||
|
hasQuery: typeof window.electron.sqlite.query === "function", |
||||
|
hasRun: typeof window.electron.sqlite.run === "function", |
||||
|
hasExecute: typeof window.electron.sqlite.execute === "function", |
||||
|
hasGetPlatform: typeof window.electron.sqlite.getPlatform === "function", |
||||
|
}); |
||||
|
|
||||
|
// Test SQLite availability
|
||||
|
try { |
||||
|
const isAvailable = await window.electron.sqlite.isAvailable(); |
||||
|
logger.info("[Debug] SQLite availability check:", { isAvailable }); |
||||
|
} catch (error) { |
||||
|
logger.error("[Debug] SQLite availability check failed:", error); |
||||
|
} |
||||
|
|
||||
|
// Test echo functionality
|
||||
|
try { |
||||
|
const echoResult = await window.electron.sqlite.echo("test"); |
||||
|
logger.info("[Debug] SQLite echo test:", echoResult); |
||||
|
} catch (error) { |
||||
|
logger.error("[Debug] SQLite echo test failed:", error); |
||||
|
} |
||||
|
|
||||
|
// Verify environment
|
||||
|
logger.info("[Debug] Environment:", { |
||||
|
platform: window.electron.env.platform, |
||||
|
isDev: window.electron.env.isDev, |
||||
|
}); |
||||
|
|
||||
|
logger.info("[Debug] Electron API verification complete"); |
||||
|
} |
||||
|
|
||||
|
// Export a function to test SQLite operations
|
||||
|
export async function testSQLiteOperations(): Promise<void> { |
||||
|
logger.info("[Debug] Testing SQLite operations..."); |
||||
|
|
||||
|
try { |
||||
|
// Test connection creation
|
||||
|
logger.info("[Debug] Creating test connection..."); |
||||
|
await window.electron.sqlite.createConnection({ |
||||
|
database: "test", |
||||
|
version: 1, |
||||
|
readOnly: false, |
||||
|
}); |
||||
|
logger.info("[Debug] Test connection created successfully"); |
||||
|
|
||||
|
// Test query
|
||||
|
logger.info("[Debug] Testing query operation..."); |
||||
|
const queryResult = await window.electron.sqlite.query({ |
||||
|
statement: "SELECT 1 as test", |
||||
|
}); |
||||
|
logger.info("[Debug] Query test result:", queryResult); |
||||
|
|
||||
|
// Test run
|
||||
|
logger.info("[Debug] Testing run operation..."); |
||||
|
const runResult = await window.electron.sqlite.run({ |
||||
|
statement: |
||||
|
"CREATE TABLE IF NOT EXISTS test_table (id INTEGER PRIMARY KEY)", |
||||
|
}); |
||||
|
logger.info("[Debug] Run test result:", runResult); |
||||
|
|
||||
|
// Test execute
|
||||
|
logger.info("[Debug] Testing execute operation..."); |
||||
|
const executeResult = await window.electron.sqlite.execute({ |
||||
|
statements: [ |
||||
|
{ statement: "INSERT INTO test_table (id) VALUES (1)" }, |
||||
|
{ statement: "SELECT * FROM test_table" }, |
||||
|
], |
||||
|
}); |
||||
|
logger.info("[Debug] Execute test result:", executeResult); |
||||
|
|
||||
|
// Clean up
|
||||
|
logger.info("[Debug] Closing test connection..."); |
||||
|
await window.electron.sqlite.closeConnection({ database: "test" }); |
||||
|
logger.info("[Debug] Test connection closed"); |
||||
|
} catch (error) { |
||||
|
logger.error("[Debug] SQLite operation test failed:", error); |
||||
|
throw error; |
||||
|
} |
||||
|
|
||||
|
logger.info("[Debug] SQLite operations test complete"); |
||||
|
} |
@ -0,0 +1,42 @@ |
|||||
|
/** |
||||
|
* vite.config.app.electron.mts |
||||
|
* |
||||
|
* Vite configuration for building the web application for Electron. |
||||
|
* This config outputs to 'dist/' (like the web build), sets VITE_PLATFORM to 'electron', |
||||
|
* and disables PWA plugins and web-only features. Use this when you want to package |
||||
|
* the web app for Electron but keep the output structure identical to the web build. |
||||
|
* |
||||
|
* Author: Matthew Raymer |
||||
|
*/ |
||||
|
|
||||
|
import { defineConfig, mergeConfig } from 'vite'; |
||||
|
import { createBuildConfig } from './vite.config.common.mts'; |
||||
|
import { loadAppConfig } from './vite.config.utils.mts'; |
||||
|
import path from 'path'; |
||||
|
|
||||
|
export default defineConfig(async () => { |
||||
|
// Set mode to 'electron' for platform-specific config |
||||
|
const mode = 'electron'; |
||||
|
const baseConfig = await createBuildConfig(mode); |
||||
|
const appConfig = await loadAppConfig(); |
||||
|
|
||||
|
// Override build output directory to 'dist/' |
||||
|
const buildConfig = { |
||||
|
outDir: path.resolve(__dirname, 'dist'), |
||||
|
emptyOutDir: true, |
||||
|
rollupOptions: { |
||||
|
input: path.resolve(__dirname, 'index.html'), |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
// No PWA plugins or web-only plugins for Electron |
||||
|
return mergeConfig(baseConfig, { |
||||
|
build: buildConfig, |
||||
|
plugins: [], |
||||
|
define: { |
||||
|
'process.env.VITE_PLATFORM': JSON.stringify('electron'), |
||||
|
'process.env.VITE_PWA_ENABLED': JSON.stringify(false), |
||||
|
'process.env.VITE_DISABLE_PWA': JSON.stringify(true), |
||||
|
}, |
||||
|
}); |
||||
|
}); |
@ -1,104 +1,102 @@ |
|||||
import { defineConfig, mergeConfig } from "vite"; |
import { defineConfig } from "vite"; |
||||
import { createBuildConfig } from "./vite.config.common.mts"; |
|
||||
import path from 'path'; |
import path from 'path'; |
||||
|
|
||||
export default defineConfig(async () => { |
export default defineConfig({ |
||||
const baseConfig = await createBuildConfig('electron'); |
build: { |
||||
|
outDir: 'dist-electron', |
||||
return mergeConfig(baseConfig, { |
rollupOptions: { |
||||
build: { |
input: { |
||||
outDir: 'dist-electron', |
main: path.resolve(__dirname, 'src/electron/main.ts'), |
||||
rollupOptions: { |
preload: path.resolve(__dirname, 'electron/src/preload.ts') |
||||
input: { |
|
||||
main: path.resolve(__dirname, 'src/electron/main.ts'), |
|
||||
preload: path.resolve(__dirname, 'src/electron/preload.js'), |
|
||||
}, |
|
||||
external: ['electron'], |
|
||||
output: { |
|
||||
format: 'cjs', |
|
||||
entryFileNames: '[name].js', |
|
||||
assetFileNames: 'assets/[name].[ext]', |
|
||||
}, |
|
||||
}, |
}, |
||||
target: 'node18', |
external: [ |
||||
minify: false, |
// Node.js built-ins |
||||
sourcemap: true, |
'stream', |
||||
}, |
'path', |
||||
resolve: { |
'fs', |
||||
alias: { |
'crypto', |
||||
'@': path.resolve(__dirname, 'src'), |
'buffer', |
||||
|
'util', |
||||
|
'events', |
||||
|
'url', |
||||
|
'assert', |
||||
|
'os', |
||||
|
'net', |
||||
|
'http', |
||||
|
'https', |
||||
|
'zlib', |
||||
|
'child_process', |
||||
|
// Electron and Capacitor |
||||
|
'electron', |
||||
|
'@capacitor/core', |
||||
|
'@capacitor-community/sqlite', |
||||
|
'@capacitor-community/sqlite/electron', |
||||
|
'@capacitor-community/sqlite/electron/dist/plugin', |
||||
|
'better-sqlite3-multiple-ciphers', |
||||
|
// HTTP clients |
||||
|
'axios', |
||||
|
'axios/dist/axios', |
||||
|
'axios/dist/node/axios.cjs' |
||||
|
], |
||||
|
output: { |
||||
|
format: 'es', |
||||
|
entryFileNames: '[name].mjs', |
||||
|
assetFileNames: 'assets/[name].[ext]', |
||||
}, |
}, |
||||
}, |
}, |
||||
optimizeDeps: { |
target: 'node18', |
||||
include: ['@/utils/logger'] |
minify: false, |
||||
|
sourcemap: true, |
||||
|
}, |
||||
|
resolve: { |
||||
|
alias: { |
||||
|
'@': path.resolve(__dirname, 'src'), |
||||
|
// Use Node.js version of axios in electron |
||||
|
'axios': 'axios/dist/node/axios.cjs' |
||||
}, |
}, |
||||
plugins: [ |
}, |
||||
{ |
optimizeDeps: { |
||||
name: 'typescript-transform', |
exclude: [ |
||||
transform(code: string, id: string) { |
'stream', |
||||
if (id.endsWith('main.ts')) { |
'path', |
||||
// Replace the logger import with inline logger |
'fs', |
||||
return code.replace( |
'crypto', |
||||
/import\s*{\s*logger\s*}\s*from\s*['"]@\/utils\/logger['"];?/, |
'buffer', |
||||
`const logger = { |
'util', |
||||
log: (...args) => console.log(...args), |
'events', |
||||
error: (...args) => console.error(...args), |
'url', |
||||
info: (...args) => console.info(...args), |
'assert', |
||||
warn: (...args) => console.warn(...args), |
'os', |
||||
debug: (...args) => console.debug(...args), |
'net', |
||||
};` |
'http', |
||||
); |
'https', |
||||
} |
'zlib', |
||||
return code; |
'child_process', |
||||
} |
'axios', |
||||
}, |
'axios/dist/axios', |
||||
{ |
'axios/dist/node/axios.cjs' |
||||
name: 'remove-sw-imports', |
] |
||||
transform(code: string, id: string) { |
}, |
||||
// Remove service worker imports and registrations |
plugins: [ |
||||
if (id.includes('registerServiceWorker') || |
{ |
||||
id.includes('register-service-worker') || |
name: 'typescript-transform', |
||||
id.includes('sw_scripts') || |
transform(code: string, id: string) { |
||||
id.includes('PushNotificationPermission') || |
if (id.endsWith('main.ts')) { |
||||
code.includes('navigator.serviceWorker')) { |
return code.replace( |
||||
return { |
/import\s*{\s*logger\s*}\s*from\s*['"]@\/utils\/logger['"];?/, |
||||
code: code |
`const logger = { |
||||
.replace(/import.*registerServiceWorker.*$/mg, '') |
log: (...args) => console.log(...args), |
||||
.replace(/import.*register-service-worker.*$/mg, '') |
error: (...args) => console.error(...args), |
||||
.replace(/navigator\.serviceWorker/g, 'undefined') |
info: (...args) => console.info(...args), |
||||
.replace(/if\s*\([^)]*serviceWorker[^)]*\)\s*{[^}]*}/g, '') |
warn: (...args) => console.warn(...args), |
||||
.replace(/import.*workbox.*$/mg, '') |
debug: (...args) => console.debug(...args), |
||||
.replace(/importScripts\([^)]*\)/g, '') |
};` |
||||
}; |
); |
||||
} |
|
||||
return code; |
|
||||
} |
|
||||
}, |
|
||||
{ |
|
||||
name: 'remove-sw-files', |
|
||||
enforce: 'pre', |
|
||||
resolveId(id: string) { |
|
||||
// Prevent service worker files from being included |
|
||||
if (id.includes('sw.js') || |
|
||||
id.includes('workbox') || |
|
||||
id.includes('registerSW.js') || |
|
||||
id.includes('manifest.webmanifest')) { |
|
||||
return '\0empty'; |
|
||||
} |
|
||||
return null; |
|
||||
}, |
|
||||
load(id: string) { |
|
||||
if (id === '\0empty') { |
|
||||
return 'export default {}'; |
|
||||
} |
|
||||
return null; |
|
||||
} |
} |
||||
|
return code; |
||||
} |
} |
||||
], |
} |
||||
ssr: { |
], |
||||
noExternal: ['@/utils/logger'] |
base: './', |
||||
}, |
publicDir: 'public', |
||||
base: './', |
|
||||
publicDir: 'public', |
|
||||
}); |
|
||||
}); |
}); |
@ -0,0 +1,21 @@ |
|||||
|
import { defineConfig } from 'vite'; |
||||
|
import vue from '@vitejs/plugin-vue'; |
||||
|
import path from 'path'; |
||||
|
|
||||
|
export default defineConfig({ |
||||
|
root: path.resolve(__dirname, '.'), |
||||
|
base: './', |
||||
|
build: { |
||||
|
outDir: path.resolve(__dirname, 'dist-electron/www'), |
||||
|
emptyOutDir: false, |
||||
|
rollupOptions: { |
||||
|
input: path.resolve(__dirname, 'dist/www/index.html'), |
||||
|
}, |
||||
|
}, |
||||
|
plugins: [vue()], |
||||
|
resolve: { |
||||
|
alias: { |
||||
|
'@': path.resolve(__dirname, 'src'), |
||||
|
}, |
||||
|
}, |
||||
|
}); |