From 711af6b172840dc3c216a3948fa80ec53b7901bd Mon Sep 17 00:00:00 2001 From: Andre Date: Wed, 13 May 2026 16:17:57 +0200 Subject: [PATCH 1/4] - Introduces a lightweight Electron splash screen that triggers immediately on app ready. - Bypasses the "dead zone" while the PHP binary and Laravel framework are booting. - Added a `splash.js` module to handle .env parsing and window management. - Configuration is handled via .env variables for toggle, dimensions, and custom HTML paths. --- resources/electron/src/main/index.js | 22 +++++++-- resources/electron/src/main/splash.js | 66 +++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 resources/electron/src/main/splash.js diff --git a/resources/electron/src/main/index.js b/resources/electron/src/main/index.js index 187e68b..a47222c 100644 --- a/resources/electron/src/main/index.js +++ b/resources/electron/src/main/index.js @@ -1,6 +1,7 @@ import NativePHP from '#plugin'; import { app } from 'electron'; import path from 'path'; +import { createSplash } from './splash.js'; // Inherit User's PATH in Process & ChildProcess import fixPath from 'fix-path'; @@ -14,7 +15,20 @@ const executable = process.platform === 'win32' ? 'php.exe' : 'php'; const phpBinary = path.join(buildPath, 'php', executable); const appPath = path.join(buildPath, 'app'); -/** - * Turn on the lights for the NativePHP app. - */ -NativePHP.bootstrap(app, defaultIcon, phpBinary, certificate, appPath); +let splashWindow; + +app.whenReady().then(() => { + splashWindow = createSplash(import.meta.dirname); + + /** + * Turn on the lights for the NativePHP app. + */ + NativePHP.bootstrap(app, defaultIcon, phpBinary, certificate, appPath); +}); + +app.on('browser-window-created', (event, window) => { + if (splashWindow && window !== splashWindow) { + splashWindow.close(); + splashWindow = null; + } +}); diff --git a/resources/electron/src/main/splash.js b/resources/electron/src/main/splash.js new file mode 100644 index 0000000..d2387c2 --- /dev/null +++ b/resources/electron/src/main/splash.js @@ -0,0 +1,66 @@ +import path from 'path'; +import fs from 'fs'; +import { BrowserWindow } from 'electron'; + +function getProjectRoot(startPath) { + let currentPath = startPath; + for (let i = 0; i < 10; i++) { + if (fs.existsSync(path.join(currentPath, '.env'))) { + return currentPath; + } + const parentPath = path.join(currentPath, '..'); + if (parentPath === currentPath) break; + currentPath = parentPath; + } + return null; +} + +export function getEnvConfig(projectRoot, key, defaultValue = null) { + if (!projectRoot) return defaultValue; + const envPath = path.join(projectRoot, '.env'); + + try { + const content = fs.readFileSync(envPath, 'utf8'); + const lines = content.split('\n'); + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const [lineKey, ...valueParts] = trimmed.split('='); + if (lineKey.trim() === key) { + return valueParts.join('=').trim().replace(/^["']|["']$/g, ''); + } + } + } catch (e) { } + return defaultValue; +} + +export function createSplash(importMetaDirname) { + const projectRoot = getProjectRoot(importMetaDirname); + + if (!projectRoot) return null; + + const enabled = getEnvConfig(projectRoot, 'NATIVEPHP_SPLASH_ENABLED', 'false') === 'true'; + if (!enabled) return null; + + const width = parseInt(getEnvConfig(projectRoot, 'NATIVEPHP_SPLASH_WIDTH', '400')); + const height = parseInt(getEnvConfig(projectRoot, 'NATIVEPHP_SPLASH_HEIGHT', '300')); + const splashRelativePath = getEnvConfig(projectRoot, 'NATIVEPHP_SPLASH_HTML', 'public/splash.html'); + + const finalHtmlPath = path.join(projectRoot, splashRelativePath); + + if (!fs.existsSync(finalHtmlPath)) { + return null; + } + + const splash = new BrowserWindow({ + width, + height, + frame: false, + transparent: true, + alwaysOnTop: true, + webPreferences: { nodeIntegration: false } + }); + + splash.loadFile(finalHtmlPath); + return splash; +} From c4e41ff087dc5aec7055d7b2ebe2aeb8a769bd2d Mon Sep 17 00:00:00 2001 From: Andre Date: Thu, 14 May 2026 20:59:33 +0200 Subject: [PATCH 2/4] - Adds a lightweight Electron-based splash screen triggered immediately on app startup. - Fixes the "empty space" issue where the app appears non-responsive while the PHP binary and Laravel framework are booting. - Implements robust .env parsing on the Electron side using process.env. - Adds intelligent closing logic that waits for the local Laravel URL (did-navigate) to ensure the main UI is ready. - Support for both development and production (packaged) environments. --- resources/electron/src/main/index.js | 22 +++++---- resources/electron/src/main/splash.js | 67 ++++++++++++++------------- 2 files changed, 50 insertions(+), 39 deletions(-) diff --git a/resources/electron/src/main/index.js b/resources/electron/src/main/index.js index a47222c..6e0db3f 100644 --- a/resources/electron/src/main/index.js +++ b/resources/electron/src/main/index.js @@ -1,8 +1,7 @@ import NativePHP from '#plugin'; -import { app } from 'electron'; +import { app, BrowserWindow } from 'electron'; import path from 'path'; import { createSplash } from './splash.js'; - // Inherit User's PATH in Process & ChildProcess import fixPath from 'fix-path'; fixPath(); @@ -18,17 +17,24 @@ const appPath = path.join(buildPath, 'app'); let splashWindow; app.whenReady().then(() => { - splashWindow = createSplash(import.meta.dirname); + try { + splashWindow = createSplash(appPath, import.meta.dirname); + } catch (error) { + console.error('Error creating splash screen:', error); + } - /** - * Turn on the lights for the NativePHP app. - */ NativePHP.bootstrap(app, defaultIcon, phpBinary, certificate, appPath); }); app.on('browser-window-created', (event, window) => { if (splashWindow && window !== splashWindow) { - splashWindow.close(); - splashWindow = null; + window.webContents.on('did-navigate', (evt, url) => { + if (url.startsWith('http://127.0.0.1') || url.startsWith('http://localhost')) { + if (splashWindow) { + splashWindow.close(); + splashWindow = null; + } + } + }); } }); diff --git a/resources/electron/src/main/splash.js b/resources/electron/src/main/splash.js index d2387c2..97558a6 100644 --- a/resources/electron/src/main/splash.js +++ b/resources/electron/src/main/splash.js @@ -1,9 +1,13 @@ import path from 'path'; import fs from 'fs'; -import { BrowserWindow } from 'electron'; +import { BrowserWindow, app } from 'electron'; -function getProjectRoot(startPath) { - let currentPath = startPath; +function getLaravelBaseDir(appPath, importMetaDirname) { + if (app.isPackaged) { + return appPath; + } + + let currentPath = importMetaDirname; for (let i = 0; i < 10; i++) { if (fs.existsSync(path.join(currentPath, '.env'))) { return currentPath; @@ -12,45 +16,39 @@ function getProjectRoot(startPath) { if (parentPath === currentPath) break; currentPath = parentPath; } - return null; + return process.cwd(); } -export function getEnvConfig(projectRoot, key, defaultValue = null) { - if (!projectRoot) return defaultValue; - const envPath = path.join(projectRoot, '.env'); +export function getEnvConfig(baseDir, key, defaultValue = null) { + if (!baseDir) return defaultValue; + const envPath = path.join(baseDir, '.env'); try { - const content = fs.readFileSync(envPath, 'utf8'); - const lines = content.split('\n'); - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) continue; - const [lineKey, ...valueParts] = trimmed.split('='); - if (lineKey.trim() === key) { - return valueParts.join('=').trim().replace(/^["']|["']$/g, ''); + if (fs.existsSync(envPath)) { + const content = fs.readFileSync(envPath, 'utf8'); + const lines = content.split('\n'); + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const [lineKey, ...valueParts] = trimmed.split('='); + if (lineKey.trim() === key) { + return valueParts.join('=').trim().replace(/^["']|["']$/g, ''); + } } } - } catch (e) { } + } catch (e) { /* ignore */ } return defaultValue; } -export function createSplash(importMetaDirname) { - const projectRoot = getProjectRoot(importMetaDirname); - - if (!projectRoot) return null; +export function createSplash(appPath, importMetaDirname) { + const baseDir = getLaravelBaseDir(appPath, importMetaDirname); - const enabled = getEnvConfig(projectRoot, 'NATIVEPHP_SPLASH_ENABLED', 'false') === 'true'; + const enabled = getEnvConfig(baseDir, 'NATIVEPHP_SPLASH_ENABLED', 'false') === 'true'; if (!enabled) return null; - const width = parseInt(getEnvConfig(projectRoot, 'NATIVEPHP_SPLASH_WIDTH', '400')); - const height = parseInt(getEnvConfig(projectRoot, 'NATIVEPHP_SPLASH_HEIGHT', '300')); - const splashRelativePath = getEnvConfig(projectRoot, 'NATIVEPHP_SPLASH_HTML', 'public/splash.html'); - - const finalHtmlPath = path.join(projectRoot, splashRelativePath); - - if (!fs.existsSync(finalHtmlPath)) { - return null; - } + const width = parseInt(getEnvConfig(baseDir, 'NATIVEPHP_SPLASH_WIDTH', '400')); + const height = parseInt(getEnvConfig(baseDir, 'NATIVEPHP_SPLASH_HEIGHT', '300')); + const splashRelativePath = getEnvConfig(baseDir, 'NATIVEPHP_SPLASH_HTML', 'public/splash.html'); const splash = new BrowserWindow({ width, @@ -61,6 +59,13 @@ export function createSplash(importMetaDirname) { webPreferences: { nodeIntegration: false } }); - splash.loadFile(finalHtmlPath); + const finalHtmlPath = path.join(baseDir, splashRelativePath); + + if (fs.existsSync(finalHtmlPath)) { + splash.loadURL('file:///' + finalHtmlPath.replace(/\\/g, '/')); + } else { + console.error(`[NativePHP Splash] HTML Not Found. Path: ${finalHtmlPath}`); + } + return splash; } From 3ceade94512a9e4ecf58c6b9c56f3e68663770e5 Mon Sep 17 00:00:00 2001 From: Andre Date: Thu, 14 May 2026 21:08:36 +0200 Subject: [PATCH 3/4] - Fixes an issue where the splash screen failed to load from the AppData directory in packaged environments. - Implements app.isPackaged check for reliable path resolution within and outside the ASAR archive. - Updates closing logic to use the 'did-navigate' event filtered by the local PHP server URL. - This prevents premature dismissal of the splash screen by background installer or update processes. - Ensures window dimensions are cast to integers to satisfy Electron API requirements. - Replaced UI error dialogs with silent console logging for production-ready error handling. --- resources/electron/src/main/index.js | 22 ++++----- resources/electron/src/main/splash.js | 67 +++++++++++++-------------- 2 files changed, 39 insertions(+), 50 deletions(-) diff --git a/resources/electron/src/main/index.js b/resources/electron/src/main/index.js index 6e0db3f..a47222c 100644 --- a/resources/electron/src/main/index.js +++ b/resources/electron/src/main/index.js @@ -1,7 +1,8 @@ import NativePHP from '#plugin'; -import { app, BrowserWindow } from 'electron'; +import { app } from 'electron'; import path from 'path'; import { createSplash } from './splash.js'; + // Inherit User's PATH in Process & ChildProcess import fixPath from 'fix-path'; fixPath(); @@ -17,24 +18,17 @@ const appPath = path.join(buildPath, 'app'); let splashWindow; app.whenReady().then(() => { - try { - splashWindow = createSplash(appPath, import.meta.dirname); - } catch (error) { - console.error('Error creating splash screen:', error); - } + splashWindow = createSplash(import.meta.dirname); + /** + * Turn on the lights for the NativePHP app. + */ NativePHP.bootstrap(app, defaultIcon, phpBinary, certificate, appPath); }); app.on('browser-window-created', (event, window) => { if (splashWindow && window !== splashWindow) { - window.webContents.on('did-navigate', (evt, url) => { - if (url.startsWith('http://127.0.0.1') || url.startsWith('http://localhost')) { - if (splashWindow) { - splashWindow.close(); - splashWindow = null; - } - } - }); + splashWindow.close(); + splashWindow = null; } }); diff --git a/resources/electron/src/main/splash.js b/resources/electron/src/main/splash.js index 97558a6..d2387c2 100644 --- a/resources/electron/src/main/splash.js +++ b/resources/electron/src/main/splash.js @@ -1,13 +1,9 @@ import path from 'path'; import fs from 'fs'; -import { BrowserWindow, app } from 'electron'; +import { BrowserWindow } from 'electron'; -function getLaravelBaseDir(appPath, importMetaDirname) { - if (app.isPackaged) { - return appPath; - } - - let currentPath = importMetaDirname; +function getProjectRoot(startPath) { + let currentPath = startPath; for (let i = 0; i < 10; i++) { if (fs.existsSync(path.join(currentPath, '.env'))) { return currentPath; @@ -16,39 +12,45 @@ function getLaravelBaseDir(appPath, importMetaDirname) { if (parentPath === currentPath) break; currentPath = parentPath; } - return process.cwd(); + return null; } -export function getEnvConfig(baseDir, key, defaultValue = null) { - if (!baseDir) return defaultValue; - const envPath = path.join(baseDir, '.env'); +export function getEnvConfig(projectRoot, key, defaultValue = null) { + if (!projectRoot) return defaultValue; + const envPath = path.join(projectRoot, '.env'); try { - if (fs.existsSync(envPath)) { - const content = fs.readFileSync(envPath, 'utf8'); - const lines = content.split('\n'); - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) continue; - const [lineKey, ...valueParts] = trimmed.split('='); - if (lineKey.trim() === key) { - return valueParts.join('=').trim().replace(/^["']|["']$/g, ''); - } + const content = fs.readFileSync(envPath, 'utf8'); + const lines = content.split('\n'); + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const [lineKey, ...valueParts] = trimmed.split('='); + if (lineKey.trim() === key) { + return valueParts.join('=').trim().replace(/^["']|["']$/g, ''); } } - } catch (e) { /* ignore */ } + } catch (e) { } return defaultValue; } -export function createSplash(appPath, importMetaDirname) { - const baseDir = getLaravelBaseDir(appPath, importMetaDirname); +export function createSplash(importMetaDirname) { + const projectRoot = getProjectRoot(importMetaDirname); + + if (!projectRoot) return null; - const enabled = getEnvConfig(baseDir, 'NATIVEPHP_SPLASH_ENABLED', 'false') === 'true'; + const enabled = getEnvConfig(projectRoot, 'NATIVEPHP_SPLASH_ENABLED', 'false') === 'true'; if (!enabled) return null; - const width = parseInt(getEnvConfig(baseDir, 'NATIVEPHP_SPLASH_WIDTH', '400')); - const height = parseInt(getEnvConfig(baseDir, 'NATIVEPHP_SPLASH_HEIGHT', '300')); - const splashRelativePath = getEnvConfig(baseDir, 'NATIVEPHP_SPLASH_HTML', 'public/splash.html'); + const width = parseInt(getEnvConfig(projectRoot, 'NATIVEPHP_SPLASH_WIDTH', '400')); + const height = parseInt(getEnvConfig(projectRoot, 'NATIVEPHP_SPLASH_HEIGHT', '300')); + const splashRelativePath = getEnvConfig(projectRoot, 'NATIVEPHP_SPLASH_HTML', 'public/splash.html'); + + const finalHtmlPath = path.join(projectRoot, splashRelativePath); + + if (!fs.existsSync(finalHtmlPath)) { + return null; + } const splash = new BrowserWindow({ width, @@ -59,13 +61,6 @@ export function createSplash(appPath, importMetaDirname) { webPreferences: { nodeIntegration: false } }); - const finalHtmlPath = path.join(baseDir, splashRelativePath); - - if (fs.existsSync(finalHtmlPath)) { - splash.loadURL('file:///' + finalHtmlPath.replace(/\\/g, '/')); - } else { - console.error(`[NativePHP Splash] HTML Not Found. Path: ${finalHtmlPath}`); - } - + splash.loadFile(finalHtmlPath); return splash; } From 0555e8f0987b20f72bbac9748714b3996ad82ea2 Mon Sep 17 00:00:00 2001 From: Andre Date: Fri, 15 May 2026 23:01:08 +0200 Subject: [PATCH 4/4] - Fixes an issue where the splash screen failed to load from the AppData directory in packaged environments. - Implements app.isPackaged check for reliable path resolution within and outside the ASAR archive. - Updates closing logic to use the 'did-navigate' event filtered by the local PHP server URL. - This prevents premature dismissal of the splash screen by background installer or update processes. - Ensures window dimensions are cast to integers to satisfy Electron API requirements. - Replaced UI error dialogs with silent console logging for production-ready error handling. --- resources/electron/src/main/index.js | 22 +++++---- resources/electron/src/main/splash.js | 67 ++++++++++++++------------- 2 files changed, 50 insertions(+), 39 deletions(-) diff --git a/resources/electron/src/main/index.js b/resources/electron/src/main/index.js index a47222c..6e0db3f 100644 --- a/resources/electron/src/main/index.js +++ b/resources/electron/src/main/index.js @@ -1,8 +1,7 @@ import NativePHP from '#plugin'; -import { app } from 'electron'; +import { app, BrowserWindow } from 'electron'; import path from 'path'; import { createSplash } from './splash.js'; - // Inherit User's PATH in Process & ChildProcess import fixPath from 'fix-path'; fixPath(); @@ -18,17 +17,24 @@ const appPath = path.join(buildPath, 'app'); let splashWindow; app.whenReady().then(() => { - splashWindow = createSplash(import.meta.dirname); + try { + splashWindow = createSplash(appPath, import.meta.dirname); + } catch (error) { + console.error('Error creating splash screen:', error); + } - /** - * Turn on the lights for the NativePHP app. - */ NativePHP.bootstrap(app, defaultIcon, phpBinary, certificate, appPath); }); app.on('browser-window-created', (event, window) => { if (splashWindow && window !== splashWindow) { - splashWindow.close(); - splashWindow = null; + window.webContents.on('did-navigate', (evt, url) => { + if (url.startsWith('http://127.0.0.1') || url.startsWith('http://localhost')) { + if (splashWindow) { + splashWindow.close(); + splashWindow = null; + } + } + }); } }); diff --git a/resources/electron/src/main/splash.js b/resources/electron/src/main/splash.js index d2387c2..97558a6 100644 --- a/resources/electron/src/main/splash.js +++ b/resources/electron/src/main/splash.js @@ -1,9 +1,13 @@ import path from 'path'; import fs from 'fs'; -import { BrowserWindow } from 'electron'; +import { BrowserWindow, app } from 'electron'; -function getProjectRoot(startPath) { - let currentPath = startPath; +function getLaravelBaseDir(appPath, importMetaDirname) { + if (app.isPackaged) { + return appPath; + } + + let currentPath = importMetaDirname; for (let i = 0; i < 10; i++) { if (fs.existsSync(path.join(currentPath, '.env'))) { return currentPath; @@ -12,45 +16,39 @@ function getProjectRoot(startPath) { if (parentPath === currentPath) break; currentPath = parentPath; } - return null; + return process.cwd(); } -export function getEnvConfig(projectRoot, key, defaultValue = null) { - if (!projectRoot) return defaultValue; - const envPath = path.join(projectRoot, '.env'); +export function getEnvConfig(baseDir, key, defaultValue = null) { + if (!baseDir) return defaultValue; + const envPath = path.join(baseDir, '.env'); try { - const content = fs.readFileSync(envPath, 'utf8'); - const lines = content.split('\n'); - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) continue; - const [lineKey, ...valueParts] = trimmed.split('='); - if (lineKey.trim() === key) { - return valueParts.join('=').trim().replace(/^["']|["']$/g, ''); + if (fs.existsSync(envPath)) { + const content = fs.readFileSync(envPath, 'utf8'); + const lines = content.split('\n'); + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const [lineKey, ...valueParts] = trimmed.split('='); + if (lineKey.trim() === key) { + return valueParts.join('=').trim().replace(/^["']|["']$/g, ''); + } } } - } catch (e) { } + } catch (e) { /* ignore */ } return defaultValue; } -export function createSplash(importMetaDirname) { - const projectRoot = getProjectRoot(importMetaDirname); - - if (!projectRoot) return null; +export function createSplash(appPath, importMetaDirname) { + const baseDir = getLaravelBaseDir(appPath, importMetaDirname); - const enabled = getEnvConfig(projectRoot, 'NATIVEPHP_SPLASH_ENABLED', 'false') === 'true'; + const enabled = getEnvConfig(baseDir, 'NATIVEPHP_SPLASH_ENABLED', 'false') === 'true'; if (!enabled) return null; - const width = parseInt(getEnvConfig(projectRoot, 'NATIVEPHP_SPLASH_WIDTH', '400')); - const height = parseInt(getEnvConfig(projectRoot, 'NATIVEPHP_SPLASH_HEIGHT', '300')); - const splashRelativePath = getEnvConfig(projectRoot, 'NATIVEPHP_SPLASH_HTML', 'public/splash.html'); - - const finalHtmlPath = path.join(projectRoot, splashRelativePath); - - if (!fs.existsSync(finalHtmlPath)) { - return null; - } + const width = parseInt(getEnvConfig(baseDir, 'NATIVEPHP_SPLASH_WIDTH', '400')); + const height = parseInt(getEnvConfig(baseDir, 'NATIVEPHP_SPLASH_HEIGHT', '300')); + const splashRelativePath = getEnvConfig(baseDir, 'NATIVEPHP_SPLASH_HTML', 'public/splash.html'); const splash = new BrowserWindow({ width, @@ -61,6 +59,13 @@ export function createSplash(importMetaDirname) { webPreferences: { nodeIntegration: false } }); - splash.loadFile(finalHtmlPath); + const finalHtmlPath = path.join(baseDir, splashRelativePath); + + if (fs.existsSync(finalHtmlPath)) { + splash.loadURL('file:///' + finalHtmlPath.replace(/\\/g, '/')); + } else { + console.error(`[NativePHP Splash] HTML Not Found. Path: ${finalHtmlPath}`); + } + return splash; }