diff --git a/electron-builder.json b/electron-builder.json new file mode 100644 index 0000000..3e630d9 --- /dev/null +++ b/electron-builder.json @@ -0,0 +1,36 @@ +{ + "appId": "app.timesafari.app", + "productName": "TimeSafari", + "directories": { + "output": "dist-electron-packages", + "buildResources": "build" + }, + "files": [ + "dist-electron/**/*", + "node_modules/**/*", + "package.json", + "src/electron/electron-logger.js" + ], + "extraResources": [ + { + "from": "src/utils", + "to": "utils", + "filter": ["**/*"] + } + ], + "extraMetadata": { + "main": "src/electron/main.js" + }, + "linux": { + "target": ["AppImage"], + "category": "Utility", + "maintainer": "TimeSafari Team" + }, + "mac": { + "target": ["dmg"], + "category": "public.app-category.productivity" + }, + "win": { + "target": ["nsis"] + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3bbeb7c..be1cf75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4666,12 +4666,6 @@ "scrypt-js": "3.0.1" } }, - "node_modules/@ethersproject/json-wallets/node_modules/aes-js": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", - "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==", - "license": "MIT" - }, "node_modules/@ethersproject/keccak256": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.8.0.tgz", @@ -5004,9 +4998,9 @@ } }, "node_modules/@expo/cli": { - "version": "0.22.19", - "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.22.19.tgz", - "integrity": "sha512-vgAM3gUsAUQWgDm10RIYSrWQ5q235Ir/lMUdx5Yd/gObYaDlaBIdsq5H72eJ44QB+4ndvhm2wbqntTI19kBIRw==", + "version": "0.22.20", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.22.20.tgz", + "integrity": "sha512-BU2ASlw0Gaj3ou/TxVsgvzK+XK8Z14Yq3mmLyvMcMAQrdExZLNmvMZ3A3x6q2uMgSJM3aoQBUuVXS/Ny+lYgDA==", "license": "MIT", "optional": true, "peer": true, @@ -10391,9 +10385,9 @@ } }, "node_modules/@vitejs/plugin-vue": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz", - "integrity": "sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.2.tgz", + "integrity": "sha512-IY0aPonWZI2huxrWjoSBUQX14GThitmr1sc2OUJymcgnY5RlUI7HoXGAnFEoVNRsck/kS6inGvxCN6CoHu86yQ==", "dev": true, "license": "MIT", "engines": { @@ -10913,9 +10907,9 @@ } }, "node_modules/aes-js": { - "version": "4.0.0-beta.5", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", - "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", + "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==", "license": "MIT" }, "node_modules/agent-base": { @@ -12594,9 +12588,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001704", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001704.tgz", - "integrity": "sha512-+L2IgBbV6gXB4ETf0keSvLr7JUrRVbIaB/lrQ1+z8mRcQiisG5k+lG6O4n6Y5q6f5EuNfaYXKgymucphlEXQew==", + "version": "1.0.30001705", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001705.tgz", + "integrity": "sha512-S0uyMMiYvA7CxNgomYBwwwPUnWzFD83f3B1ce5jHUfHTH//QL6hHsreI8RVC5606R4ssqravelYO5TU6t8sEyg==", "devOptional": true, "funding": [ { @@ -14518,9 +14512,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.118", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.118.tgz", - "integrity": "sha512-yNDUus0iultYyVoEFLnQeei7LOQkL8wg8GQpkPCRrOlJXlcCwa6eGKZkxQ9ciHsqZyYbj8Jd94X1CTPzGm+uIA==", + "version": "1.5.119", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.119.tgz", + "integrity": "sha512-Ku4NMzUjz3e3Vweh7PhApPrZSS4fyiCIbcIrG9eKrriYVLmbMepETR/v6SU7xPm98QTqMSYiCwfO89QNjXLkbQ==", "devOptional": true, "license": "ISC" }, @@ -15303,6 +15297,12 @@ "undici-types": "~6.19.2" } }, + "node_modules/ethers/node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "license": "MIT" + }, "node_modules/ethers/node_modules/tslib": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", @@ -15554,15 +15554,15 @@ } }, "node_modules/expo": { - "version": "52.0.38", - "resolved": "https://registry.npmjs.org/expo/-/expo-52.0.38.tgz", - "integrity": "sha512-6DZJjN/oEeYOPGoNUWE41vUuwVSl/Cg9o3rTbP62Pchgspp61Elsf8G7FtdcAtdgOzkJmbnPrPqklpMXGwpgfA==", + "version": "52.0.39", + "resolved": "https://registry.npmjs.org/expo/-/expo-52.0.39.tgz", + "integrity": "sha512-EOnrgj8MHSt0o0SIBhM7jCim2QpJJNonbSATn9LqNtVgKtotIg718G/OrP5/g0GUAOBDyxHH9PfNu/aq9c0vDw==", "license": "MIT", "optional": true, "peer": true, "dependencies": { "@babel/runtime": "^7.20.0", - "@expo/cli": "0.22.19", + "@expo/cli": "0.22.20", "@expo/config": "~10.0.11", "@expo/config-plugins": "~9.0.17", "@expo/fingerprint": "0.11.11", @@ -16242,9 +16242,9 @@ "peer": true }, "node_modules/flow-parser": { - "version": "0.265.0", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.265.0.tgz", - "integrity": "sha512-C+bg/TZsDVlLMF14+q9P9FB2pjQSgWwYs0pkIMPE1FsZWS4A0kk1M28V6YphpxAPr3AISVRZ6VgpDepvCk6dGw==", + "version": "0.265.2", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.265.2.tgz", + "integrity": "sha512-DX2mp5u3lNJHl5dH8R1KrcrDsiJC02zFcG95p4b0YcDCzZZW+v9za2Csv5bQ0cq4jNzGx0gFU9jFZyM7FOyNFw==", "license": "MIT", "optional": true, "peer": true, @@ -21378,9 +21378,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.9.tgz", - "integrity": "sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg==", + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.10.tgz", + "integrity": "sha512-vSJJTG+t/dIKAUhUDw/dLdZ9s//5OxcHqLaDWWrW4Cdq7o6tdLIczUkMXt2MBNmk6sJRZBZRXVixs7URY1CmIg==", "funding": [ { "type": "github", @@ -21729,9 +21729,9 @@ } }, "node_modules/nostr-tools": { - "version": "2.10.4", - "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.10.4.tgz", - "integrity": "sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.11.0.tgz", + "integrity": "sha512-kRtXI9j5f45NvIcdJacQ0UEAfEb7p/jhZqhAGLQWtUd5idZJPYdSyR8hdw+MmpGH4TCMH5plZrXzFltIIZrkEA==", "license": "Unlicense", "dependencies": { "@noble/ciphers": "^0.5.1", diff --git a/package.json b/package.json index c54c89e..c7b2930 100644 --- a/package.json +++ b/package.json @@ -23,14 +23,14 @@ "check:ios-device": "xcrun xctrace list devices 2>&1 | grep -w 'Booted' || (echo 'No iOS simulator running' && exit 1)", "clean:electron": "rimraf dist-electron", "build:pywebview": "vite build --config vite.config.pywebview.mts", - "build:electron": "npm run clean:electron && vite build --config vite.config.electron.mts && node scripts/build-electron.js", + "build:electron": "npm run check:electron && npm run clean:electron && vite build --config vite.config.electron.mts && node scripts/build-electron.js", "build:capacitor": "vite build --config vite.config.capacitor.mts", "build:web": "vite build --config vite.config.web.mts", - "electron:dev": "npm run build && electron dist-electron", + "electron:dev": "concurrently \"vite --config vite.config.electron.mts\" \"electron .\"", "electron:start": "electron dist-electron", - "electron:build-linux": "npm run build:electron && electron-builder --linux AppImage", - "electron:build-linux-deb": "npm run build:electron && electron-builder --linux deb", - "electron:build-linux-prod": "NODE_ENV=production npm run build:electron && electron-builder --linux AppImage", + "electron:build-linux": "npm run check:electron && npm run build:electron && electron-builder --linux AppImage", + "electron:build-linux-deb": "npm run check:electron && npm run build:electron && electron-builder --linux deb", + "electron:build-linux-prod": "NODE_ENV=production npm run check:electron &&npm run build:electron && electron-builder --linux AppImage", "build:electron-prod": "NODE_ENV=production npm run build:electron", "pywebview:dev": "vite build --config vite.config.pywebview.mts && .venv/bin/python src/pywebview/main.py", "pywebview:build": "vite build --config vite.config.pywebview.mts && .venv/bin/python src/pywebview/main.py", @@ -40,7 +40,10 @@ "fastlane:ios:beta": "cd ios && fastlane beta", "fastlane:ios:release": "cd ios && fastlane release", "fastlane:android:beta": "cd android && fastlane beta", - "fastlane:android:release": "cd android && fastlane release" + "fastlane:android:release": "cd android && fastlane release", + "check:electron": "node scripts/check-electron-prerequisites.js", + "electron:build": "npm run check:electron && vite build --config vite.config.electron.mts && node scripts/fix-electron-paths.js && electron-builder", + "postinstall": "electron-builder install-app-deps" }, "dependencies": { "@capacitor/android": "^6.2.0", @@ -157,28 +160,31 @@ "build": { "appId": "app.timesafari", "productName": "TimeSafari", - "directories": { - "output": "dist-electron-packages" - }, "files": [ "dist-electron/**/*", - "src/electron/**/*", - "main.js" + "!dist-electron/node_modules/**/*" ], - "extraResources": [ - { - "from": "dist-electron", - "to": "." - } + "directories": { + "output": "dist-electron-packages", + "buildResources": "build-resources" + }, + "extraResources": [], + "asar": true, + "asarUnpack": [ + "dist-electron/www/assets/**/*" ], "linux": { - "target": [ - "AppImage", - "deb" - ], - "category": "Office", - "icon": "build/icon.png" + "target": ["AppImage"], + "category": "Utility", + "executableName": "TimeSafari" + }, + "mac": { + "category": "public.app-category.productivity" + }, + "win": { + "target": ["nsis"] }, - "asar": true + "artifactName": "TimeSafari-${version}-${arch}.${ext}", + "publish": null } } diff --git a/scripts/build-electron.js b/scripts/build-electron.js index 2eb7157..e3f7d9c 100644 --- a/scripts/build-electron.js +++ b/scripts/build-electron.js @@ -88,6 +88,12 @@ async function main() { throw new Error('package.json not found in build directory'); } + // Copy the electron-logger.js file + const loggerSrc = path.join(__dirname, '../src/electron/electron-logger.js'); + const loggerDest = path.join(distElectronDir, 'electron-logger.js'); + fs.copyFileSync(loggerSrc, loggerDest); + console.log(`Copying src/electron/electron-logger.js to ${loggerDest}`); + console.log('Build completed successfully!'); } catch (error) { console.error('Build failed:', error); diff --git a/scripts/check-electron-prerequisites.js b/scripts/check-electron-prerequisites.js new file mode 100644 index 0000000..e853a0b --- /dev/null +++ b/scripts/check-electron-prerequisites.js @@ -0,0 +1,177 @@ +#!/usr/bin/env node + +/** + * @file check-electron-prerequisites.js + * @description Verifies and installs required dependencies for Electron builds + * + * This script checks if Python's distutils module is available, which is required + * by node-gyp when compiling native Node.js modules during Electron packaging. + * Without distutils, builds will fail with "ModuleNotFoundError: No module named 'distutils'". + * + * The script performs the following actions: + * 1. Checks if Python's distutils module is available + * 2. If missing, offers to install setuptools package which provides distutils + * 3. Attempts installation through pip or pip3 + * 4. Provides manual installation instructions if automated installation fails + * + * Usage: + * - Direct execution: node scripts/check-electron-prerequisites.js + * - As npm script: npm run check:electron + * - Before builds: npm run check:electron && electron-builder + * + * Exit codes: + * - 0: All prerequisites are met or were successfully installed + * - 1: Prerequisites are missing and weren't installed + * + * @author [YOUR_NAME] + * @version 1.0.0 + * @license MIT + */ + +const { execSync } = require('child_process'); +const readline = require('readline'); +const chalk = require('chalk'); // You might need to add this to your dependencies + +console.log(chalk.blue('🔍 Checking Electron build prerequisites...')); + +/** + * Checks if Python's distutils module is available + * + * This function attempts to import the distutils module in Python. + * If successful, it means node-gyp will be able to compile native modules. + * If unsuccessful, the Electron build will likely fail when compiling native dependencies. + * + * @returns {boolean} True if distutils is available, false otherwise + * + * @example + * if (checkDistutils()) { + * console.log('Ready to build Electron app'); + * } + */ +function checkDistutils() { + try { + // Attempt to import distutils using Python + // We use stdio: 'ignore' to suppress any Python output + execSync('python -c "import distutils"', { stdio: 'ignore' }); + console.log(chalk.green('✅ Python distutils is available')); + return true; + } catch (e) { + // This error occurs if either Python is not found or if distutils is missing + console.log(chalk.red('❌ Python distutils is missing')); + return false; + } +} + +/** + * Installs the setuptools package which provides distutils + * + * This function attempts to install setuptools using pip or pip3. + * Setuptools is a package that provides the distutils module needed by node-gyp. + * In Python 3.12+, distutils was moved out of the standard library into setuptools. + * + * The function tries multiple installation methods: + * 1. First attempts with pip + * 2. If that fails, tries with pip3 + * 3. If both fail, provides instructions for manual installation + * + * @returns {Promise} True if installation succeeded, false otherwise + * + * @example + * const success = await installSetuptools(); + * if (success) { + * console.log('Ready to proceed with build'); + * } else { + * console.log('Please fix prerequisites manually'); + * } + */ +async function installSetuptools() { + console.log(chalk.yellow('📦 Attempting to install setuptools...')); + + try { + // First try with pip, commonly used on all platforms + execSync('pip install setuptools', { stdio: 'inherit' }); + console.log(chalk.green('✅ Successfully installed setuptools')); + return true; + } catch (pipError) { + try { + // If pip fails, try with pip3 (common on Linux distributions) + console.log(chalk.yellow('⚠️ Trying with pip3...')); + execSync('pip3 install setuptools', { stdio: 'inherit' }); + console.log(chalk.green('✅ Successfully installed setuptools using pip3')); + return true; + } catch (pip3Error) { + // If both methods fail, provide manual installation guidance + console.log(chalk.red('❌ Failed to install setuptools automatically')); + console.log(chalk.yellow('📝 Please install it manually with:')); + console.log(' pip install setuptools'); + console.log(' or'); + console.log(' sudo apt install python3-setuptools (on Debian/Ubuntu)'); + console.log(' sudo pacman -S python-setuptools (on Arch Linux)'); + console.log(' sudo dnf install python3-setuptools (on Fedora)'); + console.log(' brew install python-setuptools (on macOS with Homebrew)'); + return false; + } + } +} + +/** + * Main execution function + * + * This function orchestrates the checking and installation process: + * 1. Checks if distutils is already available + * 2. If not, informs the user and prompts for installation + * 3. Based on user input, attempts to install or exits + * + * The function handles interactive user prompts and orchestrates + * the overall flow of the script. + * + * @returns {Promise} + * @throws Will exit process with code 1 if prerequisites aren't met + */ +async function main() { + // First check if distutils is already available + if (checkDistutils()) { + // All prerequisites are met, exit successfully + process.exit(0); + } + + // Inform the user about the missing prerequisite + console.log(chalk.yellow('⚠️ Python distutils is required for Electron builds')); + console.log(chalk.yellow('⚠️ This is needed to compile native modules during the build process')); + + // Set up readline interface for user interaction + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + // Prompt the user for installation permission + const answer = await new Promise(resolve => { + rl.question(chalk.blue('Would you like to install setuptools now? (y/n) '), resolve); + }); + + // Clean up readline interface + rl.close(); + + if (answer.toLowerCase() === 'y') { + // User agreed to installation + const success = await installSetuptools(); + if (success) { + // Installation succeeded, exit successfully + process.exit(0); + } else { + // Installation failed, exit with error + process.exit(1); + } + } else { + // User declined installation + console.log(chalk.yellow('⚠️ Build may fail without distutils')); + process.exit(1); + } +} + +// Execute the main function and handle any uncaught errors +main().catch(error => { + console.error(chalk.red('Error during prerequisites check:'), error); + process.exit(1); +}); \ No newline at end of file diff --git a/scripts/fix-electron-paths.js b/scripts/fix-electron-paths.js new file mode 100644 index 0000000..8807842 --- /dev/null +++ b/scripts/fix-electron-paths.js @@ -0,0 +1,61 @@ +/** + * Fix path resolution issues in the Electron build + */ +const fs = require('fs'); +const path = require('path'); +const glob = require('glob'); + +// Fix asset paths in HTML file +function fixHtmlPaths() { + const htmlFile = path.join(__dirname, '../dist-electron/index.html'); + if (fs.existsSync(htmlFile)) { + let html = fs.readFileSync(htmlFile, 'utf8'); + + // Convert absolute paths to relative + html = html.replace(/src="\//g, 'src="./'); + html = html.replace(/href="\//g, 'href="./'); + + fs.writeFileSync(htmlFile, html); + console.log('✅ Fixed paths in index.html'); + } +} + +// Fix asset imports in JS files +function fixJsPaths() { + const jsFiles = glob.sync('dist-electron/assets/*.js'); + + jsFiles.forEach(file => { + let content = fs.readFileSync(file, 'utf8'); + + // Replace absolute imports with relative ones + const originalContent = content; + content = content.replace(/["']\/assets\//g, '"./assets/'); + + if (content !== originalContent) { + fs.writeFileSync(file, content); + console.log(`✅ Fixed paths in ${path.basename(file)}`); + } + }); +} + +// Add base href to HTML +function addBaseHref() { + const htmlFile = path.join(__dirname, '../dist-electron/index.html'); + if (fs.existsSync(htmlFile)) { + let html = fs.readFileSync(htmlFile, 'utf8'); + + // Add base href if not present + if (!html.includes('', '\n'); + fs.writeFileSync(htmlFile, html); + console.log('✅ Added base href to index.html'); + } + } +} + +// Run all fixes +fixHtmlPaths(); +fixJsPaths(); +addBaseHref(); + +console.log('🎉 Electron path fixes completed'); \ No newline at end of file diff --git a/scripts/notarize.js b/scripts/notarize.js new file mode 100644 index 0000000..53d586f --- /dev/null +++ b/scripts/notarize.js @@ -0,0 +1,14 @@ +// This is a placeholder notarize script that does nothing for non-macOS platforms +// Only necessary for macOS app store submissions + +exports.default = async function notarizing(context) { + // Only notarize macOS builds + if (context.electronPlatformName !== 'darwin') { + console.log('Skipping notarization for non-macOS platform'); + return; + } + + // For macOS, we would implement actual notarization here + console.log('This is where macOS notarization would happen'); + // We're just returning with no action for non-macOS builds +}; \ No newline at end of file diff --git a/src/electron/electron-logger.js b/src/electron/electron-logger.js new file mode 100644 index 0000000..ef74f1b --- /dev/null +++ b/src/electron/electron-logger.js @@ -0,0 +1,33 @@ +/** + * Electron-specific logger implementation + */ +const fs = require('fs'); +const path = require('path'); +const { app } = require('electron'); + +// 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 }); +} + +const logFile = path.join(logsDir, `electron-${new Date().toISOString().split('T')[0]}.log`); + +function log(level, message) { + const timestamp = new Date().toISOString(); + const logMessage = `[${timestamp}] [${level}] ${message}\n`; + + // Write to log file + fs.appendFileSync(logFile, logMessage); + + // Also output to console + console[level.toLowerCase()](message); +} + +module.exports = { + info: (message) => log('INFO', message), + warn: (message) => log('WARN', message), + error: (message) => log('ERROR', message), + debug: (message) => log('DEBUG', message), + getLogPath: () => logFile +}; \ No newline at end of file diff --git a/src/electron/main.js b/src/electron/main.js index 978e105..8b20987 100644 --- a/src/electron/main.js +++ b/src/electron/main.js @@ -1,174 +1,217 @@ -const { app, BrowserWindow } = require("electron"); -const path = require("path"); -const fs = require("fs"); -const logger = require("../utils/logger"); +const { app, BrowserWindow, session, protocol, ipcMain, dialog } = require('electron'); +const path = require('path'); +const fs = require('fs'); +const url = require('url'); -// Check if running in dev mode -const isDev = process.argv.includes("--inspect"); +// Global window reference +let mainWindow = null; -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)); +// Debug flags +const isDev = !app.isPackaged; + +// Helper for logging +function logDebug(...args) { + console.log('[DEBUG]', ...args); +} + +function logError(...args) { + console.error('[ERROR]', ...args); + if (!isDev && mainWindow) { + dialog.showErrorBox('TimeSafari Error', args.join(' ')); + } +} - // Create the browser window. - const mainWindow = new BrowserWindow({ +// Get the most appropriate app path +function getAppPath() { + if (app.isPackaged) { + const possiblePaths = [ + path.join(process.resourcesPath, 'app.asar', 'dist-electron'), + path.join(process.resourcesPath, 'app.asar'), + path.join(process.resourcesPath, 'app'), + app.getAppPath() + ]; + + for (const testPath of possiblePaths) { + const testFile = path.join(testPath, 'www', 'index.html'); + if (fs.existsSync(testFile)) { + logDebug(`Found valid app path: ${testPath}`); + return testPath; + } + } + + logError('Could not find valid app path'); + return path.join(process.resourcesPath, 'app.asar'); // Default fallback + } else { + return __dirname; + } +} + +// Create the browser window +function createWindow() { + logDebug('Creating window with paths:'); + logDebug('- process.resourcesPath:', process.resourcesPath); + logDebug('- app.getAppPath():', app.getAppPath()); + logDebug('- __dirname:', __dirname); + + // Create the browser window + mainWindow = new BrowserWindow({ width: 1200, height: 800, webPreferences: { - nodeIntegration: false, + preload: path.join(__dirname, 'preload.js'), contextIsolation: true, - webSecurity: true, - allowRunningInsecureContent: false, - preload: path.join(__dirname, "preload.js"), - }, + nodeIntegration: false, + webSecurity: true + } }); - - // 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 - "", // 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)}`; + + // Fix root file paths - replaces all protocol handling + protocol.interceptFileProtocol('file', (request, callback) => { + let urlPath = request.url.substr(7); // Remove 'file://' prefix + urlPath = decodeURIComponent(urlPath); // Handle special characters + + // Debug all asset requests + if (urlPath.includes('assets/') || urlPath.endsWith('.js') || urlPath.endsWith('.css') || urlPath.endsWith('.html')) { + logDebug(`Intercepted request for: ${urlPath}`); + } + + // Fix paths for files at root like registerSW.js or manifest.webmanifest + if (urlPath.endsWith('registerSW.js') || + urlPath.endsWith('manifest.webmanifest') || + urlPath.endsWith('sw.js')) { + + const appBasePath = getAppPath(); + const filePath = path.join(appBasePath, 'www', path.basename(urlPath)); + + if (fs.existsSync(filePath)) { + logDebug(`Serving ${urlPath} from ${filePath}`); + return callback({ path: filePath }); + } else { + // For service worker, provide empty content to avoid errors + if (urlPath.endsWith('registerSW.js') || urlPath.endsWith('sw.js')) { + logDebug(`Providing empty SW file for ${urlPath}`); + // Create an empty JS file content that does nothing + const tempFile = path.join(app.getPath('temp'), path.basename(urlPath)); + fs.writeFileSync(tempFile, '// Service workers disabled in Electron\n'); + return callback({ path: tempFile }); + } } - - // 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; + } + + // Handle assets paths that might be requested from root + if (urlPath.startsWith('/assets/') || urlPath === '/assets') { + const appBasePath = getAppPath(); + const filePath = path.join(appBasePath, 'www', urlPath); + logDebug(`Redirecting ${urlPath} to ${filePath}`); + return callback({ path: filePath }); + } + + // Handle assets paths that are missing the www folder + if (urlPath.includes('/assets/')) { + const appBasePath = getAppPath(); + const relativePath = urlPath.substring(urlPath.indexOf('/assets/')); + const filePath = path.join(appBasePath, 'www', relativePath); + if (fs.existsSync(filePath)) { + logDebug(`Fixing asset path ${urlPath} to ${filePath}`); + return callback({ path: filePath }); } - - 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"); + } + + // For all other paths, just pass them through + callback({ path: urlPath }); + }); + + // Set up CSP headers - more permissive in dev mode + session.defaultSession.webRequest.onHeadersReceived((details, callback) => { + callback({ + responseHeaders: { + ...details.responseHeaders, + 'Content-Security-Policy': [ + isDev + ? "default-src 'self' file:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob: https://*; connect-src 'self' https://*" + : "default-src 'self' file:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob: https://image.timesafari.app https://*.americancloud.com; connect-src 'self' https://api.timesafari.app https://api.endorser.ch https://test-api.endorser.ch https://fonts.googleapis.com" + ] } - }) - .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 + + // Load the index.html with modifications + try { + const appPath = getAppPath(); + const wwwFolder = path.join(appPath, 'www'); + const indexPath = path.join(wwwFolder, 'index.html'); + + logDebug('Loading app from:', indexPath); + + // Check if the file exists + if (fs.existsSync(indexPath)) { + // Read and modify index.html to disable service worker + let indexContent = fs.readFileSync(indexPath, 'utf8'); + + // 1. Add base tag for proper path resolution + indexContent = indexContent.replace('', + `\n `); + + // 2. Disable service worker registration by replacing the script + if (indexContent.includes('registerSW.js')) { + indexContent = indexContent.replace( + /' + ); + } + + // Create a temp file with modified content + const tempDir = app.getPath('temp'); + const tempIndexPath = path.join(tempDir, 'timesafari-index.html'); + fs.writeFileSync(tempIndexPath, indexContent); + + // Load the modified index.html + mainWindow.loadFile(tempIndexPath).catch(err => { + logError('Failed to load via loadFile:', err); + + // Fallback to direct URL loading + mainWindow.loadURL(`file://${tempIndexPath}`).catch(err2 => { + logError('Both loading methods failed:', err2); + mainWindow.loadURL('data:text/html,

Error: Failed to load TimeSafari

Please contact support.

'); + }); + }); + } else { + logError(`Index file not found at: ${indexPath}`); + mainWindow.loadURL('data:text/html,

Error: Cannot find application

index.html not found

'); + } + } catch (err) { + logError('Failed to load app:', err); + } + + // Open DevTools in development if (isDev) { mainWindow.webContents.openDevTools(); } + + mainWindow.on('closed', () => { + mainWindow = null; + }); } -// Handle app ready -app.whenReady().then(createWindow); - -// Handle all windows closed -app.on("window-all-closed", () => { - if (process.platform !== "darwin") { - app.quit(); - } +// App lifecycle events +app.whenReady().then(() => { + logDebug(`Starting TimeSafari v${app.getVersion()}`); + + // Skip the service worker registration for file:// protocol + process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true'; + + createWindow(); + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) createWindow(); + }); }); -app.on("activate", () => { - if (BrowserWindow.getAllWindows().length === 0) { - createWindow(); - } +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') app.quit(); }); -// Handle any errors -process.on("uncaughtException", (error) => { - logger.error("Uncaught Exception:", error); +// Handle uncaught exceptions +process.on('uncaughtException', (error) => { + logError('Uncaught Exception:', error); }); diff --git a/src/electron/preload.js b/src/electron/preload.js index a26f30c..92cfa78 100644 --- a/src/electron/preload.js +++ b/src/electron/preload.js @@ -1,78 +1,92 @@ const { contextBridge, ipcRenderer } = require("electron"); -const logger = { - log: (message, ...args) => { - if (process.env.NODE_ENV !== "production") { - /* eslint-disable no-console */ - console.log(message, ...args); - /* eslint-enable no-console */ - } - }, - warn: (message, ...args) => { - if (process.env.NODE_ENV !== "production") { - /* eslint-disable no-console */ - console.warn(message, ...args); - /* eslint-enable no-console */ - } - }, - error: (message, ...args) => { - /* eslint-disable no-console */ - console.error(message, ...args); // Errors should always be logged - /* 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 ""; +// Safety wrapper for logging +function safeLog(message) { + try { + console.log('[Preload]', message); + } catch (e) { + // Silent fail for logging } -}; +} -logger.log("Preload script starting..."); +// Initialize +safeLog('Preload script starting...'); try { - contextBridge.exposeInMainWorld("electronAPI", { - // Path utilities - getPath, - - // IPC functions + // Mock service worker registration to prevent errors + if (window.navigator) { + // Override the service worker registration to return a fake promise that resolves with nothing + window.navigator.serviceWorker = { + register: () => Promise.resolve({}), + getRegistration: () => Promise.resolve(null), + ready: Promise.resolve({}) + }; + } + + // Safely expose specific APIs to the renderer process + contextBridge.exposeInMainWorld('electronAPI', { + // Basic flags/info + isElectron: true, + + // Disable service worker in Electron + disableServiceWorker: true, + + // Logging + log: (message) => { + try { + console.log('[Renderer]', message); + } catch (e) { + // Silence any errors from logging + } + }, + + // Report errors to main process + reportError: (error) => { + try { + ipcRenderer.send('app-error', error.toString()); + } catch (e) { + console.error('Failed to report error to main process', e); + } + }, + + // Safe path handling helper (no Node modules needed) + joinPath: (...parts) => { + return parts.join('/').replace(/\/\//g, '/'); + }, + + // Fix asset URLs + resolveAssetUrl: (assetPath) => { + if (assetPath.startsWith('/assets/')) { + return assetPath; // Already properly formed + } + if (assetPath.startsWith('assets/')) { + return '/' + assetPath; // Add leading slash + } + return assetPath; + }, + + // Send messages to main process send: (channel, data) => { - const validChannels = ["toMain"]; + // Whitelist channels for security + const validChannels = ['app-event', 'log-event', 'app-error']; if (validChannels.includes(channel)) { ipcRenderer.send(channel, data); } }, + + // Receive messages from main process receive: (channel, func) => { - const validChannels = ["fromMain"]; + const validChannels = ['app-notification', 'log-response']; if (validChannels.includes(channel)) { - ipcRenderer.on(channel, (event, ...args) => func(...args)); + // Remove old listeners to avoid memory leaks + ipcRenderer.removeAllListeners(channel); + // Add the new listener + ipcRenderer.on(channel, (_, ...args) => func(...args)); } - }, - // Environment info - env: { - isElectron: true, - isDev: process.env.NODE_ENV === "development", - }, - // Path utilities - getBasePath: () => { - return process.env.NODE_ENV === "development" ? "/" : "./"; - }, + } }); - logger.log("Preload script completed successfully"); -} catch (error) { - logger.error("Error in preload script:", error); + safeLog('Preload script completed successfully'); +} catch (err) { + safeLog('Error in preload script: ' + err.toString()); } diff --git a/src/registerServiceWorker.ts b/src/registerServiceWorker.ts index d3182ac..4df4723 100644 --- a/src/registerServiceWorker.ts +++ b/src/registerServiceWorker.ts @@ -37,3 +37,16 @@ if ( "Service worker registration skipped - not enabled or not in production", ); } + +export function registerServiceWorker() { + // Skip service worker registration in Electron + if (window.electronAPI?.isElectron) { + console.log('Running in Electron - skipping service worker registration'); + return; + } + + // Regular service worker registration for web + if ('serviceWorker' in navigator) { + // ... existing code ... + } +} diff --git a/test-scripts/requirements.txt b/test-scripts/requirements.txt index 9df2e50..e6b474f 100644 --- a/test-scripts/requirements.txt +++ b/test-scripts/requirements.txt @@ -8,4 +8,5 @@ web3>=6.0.0 # For Ethereum interaction eth-utils>=2.1.0 # For Ethereum utilities pyjwt>=2.8.0 # For JWT operations cryptography>=42.0.0 # For key format conversion -jwcrypto \ No newline at end of file +jwcrypto +setuptools diff --git a/vite.config.electron.mts b/vite.config.electron.mts index b83e380..7d5630a 100644 --- a/vite.config.electron.mts +++ b/vite.config.electron.mts @@ -24,6 +24,25 @@ export default defineConfig(async () => { }; } } - }] + }], + build: { + outDir: 'dist-electron', + emptyOutDir: true, + rollupOptions: { + output: { + manualChunks: { + vendor: ['vue', 'vue-router', 'pinia'] + } + } + }, + assetsDir: 'assets', + minify: 'terser', + terserOptions: { + compress: { + drop_console: false, + }, + }, + }, + base: './', }); }); \ No newline at end of file