diff --git a/package-lock.json b/package-lock.json index d0069ba..2babc3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2814,11 +2814,12 @@ "@types/node": "*" } }, - "node_modules/@types/minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", - "dev": true + "node_modules/@types/luxon": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==", + "dev": true, + "license": "MIT" }, "node_modules/@types/ms": { "version": "2.1.0", @@ -8311,6 +8312,34 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/lzma-native": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/lzma-native/-/lzma-native-8.0.6.tgz", + "integrity": "sha512-09xfg67mkL2Lz20PrrDeNYZxzeW7ADtpYFbwSQh9U8+76RIzx5QsJBMy8qikv3hbUPfpy6hqwxt6FcGK81g9AA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^3.1.0", + "node-gyp-build": "^4.2.1", + "readable-stream": "^3.6.0" + }, + "bin": { + "lzmajs": "bin/lzmajs" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", diff --git a/package.json b/package.json index 1f2778b..d54bd5f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "dev": "npm-run-all --parallel dev:react dev:electron", "dev:react": "vite", - "dev:electron": "npm run build && cross-env NODE_ENV=development electron --inspect=5858 .", + "dev:electron": "npm run build && cross-env NODE_ENV=development electron .", "prod:electron": "npm run build && cross-env NODE_ENV=production electron .", "build": "tsc && vite build", "preview": "vite preview", diff --git a/src/assets/logo-colored-vertical.svg b/src/assets/logo-colored-vertical.svg new file mode 100644 index 0000000..1a98240 --- /dev/null +++ b/src/assets/logo-colored-vertical.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/logo-white-vertical.svg b/src/assets/logo-white-vertical.svg new file mode 100644 index 0000000..c84b182 --- /dev/null +++ b/src/assets/logo-white-vertical.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/splash.html b/src/assets/splash.html new file mode 100644 index 0000000..ffddd43 --- /dev/null +++ b/src/assets/splash.html @@ -0,0 +1,29 @@ + + +
+ + + + + + diff --git a/src/electron/main.ts b/src/electron/main.ts index da22710..3d9d9e5 100644 --- a/src/electron/main.ts +++ b/src/electron/main.ts @@ -1,105 +1,179 @@ -import { app, BrowserWindow, ipcMain, session } from 'electron'; -import { ipcMainHandle, ipcMainOn, isDev } from './util.js'; -import { getStaticData, pollResources } from './resourceManager.js'; -import { getPreloadPath, getUIPath } from './pathResolver.js'; +import { app, BrowserWindow, globalShortcut, session } from "electron"; +import { isDev } from "./util.js"; +// import { pollResources } from "./resourceManager.js"; +import { getPreloadPath, getUIPath } from "./pathResolver.js"; +import { registerIPCHandlers } from "./ipc/ipcHandler.js"; import "./ipc/ipcHandler.js"; -import { registerIPCHandlers } from './ipc/ipcHandler.js'; +import path from "path"; +import { fileURLToPath } from "url"; -let mainWindow:any = null; +app.setAppUserModelId("com.tracker.app"); -app.on('certificate-error', (event, webContents, url, error, certificate, callback) => { - // Prevent the default behavior of showing a certificate error dialog - event.preventDefault(); - // Allow the certificate, effectively ignoring the error - callback(true); +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// --------------------------- +// SINGLE INSTANCE LOCK +// --------------------------- +const gotTheLock = app.requestSingleInstanceLock(); + +if (!gotTheLock) { + app.quit(); + process.exit(0); +} + +let mainWindow: BrowserWindow | null = null; +let splashWindow: BrowserWindow | null = null; + +// --------------------------- +// HANDLE DOUBLE OPEN ATTEMPT +// --------------------------- +app.on("second-instance", () => { + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.show(); + mainWindow.focus(); + } }); -app.on('ready', async () => { - console.log("ready"); +// --------------------------- +// MAIN APP READY +// --------------------------- +app.on("ready", async () => { - mainWindow = new BrowserWindow({ - width: 500, // fixed width - height: 600, // fixed height - - resizable: false, // prevents resizing - fullscreenable: false, // disables fullscreen toggle (F11 or double-click title bar) - minimizable: false, // disables minimize button - maximizable: false, // prevents maximize + + splashWindow = new BrowserWindow({ + width: 300, + height: 300, + frame: false, + transparent: false, + alwaysOnTop: false, // splash does NOT need to be on top || Yooo Don't make it onTop + resizable: false, + show: true, + skipTaskbar: true, + webPreferences: { + devTools: false, + } + }); + + + splashWindow.loadFile( + path.join(__dirname, "../assets/splash.html") + ); + + // Create the main window + const win = new BrowserWindow({ + width: 500, + height: 600, + resizable: false, + fullscreenable: false, + minimizable: false, + maximizable: false, closable: true, alwaysOnTop: true, + skipTaskbar: true, autoHideMenuBar: true, - - frame: process.platform === 'darwin' ? false : true, + frame: process.platform === "darwin" ? false : true, + show: false, // speed improvement webPreferences: { + devTools: false, preload: getPreloadPath(), + contextIsolation: true, + nodeIntegration: false, }, }); - session.defaultSession.webRequest.onBeforeRequest((details, callback) => { - console.log('🌐 Request:', details.method, details.url); - callback({ cancel: false }); - }); + mainWindow = win; - if (isDev()) { - mainWindow.loadURL('http://localhost:5123'); - mainWindow.webContents.openDevTools(); - } else { - mainWindow.loadFile(getUIPath()); + // ------------------------------------ + // ONLY LOG NETWORK REQUESTS IN DEV + // ------------------------------------ + if (isDev() && process.env.LOG_REQUESTS === "true") { + session.defaultSession.webRequest.onBeforeRequest((details, callback) => { + console.log("🌐 Request:", details.method, details.url); + callback({ cancel: false }); + }); } - // pollResources(mainWindow); - // 🔹 Register all IPC handlers (includes polling) - registerIPCHandlers(mainWindow); + // ------------------------------------ + // LOAD FRONTEND + // ------------------------------------ + if (isDev()) { + win.loadURL("http://localhost:5123"); + // win.webContents.openDevTools(); // Optional (slows startup) + } else { + win.loadFile(getUIPath()); + } - pollResources(mainWindow); + // ------------------------------------ + // SHOW WINDOW WHEN READY (no white flash) + // ------------------------------------ + win.once("ready-to-show", () => { + // Delay heavy work to improve startup speed + setTimeout(() => { + splashWindow?.close(); + splashWindow = null; - // IPC handlers - // ipcMainHandle('getStaticData', () => getStaticData()); + win.show(); + // pollResources(win); + registerIPCHandlers(win); + }, 500); + }); - // ipcMainOn('sendFrameAction', (payload) => { - // switch (payload) { - // case 'CLOSE': - // mainWindow.close(); - // break; - // case 'MAXIMIZE': - // // prevent maximize - // break; - // case 'MINIMIZE': - // mainWindow.minimize(); - // break; - // } - // }); - - handleCloseEvents(mainWindow); + // ------------------------------------ + // HANDLE CLOSE EVENTS + // ------------------------------------ + handleCloseEvents(win); }); -function handleCloseEvents(mainWindow: BrowserWindow) { + +app.on('browser-window-focus', () => { + globalShortcut.register('CommandOrControl+R', () => { + // Do nothing; disables reload + }); + globalShortcut.register('F5', () => { + // Do nothing; disables reload + }); + globalShortcut.register('CommandOrControl+Shift+I', () => { + // Do nothing; disables DevTools + }); +}); + +app.on('browser-window-blur', () => { + globalShortcut.unregisterAll(); +}); + + +// --------------------------- +// CUSTOM CLOSE BEHAVIOR +// --------------------------- +function handleCloseEvents(window: BrowserWindow) { let willClose = false; - mainWindow.on('close', (e) => { + window.on("close", (e) => { if (willClose) return; e.preventDefault(); - mainWindow.hide(); + window.hide(); if (app.dock) { app.dock.hide(); } }); - app.on('before-quit', () => { + app.on("before-quit", () => { willClose = true; }); - mainWindow.on('show', () => { + window.on("show", () => { willClose = false; }); } - - +// --------------------------- +// GET MAIN WINDOW EXPORT +// --------------------------- export function getMainWindow() { return mainWindow; } - diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 83c82d4..11a4799 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -99,8 +99,6 @@ const App: React.FC = () => { const theme = ThemeToggle(); const dispatch = useDispatch