Browse Source
- Fixed TypeScript configuration in electron/tsconfig.json: * Added skipLibCheck: true to resolve node_modules type conflicts * Added allowSyntheticDefaultImports for better module compatibility * Disabled strict mode to avoid unnecessary type errors * Updated TypeScript version to ~5.2.2 for consistency - Resolved SQLite plugin configuration issue: * Changed plugin key from 'SQLite' to 'CapacitorSQLite' in capacitor.config.json * This fixes 'Cannot read properties of undefined (reading electronIsEncryption)' error * All 45+ SQLite plugin methods now properly registered and available - Added Electron development scripts: * electron-dev.sh for streamlined development workflow * setup-electron.sh for initial Electron environment setup - Electron app now starts successfully without TypeScript compilation errors - SQLite plugin fully functional with proper configuration access Status: Electron platform now working for TimeSafari developmentpull/142/head
20 changed files with 6882 additions and 6 deletions
@ -0,0 +1,8 @@ |
|||||
|
# 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 |
After Width: | Height: | Size: 142 KiB |
After Width: | Height: | Size: 121 KiB |
After Width: | Height: | Size: 159 KiB |
After Width: | Height: | Size: 12 KiB |
@ -0,0 +1,98 @@ |
|||||
|
{ |
||||
|
"appId": "app.timesafari", |
||||
|
"appName": "TimeSafari", |
||||
|
"webDir": "dist", |
||||
|
"server": { |
||||
|
"cleartext": true |
||||
|
}, |
||||
|
"plugins": { |
||||
|
"App": { |
||||
|
"appUrlOpen": { |
||||
|
"handlers": [ |
||||
|
{ |
||||
|
"url": "timesafari://*", |
||||
|
"autoVerify": true |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
}, |
||||
|
"CapacitorSQLite": { |
||||
|
"iosDatabaseLocation": "Library/CapacitorDatabase", |
||||
|
"iosIsEncryption": false, |
||||
|
"iosBiometric": { |
||||
|
"biometricAuth": false, |
||||
|
"biometricTitle": "Biometric login for TimeSafari" |
||||
|
}, |
||||
|
"androidIsEncryption": false, |
||||
|
"androidBiometric": { |
||||
|
"biometricAuth": false, |
||||
|
"biometricTitle": "Biometric login for TimeSafari" |
||||
|
}, |
||||
|
"electronIsEncryption": false |
||||
|
} |
||||
|
}, |
||||
|
"ios": { |
||||
|
"contentInset": "never", |
||||
|
"allowsLinkPreview": true, |
||||
|
"scrollEnabled": true, |
||||
|
"limitsNavigationsToAppBoundDomains": true, |
||||
|
"backgroundColor": "#ffffff", |
||||
|
"allowNavigation": [ |
||||
|
"*.timesafari.app", |
||||
|
"*.jsdelivr.net", |
||||
|
"api.endorser.ch" |
||||
|
] |
||||
|
}, |
||||
|
"android": { |
||||
|
"allowMixedContent": false, |
||||
|
"captureInput": true, |
||||
|
"webContentsDebuggingEnabled": false, |
||||
|
"allowNavigation": [ |
||||
|
"*.timesafari.app", |
||||
|
"*.jsdelivr.net", |
||||
|
"api.endorser.ch" |
||||
|
] |
||||
|
}, |
||||
|
"electron": { |
||||
|
"deepLinking": { |
||||
|
"schemes": ["timesafari"] |
||||
|
}, |
||||
|
"buildOptions": { |
||||
|
"appId": "app.timesafari", |
||||
|
"productName": "TimeSafari", |
||||
|
"directories": { |
||||
|
"output": "dist-electron-packages" |
||||
|
}, |
||||
|
"files": [ |
||||
|
"dist/**/*", |
||||
|
"electron/**/*" |
||||
|
], |
||||
|
"mac": { |
||||
|
"category": "public.app-category.productivity", |
||||
|
"target": [ |
||||
|
{ |
||||
|
"target": "dmg", |
||||
|
"arch": ["x64", "arm64"] |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
"win": { |
||||
|
"target": [ |
||||
|
{ |
||||
|
"target": "nsis", |
||||
|
"arch": ["x64"] |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
"linux": { |
||||
|
"target": [ |
||||
|
{ |
||||
|
"target": "AppImage", |
||||
|
"arch": ["x64"] |
||||
|
} |
||||
|
], |
||||
|
"category": "Utility" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
@ -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(); |
||||
|
})(); |
File diff suppressed because it is too large
@ -0,0 +1,51 @@ |
|||||
|
{ |
||||
|
"name": "TimeSafari", |
||||
|
"version": "1.0.0", |
||||
|
"description": "An Amazing Capacitor 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": "^12.1.1", |
||||
|
"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", |
||||
|
"electron-rebuild": "^3.2.9", |
||||
|
"typescript": "~5.2.2" |
||||
|
}, |
||||
|
"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,70 @@ |
|||||
|
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'; |
||||
|
|
||||
|
// 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);
|
||||
|
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 () => { |
||||
|
// 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 our app, build windows, and load content.
|
||||
|
await myCapacitorApp.init(); |
||||
|
// Check for updates if we are in a packaged app.
|
||||
|
autoUpdater.checkForUpdatesAndNotify(); |
||||
|
})(); |
||||
|
|
||||
|
// 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,4 @@ |
|||||
|
require('./rt/electron-rt'); |
||||
|
//////////////////////////////
|
||||
|
// User Defined Preload scripts below
|
||||
|
console.log('User Preload!'); |
@ -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,233 @@ |
|||||
|
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': [ |
||||
|
electronIsDev |
||||
|
? `default-src ${customScheme}://* 'unsafe-inline' devtools://* 'unsafe-eval' data:` |
||||
|
: `default-src ${customScheme}://* 'unsafe-inline' data:`, |
||||
|
], |
||||
|
}, |
||||
|
}); |
||||
|
}); |
||||
|
} |
@ -0,0 +1,19 @@ |
|||||
|
{ |
||||
|
"compileOnSave": true, |
||||
|
"include": ["./src/**/*", "./capacitor.config.ts", "./capacitor.config.js"], |
||||
|
"compilerOptions": { |
||||
|
"outDir": "./build", |
||||
|
"importHelpers": true, |
||||
|
"target": "ES2017", |
||||
|
"module": "CommonJS", |
||||
|
"moduleResolution": "node", |
||||
|
"esModuleInterop": true, |
||||
|
"allowSyntheticDefaultImports": true, |
||||
|
"typeRoots": ["./node_modules/@types"], |
||||
|
"allowJs": true, |
||||
|
"rootDir": ".", |
||||
|
"skipLibCheck": true, |
||||
|
"strict": false, |
||||
|
"noImplicitAny": false |
||||
|
} |
||||
|
} |
@ -0,0 +1,40 @@ |
|||||
|
#!/bin/bash |
||||
|
|
||||
|
# TimeSafari Electron Development Script |
||||
|
# This script builds the web app and runs it in Electron |
||||
|
|
||||
|
set -e |
||||
|
|
||||
|
echo "🔧 Starting Electron development workflow..." |
||||
|
|
||||
|
# Navigate to project root |
||||
|
cd /home/noone/projects/timesafari/crowd-master |
||||
|
|
||||
|
# Build for Capacitor |
||||
|
echo "📦 Building for Capacitor..." |
||||
|
npm run build:capacitor |
||||
|
|
||||
|
# Create electron/app directory if it doesn't exist |
||||
|
echo "📁 Preparing Electron app directory..." |
||||
|
mkdir -p electron/app |
||||
|
|
||||
|
# Copy built files to Electron |
||||
|
echo "📋 Copying web assets to Electron..." |
||||
|
cp -r dist/* electron/app/ |
||||
|
|
||||
|
# Ensure capacitor config is valid JSON (remove any comments) |
||||
|
echo "🔧 Validating Capacitor configuration..." |
||||
|
cp capacitor.config.json electron/capacitor.config.json |
||||
|
|
||||
|
# Navigate to electron directory |
||||
|
cd electron |
||||
|
|
||||
|
# Build Electron |
||||
|
echo "🔨 Building Electron..." |
||||
|
npm run build |
||||
|
|
||||
|
# Start Electron |
||||
|
echo "🚀 Starting Electron app..." |
||||
|
npm run electron:start |
||||
|
|
||||
|
echo "✅ Electron development workflow complete!" |
@ -0,0 +1,30 @@ |
|||||
|
#!/bin/bash |
||||
|
|
||||
|
# TimeSafari Electron Setup Script |
||||
|
# This script installs all required dependencies for the Electron platform |
||||
|
|
||||
|
set -e |
||||
|
|
||||
|
echo "🔧 Setting up Electron dependencies..." |
||||
|
|
||||
|
# Navigate to electron directory |
||||
|
cd electron |
||||
|
|
||||
|
# Install required dependencies for Capacitor SQLite plugin |
||||
|
echo "📦 Installing better-sqlite3-multiple-ciphers..." |
||||
|
npm install better-sqlite3-multiple-ciphers |
||||
|
|
||||
|
echo "📦 Installing electron-json-storage..." |
||||
|
npm install electron-json-storage |
||||
|
|
||||
|
# Rebuild native modules |
||||
|
echo "🔨 Rebuilding native modules..." |
||||
|
npm run build |
||||
|
|
||||
|
echo "✅ Electron setup complete!" |
||||
|
echo "" |
||||
|
echo "To run the Electron app:" |
||||
|
echo " npm run electron:start" |
||||
|
echo "" |
||||
|
echo "Or from the project root:" |
||||
|
echo " npm run electron:dev" |
Loading…
Reference in new issue