WIP: Improve SQLite initialization and error handling
- Implement XDG Base Directory Specification for data storage - Use $XDG_DATA_HOME (defaults to ~/.local/share) for data files - Add proper directory permissions (700) for security - Fallback to ~/.timesafari if XDG paths fail - Add graceful degradation for SQLite failures - Allow app to boot even if SQLite initialization fails - Track and expose initialization errors via IPC - Add availability checks to all SQLite operations - Improve error reporting and logging - Security improvements - Set secure permissions (700) on data directories - Verify directory permissions on existing paths - Add proper error handling for permission issues TODO: - Fix database creation - Add retry logic for initialization - Add reinitialization capability - Add more detailed error reporting - Consider fallback storage options
This commit is contained in:
55
electron/.gitignore
vendored
Normal file
55
electron/.gitignore
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# NPM renames .gitignore to .npmignore
|
||||||
|
# In order to prevent that, we remove the initial "."
|
||||||
|
# And the CLI then renames it
|
||||||
|
app
|
||||||
|
node_modules
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
logs
|
||||||
|
# Node.js dependencies
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Capacitor build outputs
|
||||||
|
web/
|
||||||
|
ios/
|
||||||
|
android/
|
||||||
|
electron/app/
|
||||||
|
|
||||||
|
# Capacitor SQLite plugin data (important!)
|
||||||
|
capacitor-sqlite/
|
||||||
|
|
||||||
|
# TypeScript / build output
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Development / IDE files
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# JetBrains IDEs (IntelliJ, WebStorm, etc.)
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# macOS specific
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
*.tmp
|
||||||
|
|
||||||
|
# Windows specific
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
$RECYCLE.BIN/
|
||||||
BIN
electron/assets/appIcon.ico
Normal file
BIN
electron/assets/appIcon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
BIN
electron/assets/appIcon.png
Normal file
BIN
electron/assets/appIcon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
BIN
electron/assets/splash.gif
Normal file
BIN
electron/assets/splash.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 159 KiB |
BIN
electron/assets/splash.png
Normal file
BIN
electron/assets/splash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
63
electron/capacitor.config.json
Normal file
63
electron/capacitor.config.json
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"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",
|
||||||
|
"electronLinuxLocation": "~/.local/share/TimeSafari"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ios": {
|
||||||
|
"contentInset": "always",
|
||||||
|
"allowsLinkPreview": true,
|
||||||
|
"scrollEnabled": true,
|
||||||
|
"limitsNavigationsToAppBoundDomains": true,
|
||||||
|
"backgroundColor": "#ffffff",
|
||||||
|
"allowNavigation": [
|
||||||
|
"*.timesafari.app",
|
||||||
|
"*.jsdelivr.net",
|
||||||
|
"api.endorser.ch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"android": {
|
||||||
|
"allowMixedContent": false,
|
||||||
|
"captureInput": true,
|
||||||
|
"webContentsDebuggingEnabled": false,
|
||||||
|
"allowNavigation": [
|
||||||
|
"*.timesafari.app",
|
||||||
|
"*.jsdelivr.net",
|
||||||
|
"api.endorser.ch"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
28
electron/electron-builder.config.json
Normal file
28
electron/electron-builder.config.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"appId": "com.yourdoamnin.yourapp",
|
||||||
|
"directories": {
|
||||||
|
"buildResources": "resources"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"assets/**/*",
|
||||||
|
"build/**/*",
|
||||||
|
"capacitor.config.*",
|
||||||
|
"app/**/*"
|
||||||
|
],
|
||||||
|
"publish": {
|
||||||
|
"provider": "github"
|
||||||
|
},
|
||||||
|
"nsis": {
|
||||||
|
"allowElevation": true,
|
||||||
|
"oneClick": false,
|
||||||
|
"allowToChangeInstallationDirectory": true
|
||||||
|
},
|
||||||
|
"win": {
|
||||||
|
"target": "nsis",
|
||||||
|
"icon": "assets/appIcon.ico"
|
||||||
|
},
|
||||||
|
"mac": {
|
||||||
|
"category": "your.app.category.type",
|
||||||
|
"target": "dmg"
|
||||||
|
}
|
||||||
|
}
|
||||||
75
electron/live-runner.js
Normal file
75
electron/live-runner.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/* eslint-disable no-undef */
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const cp = require('child_process');
|
||||||
|
const chokidar = require('chokidar');
|
||||||
|
const electron = require('electron');
|
||||||
|
|
||||||
|
let child = null;
|
||||||
|
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||||
|
const reloadWatcher = {
|
||||||
|
debouncer: null,
|
||||||
|
ready: false,
|
||||||
|
watcher: null,
|
||||||
|
restarting: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
///*
|
||||||
|
function runBuild() {
|
||||||
|
return new Promise((resolve, _reject) => {
|
||||||
|
let tempChild = cp.spawn(npmCmd, ['run', 'build']);
|
||||||
|
tempChild.once('exit', () => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
tempChild.stdout.pipe(process.stdout);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//*/
|
||||||
|
|
||||||
|
async function spawnElectron() {
|
||||||
|
if (child !== null) {
|
||||||
|
child.stdin.pause();
|
||||||
|
child.kill();
|
||||||
|
child = null;
|
||||||
|
await runBuild();
|
||||||
|
}
|
||||||
|
child = cp.spawn(electron, ['--inspect=5858', './']);
|
||||||
|
child.on('exit', () => {
|
||||||
|
if (!reloadWatcher.restarting) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
child.stdout.pipe(process.stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupReloadWatcher() {
|
||||||
|
reloadWatcher.watcher = chokidar
|
||||||
|
.watch('./src/**/*', {
|
||||||
|
ignored: /[/\\]\./,
|
||||||
|
persistent: true,
|
||||||
|
})
|
||||||
|
.on('ready', () => {
|
||||||
|
reloadWatcher.ready = true;
|
||||||
|
})
|
||||||
|
.on('all', (_event, _path) => {
|
||||||
|
if (reloadWatcher.ready) {
|
||||||
|
clearTimeout(reloadWatcher.debouncer);
|
||||||
|
reloadWatcher.debouncer = setTimeout(async () => {
|
||||||
|
console.log('Restarting');
|
||||||
|
reloadWatcher.restarting = true;
|
||||||
|
await spawnElectron();
|
||||||
|
reloadWatcher.restarting = false;
|
||||||
|
reloadWatcher.ready = false;
|
||||||
|
clearTimeout(reloadWatcher.debouncer);
|
||||||
|
reloadWatcher.debouncer = null;
|
||||||
|
reloadWatcher.watcher = null;
|
||||||
|
setupReloadWatcher();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
await runBuild();
|
||||||
|
await spawnElectron();
|
||||||
|
setupReloadWatcher();
|
||||||
|
})();
|
||||||
5243
electron/package-lock.json
generated
Normal file
5243
electron/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
51
electron/package.json
Normal file
51
electron/package.json
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
|
"@types/crypto-js": "^4.2.2",
|
||||||
|
"@types/electron-json-storage": "^4.5.4",
|
||||||
|
"electron": "^26.2.2",
|
||||||
|
"electron-builder": "~23.6.0",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"typescript": "^5.0.4"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"capacitor",
|
||||||
|
"electron"
|
||||||
|
]
|
||||||
|
}
|
||||||
10
electron/resources/electron-publisher-custom.js
Normal file
10
electron/resources/electron-publisher-custom.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/* eslint-disable no-undef */
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const electronPublish = require('electron-publish');
|
||||||
|
|
||||||
|
class Publisher extends electronPublish.Publisher {
|
||||||
|
async upload(task) {
|
||||||
|
console.log('electron-publisher-custom', task.file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = Publisher;
|
||||||
110
electron/src/index.ts
Normal file
110
electron/src/index.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
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.
|
||||||
|
await app.whenReady();
|
||||||
|
|
||||||
|
// Security - Set Content-Security-Policy based on whether or not we are in dev mode.
|
||||||
|
setupContentSecurityPolicy(myCapacitorApp.getCustomURLScheme());
|
||||||
|
|
||||||
|
// Initialize SQLite and register handlers BEFORE app initialization
|
||||||
|
console.log('[Main] Starting SQLite initialization...');
|
||||||
|
try {
|
||||||
|
// Register handlers first to prevent "no handler" errors
|
||||||
|
setupSQLiteHandlers();
|
||||||
|
console.log('[Main] SQLite handlers registered');
|
||||||
|
|
||||||
|
// Then initialize the plugin
|
||||||
|
await initializeSQLite();
|
||||||
|
console.log('[Main] SQLite plugin initialized successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Main] Failed to initialize SQLite:', error);
|
||||||
|
// Don't proceed with app initialization if SQLite fails
|
||||||
|
throw new Error(`SQLite initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize our app, build windows, and load content.
|
||||||
|
console.log('[Main] Starting app initialization...');
|
||||||
|
await myCapacitorApp.init();
|
||||||
|
console.log('[Main] App initialization complete');
|
||||||
|
|
||||||
|
// Check for updates if we are in a packaged app.
|
||||||
|
if (!electronIsDev) {
|
||||||
|
console.log('[Main] Checking for updates...');
|
||||||
|
autoUpdater.checkForUpdatesAndNotify();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Main] Fatal error during app initialization:', error);
|
||||||
|
// Ensure we notify the user before quitting
|
||||||
|
const mainWindow = myCapacitorApp.getMainWindow();
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('app-error', {
|
||||||
|
type: 'initialization',
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
});
|
||||||
|
// Give the window time to show the error
|
||||||
|
setTimeout(() => app.quit(), 5000);
|
||||||
|
} else {
|
||||||
|
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
|
||||||
76
electron/src/preload.ts
Normal file
76
electron/src/preload.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* Preload script for Electron
|
||||||
|
* Sets up context bridge and handles security policies
|
||||||
|
* @author Matthew Raymer
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { contextBridge, ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
// Enable source maps in development
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
require('source-map-support').install();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log that preload is running
|
||||||
|
console.log('[Preload] Script starting...');
|
||||||
|
|
||||||
|
// Create a proxy for the CapacitorSQLite plugin
|
||||||
|
const createSQLiteProxy = () => {
|
||||||
|
const MAX_RETRIES = 5;
|
||||||
|
const RETRY_DELAY = 1000; // 1 second
|
||||||
|
|
||||||
|
const withRetry = async (operation: string, ...args: any[]) => {
|
||||||
|
let lastError;
|
||||||
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||||
|
try {
|
||||||
|
return await ipcRenderer.invoke(`sqlite-${operation}`, ...args);
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
console.warn(`[Preload] SQLite operation ${operation} failed (attempt ${attempt}/${MAX_RETRIES}), retrying...`, error);
|
||||||
|
if (attempt < MAX_RETRIES) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`SQLite operation ${operation} failed after ${MAX_RETRIES} attempts: ${lastError?.message || 'Unknown error'}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
echo: (value: string) => withRetry('echo', value),
|
||||||
|
createConnection: (options: any) => withRetry('create-connection', options),
|
||||||
|
closeConnection: (options: any) => withRetry('close-connection', options),
|
||||||
|
execute: (options: any) => withRetry('execute', options),
|
||||||
|
query: (options: any) => withRetry('query', options),
|
||||||
|
isAvailable: () => withRetry('is-available'),
|
||||||
|
getPlatform: () => Promise.resolve('electron')
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set up context bridge for secure IPC communication
|
||||||
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
// SQLite operations
|
||||||
|
sqlite: createSQLiteProxy(),
|
||||||
|
|
||||||
|
// Database status events
|
||||||
|
onDatabaseStatus: (callback: (status: { status: string; error?: string }) => void) => {
|
||||||
|
ipcRenderer.on('database-status', (_event, status) => callback(status));
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.removeAllListeners('database-status');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expose CapacitorSQLite globally for the plugin system
|
||||||
|
contextBridge.exposeInMainWorld('CapacitorSQLite', createSQLiteProxy());
|
||||||
|
|
||||||
|
// Handle uncaught errors
|
||||||
|
window.addEventListener('unhandledrejection', (event) => {
|
||||||
|
console.error('[Preload] Unhandled promise rejection:', event.reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('error', (event) => {
|
||||||
|
console.error('[Preload] Unhandled error:', event.error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log that preload is complete
|
||||||
|
console.log('[Preload] Script complete');
|
||||||
6
electron/src/rt/electron-plugins.js
Normal file
6
electron/src/rt/electron-plugins.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const CapacitorCommunitySqlite = require('../../../node_modules/@capacitor-community/sqlite/electron/dist/plugin.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
CapacitorCommunitySqlite,
|
||||||
|
}
|
||||||
88
electron/src/rt/electron-rt.ts
Normal file
88
electron/src/rt/electron-rt.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { randomBytes } from 'crypto';
|
||||||
|
import { ipcRenderer, contextBridge } from 'electron';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const plugins = require('./electron-plugins');
|
||||||
|
|
||||||
|
const randomId = (length = 5) => randomBytes(length).toString('hex');
|
||||||
|
|
||||||
|
const contextApi: {
|
||||||
|
[plugin: string]: { [functionName: string]: () => Promise<any> };
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
Object.keys(plugins).forEach((pluginKey) => {
|
||||||
|
Object.keys(plugins[pluginKey])
|
||||||
|
.filter((className) => className !== 'default')
|
||||||
|
.forEach((classKey) => {
|
||||||
|
const functionList = Object.getOwnPropertyNames(plugins[pluginKey][classKey].prototype).filter(
|
||||||
|
(v) => v !== 'constructor'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!contextApi[classKey]) {
|
||||||
|
contextApi[classKey] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
functionList.forEach((functionName) => {
|
||||||
|
if (!contextApi[classKey][functionName]) {
|
||||||
|
contextApi[classKey][functionName] = (...args) => ipcRenderer.invoke(`${classKey}-${functionName}`, ...args);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Events
|
||||||
|
if (plugins[pluginKey][classKey].prototype instanceof EventEmitter) {
|
||||||
|
const listeners: { [key: string]: { type: string; listener: (...args: any[]) => void } } = {};
|
||||||
|
const listenersOfTypeExist = (type) =>
|
||||||
|
!!Object.values(listeners).find((listenerObj) => listenerObj.type === type);
|
||||||
|
|
||||||
|
Object.assign(contextApi[classKey], {
|
||||||
|
addListener(type: string, callback: (...args) => void) {
|
||||||
|
const id = randomId();
|
||||||
|
|
||||||
|
// Deduplicate events
|
||||||
|
if (!listenersOfTypeExist(type)) {
|
||||||
|
ipcRenderer.send(`event-add-${classKey}`, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventHandler = (_, ...args) => callback(...args);
|
||||||
|
|
||||||
|
ipcRenderer.addListener(`event-${classKey}-${type}`, eventHandler);
|
||||||
|
listeners[id] = { type, listener: eventHandler };
|
||||||
|
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
removeListener(id: string) {
|
||||||
|
if (!listeners[id]) {
|
||||||
|
throw new Error('Invalid id');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type, listener } = listeners[id];
|
||||||
|
|
||||||
|
ipcRenderer.removeListener(`event-${classKey}-${type}`, listener);
|
||||||
|
|
||||||
|
delete listeners[id];
|
||||||
|
|
||||||
|
if (!listenersOfTypeExist(type)) {
|
||||||
|
ipcRenderer.send(`event-remove-${classKey}-${type}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeAllListeners(type: string) {
|
||||||
|
Object.entries(listeners).forEach(([id, listenerObj]) => {
|
||||||
|
if (!type || listenerObj.type === type) {
|
||||||
|
ipcRenderer.removeListener(`event-${classKey}-${listenerObj.type}`, listenerObj.listener);
|
||||||
|
ipcRenderer.send(`event-remove-${classKey}-${listenerObj.type}`);
|
||||||
|
delete listeners[id];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('CapacitorCustomPlatform', {
|
||||||
|
name: 'electron',
|
||||||
|
plugins: contextApi,
|
||||||
|
});
|
||||||
|
////////////////////////////////////////////////////////
|
||||||
356
electron/src/rt/sqlite-init.ts
Normal file
356
electron/src/rt/sqlite-init.ts
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
/**
|
||||||
|
* SQLite initialization for Capacitor Electron
|
||||||
|
* Handles database path setup, plugin initialization, and IPC handlers
|
||||||
|
*
|
||||||
|
* Database Path Handling:
|
||||||
|
* - Uses XDG Base Directory Specification for data storage
|
||||||
|
* - Uses modern plugin conventions (Capacitor 6+ / Plugin 6.x+)
|
||||||
|
* - Supports custom database names without enforced extensions
|
||||||
|
* - Maintains backward compatibility with legacy paths
|
||||||
|
*
|
||||||
|
* XDG Base Directory Specification:
|
||||||
|
* - Uses $XDG_DATA_HOME (defaults to ~/.local/share) for data files
|
||||||
|
* - Falls back to legacy path if XDG environment variables are not set
|
||||||
|
*
|
||||||
|
* @author Matthew Raymer
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { app, ipcMain } from 'electron';
|
||||||
|
import { CapacitorSQLite } from '@capacitor-community/sqlite/electron/dist/plugin.js';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
|
||||||
|
// Simple logger implementation with structured logging
|
||||||
|
const logger = {
|
||||||
|
log: (...args: unknown[]) => console.log('[SQLite]', ...args),
|
||||||
|
error: (...args: unknown[]) => console.error('[SQLite]', ...args),
|
||||||
|
info: (...args: unknown[]) => console.info('[SQLite]', ...args),
|
||||||
|
warn: (...args: unknown[]) => console.warn('[SQLite]', ...args),
|
||||||
|
debug: (...args: unknown[]) => console.debug('[SQLite]', ...args),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Database path resolution utilities following XDG Base Directory Specification
|
||||||
|
const getAppDataPath = async (): Promise<string> => {
|
||||||
|
try {
|
||||||
|
// Get XDG_DATA_HOME or fallback to default
|
||||||
|
const xdgDataHome = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
|
||||||
|
logger.info('XDG_DATA_HOME:', xdgDataHome);
|
||||||
|
|
||||||
|
// Create app data directory following XDG spec
|
||||||
|
const appDataDir = path.join(xdgDataHome, 'timesafari');
|
||||||
|
logger.info('App data directory:', appDataDir);
|
||||||
|
|
||||||
|
// Ensure directory exists with proper permissions (700)
|
||||||
|
if (!fs.existsSync(appDataDir)) {
|
||||||
|
await fs.promises.mkdir(appDataDir, {
|
||||||
|
recursive: true,
|
||||||
|
mode: 0o700 // rwx------ for security
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Ensure existing directory has correct permissions
|
||||||
|
await fs.promises.chmod(appDataDir, 0o700);
|
||||||
|
}
|
||||||
|
|
||||||
|
return appDataDir;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error getting app data path:', error);
|
||||||
|
// Fallback to legacy path in user's home
|
||||||
|
const fallbackDir = path.join(os.homedir(), '.timesafari');
|
||||||
|
logger.warn('Using fallback app data directory:', fallbackDir);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(fallbackDir)) {
|
||||||
|
await fs.promises.mkdir(fallbackDir, {
|
||||||
|
recursive: true,
|
||||||
|
mode: 0o700
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await fs.promises.chmod(fallbackDir, 0o700);
|
||||||
|
}
|
||||||
|
} catch (mkdirError) {
|
||||||
|
logger.error('Failed to create fallback directory:', mkdirError);
|
||||||
|
throw new Error('Could not create any suitable data directory');
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallbackDir;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize database paths
|
||||||
|
let dbPath: string | undefined;
|
||||||
|
let dbDir: string | undefined;
|
||||||
|
let dbPathInitialized = false;
|
||||||
|
let dbPathInitializationPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
|
const initializeDatabasePaths = async (): Promise<void> => {
|
||||||
|
if (dbPathInitializationPromise) return dbPathInitializationPromise;
|
||||||
|
if (dbPathInitialized) return;
|
||||||
|
|
||||||
|
dbPathInitializationPromise = (async () => {
|
||||||
|
try {
|
||||||
|
// Get the app data directory in user's home
|
||||||
|
const absolutePath = await getAppDataPath();
|
||||||
|
logger.info('Absolute database path:', absolutePath);
|
||||||
|
|
||||||
|
// For the plugin, we need to use a path relative to the electron folder
|
||||||
|
// So we'll use a relative path from the electron folder to the home directory
|
||||||
|
const electronPath = app.getAppPath();
|
||||||
|
const relativeToElectron = path.relative(electronPath, absolutePath);
|
||||||
|
dbDir = relativeToElectron;
|
||||||
|
|
||||||
|
logger.info('Database directory (relative to electron):', dbDir);
|
||||||
|
|
||||||
|
// Ensure directory exists using absolute path
|
||||||
|
if (!fs.existsSync(absolutePath)) {
|
||||||
|
await fs.promises.mkdir(absolutePath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use modern plugin conventions - no enforced extension
|
||||||
|
const baseName = 'timesafariSQLite';
|
||||||
|
dbPath = path.join(dbDir, baseName);
|
||||||
|
logger.info('Database path initialized (relative to electron):', dbPath);
|
||||||
|
|
||||||
|
// Verify we can write to the directory using absolute path
|
||||||
|
const testFile = path.join(absolutePath, '.write-test');
|
||||||
|
try {
|
||||||
|
await fs.promises.writeFile(testFile, 'test');
|
||||||
|
await fs.promises.unlink(testFile);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Cannot write to database directory: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set environment variable for the plugin using relative path
|
||||||
|
process.env.CAPACITOR_SQLITE_DB_PATH = dbDir;
|
||||||
|
logger.info('Set CAPACITOR_SQLITE_DB_PATH:', process.env.CAPACITOR_SQLITE_DB_PATH);
|
||||||
|
|
||||||
|
dbPathInitialized = true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to initialize database paths:', error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
dbPathInitializationPromise = null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return dbPathInitializationPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Track initialization state
|
||||||
|
let initializationError: Error | null = null;
|
||||||
|
|
||||||
|
// Initialize SQLite plugin
|
||||||
|
let sqlitePlugin: any = null;
|
||||||
|
let sqliteInitialized = false;
|
||||||
|
let sqliteInitializationPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
|
export async function initializeSQLite(): Promise<void> {
|
||||||
|
if (sqliteInitializationPromise) {
|
||||||
|
logger.info('SQLite initialization already in progress, waiting...');
|
||||||
|
return sqliteInitializationPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sqliteInitialized) {
|
||||||
|
logger.info('SQLite already initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqliteInitializationPromise = (async () => {
|
||||||
|
try {
|
||||||
|
logger.info('Starting SQLite plugin initialization...');
|
||||||
|
|
||||||
|
// Initialize database paths first
|
||||||
|
await initializeDatabasePaths();
|
||||||
|
|
||||||
|
if (!dbPath || !dbDir) {
|
||||||
|
throw new Error('Database path not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create plugin instance
|
||||||
|
logger.info('Creating SQLite plugin instance...');
|
||||||
|
sqlitePlugin = new CapacitorSQLite();
|
||||||
|
|
||||||
|
if (!sqlitePlugin) {
|
||||||
|
throw new Error('Failed to create SQLite plugin instance');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the plugin
|
||||||
|
logger.info('Testing SQLite plugin...');
|
||||||
|
const echoResult = await sqlitePlugin.echo({ value: 'test' });
|
||||||
|
if (!echoResult || echoResult.value !== 'test') {
|
||||||
|
throw new Error('SQLite plugin echo test failed');
|
||||||
|
}
|
||||||
|
logger.info('SQLite plugin echo test successful');
|
||||||
|
|
||||||
|
// Initialize database connection using modern plugin conventions
|
||||||
|
const connectionOptions = {
|
||||||
|
database: 'timesafariSQLite',
|
||||||
|
version: 1,
|
||||||
|
readOnly: false,
|
||||||
|
encryption: 'no-encryption',
|
||||||
|
useNative: true,
|
||||||
|
mode: 'rwc',
|
||||||
|
location: dbDir // Use path relative to electron folder
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info('Creating initial database connection with options:', {
|
||||||
|
...connectionOptions,
|
||||||
|
expectedPath: dbPath // Path relative to electron folder
|
||||||
|
});
|
||||||
|
|
||||||
|
const db = await sqlitePlugin.createConnection(connectionOptions);
|
||||||
|
|
||||||
|
if (!db || typeof db !== 'object') {
|
||||||
|
throw new Error(`Failed to create database connection - invalid response. Path: ${dbPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify connection with a simple query
|
||||||
|
logger.info('Verifying database connection...');
|
||||||
|
try {
|
||||||
|
const result = await db.query({ statement: 'SELECT 1 as test;' });
|
||||||
|
if (!result || !result.values || result.values.length === 0) {
|
||||||
|
throw new Error('Database connection test query returned no results');
|
||||||
|
}
|
||||||
|
logger.info('Database connection verified successfully');
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Database connection test failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
sqliteInitialized = true;
|
||||||
|
logger.info('SQLite plugin initialization completed successfully');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('SQLite plugin initialization failed:', error);
|
||||||
|
// Store the error but don't throw
|
||||||
|
initializationError = error instanceof Error ? error : new Error(String(error));
|
||||||
|
// Reset state on failure but allow app to continue
|
||||||
|
sqlitePlugin = null;
|
||||||
|
sqliteInitialized = false;
|
||||||
|
} finally {
|
||||||
|
sqliteInitializationPromise = null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return sqliteInitializationPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up IPC handlers
|
||||||
|
export function setupSQLiteHandlers(): void {
|
||||||
|
// Remove any existing handlers to prevent duplicates
|
||||||
|
try {
|
||||||
|
ipcMain.removeHandler('sqlite-is-available');
|
||||||
|
ipcMain.removeHandler('sqlite-echo');
|
||||||
|
ipcMain.removeHandler('sqlite-create-connection');
|
||||||
|
ipcMain.removeHandler('sqlite-execute');
|
||||||
|
ipcMain.removeHandler('sqlite-query');
|
||||||
|
ipcMain.removeHandler('sqlite-close-connection');
|
||||||
|
ipcMain.removeHandler('sqlite-get-error');
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Error removing existing handlers:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register all handlers before any are called
|
||||||
|
ipcMain.handle('sqlite-is-available', async () => {
|
||||||
|
try {
|
||||||
|
// Check both plugin instance and initialization state
|
||||||
|
return sqlitePlugin !== null && sqliteInitialized;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error in sqlite-is-available:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add handler to get initialization error
|
||||||
|
ipcMain.handle('sqlite-get-error', async () => {
|
||||||
|
return initializationError ? {
|
||||||
|
message: initializationError.message,
|
||||||
|
stack: initializationError.stack,
|
||||||
|
name: initializationError.name
|
||||||
|
} : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update other handlers to handle unavailable state gracefully
|
||||||
|
ipcMain.handle('sqlite-echo', async (_event, value) => {
|
||||||
|
try {
|
||||||
|
if (!sqlitePlugin || !sqliteInitialized) {
|
||||||
|
throw new Error('SQLite plugin not available');
|
||||||
|
}
|
||||||
|
return await sqlitePlugin.echo({ value });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error in sqlite-echo:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('sqlite-create-connection', async (_event, options) => {
|
||||||
|
try {
|
||||||
|
if (!sqlitePlugin || !sqliteInitialized) {
|
||||||
|
throw new Error('SQLite plugin not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dbPath || !dbDir) {
|
||||||
|
throw new Error('Database path not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use modern plugin conventions for connection options
|
||||||
|
const connectionOptions = {
|
||||||
|
...options,
|
||||||
|
database: 'timesafariSQLite',
|
||||||
|
readOnly: false,
|
||||||
|
mode: 'rwc',
|
||||||
|
encryption: 'no-encryption',
|
||||||
|
useNative: true,
|
||||||
|
location: dbDir // Use path relative to electron folder
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info('Creating database connection with options:', connectionOptions);
|
||||||
|
const result = await sqlitePlugin.createConnection(connectionOptions);
|
||||||
|
|
||||||
|
if (!result || typeof result !== 'object') {
|
||||||
|
throw new Error('Failed to create database connection - invalid response');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error in sqlite-create-connection:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('sqlite-execute', async (_event, options) => {
|
||||||
|
try {
|
||||||
|
if (!sqlitePlugin || !sqliteInitialized) {
|
||||||
|
throw new Error('SQLite plugin not available');
|
||||||
|
}
|
||||||
|
return await sqlitePlugin.execute(options);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error in sqlite-execute:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('sqlite-query', async (_event, options) => {
|
||||||
|
try {
|
||||||
|
if (!sqlitePlugin || !sqliteInitialized) {
|
||||||
|
throw new Error('SQLite plugin not available');
|
||||||
|
}
|
||||||
|
return await sqlitePlugin.query(options);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error in sqlite-query:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('sqlite-close-connection', async (_event, options) => {
|
||||||
|
try {
|
||||||
|
if (!sqlitePlugin || !sqliteInitialized) {
|
||||||
|
throw new Error('SQLite plugin not available');
|
||||||
|
}
|
||||||
|
return await sqlitePlugin.closeConnection(options);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error in sqlite-close-connection:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('SQLite IPC handlers registered successfully');
|
||||||
|
}
|
||||||
244
electron/src/setup.ts
Normal file
244
electron/src/setup.ts
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
// Define components for a watcher to detect when the webapp is changed so we can reload in Dev mode.
|
||||||
|
const reloadWatcher = {
|
||||||
|
debouncer: null,
|
||||||
|
ready: false,
|
||||||
|
watcher: null,
|
||||||
|
};
|
||||||
|
export function setupReloadWatcher(electronCapacitorApp: ElectronCapacitorApp): void {
|
||||||
|
reloadWatcher.watcher = chokidar
|
||||||
|
.watch(join(app.getAppPath(), 'app'), {
|
||||||
|
ignored: /[/\\]\./,
|
||||||
|
persistent: true,
|
||||||
|
})
|
||||||
|
.on('ready', () => {
|
||||||
|
reloadWatcher.ready = true;
|
||||||
|
})
|
||||||
|
.on('all', (_event, _path) => {
|
||||||
|
if (reloadWatcher.ready) {
|
||||||
|
clearTimeout(reloadWatcher.debouncer);
|
||||||
|
reloadWatcher.debouncer = setTimeout(async () => {
|
||||||
|
electronCapacitorApp.getMainWindow().webContents.reload();
|
||||||
|
reloadWatcher.ready = false;
|
||||||
|
clearTimeout(reloadWatcher.debouncer);
|
||||||
|
reloadWatcher.debouncer = null;
|
||||||
|
reloadWatcher.watcher = null;
|
||||||
|
setupReloadWatcher(electronCapacitorApp);
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 and construct our main window.
|
||||||
|
const preloadPath = join(app.getAppPath(), 'build', 'src', 'preload.js');
|
||||||
|
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: true,
|
||||||
|
contextIsolation: true,
|
||||||
|
// Use preload to inject the electron varriant overrides for capacitor plugins.
|
||||||
|
// preload: join(app.getAppPath(), "node_modules", "@capacitor-community", "electron", "dist", "runtime", "electron-rt.js"),
|
||||||
|
preload: preloadPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
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}://* 'unsafe-inline' data:;`,
|
||||||
|
// Allow Google Fonts
|
||||||
|
`style-src ${customScheme}://* 'unsafe-inline' https://fonts.googleapis.com;`,
|
||||||
|
`font-src ${customScheme}://* https://fonts.gstatic.com;`,
|
||||||
|
// Allow images and media
|
||||||
|
`img-src ${customScheme}://* data: https:;`,
|
||||||
|
// Allow connections to HTTPS resources
|
||||||
|
`connect-src ${customScheme}://* https:;`,
|
||||||
|
// Add dev-specific policies
|
||||||
|
...(electronIsDev ? [
|
||||||
|
`script-src ${customScheme}://* 'unsafe-inline' 'unsafe-eval' devtools://*;`,
|
||||||
|
`default-src ${customScheme}://* 'unsafe-inline' devtools://* 'unsafe-eval' data:;`
|
||||||
|
] : [])
|
||||||
|
].join(' ')
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
18
electron/tsconfig.json
Normal file
18
electron/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compileOnSave": true,
|
||||||
|
"include": ["./src/**/*", "./capacitor.config.ts", "./capacitor.config.js"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./build",
|
||||||
|
"importHelpers": true,
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"typeRoots": ["./node_modules/@types"],
|
||||||
|
"allowJs": true,
|
||||||
|
"rootDir": ".",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
81
package-lock.json
generated
81
package-lock.json
generated
@@ -19,8 +19,8 @@
|
|||||||
"@capacitor/ios": "^6.2.0",
|
"@capacitor/ios": "^6.2.0",
|
||||||
"@capacitor/share": "^6.0.3",
|
"@capacitor/share": "^6.0.3",
|
||||||
"@capawesome/capacitor-file-picker": "^6.2.0",
|
"@capawesome/capacitor-file-picker": "^6.2.0",
|
||||||
"@dicebear/collection": "^5.4.1",
|
"@dicebear/collection": "^5.4.3",
|
||||||
"@dicebear/core": "^5.4.1",
|
"@dicebear/core": "^5.4.3",
|
||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
"@ethersproject/wallet": "^5.8.0",
|
"@ethersproject/wallet": "^5.8.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
"@peculiar/asn1-schema": "^2.3.8",
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
||||||
"@simplewebauthn/browser": "^10.0.0",
|
"@simplewebauthn/browser": "^10.0.0",
|
||||||
"@simplewebauthn/server": "^10.0.0",
|
"@simplewebauthn/server": "^10.0.1",
|
||||||
"@tweenjs/tween.js": "^21.1.1",
|
"@tweenjs/tween.js": "^21.1.1",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@veramo/core": "^5.6.0",
|
"@veramo/core": "^5.6.0",
|
||||||
@@ -57,22 +57,22 @@
|
|||||||
"did-resolver": "^4.1.0",
|
"did-resolver": "^4.1.0",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"electron-json-storage": "^4.6.0",
|
"electron-json-storage": "^4.6.0",
|
||||||
"ethereum-cryptography": "^2.1.3",
|
"ethereum-cryptography": "^2.2.1",
|
||||||
"ethereumjs-util": "^7.1.5",
|
"ethereumjs-util": "^7.1.5",
|
||||||
"jdenticon": "^3.2.0",
|
"jdenticon": "^3.3.0",
|
||||||
"js-generate-password": "^0.1.9",
|
"js-generate-password": "^0.1.9",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsqr": "^1.4.0",
|
"jsqr": "^1.4.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"localstorage-slim": "^2.7.0",
|
"localstorage-slim": "^2.7.0",
|
||||||
"lru-cache": "^10.2.0",
|
"lru-cache": "^10.4.3",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"merkletreejs": "^0.3.11",
|
"merkletreejs": "^0.3.11",
|
||||||
"nostr-tools": "^2.10.4",
|
"nostr-tools": "^2.13.1",
|
||||||
"notiwind": "^2.0.2",
|
"notiwind": "^2.0.2",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"pina": "^0.20.2204228",
|
"pina": "^0.20.2204228",
|
||||||
"pinia-plugin-persistedstate": "^3.2.1",
|
"pinia-plugin-persistedstate": "^3.2.3",
|
||||||
"qr-code-generator-vue3": "^1.4.21",
|
"qr-code-generator-vue3": "^1.4.21",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"ramda": "^0.29.1",
|
"ramda": "^0.29.1",
|
||||||
@@ -88,12 +88,13 @@
|
|||||||
"vue-axios": "^3.5.2",
|
"vue-axios": "^3.5.2",
|
||||||
"vue-facing-decorator": "^3.0.4",
|
"vue-facing-decorator": "^3.0.4",
|
||||||
"vue-picture-cropper": "^0.7.0",
|
"vue-picture-cropper": "^0.7.0",
|
||||||
"vue-qrcode-reader": "^5.5.3",
|
"vue-qrcode-reader": "^5.7.2",
|
||||||
"vue-router": "^4.5.0",
|
"vue-router": "^4.5.0",
|
||||||
"web-did-resolver": "^2.0.27",
|
"web-did-resolver": "^2.0.30",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@capacitor-community/electron": "^5.0.1",
|
||||||
"@capacitor/assets": "^3.0.5",
|
"@capacitor/assets": "^3.0.5",
|
||||||
"@playwright/test": "^1.45.2",
|
"@playwright/test": "^1.45.2",
|
||||||
"@types/dom-webcodecs": "^0.1.7",
|
"@types/dom-webcodecs": "^0.1.7",
|
||||||
@@ -108,7 +109,7 @@
|
|||||||
"@types/ua-parser-js": "^0.7.39",
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
"@typescript-eslint/parser": "^6.21.0",
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.4",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"browserify-fs": "^1.0.0",
|
"browserify-fs": "^1.0.0",
|
||||||
@@ -128,6 +129,7 @@
|
|||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "~5.2.2",
|
"typescript": "~5.2.2",
|
||||||
"vite": "^5.2.0",
|
"vite": "^5.2.0",
|
||||||
@@ -2514,6 +2516,40 @@
|
|||||||
"node": ">=8.9"
|
"node": ">=8.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@capacitor-community/electron": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@capacitor-community/electron/-/electron-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-4/x12ycTq0Kq8JIn/BmIBdFVP5Cqw8iA6SU6YfFjmONfjW3OELwsB3zwLxOwAjLxnjyCMOBHl4ci9E5jLgZgAQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@capacitor/cli": ">=5.4.0",
|
||||||
|
"@capacitor/core": ">=5.4.0",
|
||||||
|
"@ionic/utils-fs": "~3.1.6",
|
||||||
|
"chalk": "^4.1.2",
|
||||||
|
"electron-is-dev": "~2.0.0",
|
||||||
|
"events": "~3.3.0",
|
||||||
|
"fs-extra": "~11.1.1",
|
||||||
|
"keyv": "^4.5.2",
|
||||||
|
"mime-types": "~2.1.35",
|
||||||
|
"ora": "^5.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@capacitor-community/electron/node_modules/fs-extra": {
|
||||||
|
"version": "11.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz",
|
||||||
|
"integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.2.0",
|
||||||
|
"jsonfile": "^6.0.1",
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@capacitor-community/sqlite": {
|
"node_modules/@capacitor-community/sqlite": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor-community/sqlite/-/sqlite-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor-community/sqlite/-/sqlite-6.0.2.tgz",
|
||||||
@@ -15261,6 +15297,16 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/electron-is-dev": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-3X99K852Yoqu9AcW50qz3ibYBWY79/pBhlMCab8ToEWS48R0T9tyxRiQhwylE7zQdXrMnx2JKqUJyMPmt5FBqA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-json-storage": {
|
"node_modules/electron-json-storage": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/electron-json-storage/-/electron-json-storage-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/electron-json-storage/-/electron-json-storage-4.6.0.tgz",
|
||||||
@@ -23081,9 +23127,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nostr-tools": {
|
"node_modules/nostr-tools": {
|
||||||
"version": "2.13.0",
|
"version": "2.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.13.1.tgz",
|
||||||
"integrity": "sha512-A1arGsvpULqVK0NmZQqK1imwaCiPm8gcG/lo+cTax2NbNqBEYsuplbqAFdVqcGHEopmkByYbTwF76x25+oEbew==",
|
"integrity": "sha512-EKcicym1ree14m8lU0b6sZIf46NcxOHvGwUWWAuxjhutOmJM5Pc9ylR1YqxbgpqDQpkEYQwv/d3GupK5CJj9ow==",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/ciphers": "^0.5.1",
|
"@noble/ciphers": "^0.5.1",
|
||||||
@@ -23091,9 +23137,7 @@
|
|||||||
"@noble/hashes": "1.3.1",
|
"@noble/hashes": "1.3.1",
|
||||||
"@scure/base": "1.1.1",
|
"@scure/base": "1.1.1",
|
||||||
"@scure/bip32": "1.3.1",
|
"@scure/bip32": "1.3.1",
|
||||||
"@scure/bip39": "1.2.1"
|
"@scure/bip39": "1.2.1",
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"nostr-wasm": "0.1.0"
|
"nostr-wasm": "0.1.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -23205,8 +23249,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz",
|
||||||
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
|
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/notiwind": {
|
"node_modules/notiwind": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
|
|||||||
24
package.json
24
package.json
@@ -58,8 +58,8 @@
|
|||||||
"@capacitor/ios": "^6.2.0",
|
"@capacitor/ios": "^6.2.0",
|
||||||
"@capacitor/share": "^6.0.3",
|
"@capacitor/share": "^6.0.3",
|
||||||
"@capawesome/capacitor-file-picker": "^6.2.0",
|
"@capawesome/capacitor-file-picker": "^6.2.0",
|
||||||
"@dicebear/collection": "^5.4.1",
|
"@dicebear/collection": "^5.4.3",
|
||||||
"@dicebear/core": "^5.4.1",
|
"@dicebear/core": "^5.4.3",
|
||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
"@ethersproject/wallet": "^5.8.0",
|
"@ethersproject/wallet": "^5.8.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
"@peculiar/asn1-schema": "^2.3.8",
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
||||||
"@simplewebauthn/browser": "^10.0.0",
|
"@simplewebauthn/browser": "^10.0.0",
|
||||||
"@simplewebauthn/server": "^10.0.0",
|
"@simplewebauthn/server": "^10.0.1",
|
||||||
"@tweenjs/tween.js": "^21.1.1",
|
"@tweenjs/tween.js": "^21.1.1",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@veramo/core": "^5.6.0",
|
"@veramo/core": "^5.6.0",
|
||||||
@@ -96,22 +96,22 @@
|
|||||||
"did-resolver": "^4.1.0",
|
"did-resolver": "^4.1.0",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"electron-json-storage": "^4.6.0",
|
"electron-json-storage": "^4.6.0",
|
||||||
"ethereum-cryptography": "^2.1.3",
|
"ethereum-cryptography": "^2.2.1",
|
||||||
"ethereumjs-util": "^7.1.5",
|
"ethereumjs-util": "^7.1.5",
|
||||||
"jdenticon": "^3.2.0",
|
"jdenticon": "^3.3.0",
|
||||||
"js-generate-password": "^0.1.9",
|
"js-generate-password": "^0.1.9",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsqr": "^1.4.0",
|
"jsqr": "^1.4.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"localstorage-slim": "^2.7.0",
|
"localstorage-slim": "^2.7.0",
|
||||||
"lru-cache": "^10.2.0",
|
"lru-cache": "^10.4.3",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"merkletreejs": "^0.3.11",
|
"merkletreejs": "^0.3.11",
|
||||||
"nostr-tools": "^2.10.4",
|
"nostr-tools": "^2.13.1",
|
||||||
"notiwind": "^2.0.2",
|
"notiwind": "^2.0.2",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"pina": "^0.20.2204228",
|
"pina": "^0.20.2204228",
|
||||||
"pinia-plugin-persistedstate": "^3.2.1",
|
"pinia-plugin-persistedstate": "^3.2.3",
|
||||||
"qr-code-generator-vue3": "^1.4.21",
|
"qr-code-generator-vue3": "^1.4.21",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"ramda": "^0.29.1",
|
"ramda": "^0.29.1",
|
||||||
@@ -127,12 +127,13 @@
|
|||||||
"vue-axios": "^3.5.2",
|
"vue-axios": "^3.5.2",
|
||||||
"vue-facing-decorator": "^3.0.4",
|
"vue-facing-decorator": "^3.0.4",
|
||||||
"vue-picture-cropper": "^0.7.0",
|
"vue-picture-cropper": "^0.7.0",
|
||||||
"vue-qrcode-reader": "^5.5.3",
|
"vue-qrcode-reader": "^5.7.2",
|
||||||
"vue-router": "^4.5.0",
|
"vue-router": "^4.5.0",
|
||||||
"web-did-resolver": "^2.0.27",
|
"web-did-resolver": "^2.0.30",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@capacitor-community/electron": "^5.0.1",
|
||||||
"@capacitor/assets": "^3.0.5",
|
"@capacitor/assets": "^3.0.5",
|
||||||
"@playwright/test": "^1.45.2",
|
"@playwright/test": "^1.45.2",
|
||||||
"@types/dom-webcodecs": "^0.1.7",
|
"@types/dom-webcodecs": "^0.1.7",
|
||||||
@@ -147,7 +148,7 @@
|
|||||||
"@types/ua-parser-js": "^0.7.39",
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
"@typescript-eslint/parser": "^6.21.0",
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.4",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"browserify-fs": "^1.0.0",
|
"browserify-fs": "^1.0.0",
|
||||||
@@ -167,6 +168,7 @@
|
|||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "~5.2.2",
|
"typescript": "~5.2.2",
|
||||||
"vite": "^5.2.0",
|
"vite": "^5.2.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user