mirror of
https://github.com/mileswolfallen2/gelectron.git
synced 2026-09-08 12:43:16 +00:00
feat: implement application icon support for macOS
- Removed package.json and code signature files from the macOS app bundle. - Added support for setting application icons via base64 encoded PNG strings. - Enhanced the main application logic to detect and apply icons from various locations. - Updated the Electron app and browser window classes to handle icon setting. - Introduced new methods for decoding PNG icons and applying them to the application and window. - Updated Cargo.toml to include the cocoa dependency for macOS.
This commit is contained in:
Generated
+1
@@ -1405,6 +1405,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"base64",
|
||||
"cocoa",
|
||||
"env_logger",
|
||||
"log",
|
||||
"muda",
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>GeelectronDemo</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>GeelectronDemo</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.gelectron.geelectrondemo</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>GeelectronDemo</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>NSRequiresAquaSystemAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
export PATH="$DIR:$PATH"
|
||||
export GELECTRON_NATIVE=1
|
||||
exec "$DIR/gelectron-bin" "$@"
|
||||
@@ -1,352 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Gelectron - app module (Electron compatible)
|
||||
*
|
||||
* Full implementation matching the Electron app API surface.
|
||||
* Events: ready, second-instance, activate, window-all-closed, before-quit,
|
||||
* will-quit, quit, focus, blur, browser-window-focus, browser-window-blur,
|
||||
* web-contents-created, render-process-gone, child-process-gone,
|
||||
* keyboard-visibility-changed, new-window-for-tab
|
||||
*/
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
const path = require('path');
|
||||
|
||||
class App extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this._ready = false;
|
||||
this._windowCount = 0;
|
||||
this._relaunchOptions = null;
|
||||
this._relaunching = false;
|
||||
this._aboutPanelOptions = {};
|
||||
this._badgeCount = 0;
|
||||
this._secureKeyboardEntryEnabled = false;
|
||||
this._userAgent = null;
|
||||
this._name = 'Gelectron App';
|
||||
this._names = null;
|
||||
|
||||
const os = require('os');
|
||||
const home = os.homedir();
|
||||
this._paths = {
|
||||
home,
|
||||
appData: path.join(home, process.platform === 'win32' ? '' : '.', process.platform === 'darwin' ? 'Library/Application Support' : 'config', 'gelectron'),
|
||||
userData: path.join(home, process.platform === 'win32' ? '' : '.', process.platform === 'darwin' ? 'Library/Application Support' : 'config', 'gelectron'),
|
||||
desktop: path.join(home, 'Desktop'),
|
||||
documents: path.join(home, 'Documents'),
|
||||
downloads: path.join(home, 'Downloads'),
|
||||
temp: os.tmpdir(),
|
||||
exe: process.execPath,
|
||||
module: __dirname,
|
||||
crashDumps: path.join(home, process.platform === 'win32' ? '' : '.', process.platform === 'darwin' ? 'Library/Application Support' : 'config', 'gelectron', 'crashDumps'),
|
||||
logs: path.join(home, process.platform === 'win32' ? '' : '.', process.platform === 'darwin' ? 'Library/Application Support' : 'config', 'gelectron', 'logs'),
|
||||
};
|
||||
|
||||
this._commandLine = new Map();
|
||||
this._dock = process.platform === 'darwin' ? {
|
||||
setIcon: (icon) => {},
|
||||
bounce: () => 0,
|
||||
cancelBounce: () => {},
|
||||
setBadge: () => {},
|
||||
getBadge: () => '',
|
||||
hide: () => {},
|
||||
show: () => {},
|
||||
isVisible: () => true,
|
||||
setMenu: () => {},
|
||||
setBadgeCount: (count) => { return 0; },
|
||||
getBadgeCount: () => 0,
|
||||
setThumbnail: () => {},
|
||||
setThumbnailClip: () => {},
|
||||
setThumbnailToolTip: () => {},
|
||||
} : null;
|
||||
|
||||
this._argv = process.argv.slice();
|
||||
|
||||
// Emit 'ready' automatically on the next tick, matching real Electron
|
||||
process.nextTick(() => {
|
||||
if (!this._ready) {
|
||||
this._ready = true;
|
||||
this.emit('ready');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Properties ──────────────────────────────────────────────
|
||||
|
||||
get commandLine() {
|
||||
const self = this;
|
||||
return {
|
||||
appendSwitch: (name, value) => { self._commandLine.set(name, value || ''); },
|
||||
removeSwitch: (name) => { self._commandLine.delete(name); },
|
||||
getSwitch: (name) => { return self._commandLine.get(name) || ''; },
|
||||
hasSwitch: (name) => { return self._commandLine.has(name); },
|
||||
};
|
||||
}
|
||||
|
||||
get dock() { return this._dock; }
|
||||
get argv() { return this._argv; }
|
||||
|
||||
get isPackaged() {
|
||||
return !process.argv[0].includes('node') && !process.argv[0].includes('gelectron');
|
||||
}
|
||||
|
||||
get name() { return this._name; }
|
||||
set name(val) { this._name = val || 'Gelectron App'; }
|
||||
|
||||
get version() { return process.env.GELECTRON_VERSION || '0.1.0'; }
|
||||
|
||||
get locale() { return this.getLocale(); }
|
||||
get userAgent() { return this.getUserAgent(); }
|
||||
|
||||
get isReady() { return this._ready; }
|
||||
|
||||
get appPath() { return this.getAppPath(); }
|
||||
|
||||
// ─── Core Methods ────────────────────────────────────────────
|
||||
|
||||
getAppPath() { return process.env.GELECTRON_APP_PATH || process.cwd(); }
|
||||
|
||||
getPath(name) {
|
||||
if (name === 'app' || name === 'appData' || name === 'userData') {
|
||||
return this._paths[name === 'app' ? 'appData' : name] || this._paths.userData;
|
||||
}
|
||||
return this._paths[name] || this._paths.userData;
|
||||
}
|
||||
|
||||
setPath(name, value) { this._paths[name] = value; }
|
||||
|
||||
getName() { return this._name; }
|
||||
setName(name) { this._name = name; }
|
||||
|
||||
getVersion() { return this.version; }
|
||||
|
||||
getLocale() { return Intl.DateTimeFormat().resolvedOptions().locale || 'en-US'; }
|
||||
|
||||
getUserAgent() {
|
||||
return this._userAgent || `Gelectron/${this.version} (${process.platform} ${process.arch}) Node.js/${process.versions ? process.versions.node : 'unknown'}`;
|
||||
}
|
||||
|
||||
setUserAgent(userAgent) { this._userAgent = userAgent || null; }
|
||||
|
||||
getPathForProtocol(protocol) { return null; }
|
||||
|
||||
getApplicationInfoForProtocol(protocol) {
|
||||
return Promise.resolve({
|
||||
defaultIcon: null,
|
||||
icon: null,
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Lifecycle ───────────────────────────────────────────────
|
||||
|
||||
async whenReady() {
|
||||
if (this._ready) return Promise.resolve();
|
||||
return new Promise((resolve) => { this.once('ready', resolve); });
|
||||
}
|
||||
|
||||
isReady() { return this._ready; }
|
||||
|
||||
requestSingleInstanceLock() { return true; }
|
||||
acquireSingleInstanceLock() { return true; }
|
||||
releaseSingleInstanceLock() {}
|
||||
|
||||
quit(exitCode = 0) {
|
||||
if (this.listenerCount('before-quit') > 0 || this.listenerCount('will-quit') > 0) {
|
||||
this.emit('before-quit');
|
||||
this.emit('will-quit');
|
||||
}
|
||||
this.emit('quit', exitCode);
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
exit(exitCode = 0) {
|
||||
if (this._ready) {
|
||||
this.emit('before-quit');
|
||||
this.emit('will-quit');
|
||||
}
|
||||
this.emit('quit', exitCode);
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
relaunch(options = {}) {
|
||||
this._relaunchOptions = options;
|
||||
this._relaunching = true;
|
||||
this.emit('before-quit');
|
||||
const { spawn } = require('child_process');
|
||||
const execPath = options.execPath || process.execPath;
|
||||
const args = options.args || process.argv.slice(1);
|
||||
spawn(execPath, args, {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
env: process.env,
|
||||
}).unref();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// ─── Window Management Helpers ───────────────────────────────
|
||||
|
||||
focus() {
|
||||
if (process.platform === 'darwin') { this.dock && this.dock.show(); }
|
||||
this.emit('focus');
|
||||
}
|
||||
|
||||
hide() { if (this.dock) this.dock.hide(); this.emit('blur'); }
|
||||
show() { if (this.dock) this.dock.show(); this.emit('focus'); }
|
||||
isVisible() { return this.dock ? this.dock.isVisible() : true; }
|
||||
isHidden() { return this.dock ? !this.dock.isVisible() : false; }
|
||||
|
||||
// ─── App Metrics / GPU ──────────────────────────────────────
|
||||
|
||||
getAppMetrics() {
|
||||
return [
|
||||
{
|
||||
creationTime: Date.now() - (process.uptime() * 1000) | 0,
|
||||
pid: process.pid,
|
||||
rid: 0,
|
||||
type: 'Browser',
|
||||
memory: { workingSetSize: 0, peakWorkingSetSize: 0, privateBytes: 0, sharedBytes: 0 },
|
||||
sandboxed: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
getGPUInfo(infoType) {
|
||||
if (infoType === 'basic') {
|
||||
return Promise.resolve({
|
||||
gpuDevice: [{ driverVendor: 'Gelectron', driverVersion: '0.0.0', driverDate: '', active: false }],
|
||||
GPUActive: false,
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
gpuDevice: [{ driverVendor: 'Gelectron', driverVersion: '0.0.0', driverDate: '', active: false }],
|
||||
GPUActive: false,
|
||||
auxAttributes: {},
|
||||
gpuDriver: 'Gelectron',
|
||||
gpuDriverVersion: '0.0.0',
|
||||
gpuWorkingSetSize: 0,
|
||||
});
|
||||
}
|
||||
|
||||
getGPUFeatureStatus() {
|
||||
return {
|
||||
gpuCompositing: 'disabled',
|
||||
multipleRasterThreads: 'disabled',
|
||||
nativeGpuMemoryBuffers: 'disabled',
|
||||
rasterization: 'disabled',
|
||||
smoothScrolling: 'disabled',
|
||||
videoDecode: 'disabled',
|
||||
videoEncode: 'disabled',
|
||||
webgl: 'disabled',
|
||||
webgl2: 'disabled',
|
||||
};
|
||||
}
|
||||
|
||||
disableHardwareAcceleration() {}
|
||||
disableDomainBlockingFor3DAPIs() {}
|
||||
|
||||
// ─── Badge ───────────────────────────────────────────────────
|
||||
|
||||
setBadgeCount(count) {
|
||||
this._badgeCount = count || 0;
|
||||
if (this.dock) this.dock.setBadgeCount(this._badgeCount);
|
||||
return this._badgeCount;
|
||||
}
|
||||
|
||||
getBadgeCount() { return this._badgeCount; }
|
||||
|
||||
// ─── Recent Documents ────────────────────────────────────────
|
||||
|
||||
addRecentDocument(path) {}
|
||||
clearRecentDocuments() {}
|
||||
setRecentDocumentLabel(label) {}
|
||||
|
||||
// ─── App User Model ID ──────────────────────────────────────
|
||||
|
||||
setAppUserModelId(id) {}
|
||||
getAppUserModelId() { return ''; }
|
||||
|
||||
// ─── Login Items ─────────────────────────────────────────────
|
||||
|
||||
getLoginItemSettings() {
|
||||
return {
|
||||
openAtLogin: false,
|
||||
openAsHidden: false,
|
||||
launchAtLogin: false,
|
||||
launchItems: [],
|
||||
};
|
||||
}
|
||||
|
||||
setLoginItemSettings(settings) {}
|
||||
|
||||
// ─── Applications Folder ─────────────────────────────────────
|
||||
|
||||
isInApplicationsFolder() { return true; }
|
||||
moveToApplicationsFolder() {}
|
||||
|
||||
// ─── File Icons ──────────────────────────────────────────────
|
||||
|
||||
getFileIcon(path, options, callback) {
|
||||
if (typeof options === 'function') { callback = options; }
|
||||
if (callback) callback(null, null);
|
||||
}
|
||||
|
||||
// ─── About Panel ─────────────────────────────────────────────
|
||||
|
||||
setAboutPanelOptions(options = {}) {
|
||||
this._aboutPanelOptions = {
|
||||
applicationName: this._name,
|
||||
applicationVersion: this.version,
|
||||
copyright: '',
|
||||
credits: '',
|
||||
authors: [],
|
||||
website: '',
|
||||
iconPath: '',
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
getAboutPanelOptions() {
|
||||
return { ...this._aboutPanelOptions };
|
||||
}
|
||||
|
||||
// ─── Certificate Trust ───────────────────────────────────────
|
||||
|
||||
async showCertificateTrustDialog() {}
|
||||
|
||||
// ─── Secure Keyboard Entry ──────────────────────────────────
|
||||
|
||||
setSecureKeyboardEntryEnabled(enabled) {
|
||||
this._secureKeyboardEntryEnabled = !!enabled;
|
||||
}
|
||||
|
||||
isSecureKeyboardEntryEnabled() {
|
||||
return this._secureKeyboardEntryEnabled;
|
||||
}
|
||||
|
||||
// ─── Window Tracking ────────────────────────────────────────
|
||||
|
||||
_trackWindow() { this._windowCount++; }
|
||||
|
||||
_untrackWindow() {
|
||||
this._windowCount--;
|
||||
if (this._windowCount <= 0) {
|
||||
this._windowCount = 0;
|
||||
this.emit('window-all-closed');
|
||||
}
|
||||
}
|
||||
|
||||
getWindowCount() { return this._windowCount; }
|
||||
|
||||
// ─── EventEmitter overrides (return this for chaining) ──────
|
||||
|
||||
on(eventName, listener) { super.on(eventName, listener); return this; }
|
||||
once(eventName, listener) { super.once(eventName, listener); return this; }
|
||||
addListener(eventName, listener) { super.addListener(eventName, listener); return this; }
|
||||
removeListener(eventName, listener) { super.removeListener(eventName, listener); return this; }
|
||||
removeAllListeners(eventName) { super.removeAllListeners(eventName); return this; }
|
||||
}
|
||||
|
||||
const app = new App();
|
||||
|
||||
module.exports = { app, App };
|
||||
@@ -1,84 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Gelectron - autoUpdater module (Electron compatible)
|
||||
* Stub implementation for electron-updater compatibility.
|
||||
*
|
||||
* electron-updater's MacUpdater / NsisUpdater / AppImageUpdater all
|
||||
* access `require("electron").autoUpdater` and expect it to be an
|
||||
* EventEmitter with the native Electron autoUpdater API surface:
|
||||
* .on("error", …), .on("update-downloaded", …)
|
||||
* .setFeedURL(), .getFeedURL()
|
||||
* .checkForUpdates()
|
||||
* .quitAndInstall()
|
||||
* .removeListener()
|
||||
*/
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
class AutoUpdater extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this._isUpdateAvailable = false;
|
||||
this._updateInfo = null;
|
||||
this._feedURL = null;
|
||||
this.autoDownload = true;
|
||||
this.autoInstallOnAppQuit = false;
|
||||
this.autoRunAppAfterInstall = true;
|
||||
this._logger = console;
|
||||
}
|
||||
|
||||
getFeedURL() {
|
||||
return this._feedURL;
|
||||
}
|
||||
|
||||
setFeedURL(options) {
|
||||
this._feedURL = options;
|
||||
}
|
||||
|
||||
async checkForUpdates() {
|
||||
return {
|
||||
updateInfo: null,
|
||||
isUpdateAvailable: false,
|
||||
};
|
||||
}
|
||||
|
||||
async checkForAndNotifyIfAvailable() {
|
||||
return this.checkForUpdates();
|
||||
}
|
||||
|
||||
async downloadUpdate() {}
|
||||
|
||||
quitAndInstall(isSilent = false, isForceRunAfter = false) {
|
||||
this.emit('before-quit-for-update');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
quitAndInstallAgain(isSilent = false, isForceRunAfter = false) {
|
||||
this.quitAndInstall(isSilent, isForceRunAfter);
|
||||
}
|
||||
|
||||
async updateDownloaded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
get isUpdateActive() {
|
||||
return false;
|
||||
}
|
||||
|
||||
get updateInfo() {
|
||||
return this._updateInfo;
|
||||
}
|
||||
|
||||
get logger() {
|
||||
return this._logger;
|
||||
}
|
||||
|
||||
set logger(val) {
|
||||
this._logger = val;
|
||||
}
|
||||
}
|
||||
|
||||
const autoUpdater = new AutoUpdater();
|
||||
|
||||
module.exports = { autoUpdater, AutoUpdater };
|
||||
-389
@@ -1,389 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Gelectron - BrowserWindow module (Electron compatible)
|
||||
*/
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
const path = require('path');
|
||||
const { bridge, isNative } = require('./native-bridge');
|
||||
const { app } = require('./app');
|
||||
|
||||
class WebContents extends EventEmitter {
|
||||
constructor(id) {
|
||||
super();
|
||||
this.id = id;
|
||||
this._url = '';
|
||||
this._title = '';
|
||||
this._isLoading = false;
|
||||
this._zoomFactor = 1.0;
|
||||
this._zoomLevel = 0;
|
||||
this._userAgent = '';
|
||||
this._audioMuted = false;
|
||||
}
|
||||
|
||||
get URL() { return this._url; }
|
||||
get isLoading() { return this._isLoading; }
|
||||
get title() { return this._title; }
|
||||
|
||||
get session() {
|
||||
return {
|
||||
id: `session-${this.id}`,
|
||||
cookies: {
|
||||
get: async () => [],
|
||||
set: async () => {},
|
||||
remove: async () => {},
|
||||
},
|
||||
protocol: {
|
||||
registerFileProtocol: () => {},
|
||||
registerHttpProtocol: () => {},
|
||||
unregisterProtocol: () => {},
|
||||
isProtocolRegistered: () => false,
|
||||
},
|
||||
setPermissionRequestHandler: () => {},
|
||||
setPermissionCheckHandler: () => {},
|
||||
webRequest: { onBeforeRequest: () => {}, onHeadersReceived: () => {} },
|
||||
setUserAgent: () => {},
|
||||
getUserAgent: () => '',
|
||||
};
|
||||
}
|
||||
|
||||
get processId() { return process.pid; }
|
||||
|
||||
loadURL(targetUrl) {
|
||||
this._url = targetUrl;
|
||||
this._isLoading = true;
|
||||
this.emit('did-start-loading');
|
||||
if (isNative) {
|
||||
bridge.loadUrl(this.id, targetUrl);
|
||||
}
|
||||
setTimeout(() => {
|
||||
this._isLoading = false;
|
||||
this.emit('did-stop-loading');
|
||||
this.emit('did-finish-load');
|
||||
this.emit('dom-ready');
|
||||
}, isNative ? 500 : 100);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
loadFile(filePath) {
|
||||
const url = `file://${path.resolve(filePath)}`;
|
||||
this._url = url;
|
||||
this._isLoading = true;
|
||||
this.emit('did-start-loading');
|
||||
if (isNative) {
|
||||
bridge.loadFile(this.id, filePath);
|
||||
}
|
||||
setTimeout(() => {
|
||||
this._isLoading = false;
|
||||
this.emit('did-stop-loading');
|
||||
this.emit('did-finish-load');
|
||||
this.emit('dom-ready');
|
||||
}, isNative ? 500 : 100);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
reload() {
|
||||
this._isLoading = true;
|
||||
this.emit('did-start-loading');
|
||||
if (isNative && this._url) {
|
||||
bridge.loadUrl(this.id, this._url);
|
||||
}
|
||||
setTimeout(() => {
|
||||
this._isLoading = false;
|
||||
this.emit('did-stop-loading');
|
||||
this.emit('did-finish-load');
|
||||
}, 50);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
canGoBack() { return false; }
|
||||
canGoForward() { return false; }
|
||||
goBack() {}
|
||||
goForward() {}
|
||||
|
||||
executeJavaScript(code, userGesture = true) {
|
||||
if (isNative) {
|
||||
bridge.evalJs(this.id, code);
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
insertCSS(css) { return Promise.resolve(0); }
|
||||
insertJS(code, hasUserGesture = true) { return this.executeJavaScript(code, hasUserGesture); }
|
||||
|
||||
send(channel, ...args) {
|
||||
if (isNative) {
|
||||
bridge.sendToRenderer(this.id, channel, ...args);
|
||||
} else {
|
||||
console.log(`[gelectron] webContents.send('${channel}')`);
|
||||
}
|
||||
}
|
||||
|
||||
sendInputEvent() {}
|
||||
|
||||
setZoomFactor(factor) { this._zoomFactor = factor; }
|
||||
getZoomFactor() { return this._zoomFactor; }
|
||||
setZoomLevel(level) { this._zoomLevel = level; }
|
||||
getZoomLevel() { return this._zoomLevel; }
|
||||
setUserAgent(ua) { this._userAgent = ua; }
|
||||
getUserAgent() { return this._userAgent; }
|
||||
setAudioMuted(muted) { this._audioMuted = muted; }
|
||||
isAudioMuted() { return this._audioMuted; }
|
||||
|
||||
openDevTools() { console.log('[gelectron] openDevTools'); }
|
||||
closeDevTools() {}
|
||||
isDevToolsOpened() { return false; }
|
||||
toggleDevTools() {}
|
||||
inspectElement() {}
|
||||
|
||||
setIgnoreMenuShortcuts() {}
|
||||
setWindowOpenHandler() { return { action: 'deny' }; }
|
||||
setPermissionRequestHandler() {}
|
||||
setCertificateVerifyProc() {}
|
||||
setBackgroundColor() {}
|
||||
isCrashed() { return false; }
|
||||
capturePage() { return Promise.resolve(null); }
|
||||
getResourceUsage() { return { images: 0, scripts: 0, css: 0, xhr: 0, webgl: 0 }; }
|
||||
type() { return 'backgroundPage'; }
|
||||
focused() { return false; }
|
||||
isFocused() { return false; }
|
||||
destroy() {}
|
||||
}
|
||||
|
||||
class BrowserWindow extends EventEmitter {
|
||||
static _windows = new Map();
|
||||
static _nextId = 1;
|
||||
|
||||
constructor(options = {}) {
|
||||
super();
|
||||
|
||||
this.id = BrowserWindow._nextId++;
|
||||
this._options = {
|
||||
width: options.width || 800,
|
||||
height: options.height || 600,
|
||||
minWidth: options.minWidth || 0,
|
||||
minHeight: options.minHeight || 0,
|
||||
maxWidth: options.maxWidth || 0,
|
||||
maxHeight: options.maxHeight || 0,
|
||||
title: options.title || 'Gelectron',
|
||||
backgroundColor: options.backgroundColor || '#ffffff',
|
||||
show: options.show !== false,
|
||||
frame: options.frame !== false,
|
||||
resizable: options.resizable !== false,
|
||||
minimizable: options.minimizable !== false,
|
||||
maximizable: options.maximizable !== false,
|
||||
closable: options.closable !== false,
|
||||
fullscreen: options.fullscreen || false,
|
||||
alwaysOnTop: options.alwaysOnTop || false,
|
||||
transparent: options.transparent || false,
|
||||
decorations: options.decorations !== false,
|
||||
icon: options.icon || null,
|
||||
titleBarStyle: options.titleBarStyle || 'default',
|
||||
trafficLightPosition: options.trafficLightPosition || null,
|
||||
vibrancy: options.vibrancy || null,
|
||||
webPreferences: {
|
||||
preload: options.webPreferences?.preload || null,
|
||||
contextIsolation: options.webPreferences?.contextIsolation !== false,
|
||||
nodeIntegration: options.webPreferences?.nodeIntegration || false,
|
||||
sandbox: options.webPreferences?.sandbox || false,
|
||||
devTools: options.webPreferences?.devTools !== false,
|
||||
webSecurity: options.webPreferences?.webSecurity !== false,
|
||||
allowRunningInsecureContent: options.webPreferences?.allowRunningInsecureContent || false,
|
||||
experimentalFeatures: options.webPreferences?.experimentalFeatures || false,
|
||||
...options.webPreferences,
|
||||
},
|
||||
};
|
||||
|
||||
this._url = '';
|
||||
this._isDestroyed = false;
|
||||
this._isVisible = this._options.show;
|
||||
this._isMinimized = false;
|
||||
this._isMaximized = false;
|
||||
this._isFullScreen = this._options.fullscreen;
|
||||
|
||||
this.webContents = new WebContents(this.id);
|
||||
|
||||
BrowserWindow._windows.set(this.id, this);
|
||||
|
||||
if (typeof app._trackWindow === 'function') {
|
||||
app._trackWindow();
|
||||
}
|
||||
|
||||
if (isNative) {
|
||||
bridge.createWindow(this.id, {
|
||||
width: this._options.width,
|
||||
height: this._options.height,
|
||||
title: this._options.title,
|
||||
show: this._options.show,
|
||||
resizable: this._options.resizable,
|
||||
alwaysOnTop: this._options.alwaysOnTop,
|
||||
fullscreen: this._options.fullscreen,
|
||||
});
|
||||
}
|
||||
|
||||
if (this._options.show) {
|
||||
process.nextTick(() => {
|
||||
if (!this._isDestroyed) {
|
||||
this.emit('ready-to-show');
|
||||
this.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static fromWebContents(webContents) {
|
||||
for (const win of BrowserWindow._windows.values()) {
|
||||
if (win.webContents && win.webContents.id === webContents.id) return win;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static getAllWindows() {
|
||||
return Array.from(BrowserWindow._windows.values()).filter((w) => !w.isDestroyed());
|
||||
}
|
||||
|
||||
static getFocusedWindow() { return null; }
|
||||
|
||||
static fromId(id) { return BrowserWindow._windows.get(id) || null; }
|
||||
|
||||
static addExtension() {}
|
||||
static removeExtension() {}
|
||||
static getExtensions() { return {}; }
|
||||
|
||||
loadURL(targetUrl) {
|
||||
this._url = targetUrl;
|
||||
if (isNative) {
|
||||
bridge.loadUrl(this.id, targetUrl);
|
||||
}
|
||||
return this.webContents.loadURL(targetUrl);
|
||||
}
|
||||
|
||||
loadFile(filePath) {
|
||||
return this.webContents.loadFile(filePath);
|
||||
}
|
||||
|
||||
show() {
|
||||
if (this._isDestroyed) return;
|
||||
this._isVisible = true;
|
||||
this._isMinimized = false;
|
||||
if (isNative) bridge.showWindow(this.id);
|
||||
this.emit('show');
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (this._isDestroyed) return;
|
||||
this._isVisible = false;
|
||||
if (isNative) bridge.hideWindow(this.id);
|
||||
this.emit('hide');
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this._isDestroyed) return;
|
||||
this.emit('close');
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this._isDestroyed) return;
|
||||
this._isDestroyed = true;
|
||||
if (isNative) bridge.destroyWindow(this.id);
|
||||
BrowserWindow._windows.delete(this.id);
|
||||
if (typeof app._untrackWindow === 'function') {
|
||||
app._untrackWindow();
|
||||
}
|
||||
this.emit('closed');
|
||||
}
|
||||
|
||||
focus() { if (!this._isDestroyed) { if (isNative) bridge.focusWindow(this.id); this.emit('focus'); } }
|
||||
blur() {}
|
||||
|
||||
minimize() { if (!this._isDestroyed) { this._isMinimized = true; if (isNative) bridge.minimizeWindow(this.id); this.emit('minimize'); } }
|
||||
maximize() { if (!this._isDestroyed) { this._isMaximized = true; if (isNative) bridge.maximizeWindow(this.id); this.emit('maximize'); } }
|
||||
unmaximize() { if (!this._isDestroyed) { this._isMaximized = false; this.emit('unmaximize'); } }
|
||||
restore() { if (!this._isDestroyed) { this._isMinimized = false; this._isMaximized = false; this.emit('restore'); } }
|
||||
|
||||
setFullScreen(flag) { this._isFullScreen = flag; this.emit('enter-full-screen'); }
|
||||
isFullScreen() { return this._isFullScreen; }
|
||||
isMinimized() { return this._isMinimized; }
|
||||
isMaximized() { return this._isMaximized; }
|
||||
isVisible() { return this._isVisible; }
|
||||
isDestroyed() { return this._isDestroyed; }
|
||||
isFocused() { return false; }
|
||||
isNormal() { return !this._isMinimized && !this._isMaximized && !this._isFullScreen; }
|
||||
|
||||
setAlwaysOnTop(flag) { this._options.alwaysOnTop = flag; }
|
||||
isAlwaysOnTop() { return this._options.alwaysOnTop; }
|
||||
|
||||
setPosition(x, y) {}
|
||||
getPosition() { return [0, 0]; }
|
||||
setSize(w, h) { this._options.width = w; this._options.height = h; }
|
||||
getSize() { return [this._options.width, this._options.height]; }
|
||||
setMinimumSize(w, h) { this._options.minWidth = w; this._options.minHeight = h; }
|
||||
getMinimumSize() { return [this._options.minWidth, this._options.minHeight]; }
|
||||
setMaximumSize(w, h) { this._options.maxWidth = w; this._options.maxHeight = h; }
|
||||
getMaximumSize() { return [this._options.maxWidth, this._options.maxHeight]; }
|
||||
|
||||
setResizable(v) { this._options.resizable = v; }
|
||||
isResizable() { return this._options.resizable; }
|
||||
setMovable() {}
|
||||
isMovable() { return true; }
|
||||
setMinimizable(v) { this._options.minimizable = v; }
|
||||
isMinimizable() { return this._options.minimizable; }
|
||||
setMaximizable(v) { this._options.maximizable = v; }
|
||||
isMaximizable() { return this._options.maximizable; }
|
||||
setClosable(v) { this._options.closable = v; }
|
||||
isClosable() { return this._options.closable; }
|
||||
|
||||
setTitle(title) { this._options.title = title; this.webContents._title = title; if (isNative) bridge.setTitle(this.id, title); }
|
||||
getTitle() { return this._options.title; }
|
||||
|
||||
setSkipTaskbar() {}
|
||||
setKiosk() {}
|
||||
isKiosk() { return false; }
|
||||
|
||||
center() {}
|
||||
setBounds() {}
|
||||
getBounds() { return { x: 0, y: 0, width: this._options.width, height: this._options.height }; }
|
||||
setSimpleFullScreen() {}
|
||||
isSimpleFullScreen() { return false; }
|
||||
setAutoHideCursor() {}
|
||||
setContentBounds() {}
|
||||
getContentBounds() { return this.getBounds(); }
|
||||
isEnabled() { return true; }
|
||||
setEnabled() {}
|
||||
setProgressBar() {}
|
||||
setOverlayIcon() {}
|
||||
setVisibleOnAllWorkspaces() {}
|
||||
isVisibleOnAllWorkspaces() { return false; }
|
||||
setVibrancy() {}
|
||||
getNativeWindowHandle() { return Buffer.alloc(0); }
|
||||
setHasShadow() {}
|
||||
hasShadow() { return true; }
|
||||
setOpacity() {}
|
||||
getOpacity() { return 1.0; }
|
||||
setThumbarButtons() {}
|
||||
setThumbnailClip() {}
|
||||
setThumbnailToolTip() {}
|
||||
setAppDetails() {}
|
||||
setForeground() {}
|
||||
flashFrame() {}
|
||||
setIcon() {}
|
||||
setBackgroundColor(c) { this._options.backgroundColor = c; }
|
||||
getBackgroundColor() { return this._options.backgroundColor; }
|
||||
|
||||
capturePage() { return this.webContents.capturePage(); }
|
||||
print() { console.log('[gelectron] print called'); }
|
||||
printToPDF() { return Promise.resolve(Buffer.alloc(0)); }
|
||||
setParentWindow() {}
|
||||
getParentWindow() { return null; }
|
||||
getChildWindows() { return []; }
|
||||
selectPreviousTab() {}
|
||||
selectNextTab() {}
|
||||
mergeAllWindows() {}
|
||||
moveTabToNewWindow() {}
|
||||
toggleTabBar() {}
|
||||
addTabbedWindow() {}
|
||||
}
|
||||
|
||||
module.exports = { BrowserWindow, WebContents };
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Gelectron - contextBridge module (Electron compatible)
|
||||
*/
|
||||
|
||||
const contextBridge = {
|
||||
exposeInMainWorld(key, api) {
|
||||
if (typeof globalThis.window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a frozen copy of the API
|
||||
const safeApi = deepFreeze(JSON.parse(JSON.stringify(api)));
|
||||
|
||||
globalThis.window[key] = safeApi;
|
||||
|
||||
console.log(`[gelectron] contextBridge: exposed '${key}' to main world`);
|
||||
},
|
||||
};
|
||||
|
||||
function deepFreeze(obj) {
|
||||
if (typeof obj !== 'object' || obj === null) return obj;
|
||||
Object.freeze(obj);
|
||||
Object.keys(obj).forEach((key) => {
|
||||
if (typeof obj[key] === 'object' && obj[key] !== null && !Object.isFrozen(obj[key])) {
|
||||
deepFreeze(obj[key]);
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
|
||||
module.exports = contextBridge;
|
||||
@@ -1,70 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Gelectron - dialog module (Electron compatible)
|
||||
*/
|
||||
|
||||
const dialog = {
|
||||
showOpenDialog(browserWindowOrOptions, options) {
|
||||
let opts = options || browserWindowOrOptions || {};
|
||||
if (browserWindowOrOptions && browserWindowOrOptions.webContents) {
|
||||
opts = options || {};
|
||||
}
|
||||
|
||||
// Route through native if available
|
||||
if (typeof globalThis.__gelectron_dialog_open === 'function') {
|
||||
return globalThis.__gelectron_dialog_open(opts);
|
||||
}
|
||||
|
||||
return Promise.resolve({ canceled: true, filePaths: [] });
|
||||
},
|
||||
|
||||
showSaveDialog(browserWindowOrOptions, options) {
|
||||
let opts = options || browserWindowOrOptions || {};
|
||||
if (browserWindowOrOptions && browserWindowOrOptions.webContents) {
|
||||
opts = options || {};
|
||||
}
|
||||
|
||||
if (typeof globalThis.__gelectron_dialog_save === 'function') {
|
||||
return globalThis.__gelectron_dialog_save(opts);
|
||||
}
|
||||
|
||||
return Promise.resolve({ canceled: true, filePath: undefined });
|
||||
},
|
||||
|
||||
showMessageBox(browserWindowOrOptions, options) {
|
||||
let opts = options || browserWindowOrOptions || {};
|
||||
if (browserWindowOrOptions && browserWindowOrOptions.webContents) {
|
||||
opts = options || {};
|
||||
}
|
||||
|
||||
if (typeof globalThis.__gelectron_dialog_message === 'function') {
|
||||
return globalThis.__gelectron_dialog_message(opts);
|
||||
}
|
||||
|
||||
return Promise.resolve({ response: 0, checkboxChecked: false });
|
||||
},
|
||||
|
||||
showErrorBox(title, content) {
|
||||
console.error(`[gelectron] ${title}: ${content}`);
|
||||
},
|
||||
|
||||
showMessageBoxSync(browserWindowOrOptions, options) {
|
||||
let opts = options || browserWindowOrOptions || {};
|
||||
if (browserWindowOrOptions && browserWindowOrOptions.webContents) {
|
||||
opts = options || {};
|
||||
}
|
||||
|
||||
if (typeof globalThis.__gelectron_dialog_message_sync === 'function') {
|
||||
return globalThis.__gelectron_dialog_message_sync(opts);
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
|
||||
showCertificateTrustDialog(browserWindowOrOptions, options) {
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = dialog;
|
||||
@@ -1,159 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Gelectron - Electron compatibility layer.
|
||||
* Drop-in replacement for require('electron').
|
||||
*/
|
||||
|
||||
const { app } = require('./app');
|
||||
const { BrowserWindow } = require('./browser-window');
|
||||
const ipcMain = require('./ipc-main');
|
||||
const { Menu, MenuItem } = require('./menu');
|
||||
const { Tray } = require('./tray');
|
||||
const dialog = require('./dialog');
|
||||
const shell = require('./shell');
|
||||
const { Notification } = require('./notification');
|
||||
const nativeImage = require('./native-image');
|
||||
const safeStorage = require('./safe-storage');
|
||||
const contextBridge = require('./context-bridge');
|
||||
const webContents = require('./web-contents');
|
||||
const { autoUpdater, AutoUpdater } = require('./auto-updater');
|
||||
const { bridge, isNative } = require('./native-bridge');
|
||||
|
||||
if (isNative) {
|
||||
bridge.on('ipc-message', (windowId, channel, data) => {
|
||||
const event = { sender: { id: windowId }, channel };
|
||||
ipcMain._emit(channel, event, data);
|
||||
});
|
||||
}
|
||||
|
||||
// Session stub (electron-updater calls session.fromPartition)
|
||||
const sessionStub = {
|
||||
defaultSession: {
|
||||
cookies: {
|
||||
get: async () => [],
|
||||
set: async () => {},
|
||||
remove: async () => {},
|
||||
getSession: () => null,
|
||||
},
|
||||
protocol: {
|
||||
registerFileProtocol: () => {},
|
||||
registerHttpProtocol: () => {},
|
||||
unregisterProtocol: () => {},
|
||||
isProtocolRegistered: () => false,
|
||||
},
|
||||
setPermissionRequestHandler: () => {},
|
||||
setPermissionCheckHandler: () => {},
|
||||
webRequest: {
|
||||
onBeforeRequest: () => {},
|
||||
onHeadersReceived: () => {},
|
||||
},
|
||||
setUserAgent: () => {},
|
||||
getUserAgent: () => '',
|
||||
},
|
||||
fromPartition: (partition, options) => ({
|
||||
cookies: {
|
||||
get: async () => [],
|
||||
set: async () => {},
|
||||
remove: async () => {},
|
||||
},
|
||||
protocol: {
|
||||
registerFileProtocol: () => {},
|
||||
registerHttpProtocol: () => {},
|
||||
unregisterProtocol: () => {},
|
||||
isProtocolRegistered: () => false,
|
||||
},
|
||||
webRequest: {
|
||||
onBeforeRequest: () => {},
|
||||
onHeadersReceived: () => {},
|
||||
resolveProxy: async () => '',
|
||||
},
|
||||
setUserAgent: () => {},
|
||||
getUserAgent: () => '',
|
||||
clearCache: async () => {},
|
||||
clearStorageData: async () => {},
|
||||
setProxy: async () => {},
|
||||
getProxy: async () => ({ mode: 'direct' }),
|
||||
}),
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
app,
|
||||
BrowserWindow,
|
||||
ipcMain,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Tray,
|
||||
dialog,
|
||||
shell,
|
||||
Notification,
|
||||
nativeImage,
|
||||
safeStorage,
|
||||
contextBridge,
|
||||
webContents,
|
||||
autoUpdater,
|
||||
AutoUpdater,
|
||||
session: sessionStub,
|
||||
|
||||
// Aliases for common imports
|
||||
clipboard: {
|
||||
readText: () => '',
|
||||
writeText: () => {},
|
||||
readImage: () => nativeImage.createEmpty(),
|
||||
writeImage: () => {},
|
||||
},
|
||||
screen: {
|
||||
getPrimaryDisplay: () => ({
|
||||
id: 0,
|
||||
label: '',
|
||||
bounds: { x: 0, y: 0, width: 1920, height: 1080 },
|
||||
workArea: { x: 0, y: 0, width: 1920, height: 1040 },
|
||||
size: { width: 1920, height: 1080 },
|
||||
workAreaSize: { width: 1920, height: 1040 },
|
||||
scaleFactor: 1.0,
|
||||
rotation: 0,
|
||||
internal: false,
|
||||
touchSupport: 'unknown',
|
||||
}),
|
||||
getAllDisplays: () => [],
|
||||
getDisplayMatching: () => null,
|
||||
},
|
||||
systemPreferences: {
|
||||
isDarkMode: () => false,
|
||||
getAccentColor: () => '#007AFF',
|
||||
getColor: () => '#ffffff',
|
||||
isSwipeTrackingFromScrollEventsEnabled: () => false,
|
||||
subscribeNotification: () => () => {},
|
||||
unsubscribeNotification: () => {},
|
||||
subscribeLocalNotification: () => () => {},
|
||||
unsubscribeLocalNotification: () => {},
|
||||
getUserDefault: () => null,
|
||||
setUserDefault: () => {},
|
||||
removeUserDefault: () => {},
|
||||
},
|
||||
powerMonitor: {
|
||||
on: () => {},
|
||||
off: () => {},
|
||||
once: () => {},
|
||||
getSystemIdleState: () => 'active',
|
||||
getSystemIdleTime: () => 0,
|
||||
isInLowPowerMode: () => false,
|
||||
},
|
||||
globalShortcut: {
|
||||
register: () => true,
|
||||
unregister: () => {},
|
||||
unregisterAll: () => {},
|
||||
isRegistered: () => false,
|
||||
},
|
||||
net: {
|
||||
fetch: globalThis.fetch || (() => Promise.reject(new Error('fetch not available'))),
|
||||
},
|
||||
|
||||
// Constants
|
||||
IPCRenderer: {
|
||||
invoke: () => Promise.resolve(),
|
||||
send: () => {},
|
||||
on: () => () => {},
|
||||
removeListener: () => {},
|
||||
},
|
||||
};
|
||||
@@ -1,107 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Gelectron - ipcMain module (Electron compatible)
|
||||
*/
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
class IpcMain extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this._handlers = new Map();
|
||||
this._onceHandlers = new Map();
|
||||
}
|
||||
|
||||
handle(channel, handler) {
|
||||
if (typeof handler !== 'function') {
|
||||
throw new TypeError(`Expected function for channel '${channel}'`);
|
||||
}
|
||||
this._handlers.set(channel, handler);
|
||||
return this;
|
||||
}
|
||||
|
||||
handleOnce(channel, handler) {
|
||||
if (typeof handler !== 'function') {
|
||||
throw new TypeError(`Expected function for channel '${channel}'`);
|
||||
}
|
||||
this._onceHandlers.set(channel, handler);
|
||||
return this;
|
||||
}
|
||||
|
||||
on(channel, listener) {
|
||||
if (typeof listener !== 'function') {
|
||||
throw new TypeError(`Expected function for channel '${channel}'`);
|
||||
}
|
||||
super.on(channel, listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
once(channel, listener) {
|
||||
if (typeof listener !== 'function') {
|
||||
throw new TypeError(`Expected function for channel '${channel}'`);
|
||||
}
|
||||
super.once(channel, listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
removeHandler(channel) {
|
||||
this._handlers.delete(channel);
|
||||
return this;
|
||||
}
|
||||
|
||||
removeListener(channel, listener) {
|
||||
super.removeListener(channel, listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
removeAllListeners(channel) {
|
||||
super.removeAllListeners(channel);
|
||||
return this;
|
||||
}
|
||||
|
||||
listenerCount(channel) {
|
||||
return super.listenerCount(channel);
|
||||
}
|
||||
|
||||
rawListeners(channel) {
|
||||
return super.rawListeners(channel);
|
||||
}
|
||||
|
||||
eventNames() {
|
||||
return super.eventNames();
|
||||
}
|
||||
|
||||
_invoke(channel, ...args) {
|
||||
const handler = this._handlers.get(channel);
|
||||
if (!handler) {
|
||||
return Promise.reject(new Error(`No handler registered for channel '${channel}'`));
|
||||
}
|
||||
try {
|
||||
const result = handler(...args);
|
||||
if (result instanceof Promise) {
|
||||
return result;
|
||||
}
|
||||
return Promise.resolve(result);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
_emit(channel, ...args) {
|
||||
return this.emit(channel, ...args);
|
||||
}
|
||||
|
||||
_emitOnce(channel, ...args) {
|
||||
const handler = this._onceHandlers.get(channel);
|
||||
if (handler) {
|
||||
this._onceHandlers.delete(channel);
|
||||
return handler(...args);
|
||||
}
|
||||
return this.emit(channel, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
const ipcMain = new IpcMain();
|
||||
|
||||
module.exports = ipcMain;
|
||||
@@ -1,145 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Gelectron - ipcRenderer module (Electron compatible)
|
||||
*/
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
class IpcRendererEvent {
|
||||
constructor(sender, channel) {
|
||||
this.sender = sender;
|
||||
this.channel = channel;
|
||||
this._returnValue = undefined;
|
||||
this._defaultPrevented = false;
|
||||
}
|
||||
|
||||
preventDefault() {
|
||||
this._defaultPrevented = true;
|
||||
}
|
||||
|
||||
get defaultPrevented() {
|
||||
return this._defaultPrevented;
|
||||
}
|
||||
}
|
||||
|
||||
class IpcRenderer extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this._requestId = 0;
|
||||
this._pending = new Map();
|
||||
this._listeners = new Map();
|
||||
}
|
||||
|
||||
async invoke(channel, ...args) {
|
||||
const requestId = `ipc-${++this._requestId}-${Date.now()}`;
|
||||
const argsJson = JSON.stringify(args);
|
||||
|
||||
// In full integration, this calls the native ipc_renderer_invoke
|
||||
// For now, route through the global IPC bridge if available
|
||||
if (typeof globalThis.__gelectron_ipc === 'function') {
|
||||
return globalThis.__gelectron_ipc(channel, ...args);
|
||||
}
|
||||
|
||||
// Fallback: emit event for main process handling
|
||||
return new Promise((resolve, reject) => {
|
||||
this._pending.set(requestId, { resolve, reject });
|
||||
|
||||
// Emit for any registered handlers
|
||||
this.emit(`invoke:${channel}`, ...args);
|
||||
|
||||
// Timeout after 30 seconds
|
||||
setTimeout(() => {
|
||||
if (this._pending.has(requestId)) {
|
||||
this._pending.delete(requestId);
|
||||
reject(new Error(`IPC invoke timed out for channel '${channel}'`));
|
||||
}
|
||||
}, 30000);
|
||||
});
|
||||
}
|
||||
|
||||
send(channel, ...args) {
|
||||
const argsJson = JSON.stringify(args);
|
||||
|
||||
if (typeof globalThis.__gelectron_send === 'function') {
|
||||
globalThis.__gelectron_send(channel, ...args);
|
||||
}
|
||||
|
||||
this.emit(`send:${channel}`, ...args);
|
||||
}
|
||||
|
||||
sendSync(channel, ...args) {
|
||||
// Synchronous IPC is deprecated in Electron; return undefined
|
||||
return undefined;
|
||||
}
|
||||
|
||||
postMessage(channel, message, transfer) {
|
||||
this.send(channel, message);
|
||||
}
|
||||
|
||||
on(channel, listener) {
|
||||
const wrappedListener = (event, ...args) => {
|
||||
listener(event, ...args);
|
||||
};
|
||||
|
||||
// Store wrapped reference for removeListener
|
||||
if (!this._listeners.has(channel)) {
|
||||
this._listeners.set(channel, new Map());
|
||||
}
|
||||
this._listeners.get(channel).set(listener, wrappedListener);
|
||||
|
||||
return super.on(channel, wrappedListener);
|
||||
}
|
||||
|
||||
once(channel, listener) {
|
||||
const wrappedListener = (event, ...args) => {
|
||||
listener(event, ...args);
|
||||
};
|
||||
return super.once(channel, wrappedListener);
|
||||
}
|
||||
|
||||
removeListener(channel, listener) {
|
||||
const channelListeners = this._listeners.get(channel);
|
||||
if (channelListeners) {
|
||||
const wrapped = channelListeners.get(listener);
|
||||
if (wrapped) {
|
||||
channelListeners.delete(listener);
|
||||
return super.removeListener(channel, wrapped);
|
||||
}
|
||||
}
|
||||
return super.removeListener(channel, listener);
|
||||
}
|
||||
|
||||
removeAllListeners(channel) {
|
||||
this._listeners.delete(channel);
|
||||
return super.removeAllListeners(channel);
|
||||
}
|
||||
|
||||
// Internal method to receive messages from main process
|
||||
_receiveMessage(channel, data) {
|
||||
const event = new IpcRendererEvent(this, channel);
|
||||
this.emit(channel, event, data);
|
||||
}
|
||||
|
||||
// Internal method to resolve a pending invoke
|
||||
_resolveInvoke(requestId, result) {
|
||||
const pending = this._pending.get(requestId);
|
||||
if (pending) {
|
||||
this._pending.delete(requestId);
|
||||
pending.resolve(result);
|
||||
}
|
||||
}
|
||||
|
||||
// Internal method to reject a pending invoke
|
||||
_rejectInvoke(requestId, error) {
|
||||
const pending = this._pending.get(requestId);
|
||||
if (pending) {
|
||||
this._pending.delete(requestId);
|
||||
pending.reject(new Error(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ipcRenderer = new IpcRenderer();
|
||||
|
||||
module.exports = { ipcRenderer, IpcRendererEvent };
|
||||
@@ -1,183 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Gelectron - menu module (Electron compatible)
|
||||
*
|
||||
* Supports native menu rendering through the Rust/tao bridge.
|
||||
* When running in native mode, setApplicationMenu and popup route
|
||||
* through the bridge for native OS menu rendering.
|
||||
*/
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
class MenuItem extends EventEmitter {
|
||||
constructor(options = {}) {
|
||||
super();
|
||||
this.id = options.id || '';
|
||||
this.label = options.label || '';
|
||||
this.type = options.type || 'normal';
|
||||
this.role = options.role || '';
|
||||
this.accelerator = options.accelerator || '';
|
||||
this.enabled = options.enabled !== false;
|
||||
this.visible = options.visible !== false;
|
||||
this.checked = options.checked || false;
|
||||
this.submenu = options.submenu || null;
|
||||
this.toolTip = options.toolTip || '';
|
||||
this.icon = options.icon || null;
|
||||
this._click = options.click || null;
|
||||
|
||||
// Recursively build submenu
|
||||
if (this.submenu && !(this.submenu instanceof Menu)) {
|
||||
if (Array.isArray(this.submenu)) {
|
||||
this.submenu = Menu.buildFromTemplate(this.submenu);
|
||||
} else {
|
||||
this.submenu = Menu.buildFromTemplate(this.submenu.items || []);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
click() {
|
||||
if (this._click) {
|
||||
this._click(this, null);
|
||||
}
|
||||
this.emit('click');
|
||||
}
|
||||
}
|
||||
|
||||
class Menu extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this.items = [];
|
||||
this._id = Menu._nextId++;
|
||||
}
|
||||
|
||||
static _nextId = 1;
|
||||
static _applicationMenu = null;
|
||||
|
||||
static buildFromTemplate(template) {
|
||||
const menu = new Menu();
|
||||
if (Array.isArray(template)) {
|
||||
menu.items = template.map((item) => {
|
||||
if (item instanceof MenuItem) return item;
|
||||
if (item.type === 'separator') {
|
||||
return new MenuItem({ type: 'separator' });
|
||||
}
|
||||
return new MenuItem(item);
|
||||
});
|
||||
}
|
||||
return menu;
|
||||
}
|
||||
|
||||
static getApplicationMenu() {
|
||||
return Menu._applicationMenu || null;
|
||||
}
|
||||
|
||||
static setApplicationMenu(menu) {
|
||||
Menu._applicationMenu = menu;
|
||||
if (menu) {
|
||||
// Route through bridge if available
|
||||
try {
|
||||
const { bridge, isNative } = require('./native-bridge');
|
||||
if (isNative && bridge) {
|
||||
bridge._send({ type: 'set-application-menu', menu: menu._serialize() });
|
||||
}
|
||||
} catch (e) {
|
||||
// Not available in all contexts
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
append(menuItem) {
|
||||
if (menuItem instanceof MenuItem) {
|
||||
this.items.push(menuItem);
|
||||
} else {
|
||||
this.items.push(new MenuItem(menuItem));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
insert(menuItem, position) {
|
||||
if (menuItem instanceof MenuItem) {
|
||||
this.items.splice(position, 0, menuItem);
|
||||
} else {
|
||||
this.items.splice(position, 0, new MenuItem(menuItem));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
popup(options = {}) {
|
||||
try {
|
||||
const { bridge, isNative } = require('./native-bridge');
|
||||
if (isNative && bridge) {
|
||||
bridge._send({ type: 'popup-menu', menu: this._serialize(), x: options.x, y: options.y });
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
console.log('[gelectron] Menu.popup called');
|
||||
}
|
||||
|
||||
closePopup() {
|
||||
try {
|
||||
const { bridge, isNative } = require('./native-bridge');
|
||||
if (isNative && bridge) {
|
||||
bridge._send({ type: 'close-popup-menu' });
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
console.log('[gelectron] Menu.closePopup called');
|
||||
}
|
||||
|
||||
getMenuItemById(id) {
|
||||
return this._findMenuItemById(this.items, id);
|
||||
}
|
||||
|
||||
_findMenuItemById(items, id) {
|
||||
for (const item of items) {
|
||||
if (item.id === id) return item;
|
||||
if (item.submenu) {
|
||||
const submenu = item.submenu instanceof Menu ? item.submenu.items : (item.submenu.items || item.submenu);
|
||||
const found = this._findMenuItemById(submenu, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_serialize() {
|
||||
return {
|
||||
items: this.items.map((item) => {
|
||||
const obj = {
|
||||
id: item.id,
|
||||
label: item.label,
|
||||
type: item.type,
|
||||
role: item.role,
|
||||
accelerator: item.accelerator,
|
||||
enabled: item.enabled,
|
||||
visible: item.visible,
|
||||
checked: item.checked,
|
||||
toolTip: item.toolTip,
|
||||
};
|
||||
if (item.submenu instanceof Menu) {
|
||||
obj.submenu = item.submenu._serialize();
|
||||
} else if (item.submenu && Array.isArray(item.submenu)) {
|
||||
obj.submenu = Menu.buildFromTemplate(item.submenu)._serialize();
|
||||
}
|
||||
return obj;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
items() {
|
||||
return this.items;
|
||||
}
|
||||
|
||||
getApplicationMenu() {
|
||||
return Menu._applicationMenu;
|
||||
}
|
||||
|
||||
setApplicationMenu(menu) {
|
||||
Menu._applicationMenu = menu;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Menu, MenuItem };
|
||||
-142
@@ -1,142 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Gelectron Native Bridge - IPC between Node.js and the Rust binary.
|
||||
* When GELECTRON_NATIVE=1, communicates via stdin/stdout JSON lines.
|
||||
*/
|
||||
|
||||
const readline = require('readline');
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
const isNative = process.env.GELECTRON_NATIVE === '1';
|
||||
|
||||
class NativeBridge extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this._ready = false;
|
||||
this._readyCallbacks = [];
|
||||
this._windowListeners = new Map();
|
||||
|
||||
if (isNative) {
|
||||
this._setupStdio();
|
||||
}
|
||||
}
|
||||
|
||||
_setupStdio() {
|
||||
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
||||
|
||||
rl.on('line', (line) => {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || !trimmed.startsWith('{')) return;
|
||||
try {
|
||||
const msg = JSON.parse(trimmed);
|
||||
this._handleMessage(msg);
|
||||
} catch (e) {
|
||||
// Not IPC, ignore
|
||||
}
|
||||
});
|
||||
|
||||
process.stdout.on('error', () => {});
|
||||
}
|
||||
|
||||
_handleMessage(msg) {
|
||||
switch (msg.type) {
|
||||
case 'ready':
|
||||
this._ready = true;
|
||||
for (const cb of this._readyCallbacks) cb();
|
||||
this._readyCallbacks = [];
|
||||
this.emit('ready');
|
||||
break;
|
||||
case 'window-closed':
|
||||
this.emit('window-closed', msg.id);
|
||||
break;
|
||||
case 'window-focus':
|
||||
this.emit('window-focus', msg.id);
|
||||
break;
|
||||
case 'ipc-message':
|
||||
this.emit('ipc-message', msg.id, msg.channel, msg.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_send(msg) {
|
||||
if (!isNative) return;
|
||||
try {
|
||||
process.stdout.write(JSON.stringify(msg) + '\n');
|
||||
} catch (e) {
|
||||
// stdout may be closed
|
||||
}
|
||||
}
|
||||
|
||||
onReady(cb) {
|
||||
if (this._ready) {
|
||||
cb();
|
||||
} else {
|
||||
this._readyCallbacks.push(cb);
|
||||
}
|
||||
}
|
||||
|
||||
createWindow(id, options) {
|
||||
this._send({ type: 'create-window', id, options });
|
||||
}
|
||||
|
||||
loadUrl(id, url) {
|
||||
this._send({ type: 'load-url', id, url });
|
||||
}
|
||||
|
||||
loadFile(id, filePath) {
|
||||
this._send({ type: 'load-file', id, path: filePath });
|
||||
}
|
||||
|
||||
destroyWindow(id) {
|
||||
this._send({ type: 'destroy-window', id });
|
||||
}
|
||||
|
||||
setTitle(id, title) {
|
||||
this._send({ type: 'set-title', id, title });
|
||||
}
|
||||
|
||||
setSize(id, width, height) {
|
||||
this._send({ type: 'set-size', id, width, height });
|
||||
}
|
||||
|
||||
showWindow(id) {
|
||||
this._send({ type: 'show', id });
|
||||
}
|
||||
|
||||
hideWindow(id) {
|
||||
this._send({ type: 'hide', id });
|
||||
}
|
||||
|
||||
focusWindow(id) {
|
||||
this._send({ type: 'focus', id });
|
||||
}
|
||||
|
||||
minimizeWindow(id) {
|
||||
this._send({ type: 'minimize', id });
|
||||
}
|
||||
|
||||
maximizeWindow(id) {
|
||||
this._send({ type: 'maximize', id });
|
||||
}
|
||||
|
||||
closeWindow(id) {
|
||||
this._send({ type: 'close', id });
|
||||
}
|
||||
|
||||
sendToRenderer(id, channel, ...data) {
|
||||
this._send({ type: 'ipc-message', id, channel, data: data.length === 1 ? data[0] : data });
|
||||
}
|
||||
|
||||
evalJs(id, script) {
|
||||
this._send({ type: 'eval-js', id, script });
|
||||
}
|
||||
|
||||
quit() {
|
||||
this._send({ type: 'quit' });
|
||||
}
|
||||
}
|
||||
|
||||
const bridge = new NativeBridge();
|
||||
|
||||
module.exports = { bridge, isNative };
|
||||
@@ -1,117 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Gelectron - nativeImage module (Electron compatible)
|
||||
*/
|
||||
|
||||
class NativeImage {
|
||||
constructor() {
|
||||
this._isEmpty = true;
|
||||
this._width = 0;
|
||||
this._height = 0;
|
||||
this._data = null;
|
||||
}
|
||||
|
||||
static createFromPath(path) {
|
||||
const img = new NativeImage();
|
||||
try {
|
||||
const fs = require('fs');
|
||||
const data = fs.readFileSync(path);
|
||||
// Basic PNG header parsing for dimensions
|
||||
if (data.length > 24 && data[0] === 0x89 && data[1] === 0x50) {
|
||||
img._width = data.readUInt32BE(16);
|
||||
img._height = data.readUInt32BE(20);
|
||||
img._data = data;
|
||||
img._isEmpty = false;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[gelectron] Failed to load image: ${path}`, err.message);
|
||||
}
|
||||
return img;
|
||||
}
|
||||
|
||||
static createFromBuffer(buffer, options = {}) {
|
||||
const img = new NativeImage();
|
||||
if (buffer && buffer.length > 0) {
|
||||
img._data = buffer;
|
||||
img._isEmpty = false;
|
||||
img._width = options.width || 0;
|
||||
img._height = options.height || 0;
|
||||
|
||||
// Try to detect PNG dimensions
|
||||
if (buffer.length > 24 && buffer[0] === 0x89 && buffer[1] === 0x50) {
|
||||
img._width = buffer.readUInt32BE(16);
|
||||
img._height = buffer.readUInt32BE(20);
|
||||
}
|
||||
}
|
||||
return img;
|
||||
}
|
||||
|
||||
static createFromDataURL(dataURL) {
|
||||
const matches = dataURL.match(/^data:[^;]+;base64,(.+)$/);
|
||||
if (matches) {
|
||||
return NativeImage.createFromBuffer(Buffer.from(matches[1], 'base64'));
|
||||
}
|
||||
return NativeImage.createEmpty();
|
||||
}
|
||||
|
||||
static createEmpty() {
|
||||
return new NativeImage();
|
||||
}
|
||||
|
||||
toPNG(options = {}) {
|
||||
return this._data || Buffer.alloc(0);
|
||||
}
|
||||
|
||||
toJPEG(quality) {
|
||||
return this._data || Buffer.alloc(0);
|
||||
}
|
||||
|
||||
toBitmap(options = {}) {
|
||||
return this._data || Buffer.alloc(0);
|
||||
}
|
||||
|
||||
toDataURL() {
|
||||
if (!this._data) return 'data:,';
|
||||
const base64 = this._data.toString('base64');
|
||||
return `data:image/png;base64,${base64}`;
|
||||
}
|
||||
|
||||
resize(options) {
|
||||
const resized = new NativeImage();
|
||||
resized._width = options.width || this._width;
|
||||
resized._height = options.height || this._height;
|
||||
resized._data = this._data;
|
||||
resized._isEmpty = this._isEmpty;
|
||||
return resized;
|
||||
}
|
||||
|
||||
crop(rect) {
|
||||
return this.resize({ width: rect.width, height: rect.height });
|
||||
}
|
||||
|
||||
getBitmap(options = {}) {
|
||||
return this._data || Buffer.alloc(0);
|
||||
}
|
||||
|
||||
getSize() {
|
||||
return { width: this._width, height: this._height };
|
||||
}
|
||||
|
||||
isEmpty() {
|
||||
return this._isEmpty;
|
||||
}
|
||||
|
||||
setTemplateImage(template) {
|
||||
this._isTemplate = template;
|
||||
}
|
||||
|
||||
isTemplateImage() {
|
||||
return this._isTemplate || false;
|
||||
}
|
||||
|
||||
toPNG(options) { return this.toPNG(options); }
|
||||
toJPEG(quality) { return this.toJPEG(quality); }
|
||||
}
|
||||
|
||||
module.exports = NativeImage;
|
||||
@@ -1,59 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Gelectron - Notification module (Electron compatible)
|
||||
*/
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
class Notification extends EventEmitter {
|
||||
static _notifications = new Map();
|
||||
static _nextId = 1;
|
||||
|
||||
constructor(options = {}) {
|
||||
super();
|
||||
this.id = Notification._nextId++;
|
||||
this.title = options.title || '';
|
||||
this.body = options.body || '';
|
||||
this.subtitle = options.subtitle || '';
|
||||
this.silent = options.silent || false;
|
||||
this.icon = options.icon || null;
|
||||
this.urgency = options.urgency || 'normal';
|
||||
this.timeoutType = options.timeoutType || 'default';
|
||||
this.closeButtonText = options.closeButtonText || '';
|
||||
this.toastXml = options.toastXml || '';
|
||||
this.actions = options.actions || [];
|
||||
this.replyPlaceholder = options.replyPlaceholder || '';
|
||||
|
||||
Notification._notifications.set(this.id, this);
|
||||
}
|
||||
|
||||
static isSupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
show() {
|
||||
if (typeof Notification !== 'undefined' && Notification.permission === 'granted') {
|
||||
new globalThis.Notification(this.title, {
|
||||
body: this.body,
|
||||
icon: this.icon,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[gelectron] Notification shown: ${this.title}`);
|
||||
this.emit('show');
|
||||
return true;
|
||||
}
|
||||
|
||||
close() {
|
||||
Notification._notifications.delete(this.id);
|
||||
console.log(`[gelectron] Notification ${this.id} closed`);
|
||||
this.emit('close');
|
||||
}
|
||||
|
||||
static fromNotification(notification) {
|
||||
return notification;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Notification };
|
||||
-156
@@ -1,156 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Gelectron - Preload script injection
|
||||
*
|
||||
* This script is injected into the renderer process before the page loads.
|
||||
* It provides the ipcRenderer and contextBridge APIs to preload scripts.
|
||||
*/
|
||||
|
||||
function getPreloadScript() {
|
||||
return `
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Expose gelectron preload APIs
|
||||
window.__gelectron = window.__gelectron || {};
|
||||
|
||||
// IPC Renderer stub
|
||||
window.__gelectron.ipcRenderer = {
|
||||
_requestId: 0,
|
||||
_pending: {},
|
||||
|
||||
invoke: function(channel) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
var requestId = 'ipc-' + (++this._requestId) + '-' + Date.now();
|
||||
var message = {
|
||||
type: 'invoke',
|
||||
requestId: requestId,
|
||||
channel: channel,
|
||||
args: args
|
||||
};
|
||||
|
||||
if (typeof window.__gelectron_send === 'function') {
|
||||
window.__gelectron_send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
window.__gelectron.ipcRenderer._pending[requestId] = { resolve: resolve, reject: reject };
|
||||
setTimeout(function() {
|
||||
if (window.__gelectron.ipcRenderer._pending[requestId]) {
|
||||
delete window.__gelectron.ipcRenderer._pending[requestId];
|
||||
reject(new Error('IPC invoke timed out for channel: ' + channel));
|
||||
}
|
||||
}, 30000);
|
||||
});
|
||||
},
|
||||
|
||||
send: function(channel) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
var message = {
|
||||
type: 'send',
|
||||
channel: channel,
|
||||
args: args
|
||||
};
|
||||
if (typeof window.__gelectron_send === 'function') {
|
||||
window.__gelectron_send(JSON.stringify(message));
|
||||
}
|
||||
},
|
||||
|
||||
sendSync: function() { return undefined; },
|
||||
postMessage: function(channel, message) { this.send(channel, message); },
|
||||
|
||||
on: function(channel, listener) {
|
||||
var wrappedListener = function(event) {
|
||||
listener(event, Array.prototype.slice.call(arguments, 1));
|
||||
};
|
||||
window.addEventListener('message', function(event) {
|
||||
if (event.data && event.data.__gelectron_channel === channel) {
|
||||
wrappedListener(event, event.data.args);
|
||||
}
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
once: function(channel, listener) {
|
||||
var self = this;
|
||||
var wrappedListener = function(event) {
|
||||
listener(event, Array.prototype.slice.call(arguments, 1));
|
||||
self.removeListener(channel, listener);
|
||||
};
|
||||
return this.on(channel, wrappedListener);
|
||||
},
|
||||
|
||||
removeListener: function(channel, listener) {
|
||||
return this;
|
||||
},
|
||||
|
||||
removeAllListeners: function(channel) {
|
||||
return this;
|
||||
},
|
||||
|
||||
_resolveInvoke: function(requestId, result) {
|
||||
var pending = this._pending[requestId];
|
||||
if (pending) {
|
||||
delete this._pending[requestId];
|
||||
pending.resolve(result);
|
||||
}
|
||||
},
|
||||
|
||||
_rejectInvoke: function(requestId, error) {
|
||||
var pending = this._pending[requestId];
|
||||
if (pending) {
|
||||
delete this._pending[requestId];
|
||||
pending.reject(new Error(error));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Context Bridge API
|
||||
window.__gelectron.contextBridge = {
|
||||
exposeInMainWorld: function(key, api) {
|
||||
if (typeof window[key] !== 'undefined') {
|
||||
console.warn('[gelectron] contextBridge: window.' + key + ' already exists, skipping');
|
||||
return;
|
||||
}
|
||||
Object.defineProperty(window, key, {
|
||||
value: Object.freeze(JSON.parse(JSON.stringify(api))),
|
||||
writable: false,
|
||||
configurable: false,
|
||||
enumerable: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Receive messages from main process
|
||||
window.addEventListener('message', function(event) {
|
||||
if (event.data && event.data.__gelectron_channel) {
|
||||
var channel = event.data.__gelectron_channel;
|
||||
var args = event.data.args;
|
||||
|
||||
// Resolve pending invoke
|
||||
if (channel === '__gelectron_ipc_resolve') {
|
||||
var requestId = args[0];
|
||||
var result = args[1];
|
||||
window.__gelectron.ipcRenderer._resolveInvoke(requestId, result);
|
||||
} else if (channel === '__gelectron_ipc_reject') {
|
||||
var requestId = args[0];
|
||||
var error = args[1];
|
||||
window.__gelectron.ipcRenderer._rejectInvoke(requestId, error);
|
||||
} else {
|
||||
// Regular IPC message from main process
|
||||
window.__gelectron.ipcRenderer._receiveMessage(channel, args);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Expose as global for preload scripts
|
||||
window.electron = window.electron || {};
|
||||
window.electron.ipcRenderer = window.__gelectron.ipcRenderer;
|
||||
window.electron.contextBridge = window.__gelectron.contextBridge;
|
||||
|
||||
})();
|
||||
`;
|
||||
}
|
||||
|
||||
module.exports = { getPreloadScript };
|
||||
@@ -1,74 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Gelectron Runtime - Node.js fallback when native binary is not available.
|
||||
* Loads and runs an Electron app's main process using the JS compatibility layer.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const Module = require('module');
|
||||
|
||||
function run(mainScript, env) {
|
||||
// Set up the environment
|
||||
Object.assign(process.env, env);
|
||||
|
||||
// Patch require so that 'electron' resolves to gelectron's compat layer
|
||||
const electronCompatPath = path.join(__dirname, 'index.js');
|
||||
const ipcRendererPath = path.join(__dirname, 'ipc-renderer.js');
|
||||
|
||||
// Purge any cached 'electron' module from the real npm package so that
|
||||
// our _resolveFilename patch takes absolute priority. The real 'electron'
|
||||
// npm package (devDependency in the target app) exports a *path string*
|
||||
// to the Electron binary – it does NOT have .autoUpdater, .app, etc.
|
||||
for (const key of Object.keys(Module._cache)) {
|
||||
const normalized = key.replace(/\\/g, '/');
|
||||
if (
|
||||
normalized.endsWith('/electron') ||
|
||||
normalized.endsWith('/electron/index.js') ||
|
||||
normalized.endsWith('/electron/index.cjs')
|
||||
) {
|
||||
delete Module._cache[key];
|
||||
}
|
||||
}
|
||||
|
||||
const originalResolveFilename = Module._resolveFilename;
|
||||
Module._resolveFilename = function (request, parent, isMain, options) {
|
||||
if (request === 'electron' || request === 'electron/main' || request === 'electron/common') {
|
||||
return electronCompatPath;
|
||||
}
|
||||
if (request === 'electron/renderer') {
|
||||
return ipcRendererPath;
|
||||
}
|
||||
return originalResolveFilename.call(this, request, parent, isMain, options);
|
||||
};
|
||||
|
||||
// Also monkey-patch Module._resolveRequest for Node ≥ 22 where it may
|
||||
// be used internally instead of _resolveFilename.
|
||||
if (typeof Module._resolveRequest === 'function') {
|
||||
const originalResolveRequest = Module._resolveRequest;
|
||||
Module._resolveRequest = function (request, parent, isMain, options) {
|
||||
if (request === 'electron' || request === 'electron/main' || request === 'electron/common') {
|
||||
return electronCompatPath;
|
||||
}
|
||||
if (request === 'electron/renderer') {
|
||||
return ipcRendererPath;
|
||||
}
|
||||
return originalResolveRequest.call(this, request, parent, isMain, options);
|
||||
};
|
||||
}
|
||||
|
||||
// Pre-load the gelectron shim into the module cache so that every
|
||||
// subsequent require('electron') hits cache immediately.
|
||||
require(electronCompatPath);
|
||||
|
||||
// Require the app's main script
|
||||
try {
|
||||
require(mainScript);
|
||||
} catch (err) {
|
||||
console.error(`[gelectron] Error loading main script: ${mainScript}`);
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
@@ -1,30 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Gelectron - safeStorage module (Electron compatible)
|
||||
*/
|
||||
|
||||
const safeStorage = {
|
||||
isAvailable() {
|
||||
return true;
|
||||
},
|
||||
|
||||
async encryptString(plaintext) {
|
||||
// Basic obfuscation; in production, this routes through the native keyring
|
||||
return Buffer.from(plaintext).toString('base64');
|
||||
},
|
||||
|
||||
async decryptString(encrypted) {
|
||||
return Buffer.from(encrypted, 'base64').toString('utf-8');
|
||||
},
|
||||
|
||||
async encryptBuffer(buffer) {
|
||||
return Buffer.from(buffer).toString('base64');
|
||||
},
|
||||
|
||||
async decryptBuffer(encrypted) {
|
||||
return Buffer.from(encrypted, 'base64');
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = safeStorage;
|
||||
@@ -1,100 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Gelectron - shell module (Electron compatible)
|
||||
*/
|
||||
|
||||
const { exec } = require('child_process');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const shell = {
|
||||
openExternal(url, options = {}) {
|
||||
const platform = os.platform();
|
||||
let cmd;
|
||||
|
||||
if (platform === 'darwin') {
|
||||
cmd = `open "${url}"`;
|
||||
} else if (platform === 'win32') {
|
||||
cmd = `start "" "${url}"`;
|
||||
} else {
|
||||
cmd = `xdg-open "${url}"`;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(cmd, (error) => {
|
||||
if (error) reject(error);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
openPath(path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const platform = os.platform();
|
||||
let cmd;
|
||||
|
||||
if (platform === 'darwin') {
|
||||
cmd = `open "${path}"`;
|
||||
} else if (platform === 'win32') {
|
||||
cmd = `start "" "${path}"`;
|
||||
} else {
|
||||
cmd = `xdg-open "${path}"`;
|
||||
}
|
||||
|
||||
exec(cmd, (error) => {
|
||||
if (error) reject(error.message);
|
||||
else resolve('');
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
showItemInFolder(fullPath) {
|
||||
const platform = os.platform();
|
||||
let cmd;
|
||||
|
||||
if (platform === 'darwin') {
|
||||
cmd = `open -R "${fullPath}"`;
|
||||
} else if (platform === 'win32') {
|
||||
cmd = `explorer /select,"${fullPath}"`;
|
||||
} else {
|
||||
const dir = path.dirname(fullPath);
|
||||
cmd = `xdg-open "${dir}"`;
|
||||
}
|
||||
|
||||
exec(cmd);
|
||||
},
|
||||
|
||||
moveItemToTrash(fullPath) {
|
||||
// Use fs.rm or platform-specific trash command
|
||||
const fs = require('fs');
|
||||
try {
|
||||
fs.rmSync(fullPath, { recursive: true, force: true });
|
||||
return Promise.resolve('');
|
||||
} catch (err) {
|
||||
return Promise.reject(err.message);
|
||||
}
|
||||
},
|
||||
|
||||
beep() {
|
||||
process.stdout.write('\x07');
|
||||
},
|
||||
|
||||
writeShortcutLink(shortcutPath, options = {}) {
|
||||
console.log('[gelectron] writeShortcutLink called');
|
||||
return true;
|
||||
},
|
||||
|
||||
readShortcutLink(shortcutPath) {
|
||||
return {
|
||||
target: '',
|
||||
cwd: '',
|
||||
args: '',
|
||||
description: '',
|
||||
icon: '',
|
||||
iconIndex: 0,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = shell;
|
||||
@@ -1,74 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Gelectron - Tray module (Electron compatible)
|
||||
*/
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
class Tray extends EventEmitter {
|
||||
static _trays = new Map();
|
||||
static _nextId = 1;
|
||||
|
||||
constructor(image) {
|
||||
super();
|
||||
this.id = Tray._nextId++;
|
||||
this._image = image;
|
||||
this._tooltip = '';
|
||||
this._menu = null;
|
||||
this._isDestroyed = false;
|
||||
|
||||
Tray._trays.set(this.id, this);
|
||||
console.log(`[gelectron] Tray ${this.id} created`);
|
||||
}
|
||||
|
||||
setToolTip(tooltip) {
|
||||
this._tooltip = tooltip;
|
||||
}
|
||||
|
||||
getToolTip() {
|
||||
return this._tooltip;
|
||||
}
|
||||
|
||||
setImage(image) {
|
||||
this._image = image;
|
||||
}
|
||||
|
||||
setPressedImage(image) {}
|
||||
|
||||
setContextMenu(menu) {
|
||||
this._menu = menu;
|
||||
}
|
||||
|
||||
getContextMenu() {
|
||||
return this._menu;
|
||||
}
|
||||
|
||||
popupContextMenu(options) {
|
||||
console.log('[gelectron] Tray.popupContextMenu');
|
||||
}
|
||||
|
||||
isDestroyed() {
|
||||
return this._isDestroyed;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._isDestroyed = true;
|
||||
Tray._trays.delete(this.id);
|
||||
this.emit('destroy');
|
||||
}
|
||||
|
||||
getBounds() {
|
||||
return { x: 0, y: 0, width: 22, height: 22 };
|
||||
}
|
||||
|
||||
static fromId(id) {
|
||||
return Tray._trays.get(id) || null;
|
||||
}
|
||||
|
||||
static getAllTrays() {
|
||||
return Array.from(Tray._trays.values());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Tray };
|
||||
@@ -1,25 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Gelectron - webContents utilities (Electron compatible)
|
||||
*/
|
||||
|
||||
const { BrowserWindow } = require('./browser-window');
|
||||
|
||||
const webContents = {
|
||||
getAllWebContents() {
|
||||
return BrowserWindow.getAllWindows().map((win) => win.webContents);
|
||||
},
|
||||
|
||||
getFocusedWebContents() {
|
||||
const focused = BrowserWindow.getFocusedWindow();
|
||||
return focused ? focused.webContents : null;
|
||||
},
|
||||
|
||||
fromId(id) {
|
||||
const win = BrowserWindow.fromId(id);
|
||||
return win ? win.webContents : null;
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = webContents;
|
||||
-1200
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@@ -1,706 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Gelectron Demo</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
background: #0f0f1a;
|
||||
color: #e0e0e0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
header {
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
border-bottom: 1px solid #2a2a4a;
|
||||
padding: 24px 32px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(90deg, #7b68ee, #00d4ff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
header p {
|
||||
margin-top: 6px;
|
||||
color: #8888aa;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
padding: 32px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #1a1a2e;
|
||||
border: 1px solid #2a2a4a;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #7b68ee;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card p, .card li {
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: #b0b0cc;
|
||||
}
|
||||
|
||||
.card ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card ul li::before {
|
||||
content: '\2713';
|
||||
color: #00d4ff;
|
||||
margin-right: 8px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.counter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.counter button {
|
||||
background: #2a2a4a;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #3a3a6a;
|
||||
border-radius: 8px;
|
||||
padding: 10px 20px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, transform 0.1s;
|
||||
}
|
||||
|
||||
.counter button:hover { background: #3a3a6a; }
|
||||
.counter button:active { transform: scale(0.95); }
|
||||
|
||||
.counter .value {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: #00d4ff;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.canvas-wrap {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
canvas {
|
||||
background: #12121f;
|
||||
border: 1px solid #2a2a4a;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.color-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.color-bar span {
|
||||
flex: 1;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
#clock {
|
||||
font-size: 32px;
|
||||
font-weight: 300;
|
||||
color: #00d4ff;
|
||||
font-variant-numeric: tabular-nums;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.env-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.env-table td {
|
||||
padding: 6px 0;
|
||||
font-size: 13px;
|
||||
border-bottom: 1px solid #1f1f35;
|
||||
}
|
||||
|
||||
.env-table td:first-child {
|
||||
color: #7b68ee;
|
||||
font-weight: 600;
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.env-table td:last-child {
|
||||
color: #b0b0cc;
|
||||
font-family: 'SF Mono', Menlo, monospace;
|
||||
}
|
||||
|
||||
.test-card { grid-column: 1 / -1; }
|
||||
.test-results { margin-top: 12px; }
|
||||
.test-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid #1f1f35;
|
||||
font-size: 13px;
|
||||
}
|
||||
.test-row:last-child { border-bottom: none; }
|
||||
.test-pass { color: #6bcb77; }
|
||||
.test-fail { color: #ff6b6b; }
|
||||
.test-name { flex: 1; color: #b0b0cc; }
|
||||
.test-value { color: #8888aa; font-family: 'SF Mono', Menlo, monospace; margin-left: 12px; }
|
||||
|
||||
.btn-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.btn-grid button {
|
||||
background: #2a2a4a;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #3a3a6a;
|
||||
border-radius: 8px;
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, transform 0.1s;
|
||||
}
|
||||
.btn-grid button:hover { background: #3a3a6a; }
|
||||
.btn-grid button:active { transform: scale(0.95); }
|
||||
.btn-grid button.active { background: #7b68ee; border-color: #7b68ee; }
|
||||
|
||||
.demo-output {
|
||||
margin-top: 10px;
|
||||
background: #12121f;
|
||||
border: 1px solid #2a2a4a;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
font-family: 'SF Mono', Menlo, monospace;
|
||||
font-size: 12px;
|
||||
color: #b0b0cc;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 16px 32px;
|
||||
border-top: 1px solid #2a2a4a;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #555577;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Gelectron Demo</h1>
|
||||
<p>HTML + CSS + JavaScript rendering via Gecko / Servo engine</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="card">
|
||||
<h2>Interactive Counter</h2>
|
||||
<p>Proof that JavaScript runs and DOM updates work.</p>
|
||||
<div class="counter">
|
||||
<button onclick="dec()">\u2212</button>
|
||||
<div class="value" id="count">0</div>
|
||||
<button onclick="inc()">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Live Clock</h2>
|
||||
<p>requestAnimationFrame-driven rendering.</p>
|
||||
<div id="clock">--:--:--</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Canvas 2D</h2>
|
||||
<p>Animated canvas with moving shapes.</p>
|
||||
<div class="canvas-wrap">
|
||||
<canvas id="scene" width="400" height="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Runtime Info</h2>
|
||||
<p>Environment details from the gelectron shim.</p>
|
||||
<table class="env-table">
|
||||
<tr><td>Platform</td><td id="env-platform"></td></tr>
|
||||
<tr><td>Arch</td><td id="env-arch"></td></tr>
|
||||
<tr><td>Node.js</td><td id="env-node"></td></tr>
|
||||
<tr><td>User Agent</td><td id="env-ua"></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card" style="grid-column: 1 / -1;">
|
||||
<h2>CSS Features</h2>
|
||||
<p>Grid, gradients, border-radius, transitions, flexbox, backdrop styling.</p>
|
||||
<div class="color-bar">
|
||||
<span style="background: #7b68ee;"></span>
|
||||
<span style="background: #00d4ff;"></span>
|
||||
<span style="background: #ff6b9d;"></span>
|
||||
<span style="background: #ffd93d;"></span>
|
||||
<span style="background: #6bcb77;"></span>
|
||||
<span style="background: #ee5a24;"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="grid-column: 1 / -1;">
|
||||
<h2>App Module Demo</h2>
|
||||
<p>Interactive demo of the Electron app API — properties, methods, and lifecycle.</p>
|
||||
<div class="btn-grid">
|
||||
<button onclick="demoApp('version')">getVersion()</button>
|
||||
<button onclick="demoApp('locale')">getLocale()</button>
|
||||
<button onclick="demoApp('userAgent')">getUserAgent()</button>
|
||||
<button onclick="demoApp('setUserAgent')">setUserAgent()</button>
|
||||
<button onclick="demoApp('appPath')">getAppPath()</button>
|
||||
<button onclick="demoApp('isReady')">isReady</button>
|
||||
<button onclick="demoApp('badge')">setBadgeCount(7)</button>
|
||||
<button onclick="demoApp('metrics')">getAppMetrics()</button>
|
||||
<button onclick="demoApp('gpu')">getGPUInfo()</button>
|
||||
<button onclick="demoApp('gpuFeatures')">getGPUFeatureStatus()</button>
|
||||
<button onclick="demoApp('loginItem')">getLoginItemSettings()</button>
|
||||
<button onclick="demoApp('aboutPanel')">getAboutPanelOptions()</button>
|
||||
<button onclick="demoApp('secureKeyboard')">setSecureKeyboardEntry()</button>
|
||||
<button onclick="demoApp('name')">app.name =</button>
|
||||
<button onclick="demoApp('paths')">getPath(home)</button>
|
||||
<button onclick="demoApp('dock')">dock.bounce()</button>
|
||||
</div>
|
||||
<div class="demo-output" id="app-demo-output"></div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="grid-column: 1 / -1;">
|
||||
<h2>Menu Module Demo</h2>
|
||||
<p>Interactive demo of the Electron Menu API — build, set, and inspect native menus.</p>
|
||||
<div class="btn-grid">
|
||||
<button onclick="demoMenu('build')">Menu.buildFromTemplate()</button>
|
||||
<button onclick="demoMenu('set')">Menu.setApplicationMenu()</button>
|
||||
<button onclick="demoMenu('get')">Menu.getApplicationMenu()</button>
|
||||
<button onclick="demoMenu('findById')">getMenuItemById()</button>
|
||||
<button onclick="demoMenu('append')">menu.append()</button>
|
||||
<button onclick="demoMenu('serialize')">menu._serialize()</button>
|
||||
<button onclick="demoMenu('count')">menu.items.length</button>
|
||||
<button onclick="demoMenu('submenu')">Submenu nesting</button>
|
||||
</div>
|
||||
<div class="demo-output" id="menu-demo-output"></div>
|
||||
</div>
|
||||
|
||||
<div class="card test-card">
|
||||
<h2>App Module Tests</h2>
|
||||
<p>Automated tests for the Electron app compatibility layer.</p>
|
||||
<div id="app-tests" class="test-results">
|
||||
<p style="color: #8888aa; font-size: 13px;">Waiting for test results...</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>Gelectron v0.1.0 — Electron API compatibility layer powered by Gecko/Servo</footer>
|
||||
|
||||
<script>
|
||||
// Counter
|
||||
var count = 0;
|
||||
var countEl = document.getElementById('count');
|
||||
function inc() { countEl.textContent = ++count; }
|
||||
function dec() { countEl.textContent = --count; }
|
||||
|
||||
// Live clock
|
||||
function tick() {
|
||||
var now = new Date();
|
||||
var h = String(now.getHours()).padStart(2, '0');
|
||||
var m = String(now.getMinutes()).padStart(2, '0');
|
||||
var s = String(now.getSeconds()).padStart(2, '0');
|
||||
document.getElementById('clock').textContent = h + ':' + m + ':' + s;
|
||||
requestAnimationFrame(tick);
|
||||
}
|
||||
tick();
|
||||
|
||||
// Canvas animation
|
||||
var canvas = document.getElementById('scene');
|
||||
var ctx = canvas.getContext('2d');
|
||||
var t = 0;
|
||||
function drawFrame() {
|
||||
t += 0.02;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// bouncing ball
|
||||
var bx = 50 + Math.sin(t) * 150;
|
||||
var by = 100 + Math.cos(t * 1.3) * 60;
|
||||
ctx.beginPath();
|
||||
ctx.arc(bx, by, 18, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#7b68ee';
|
||||
ctx.fill();
|
||||
|
||||
// second ball
|
||||
var bx2 = 200 + Math.cos(t * 0.7) * 120;
|
||||
var by2 = 80 + Math.sin(t * 1.1) * 70;
|
||||
ctx.beginPath();
|
||||
ctx.arc(bx2, by2, 14, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#00d4ff';
|
||||
ctx.fill();
|
||||
|
||||
// trailing line
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, 100 + Math.sin(t) * 40);
|
||||
for (var x = 0; x < canvas.width; x += 4) {
|
||||
ctx.lineTo(x, 100 + Math.sin(t + x * 0.03) * 40);
|
||||
}
|
||||
ctx.strokeStyle = 'rgba(123,104,238,0.3)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
requestAnimationFrame(drawFrame);
|
||||
}
|
||||
drawFrame();
|
||||
|
||||
// Runtime info
|
||||
document.getElementById('env-platform').textContent = navigator.platform || 'unknown';
|
||||
document.getElementById('env-arch').textContent = 'unknown (browser sandbox)';
|
||||
document.getElementById('env-node').textContent = 'N/A in renderer';
|
||||
document.getElementById('env-ua').textContent = navigator.userAgent;
|
||||
|
||||
// App module interactive demo
|
||||
function demoApp(action) {
|
||||
var out = document.getElementById('app-demo-output');
|
||||
out.style.display = 'block';
|
||||
var electron = window.electron;
|
||||
if (!electron || !electron.app) {
|
||||
out.textContent = 'Error: window.electron.app not available (renderer-only mode)';
|
||||
return;
|
||||
}
|
||||
var app = electron.app;
|
||||
var result = '';
|
||||
switch (action) {
|
||||
case 'version':
|
||||
result = 'app.getVersion() → ' + app.getVersion() +
|
||||
'\napp.version (property) → ' + app.version;
|
||||
break;
|
||||
case 'locale':
|
||||
result = 'app.getLocale() → ' + app.getLocale() +
|
||||
'\napp.locale (property) → ' + app.locale;
|
||||
break;
|
||||
case 'userAgent':
|
||||
result = 'app.getUserAgent() → ' + app.getUserAgent() +
|
||||
'\napp.userAgent (property) → ' + app.userAgent;
|
||||
break;
|
||||
case 'setUserAgent':
|
||||
app.setUserAgent('GelectronDemo/2.0');
|
||||
result = 'app.setUserAgent("GelectronDemo/2.0")' +
|
||||
'\napp.getUserAgent() → ' + app.getUserAgent();
|
||||
app.setUserAgent(null);
|
||||
result += '\napp.setUserAgent(null) reset → ' + app.getUserAgent();
|
||||
break;
|
||||
case 'appPath':
|
||||
result = 'app.getAppPath() → ' + app.getAppPath() +
|
||||
'\napp.appPath (property) → ' + app.appPath;
|
||||
break;
|
||||
case 'isReady':
|
||||
result = 'app.isReady (property) → ' + app.isReady +
|
||||
'\napp.isReady() method → ' + app.isReady() +
|
||||
'\napp.whenReady() → [Promise resolved]';
|
||||
app.whenReady().then(function() {
|
||||
out.textContent += '\n→ whenReady promise resolved!';
|
||||
});
|
||||
break;
|
||||
case 'badge':
|
||||
app.setBadgeCount(7);
|
||||
result = 'app.setBadgeCount(7)' +
|
||||
'\napp.getBadgeCount() → ' + app.getBadgeCount();
|
||||
app.setBadgeCount(0);
|
||||
result += '\napp.setBadgeCount(0) → ' + app.getBadgeCount();
|
||||
break;
|
||||
case 'metrics':
|
||||
var m = app.getAppMetrics();
|
||||
result = 'app.getAppMetrics() → [' + m.length + ' entries]' +
|
||||
'\n pid: ' + m[0].pid +
|
||||
'\n type: ' + m[0].type +
|
||||
'\n memory: ' + JSON.stringify(m[0].memory) +
|
||||
'\n sandboxed: ' + m[0].sandboxed;
|
||||
break;
|
||||
case 'gpu':
|
||||
app.getGPUInfo('basic').then(function(info) {
|
||||
out.textContent = 'app.getGPUInfo("basic") →\n' +
|
||||
' GPUActive: ' + info.GPUActive +
|
||||
'\n gpuDevice: ' + JSON.stringify(info.gpuDevice, null, 2);
|
||||
});
|
||||
result = 'Loading GPU info...';
|
||||
break;
|
||||
case 'gpuFeatures':
|
||||
var f = app.getGPUFeatureStatus();
|
||||
var lines = ['app.getGPUFeatureStatus() →'];
|
||||
for (var k in f) lines.push(' ' + k + ': ' + f[k]);
|
||||
result = lines.join('\n');
|
||||
break;
|
||||
case 'loginItem':
|
||||
var s = app.getLoginItemSettings();
|
||||
result = 'app.getLoginItemSettings() →\n' +
|
||||
' openAtLogin: ' + s.openAtLogin +
|
||||
'\n openAsHidden: ' + s.openAsHidden +
|
||||
'\n launchAtLogin: ' + s.launchAtLogin +
|
||||
'\n launchItems: ' + JSON.stringify(s.launchItems);
|
||||
break;
|
||||
case 'aboutPanel':
|
||||
app.setAboutPanelOptions({ applicationName: 'Gelectron Demo', copyright: 'MIT' });
|
||||
var o = app.getAboutPanelOptions();
|
||||
result = 'app.setAboutPanelOptions({ applicationName: "Gelectron Demo", copyright: "MIT" })' +
|
||||
'\napp.getAboutPanelOptions() →\n' + JSON.stringify(o, null, 2);
|
||||
break;
|
||||
case 'secureKeyboard':
|
||||
app.setSecureKeyboardEntryEnabled(true);
|
||||
result = 'app.setSecureKeyboardEntryEnabled(true)' +
|
||||
'\napp.isSecureKeyboardEntryEnabled() → ' + app.isSecureKeyboardEntryEnabled();
|
||||
app.setSecureKeyboardEntryEnabled(false);
|
||||
result += '\napp.setSecureKeyboardEntryEnabled(false)' +
|
||||
'\napp.isSecureKeyboardEntryEnabled() → ' + app.isSecureKeyboardEntryEnabled();
|
||||
break;
|
||||
case 'name':
|
||||
var old = app.name;
|
||||
app.name = 'Gelectron Demo App';
|
||||
result = 'app.name = "Gelectron Demo App"' +
|
||||
'\napp.name → ' + app.name +
|
||||
'\napp.getName() → ' + app.getName();
|
||||
app.name = old;
|
||||
result += '\n(app.name restored to "' + old + '")';
|
||||
break;
|
||||
case 'paths':
|
||||
result = 'app.getPath("home") → ' + app.getPath('home') +
|
||||
'\napp.getPath("userData") → ' + app.getPath('userData') +
|
||||
'\napp.getPath("temp") → ' + app.getPath('temp') +
|
||||
'\napp.getPath("desktop") → ' + app.getPath('desktop') +
|
||||
'\napp.getPath("documents") → ' + app.getPath('documents') +
|
||||
'\napp.getPath("downloads") → ' + app.getPath('downloads') +
|
||||
'\napp.getPath("logs") → ' + app.getPath('logs');
|
||||
break;
|
||||
case 'dock':
|
||||
if (app.dock) {
|
||||
var id = app.dock.bounce();
|
||||
result = 'app.dock.bounce() → ' + id +
|
||||
'\napp.dock.isVisible() → ' + app.dock.isVisible() +
|
||||
'\napp.dock.getBadge() → "' + app.dock.getBadge() + '"';
|
||||
} else {
|
||||
result = 'app.dock → null (not macOS)';
|
||||
}
|
||||
break;
|
||||
}
|
||||
out.textContent = result;
|
||||
}
|
||||
|
||||
// Menu module interactive demo
|
||||
function demoMenu(action) {
|
||||
var out = document.getElementById('menu-demo-output');
|
||||
out.style.display = 'block';
|
||||
var electron = window.electron;
|
||||
if (!electron || !electron.Menu) {
|
||||
out.textContent = 'Error: window.electron.Menu not available';
|
||||
return;
|
||||
}
|
||||
var Menu = electron.Menu;
|
||||
var MenuItem = electron.MenuItem;
|
||||
var result = '';
|
||||
switch (action) {
|
||||
case 'build': {
|
||||
var menu = Menu.buildFromTemplate([
|
||||
{ label: 'File', submenu: [
|
||||
{ label: 'New', accelerator: 'CmdOrCtrl+N' },
|
||||
{ label: 'Open', accelerator: 'CmdOrCtrl+O' },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Quit', accelerator: 'CmdOrCtrl+Q' },
|
||||
]},
|
||||
{ label: 'Edit', submenu: [
|
||||
{ label: 'Cut', accelerator: 'CmdOrCtrl+X' },
|
||||
{ label: 'Copy', accelerator: 'CmdOrCtrl+C' },
|
||||
{ label: 'Paste', accelerator: 'CmdOrCtrl+V' },
|
||||
]},
|
||||
{ label: 'Help', submenu: [
|
||||
{ label: 'About Gelectron' },
|
||||
]},
|
||||
]);
|
||||
result = 'Menu.buildFromTemplate([...]) → Menu {\n' +
|
||||
' items.length: ' + menu.items.length + '\n' +
|
||||
' items[0].label: "' + menu.items[0].label + '"\n' +
|
||||
' items[0].submenu.items.length: ' + menu.items[0].submenu.items.length + '\n' +
|
||||
' items[1].label: "' + menu.items[1].label + '"\n' +
|
||||
' items[2].label: "' + menu.items[2].label + '"\n' +
|
||||
'}';
|
||||
break;
|
||||
}
|
||||
case 'set': {
|
||||
var menu = Menu.buildFromTemplate([
|
||||
{ label: 'File', submenu: [
|
||||
{ label: 'New Window', accelerator: 'CmdOrCtrl+N' },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Quit', accelerator: 'CmdOrCtrl+Q' },
|
||||
]},
|
||||
{ label: 'View', submenu: [
|
||||
{ label: 'Reload', accelerator: 'CmdOrCtrl+R' },
|
||||
{ label: 'Toggle DevTools', accelerator: 'CmdOrCtrl+Shift+I' },
|
||||
]},
|
||||
]);
|
||||
Menu.setApplicationMenu(menu);
|
||||
var current = Menu.getApplicationMenu();
|
||||
result = 'Menu.setApplicationMenu(menu) → sent to native bridge' +
|
||||
'\nMenu.getApplicationMenu() → ' + (current ? 'Menu { items: ' + current.items.length + ' }' : 'null');
|
||||
break;
|
||||
}
|
||||
case 'get': {
|
||||
var current = Menu.getApplicationMenu();
|
||||
if (current) {
|
||||
var labels = current.items.map(function(i) { return i.label; });
|
||||
result = 'Menu.getApplicationMenu() → Menu {\n' +
|
||||
' items: [' + labels.join(', ') + ']\n' +
|
||||
' items.length: ' + current.items.length + '\n' +
|
||||
'}';
|
||||
} else {
|
||||
result = 'Menu.getApplicationMenu() → null\n(No application menu set yet. Click "Menu.setApplicationMenu()" first.)';
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'findById': {
|
||||
var menu = Menu.buildFromTemplate([
|
||||
{ id: 'file-menu', label: 'File', submenu: [
|
||||
{ id: 'new-file', label: 'New File' },
|
||||
{ id: 'open-file', label: 'Open File' },
|
||||
]},
|
||||
{ id: 'edit-menu', label: 'Edit', submenu: [
|
||||
{ id: 'copy-item', label: 'Copy' },
|
||||
]},
|
||||
]);
|
||||
var item = menu.getMenuItemById('new-file');
|
||||
var item2 = menu.getMenuItemById('copy-item');
|
||||
var missing = menu.getMenuItemById('nonexistent');
|
||||
result = 'menu.getMenuItemById("new-file") →\n' +
|
||||
' label: "' + (item ? item.label : 'null') + '"\n' +
|
||||
' id: "' + (item ? item.id : 'null') + '"' +
|
||||
'\nmenu.getMenuItemById("copy-item") →\n' +
|
||||
' label: "' + (item2 ? item2.label : 'null') + '"' +
|
||||
'\nmenu.getMenuItemById("nonexistent") → ' + (missing ? 'found' : 'null');
|
||||
break;
|
||||
}
|
||||
case 'append': {
|
||||
var menu = Menu.buildFromTemplate([
|
||||
{ label: 'Existing Item' },
|
||||
]);
|
||||
result = 'Before append: items.length = ' + menu.items.length;
|
||||
menu.append(new MenuItem({ label: 'Appended Item', type: 'normal' }));
|
||||
result += '\nmenu.append(new MenuItem({ label: "Appended Item" }))' +
|
||||
'\nAfter append: items.length = ' + menu.items.length +
|
||||
'\n items[0].label: "' + menu.items[0].label + '"' +
|
||||
'\n items[1].label: "' + menu.items[1].label + '"';
|
||||
break;
|
||||
}
|
||||
case 'serialize': {
|
||||
var menu = Menu.buildFromTemplate([
|
||||
{ label: 'File', submenu: [
|
||||
{ label: 'New', accelerator: 'CmdOrCtrl+N' },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Quit' },
|
||||
]},
|
||||
{ label: 'Edit', enabled: false },
|
||||
]);
|
||||
var s = menu._serialize();
|
||||
result = 'menu._serialize() →\n' + JSON.stringify(s, null, 2);
|
||||
break;
|
||||
}
|
||||
case 'count': {
|
||||
var menu = Menu.buildFromTemplate([
|
||||
{ label: 'File', submenu: [{ label: 'New' }] },
|
||||
{ label: 'Edit' },
|
||||
{ label: 'View' },
|
||||
{ label: 'Help' },
|
||||
]);
|
||||
result = 'Menu.buildFromTemplate(4 top-level items)' +
|
||||
'\nmenu.items.length → ' + menu.items.length +
|
||||
'\nmenu.items.map(i => i.label) → [' +
|
||||
menu.items.map(function(i) { return '"' + i.label + '"'; }).join(', ') + ']' +
|
||||
'\nmenu.items[0].submenu.items.length → ' + menu.items[0].submenu.items.length;
|
||||
break;
|
||||
}
|
||||
case 'submenu': {
|
||||
var menu = Menu.buildFromTemplate([
|
||||
{ label: 'Format', submenu: [
|
||||
{ label: 'Font', submenu: [
|
||||
{ label: 'Bold', type: 'checkbox' },
|
||||
{ label: 'Italic', type: 'checkbox' },
|
||||
{ label: 'Underline', type: 'checkbox' },
|
||||
]},
|
||||
{ type: 'separator' },
|
||||
{ label: 'Align Left' },
|
||||
{ label: 'Align Center' },
|
||||
{ label: 'Align Right' },
|
||||
]},
|
||||
]);
|
||||
var fmt = menu.items[0];
|
||||
result = 'Menu with nested submenus:' +
|
||||
'\nmenu.items[0].label → "' + fmt.label + '"' +
|
||||
'\nmenu.items[0].submenu.items.length → ' + fmt.submenu.items.length +
|
||||
'\nmenu.items[0].submenu.items[0].label → "' + fmt.submenu.items[0].label + '"' +
|
||||
'\nmenu.items[0].submenu.items[0].submenu.items.length → ' +
|
||||
fmt.submenu.items[0].submenu.items.length +
|
||||
'\n nested items: ' +
|
||||
fmt.submenu.items[0].submenu.items.map(function(i) {
|
||||
return '"' + i.label + '"';
|
||||
}).join(', ');
|
||||
break;
|
||||
}
|
||||
}
|
||||
out.textContent = result;
|
||||
}
|
||||
|
||||
// App test results renderer
|
||||
function renderAppTests() {
|
||||
var results = window.__appTestResults;
|
||||
if (!results) return;
|
||||
var el = document.getElementById('app-tests');
|
||||
var html = '';
|
||||
var pass = 0, fail = 0;
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
var r = results[i];
|
||||
var cls = r.status === 'PASS' ? 'test-pass' : 'test-fail';
|
||||
var icon = r.status === 'PASS' ? '\u2713' : '\u2717';
|
||||
if (r.status === 'PASS') pass++; else fail++;
|
||||
html += '<div class="test-row">' +
|
||||
'<span class="' + cls + '">' + icon + '</span>' +
|
||||
'<span class="test-name">' + r.name + '</span>' +
|
||||
'<span class="test-value">' + r.value + '</span>' +
|
||||
'</div>';
|
||||
}
|
||||
html = '<div style="margin-bottom: 12px; font-size: 14px; font-weight: 600;">' +
|
||||
'<span class="test-pass">' + pass + ' passed</span> · ' +
|
||||
'<span class="test-fail">' + fail + ' failed</span> · ' +
|
||||
(pass + fail) + ' total</div>' + html;
|
||||
el.innerHTML = html;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,488 +0,0 @@
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
const path = require('path');
|
||||
|
||||
const results = [];
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
const val = fn();
|
||||
results.push({ name, status: 'PASS', value: String(val) });
|
||||
} catch (e) {
|
||||
results.push({ name, status: 'FAIL', value: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
function asyncTest(name, fn) {
|
||||
return fn().then((val) => {
|
||||
results.push({ name, status: 'PASS', value: String(val) });
|
||||
}).catch((e) => {
|
||||
results.push({ name, status: 'FAIL', value: e.message });
|
||||
});
|
||||
}
|
||||
|
||||
let mainWindow = null;
|
||||
|
||||
// ─── Properties ────────────────────────────────────────────────
|
||||
|
||||
test('app.requestSingleInstanceLock() returns boolean', () => {
|
||||
const lock = app.requestSingleInstanceLock();
|
||||
if (typeof lock !== 'boolean') throw new Error('Expected boolean, got ' + typeof lock);
|
||||
return lock;
|
||||
});
|
||||
|
||||
test('app.isReady() (method)', () => app.isReady());
|
||||
|
||||
test('app.isReady (property)', () => {
|
||||
if (typeof app.isReady !== 'boolean') throw new Error('Expected boolean, got ' + typeof app.isReady);
|
||||
return app.isReady;
|
||||
});
|
||||
|
||||
test('app.appPath (property)', () => {
|
||||
if (typeof app.appPath !== 'string') throw new Error('Expected string, got ' + typeof app.appPath);
|
||||
return app.appPath;
|
||||
});
|
||||
|
||||
test('app.locale (property)', () => {
|
||||
if (typeof app.locale !== 'string') throw new Error('Expected string, got ' + typeof app.locale);
|
||||
return app.locale;
|
||||
});
|
||||
|
||||
test('app.userAgent (property)', () => {
|
||||
if (typeof app.userAgent !== 'string') throw new Error('Expected string, got ' + typeof app.userAgent);
|
||||
return app.userAgent;
|
||||
});
|
||||
|
||||
test('app.getVersion()', () => app.getVersion());
|
||||
|
||||
test('app.getName()', () => app.getName());
|
||||
|
||||
test('app.getAppPath()', () => app.getAppPath());
|
||||
|
||||
test('app.name get/set', () => {
|
||||
const old = app.name;
|
||||
app.name = 'TestApp';
|
||||
const result = app.name;
|
||||
app.name = old;
|
||||
if (result !== 'TestApp') throw new Error('Expected TestApp, got ' + result);
|
||||
return result;
|
||||
});
|
||||
|
||||
test('app.name set null reverts', () => {
|
||||
app.name = null;
|
||||
return app.name === 'Gelectron App';
|
||||
});
|
||||
|
||||
// ─── Path Methods ──────────────────────────────────────────────
|
||||
|
||||
test('app.getPath("home")', () => app.getPath('home'));
|
||||
|
||||
test('app.getPath("userData")', () => app.getPath('userData'));
|
||||
|
||||
test('app.getPath("temp")', () => app.getPath('temp'));
|
||||
|
||||
test('app.getPath("desktop")', () => app.getPath('desktop'));
|
||||
|
||||
test('app.getPath("documents")', () => app.getPath('documents'));
|
||||
|
||||
test('app.getPath("downloads")', () => app.getPath('downloads'));
|
||||
|
||||
test('app.getPath("logs")', () => app.getPath('logs'));
|
||||
|
||||
test('app.getPath("crashDumps")', () => app.getPath('crashDumps'));
|
||||
|
||||
test('app.getPath("app") returns userData', () => {
|
||||
return app.getPath('app') === app.getPath('userData');
|
||||
});
|
||||
|
||||
test('app.setPath / app.getPath roundtrip', () => {
|
||||
app.setPath('testPath', '/tmp/test-gelectron');
|
||||
return app.getPath('testPath') === '/tmp/test-gelectron';
|
||||
});
|
||||
|
||||
test('app.getPath("unknown") returns userData fallback', () => {
|
||||
return app.getPath('nonexistent') === app.getPath('userData');
|
||||
});
|
||||
|
||||
// ─── User Agent / Locale ───────────────────────────────────────
|
||||
|
||||
test('app.getLocale()', () => app.getLocale());
|
||||
|
||||
test('app.getUserAgent()', () => app.getUserAgent());
|
||||
|
||||
test('app.setUserAgent()', () => {
|
||||
app.setUserAgent('TestAgent/1.0');
|
||||
const result = app.getUserAgent();
|
||||
app.setUserAgent(null);
|
||||
return result;
|
||||
});
|
||||
|
||||
test('app.setUserAgent(null) resets', () => {
|
||||
app.setUserAgent(null);
|
||||
const ua = app.getUserAgent();
|
||||
if (!ua.includes('Gelectron')) throw new Error('Expected Gelectron UA, got ' + ua);
|
||||
return true;
|
||||
});
|
||||
|
||||
// ─── Lifecycle ─────────────────────────────────────────────────
|
||||
|
||||
test('app.isPackaged is boolean', () => {
|
||||
if (typeof app.isPackaged !== 'boolean') throw new Error('Expected boolean');
|
||||
return app.isPackaged;
|
||||
});
|
||||
|
||||
test('app.whenReady() returns Promise', () => {
|
||||
const p = app.whenReady();
|
||||
if (!p || typeof p.then !== 'function') throw new Error('Not a Promise');
|
||||
return true;
|
||||
});
|
||||
|
||||
test('app.requestSingleInstanceLock() sync', () => {
|
||||
const result = app.requestSingleInstanceLock();
|
||||
return typeof result === 'boolean';
|
||||
});
|
||||
|
||||
test('app.acquireSingleInstanceLock() sync', () => {
|
||||
const result = app.acquireSingleInstanceLock();
|
||||
return typeof result === 'boolean';
|
||||
});
|
||||
|
||||
test('app.releaseSingleInstanceLock() no-op', () => {
|
||||
app.releaseSingleInstanceLock();
|
||||
return true;
|
||||
});
|
||||
|
||||
// ─── Events ────────────────────────────────────────────────────
|
||||
|
||||
test('app.on / app.removeListener', () => {
|
||||
let called = false;
|
||||
const fn = () => { called = true; };
|
||||
app.on('test-event', fn);
|
||||
app.emit('test-event');
|
||||
app.removeListener('test-event', fn);
|
||||
return called;
|
||||
});
|
||||
|
||||
test('app.once fires once', () => {
|
||||
let count = 0;
|
||||
app.once('test-once', () => { count++; });
|
||||
app.emit('test-once');
|
||||
app.emit('test-once');
|
||||
return count === 1;
|
||||
});
|
||||
|
||||
test('app.addListener returns this', () => {
|
||||
const fn = () => {};
|
||||
const result = app.addListener('test-chain', fn);
|
||||
app.removeListener('test-chain', fn);
|
||||
return result === app;
|
||||
});
|
||||
|
||||
test('app.emit("activate") does not crash', () => {
|
||||
app.emit('activate');
|
||||
return true;
|
||||
});
|
||||
|
||||
test('app.emit("focus") does not crash', () => {
|
||||
app.emit('focus');
|
||||
return true;
|
||||
});
|
||||
|
||||
test('app.emit("blur") does not crash', () => {
|
||||
app.emit('blur');
|
||||
return true;
|
||||
});
|
||||
|
||||
test('app.emit("browser-window-focus") does not crash', () => {
|
||||
app.emit('browser-window-focus');
|
||||
return true;
|
||||
});
|
||||
|
||||
test('app.emit("browser-window-blur") does not crash', () => {
|
||||
app.emit('browser-window-blur');
|
||||
return true;
|
||||
});
|
||||
|
||||
test('app.emit("web-contents-created") does not crash', () => {
|
||||
app.emit('web-contents-created', {}, {});
|
||||
return true;
|
||||
});
|
||||
|
||||
// ─── Dock (macOS) ──────────────────────────────────────────────
|
||||
|
||||
test('app.dock exists (macOS)', () => {
|
||||
if (process.platform === 'darwin') {
|
||||
if (!app.dock) throw new Error('app.dock is null on macOS');
|
||||
return typeof app.dock;
|
||||
}
|
||||
return 'N/A (not macOS)';
|
||||
});
|
||||
|
||||
test('app.dock.bounce', () => {
|
||||
if (process.platform === 'darwin') return app.dock.bounce();
|
||||
return 'N/A';
|
||||
});
|
||||
|
||||
test('app.dock.setBadgeCount / getBadgeCount', () => {
|
||||
if (process.platform === 'darwin') {
|
||||
app.dock.setBadgeCount(5);
|
||||
return app.dock.getBadgeCount();
|
||||
}
|
||||
return 'N/A';
|
||||
});
|
||||
|
||||
// ─── Command Line ──────────────────────────────────────────────
|
||||
|
||||
test('app.commandLine.appendSwitch', () => {
|
||||
app.commandLine.appendSwitch('test-switch', 'test-value');
|
||||
return app.commandLine.hasSwitch('test-switch');
|
||||
});
|
||||
|
||||
test('app.commandLine.getSwitch', () => app.commandLine.getSwitch('test-switch'));
|
||||
|
||||
test('app.commandLine.removeSwitch', () => {
|
||||
app.commandLine.removeSwitch('test-switch');
|
||||
return !app.commandLine.hasSwitch('test-switch');
|
||||
});
|
||||
|
||||
// ─── Badge ─────────────────────────────────────────────────────
|
||||
|
||||
test('app.setBadgeCount / getBadgeCount', () => {
|
||||
const result = app.setBadgeCount(3);
|
||||
if (app.getBadgeCount() !== 3) throw new Error('Expected 3, got ' + app.getBadgeCount());
|
||||
app.setBadgeCount(0);
|
||||
return result;
|
||||
});
|
||||
|
||||
test('app.setBadgeCount(0) resets', () => {
|
||||
app.setBadgeCount(5);
|
||||
app.setBadgeCount(0);
|
||||
return app.getBadgeCount() === 0;
|
||||
});
|
||||
|
||||
// ─── Metrics / GPU ────────────────────────────────────────────
|
||||
|
||||
test('app.getAppMetrics() returns array', () => {
|
||||
const metrics = app.getAppMetrics();
|
||||
if (!Array.isArray(metrics)) throw new Error('Expected array');
|
||||
if (metrics.length === 0) throw new Error('Expected at least one entry');
|
||||
const m = metrics[0];
|
||||
if (typeof m.pid !== 'number') throw new Error('Expected pid to be number');
|
||||
if (!m.memory) throw new Error('Expected memory object');
|
||||
return metrics.length + ' entries';
|
||||
});
|
||||
|
||||
test('app.getGPUInfo("basic")', async () => {
|
||||
const info = await app.getGPUInfo('basic');
|
||||
if (!info || !info.gpuDevice) throw new Error('Missing gpuDevice');
|
||||
return info.GPUActive;
|
||||
});
|
||||
|
||||
test('app.getGPUInfo("complete")', async () => {
|
||||
const info = await app.getGPUInfo('complete');
|
||||
if (!info || !info.gpuDriver) throw new Error('Missing gpuDriver');
|
||||
return info.gpuDriver;
|
||||
});
|
||||
|
||||
test('app.getGPUFeatureStatus()', () => {
|
||||
const status = app.getGPUFeatureStatus();
|
||||
if (!status || typeof status.gpuCompositing !== 'string') throw new Error('Missing gpuCompositing');
|
||||
return Object.keys(status).length + ' features';
|
||||
});
|
||||
|
||||
test('app.disableHardwareAcceleration() no-op', () => {
|
||||
app.disableHardwareAcceleration();
|
||||
return true;
|
||||
});
|
||||
|
||||
test('app.disableDomainBlockingFor3DAPIs() no-op', () => {
|
||||
app.disableDomainBlockingFor3DAPIs();
|
||||
return true;
|
||||
});
|
||||
|
||||
// ─── Login Items ───────────────────────────────────────────────
|
||||
|
||||
test('app.getLoginItemSettings()', () => {
|
||||
const s = app.getLoginItemSettings();
|
||||
if (typeof s.openAtLogin !== 'boolean') throw new Error('Missing openAtLogin');
|
||||
if (!s.hasOwnProperty('launchAtLogin')) throw new Error('Missing launchAtLogin');
|
||||
return true;
|
||||
});
|
||||
|
||||
test('app.setLoginItemSettings() no-op', () => {
|
||||
app.setLoginItemSettings({ openAtLogin: true });
|
||||
return true;
|
||||
});
|
||||
|
||||
// ─── About Panel ───────────────────────────────────────────────
|
||||
|
||||
test('app.setAboutPanelOptions / getAboutPanelOptions', () => {
|
||||
app.setAboutPanelOptions({ applicationName: 'Test', copyright: 'MIT' });
|
||||
const opts = app.getAboutPanelOptions();
|
||||
if (opts.applicationName !== 'Test') throw new Error('Expected Test, got ' + opts.applicationName);
|
||||
return opts.copyright;
|
||||
});
|
||||
|
||||
test('app.setAboutPanelOptions() with no args resets', () => {
|
||||
app.setAboutPanelOptions();
|
||||
const opts = app.getAboutPanelOptions();
|
||||
return typeof opts === 'object';
|
||||
});
|
||||
|
||||
// ─── Recent Documents ──────────────────────────────────────────
|
||||
|
||||
test('app.addRecentDocument() no-op', () => { app.addRecentDocument('/tmp/test'); return true; });
|
||||
test('app.clearRecentDocuments() no-op', () => { app.clearRecentDocuments(); return true; });
|
||||
test('app.setRecentDocumentLabel() no-op', () => { app.setRecentDocumentLabel('Test'); return true; });
|
||||
|
||||
// ─── App User Model ID ────────────────────────────────────────
|
||||
|
||||
test('app.setAppUserModelId() no-op', () => { app.setAppUserModelId('com.test'); return true; });
|
||||
test('app.getAppUserModelId()', () => {
|
||||
const result = app.getAppUserModelId();
|
||||
if (typeof result !== 'string') throw new Error('Expected string');
|
||||
return result === '' ? '(empty)' : result;
|
||||
});
|
||||
|
||||
// ─── Applications Folder ──────────────────────────────────────
|
||||
|
||||
test('app.isInApplicationsFolder()', () => app.isInApplicationsFolder());
|
||||
test('app.moveToApplicationsFolder() no-op', () => { app.moveToApplicationsFolder(); return true; });
|
||||
|
||||
// ─── File Icons ────────────────────────────────────────────────
|
||||
|
||||
test('app.getFileIcon() callback', (done) => {
|
||||
return new Promise((resolve) => {
|
||||
app.getFileIcon('/tmp', (err, icon) => {
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Certificate Trust ────────────────────────────────────────
|
||||
|
||||
test('app.showCertificateTrustDialog()', async () => {
|
||||
await app.showCertificateTrustDialog();
|
||||
return true;
|
||||
});
|
||||
|
||||
// ─── Secure Keyboard Entry ────────────────────────────────────
|
||||
|
||||
test('app.setSecureKeyboardEntryEnabled / isSecureKeyboardEntryEnabled', () => {
|
||||
app.setSecureKeyboardEntryEnabled(true);
|
||||
const result = app.isSecureKeyboardEntryEnabled();
|
||||
app.setSecureKeyboardEntryEnabled(false);
|
||||
return result;
|
||||
});
|
||||
|
||||
// ─── Window Count ──────────────────────────────────────────────
|
||||
|
||||
test('app.getWindowCount() initial', () => {
|
||||
return app.getWindowCount();
|
||||
});
|
||||
|
||||
test('app.getPath("appData")', () => app.getPath('appData'));
|
||||
|
||||
test('app.getPath("exe")', () => app.getPath('exe'));
|
||||
|
||||
test('app.getPath("module")', () => app.getPath('module'));
|
||||
|
||||
// ─── Launch App ────────────────────────────────────────────────
|
||||
|
||||
const gotLock = app.requestSingleInstanceLock();
|
||||
if (!gotLock) {
|
||||
app.quit();
|
||||
} else {
|
||||
app.on('second-instance', () => {
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
});
|
||||
|
||||
app.whenReady().then(() => {
|
||||
test('app.whenReady() resolved', () => true);
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 900,
|
||||
height: 680,
|
||||
title: 'Gelectron Demo',
|
||||
backgroundColor: '#1a1a2e',
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
});
|
||||
|
||||
test('BrowserWindow created', () => mainWindow.id > 0);
|
||||
|
||||
test('BrowserWindow.getSize()', () => {
|
||||
const [w, h] = mainWindow.getSize();
|
||||
if (w < 100 || h < 100) throw new Error('Size too small: ' + w + 'x' + h);
|
||||
return w + 'x' + h;
|
||||
});
|
||||
|
||||
test('BrowserWindow.getTitle()', () => mainWindow.getTitle());
|
||||
|
||||
test('BrowserWindow.isVisible()', () => mainWindow.isVisible());
|
||||
|
||||
test('BrowserWindow.isDestroyed()', () => !mainWindow.isDestroyed());
|
||||
|
||||
test('BrowserWindow.isNormal()', () => mainWindow.isNormal());
|
||||
|
||||
test('BrowserWindow.isResizable()', () => mainWindow.isResizable());
|
||||
|
||||
test('BrowserWindow.getAllWindows().length', () => BrowserWindow.getAllWindows().length);
|
||||
|
||||
test('BrowserWindow.fromId(id)', () => {
|
||||
const win = BrowserWindow.fromId(mainWindow.id);
|
||||
return win ? win.id === mainWindow.id : false;
|
||||
});
|
||||
|
||||
test('window-all-closed listener registered', () => {
|
||||
app.on('window-all-closed', () => {
|
||||
app.quit();
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
test('app.relaunch is a function', () => typeof app.relaunch === 'function');
|
||||
|
||||
test('app.exit is a function', () => typeof app.exit === 'function');
|
||||
|
||||
test('app.quit is a function', () => typeof app.quit === 'function');
|
||||
|
||||
test('app.getWindowCount() after create', () => app.getWindowCount());
|
||||
|
||||
test('app.getPathForProtocol()', () => {
|
||||
const result = app.getPathForProtocol('https:');
|
||||
return result === null ? 'null' : result;
|
||||
});
|
||||
|
||||
test('app.getApplicationInfoForProtocol()', async () => {
|
||||
const result = await app.getApplicationInfoForProtocol('https:');
|
||||
return typeof result === 'object';
|
||||
});
|
||||
|
||||
mainWindow.loadFile(path.join(__dirname, 'index.html'));
|
||||
|
||||
mainWindow.webContents.on('did-finish-load', () => {
|
||||
const testResults = JSON.stringify(results);
|
||||
mainWindow.webContents.executeJavaScript(
|
||||
`window.__appTestResults = ${testResults}; renderAppTests();`
|
||||
);
|
||||
});
|
||||
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
app.quit();
|
||||
});
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "gelectron-demo",
|
||||
"version": "1.0.0",
|
||||
"description": "Minimal demo for gelectron — HTML, CSS, JavaScript rendering",
|
||||
"main": "main.js"
|
||||
}
|
||||
Binary file not shown.
BIN
Binary file not shown.
@@ -1,339 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>files</key>
|
||||
<dict>
|
||||
<key>Resources/app/index.html</key>
|
||||
<data>
|
||||
sG0CkHSfK6EH/aUgSy18Gzs2drg=
|
||||
</data>
|
||||
<key>Resources/app/main.js</key>
|
||||
<data>
|
||||
e16s5rQbnpal4xcLf8x/NhGbS2g=
|
||||
</data>
|
||||
<key>Resources/app/package.json</key>
|
||||
<data>
|
||||
Y7NDit/zkULoNATi0P/tZR+DKxM=
|
||||
</data>
|
||||
</dict>
|
||||
<key>files2</key>
|
||||
<dict>
|
||||
<key>MacOS/compat/app.js</key>
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
1lkepb5Ez0HGGLSXwgy8EDApOdE=
|
||||
</data>
|
||||
<key>requirement</key>
|
||||
<string>cdhash H"d6591ea5be44cf41c618b497c20cbc10302939d1"</string>
|
||||
</dict>
|
||||
<key>MacOS/compat/auto-updater.js</key>
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
6hr81wA+Q4cJzyBNArOA45tEkVY=
|
||||
</data>
|
||||
<key>requirement</key>
|
||||
<string>cdhash H"ea1afcd7003e438709cf204d02b380e39b449156"</string>
|
||||
</dict>
|
||||
<key>MacOS/compat/browser-window.js</key>
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
qAdLtfb7mpG6gR6xmy9gCKs6FBE=
|
||||
</data>
|
||||
<key>requirement</key>
|
||||
<string>cdhash H"a8074bb5f6fb9a91ba811eb19b2f6008ab3a1411"</string>
|
||||
</dict>
|
||||
<key>MacOS/compat/context-bridge.js</key>
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
KafHH/4oRUwwDuZqwipWOhxM268=
|
||||
</data>
|
||||
<key>requirement</key>
|
||||
<string>cdhash H"29a7c71ffe28454c300ee66ac22a563a1c4cdbaf"</string>
|
||||
</dict>
|
||||
<key>MacOS/compat/dialog.js</key>
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
SXCSgmcZ/vl0xxt9pd2zVWxEYjU=
|
||||
</data>
|
||||
<key>requirement</key>
|
||||
<string>cdhash H"497092826719fef974c71b7da5ddb3556c446235"</string>
|
||||
</dict>
|
||||
<key>MacOS/compat/index.js</key>
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
Nn0CHTP29YJYvNdElbvNoaIx+rc=
|
||||
</data>
|
||||
<key>requirement</key>
|
||||
<string>cdhash H"367d021d33f6f58258bcd74495bbcda1a231fab7"</string>
|
||||
</dict>
|
||||
<key>MacOS/compat/ipc-main.js</key>
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
16s5WbfI+qEVIALfGZh0f3mwIz4=
|
||||
</data>
|
||||
<key>requirement</key>
|
||||
<string>cdhash H"d7ab3959b7c8faa1152002df1998747f79b0233e"</string>
|
||||
</dict>
|
||||
<key>MacOS/compat/ipc-renderer.js</key>
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
3kHQy1UIFXY4M9e+jjEaGf+4PZA=
|
||||
</data>
|
||||
<key>requirement</key>
|
||||
<string>cdhash H"de41d0cb550815763833d7be8e311a19ffb83d90"</string>
|
||||
</dict>
|
||||
<key>MacOS/compat/menu.js</key>
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
mG00gq46cpyWtov3xc27eZWOsAQ=
|
||||
</data>
|
||||
<key>requirement</key>
|
||||
<string>cdhash H"986d3482ae3a729c96b68bf7c5cdbb79958eb004"</string>
|
||||
</dict>
|
||||
<key>MacOS/compat/native-bridge.js</key>
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
Rb9fSRqUVjk1KzuBt3EGHUE31s8=
|
||||
</data>
|
||||
<key>requirement</key>
|
||||
<string>cdhash H"45bf5f491a945639352b3b81b771061d4137d6cf"</string>
|
||||
</dict>
|
||||
<key>MacOS/compat/native-image.js</key>
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
eutIvrwoL/WLitX7ZSm7Qzo+hRo=
|
||||
</data>
|
||||
<key>requirement</key>
|
||||
<string>cdhash H"7aeb48bebc282ff58b8ad5fb6529bb433a3e851a"</string>
|
||||
</dict>
|
||||
<key>MacOS/compat/notification.js</key>
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
tzWUf6RK0ECImeqn3BvgQnZxJiI=
|
||||
</data>
|
||||
<key>requirement</key>
|
||||
<string>cdhash H"b735947fa44ad0408899eaa7dc1be04276712622"</string>
|
||||
</dict>
|
||||
<key>MacOS/compat/preload-loader.js</key>
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
2pD8SFhJ1ghYKM9eDCVn5UnRamo=
|
||||
</data>
|
||||
<key>requirement</key>
|
||||
<string>cdhash H"da90fc485849d6085828cf5e0c2567e549d16a6a"</string>
|
||||
</dict>
|
||||
<key>MacOS/compat/runtime.js</key>
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
OtT9xp7FBV99spiMrCjN4POHTD8=
|
||||
</data>
|
||||
<key>requirement</key>
|
||||
<string>cdhash H"3ad4fdc69ec5055f7db2988cac28cde0f3874c3f"</string>
|
||||
</dict>
|
||||
<key>MacOS/compat/safe-storage.js</key>
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
C6FIL05pKS9uwt8HzfQNaDijvw4=
|
||||
</data>
|
||||
<key>requirement</key>
|
||||
<string>cdhash H"0ba1482f4e69292f6ec2df07cdf40d6838a3bf0e"</string>
|
||||
</dict>
|
||||
<key>MacOS/compat/shell.js</key>
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
pdQ3SzT0lEAMd4rkCN40O3QlWWo=
|
||||
</data>
|
||||
<key>requirement</key>
|
||||
<string>cdhash H"a5d4374b34f494400c778ae408de343b7425596a"</string>
|
||||
</dict>
|
||||
<key>MacOS/compat/tray.js</key>
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
AoFk0BeiEzIY9sU1Oh+PQwgduZM=
|
||||
</data>
|
||||
<key>requirement</key>
|
||||
<string>cdhash H"028164d017a2133218f6c5353a1f8f43081db993"</string>
|
||||
</dict>
|
||||
<key>MacOS/compat/web-contents.js</key>
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
KLiqJQByq2M9brxskLOF0YX1jkk=
|
||||
</data>
|
||||
<key>requirement</key>
|
||||
<string>cdhash H"28b8aa250072ab633d6ebc6c90b385d185f58e49"</string>
|
||||
</dict>
|
||||
<key>MacOS/compat/webview-bundle.js</key>
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
XJO9bSeIHn+ykzJJLFGKXj1bL1c=
|
||||
</data>
|
||||
<key>requirement</key>
|
||||
<string>cdhash H"5c93bd6d27881e7fb29332492c518a5e3d5b2f57"</string>
|
||||
</dict>
|
||||
<key>MacOS/gelectron-bin</key>
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
jhz++m3PyWCRtj/c/AJ/ZTdlXio=
|
||||
</data>
|
||||
<key>requirement</key>
|
||||
<string>cdhash H"8e1cfefa6dcfc96091b63fdcfc027f6537655e2a"</string>
|
||||
</dict>
|
||||
<key>MacOS/node</key>
|
||||
<dict>
|
||||
<key>cdhash</key>
|
||||
<data>
|
||||
nhZUTCVVb73fdsZ0dHft7VPJ7e8=
|
||||
</data>
|
||||
<key>requirement</key>
|
||||
<string>cdhash H"9e16544c25556fbddf76c6747477eded53c9edef"</string>
|
||||
</dict>
|
||||
<key>Resources/app/index.html</key>
|
||||
<dict>
|
||||
<key>hash2</key>
|
||||
<data>
|
||||
q1YGzqi9XhTXA9N6blykkfje2XPlK7ODkCZ2za6Umjc=
|
||||
</data>
|
||||
</dict>
|
||||
<key>Resources/app/main.js</key>
|
||||
<dict>
|
||||
<key>hash2</key>
|
||||
<data>
|
||||
oRbv77Ab5dvdtFjb5nLPDG3VkG2mnGDabYl/xdgnjqg=
|
||||
</data>
|
||||
</dict>
|
||||
<key>Resources/app/package.json</key>
|
||||
<dict>
|
||||
<key>hash2</key>
|
||||
<data>
|
||||
Gkj33l7Q2sTuSFNjF+J+1cp5p3yOfGwHvM8MQI89/bw=
|
||||
</data>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>rules</key>
|
||||
<dict>
|
||||
<key>^Resources/</key>
|
||||
<true/>
|
||||
<key>^Resources/.*\.lproj/</key>
|
||||
<dict>
|
||||
<key>optional</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>1000</real>
|
||||
</dict>
|
||||
<key>^Resources/.*\.lproj/locversion.plist$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>1100</real>
|
||||
</dict>
|
||||
<key>^Resources/Base\.lproj/</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>1010</real>
|
||||
</dict>
|
||||
<key>^version.plist$</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>rules2</key>
|
||||
<dict>
|
||||
<key>.*\.dSYM($|/)</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>11</real>
|
||||
</dict>
|
||||
<key>^(.*/)?\.DS_Store$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>2000</real>
|
||||
</dict>
|
||||
<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
|
||||
<dict>
|
||||
<key>nested</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>10</real>
|
||||
</dict>
|
||||
<key>^.*</key>
|
||||
<true/>
|
||||
<key>^Info\.plist$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
<key>^PkgInfo$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
<key>^Resources/</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
<key>^Resources/.*\.lproj/</key>
|
||||
<dict>
|
||||
<key>optional</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>1000</real>
|
||||
</dict>
|
||||
<key>^Resources/.*\.lproj/locversion.plist$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>1100</real>
|
||||
</dict>
|
||||
<key>^Resources/Base\.lproj/</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>1010</real>
|
||||
</dict>
|
||||
<key>^[^/]+$</key>
|
||||
<dict>
|
||||
<key>nested</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>10</real>
|
||||
</dict>
|
||||
<key>^embedded\.provisionprofile$</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
<key>^version\.plist$</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -23,3 +23,6 @@ rfd = "0.15"
|
||||
png = "0.18"
|
||||
base64 = "0.22"
|
||||
url = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
cocoa = "0.26"
|
||||
|
||||
@@ -31,6 +31,8 @@ enum ToRust {
|
||||
SetTitle { id: u32, title: String },
|
||||
#[serde(rename = "set-size")]
|
||||
SetSize { id: u32, width: u32, height: u32 },
|
||||
#[serde(rename = "set-app-icon")]
|
||||
SetAppIcon { icon: String },
|
||||
#[serde(rename = "show")]
|
||||
Show { id: u32 },
|
||||
#[serde(rename = "hide")]
|
||||
@@ -146,6 +148,8 @@ struct WindowOpts {
|
||||
always_on_top: Option<bool>,
|
||||
#[serde(default)]
|
||||
fullscreen: Option<bool>,
|
||||
#[serde(default)]
|
||||
icon: Option<String>,
|
||||
}
|
||||
|
||||
struct WindowPair {
|
||||
@@ -161,6 +165,15 @@ struct AppState {
|
||||
node_exited: Arc<AtomicBool>,
|
||||
bundle_js: Option<String>,
|
||||
response_tx: Option<mpsc::Sender<(String, serde_json::Value)>>,
|
||||
app_icon: Option<DecodedIcon>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct DecodedIcon {
|
||||
raw: Vec<u8>,
|
||||
rgba: Vec<u8>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@@ -172,6 +185,7 @@ impl AppState {
|
||||
node_exited,
|
||||
bundle_js: None,
|
||||
response_tx: None,
|
||||
app_icon: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,6 +420,7 @@ require('{}');
|
||||
let mut state = AppState::new(node_exited.clone());
|
||||
state.node_stdin = Some(child_stdin);
|
||||
state.response_tx = Some(response_tx);
|
||||
detect_and_apply_icon(&app_path, &mut state);
|
||||
let state = Rc::new(RefCell::new(state));
|
||||
state.borrow_mut().send_to_node(&ToNode::Ready);
|
||||
|
||||
@@ -503,6 +518,7 @@ window.__gelectron_run_main(`{}`);
|
||||
let mut app_state = AppState::new(node_exited.clone());
|
||||
app_state.bundle_js = Some(bundle_js.clone());
|
||||
app_state.response_tx = Some(response_tx);
|
||||
detect_and_apply_icon(&app_path, &mut app_state);
|
||||
let state = Rc::new(RefCell::new(app_state));
|
||||
|
||||
event_loop.run(move |event, event_loop_target, control_flow| {
|
||||
@@ -731,6 +747,299 @@ fn detect_dark_mode() -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_png_icon(png_bytes: &[u8]) -> Option<DecodedIcon> {
|
||||
use std::io::Cursor;
|
||||
let cursor = Cursor::new(png_bytes);
|
||||
let decoder = png::Decoder::new(cursor);
|
||||
let mut reader = decoder.read_info().ok()?;
|
||||
let info = reader.info().clone();
|
||||
let width = info.width;
|
||||
let height = info.height;
|
||||
if width == 0 || height == 0 || width * height > 4096 * 4096 {
|
||||
return None;
|
||||
}
|
||||
let mut buf = vec![0; reader.output_buffer_size().unwrap_or(0)];
|
||||
let info = reader.next_frame(&mut buf).ok()?;
|
||||
let rgba = match info.color_type {
|
||||
png::ColorType::Rgba => buf[..(width as usize * height as usize * 4)].to_vec(),
|
||||
png::ColorType::Rgb => {
|
||||
let mut out = Vec::with_capacity(width as usize * height as usize * 4);
|
||||
for px in buf[..(width as usize * height as usize * 3)].chunks_exact(3) {
|
||||
out.extend_from_slice(&[px[0], px[1], px[2], 255]);
|
||||
}
|
||||
out
|
||||
}
|
||||
png::ColorType::Grayscale => {
|
||||
let mut out = Vec::with_capacity(width as usize * height as usize * 4);
|
||||
for &g in buf.iter().take(width as usize * height as usize) {
|
||||
out.extend_from_slice(&[g, g, g, 255]);
|
||||
}
|
||||
out
|
||||
}
|
||||
png::ColorType::GrayscaleAlpha => {
|
||||
let mut out = Vec::with_capacity(width as usize * height as usize * 4);
|
||||
for px in buf[..(width as usize * height as usize * 2)].chunks_exact(2) {
|
||||
out.extend_from_slice(&[px[0], px[0], px[0], px[1]]);
|
||||
}
|
||||
out
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
Some(DecodedIcon {
|
||||
raw: png_bytes.to_vec(),
|
||||
rgba,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
|
||||
fn decode_ico_bmp(data: &[u8], raw: Vec<u8>) -> Option<DecodedIcon> {
|
||||
if data.len() < 40 {
|
||||
return None;
|
||||
}
|
||||
let width = i32::from_le_bytes([data[4], data[5], data[6], data[7]]);
|
||||
let height = i32::from_le_bytes([data[8], data[9], data[10], data[11]]);
|
||||
let bit_count = u16::from_le_bytes([data[14], data[15]]);
|
||||
let compression = u32::from_le_bytes([data[16], data[17], data[18], data[19]]);
|
||||
if width <= 0 || height == 0 || compression != 0 || (bit_count != 32 && bit_count != 24) {
|
||||
return None;
|
||||
}
|
||||
let mut height = height.unsigned_abs();
|
||||
if bit_count == 24 && height >= 2 && height % 2 == 0 {
|
||||
height /= 2;
|
||||
}
|
||||
let width = width as usize;
|
||||
let height = height as usize;
|
||||
if width == 0 || height == 0 || width * height > 4096 * 4096 {
|
||||
return None;
|
||||
}
|
||||
let bpp = bit_count as usize / 8;
|
||||
let stride = (width * bpp + 3) & !3;
|
||||
let px_start = 40usize;
|
||||
let mut rgba = vec![0u8; width * height * 4];
|
||||
for y in 0..height {
|
||||
let src_row = px_start + (height - 1 - y) * stride;
|
||||
if src_row + width * bpp > data.len() {
|
||||
return None;
|
||||
}
|
||||
for x in 0..width {
|
||||
let si = src_row + x * bpp;
|
||||
let di = (y * width + x) * 4;
|
||||
rgba[di] = data[si + 2];
|
||||
rgba[di + 1] = data[si + 1];
|
||||
rgba[di + 2] = data[si];
|
||||
rgba[di + 3] = if bit_count == 32 { data[si + 3] } else { 255 };
|
||||
}
|
||||
}
|
||||
Some(DecodedIcon {
|
||||
raw,
|
||||
rgba,
|
||||
width: width as u32,
|
||||
height: height as u32,
|
||||
})
|
||||
}
|
||||
|
||||
fn decode_ico_icon(bytes: &[u8]) -> Option<DecodedIcon> {
|
||||
if bytes.len() < 6 {
|
||||
return None;
|
||||
}
|
||||
let count = u16::from_le_bytes([bytes[2], bytes[3]]) as usize;
|
||||
if count == 0 || 6 + 16 * count > bytes.len() {
|
||||
return None;
|
||||
}
|
||||
let mut best: Option<(u64, usize, usize)> = None;
|
||||
for i in 0..count {
|
||||
let e = 6 + 16 * i;
|
||||
let w = if bytes[e] == 0 { 256 } else { bytes[e] as usize };
|
||||
let h = if bytes[e + 1] == 0 { 256 } else { bytes[e + 1] as usize };
|
||||
let size = u32::from_le_bytes([bytes[e + 8], bytes[e + 9], bytes[e + 10], bytes[e + 11]]) as usize;
|
||||
let offset =
|
||||
u32::from_le_bytes([bytes[e + 12], bytes[e + 13], bytes[e + 14], bytes[e + 15]]) as usize;
|
||||
let area = (w * h) as u64;
|
||||
if best.map_or(true, |(a, _, _)| area > a) {
|
||||
best = Some((area, size, offset));
|
||||
}
|
||||
}
|
||||
let (_, size, offset) = best?;
|
||||
if offset + size > bytes.len() {
|
||||
return None;
|
||||
}
|
||||
let data = &bytes[offset..offset + size];
|
||||
if data.starts_with(&[0x89, 0x50, 0x4E, 0x47]) {
|
||||
return decode_png_icon(data).map(|mut ic| {
|
||||
ic.raw = bytes.to_vec();
|
||||
ic
|
||||
});
|
||||
}
|
||||
decode_ico_bmp(data, bytes.to_vec())
|
||||
}
|
||||
|
||||
fn decode_icon_bytes(bytes: &[u8]) -> Option<DecodedIcon> {
|
||||
if let Some(icon) = decode_png_icon(bytes) {
|
||||
return Some(icon);
|
||||
}
|
||||
if let Some(icon) = decode_ico_icon(bytes) {
|
||||
return Some(icon);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
if bytes.starts_with(b"icns") {
|
||||
return Some(DecodedIcon {
|
||||
raw: bytes.to_vec(),
|
||||
rgba: Vec::new(),
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn decode_base64_icon(base64_str: &str) -> Option<DecodedIcon> {
|
||||
use base64::Engine;
|
||||
let bytes = base64::engine::general_purpose::STANDARD
|
||||
.decode(base64_str.trim())
|
||||
.ok()?;
|
||||
decode_icon_bytes(&bytes)
|
||||
}
|
||||
|
||||
fn find_app_icon_file(app_path: &std::path::Path) -> Option<PathBuf> {
|
||||
let mut candidates: Vec<PathBuf> = vec![];
|
||||
for name in [
|
||||
"icon.png",
|
||||
"app.png",
|
||||
"icon.icns",
|
||||
"icon.ico",
|
||||
"app.ico",
|
||||
"assets/icon.png",
|
||||
"assets/app.png",
|
||||
"build/icon.png",
|
||||
"build/icon.icns",
|
||||
"build/icon.ico",
|
||||
"resources/icon.png",
|
||||
"resources/icon.icns",
|
||||
"resources/icon.ico",
|
||||
"public/icon.png",
|
||||
"public/icons/icon.png",
|
||||
"static/icon.png",
|
||||
] {
|
||||
candidates.push(app_path.join(name));
|
||||
}
|
||||
if let Ok(pkg) = std::fs::read_to_string(app_path.join("package.json")) {
|
||||
if let Ok(value) = serde_json::from_str::<serde_json::Value>(&pkg) {
|
||||
if let Some(s) = value.get("icon").and_then(|v| v.as_str()) {
|
||||
candidates.push(app_path.join(s));
|
||||
}
|
||||
if let Some(build) = value.get("build").and_then(|v| v.get("icon")) {
|
||||
if let Some(s) = build.as_str() {
|
||||
candidates.push(app_path.join(s));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates
|
||||
.into_iter()
|
||||
.find(|p| p.exists() && p.is_file())
|
||||
}
|
||||
|
||||
fn apply_dock_icon(icon: &DecodedIcon) {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use cocoa::appkit::{NSApplication, NSImage};
|
||||
use cocoa::base::{id, nil};
|
||||
use cocoa::foundation::NSData;
|
||||
let img_bytes = if !icon.rgba.is_empty() {
|
||||
rgba_to_png(&icon.rgba, icon.width, icon.height).unwrap_or_else(|| icon.raw.clone())
|
||||
} else {
|
||||
icon.raw.clone()
|
||||
};
|
||||
unsafe {
|
||||
let data = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
img_bytes.as_ptr() as *const std::ffi::c_void,
|
||||
img_bytes.len() as u64,
|
||||
);
|
||||
let image: id = NSImage::initWithData_(NSImage::alloc(nil), data);
|
||||
if image != nil {
|
||||
let app = NSApplication::sharedApplication(nil);
|
||||
let _: () = app.setApplicationIconImage_(image);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
let _ = icon;
|
||||
}
|
||||
}
|
||||
|
||||
fn rgba_to_png(rgba: &[u8], width: u32, height: u32) -> Option<Vec<u8>> {
|
||||
use std::io::Cursor;
|
||||
let mut out = Cursor::new(Vec::new());
|
||||
let mut encoder = png::Encoder::new(&mut out, width, height);
|
||||
encoder.set_color(png::ColorType::Rgba);
|
||||
encoder.set_depth(png::BitDepth::Eight);
|
||||
let mut writer = encoder.write_header().ok()?;
|
||||
writer.write_image_data(rgba).ok()?;
|
||||
drop(writer);
|
||||
Some(out.into_inner())
|
||||
}
|
||||
|
||||
fn apply_window_icon(window: &tao::window::Window, icon: &DecodedIcon) {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let _ = window;
|
||||
let _ = icon;
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
if let Ok(tao_icon) = tao::window::Icon::from_rgba(icon.rgba.clone(), icon.width, icon.height) {
|
||||
window.set_window_icon(Some(tao_icon));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_and_apply_icon(app_path: &std::path::Path, st: &mut AppState) {
|
||||
if st.app_icon.is_some() {
|
||||
return;
|
||||
}
|
||||
let Some(icon_path) = find_app_icon_file(app_path) else {
|
||||
return;
|
||||
};
|
||||
let is_icns = icon_path.extension().map(|e| e == "icns").unwrap_or(false);
|
||||
if is_icns {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
apply_dock_icon_from_path(&icon_path);
|
||||
log::info!("Loaded app icon from {}", icon_path.display());
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
let _ = icon_path;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let Ok(bytes) = std::fs::read(&icon_path) {
|
||||
if let Some(icon) = decode_icon_bytes(&bytes) {
|
||||
st.app_icon = Some(icon.clone());
|
||||
apply_dock_icon(&icon);
|
||||
log::info!("Loaded app icon from {}", icon_path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn apply_dock_icon_from_path(path: &std::path::Path) {
|
||||
use cocoa::appkit::{NSApplication, NSImage};
|
||||
use cocoa::base::{id, nil};
|
||||
use cocoa::foundation::NSString;
|
||||
unsafe {
|
||||
let ns_path = NSString::alloc(nil).init_str(&path.display().to_string());
|
||||
let image: id = NSImage::initWithContentsOfFile_(NSImage::alloc(nil), ns_path);
|
||||
if image != nil {
|
||||
let app = NSApplication::sharedApplication(nil);
|
||||
let _: () = app.setApplicationIconImage_(image);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_to_rust(
|
||||
msg: ToRust,
|
||||
st: &mut AppState,
|
||||
@@ -793,6 +1102,17 @@ fn handle_to_rust(
|
||||
{
|
||||
Ok(webview) => {
|
||||
let wid = window.id();
|
||||
if let Some(icon) = options
|
||||
.icon
|
||||
.as_deref()
|
||||
.and_then(decode_base64_icon)
|
||||
{
|
||||
st.app_icon = Some(icon.clone());
|
||||
apply_dock_icon(&icon);
|
||||
apply_window_icon(&window, &icon);
|
||||
} else if let Some(icon) = st.app_icon.clone() {
|
||||
apply_window_icon(&window, &icon);
|
||||
}
|
||||
st.windows.insert(id, WindowPair { window, webview });
|
||||
st.window_wids.insert(wid, id);
|
||||
log::info!("Window {} ready", id);
|
||||
@@ -1211,6 +1531,56 @@ fn handle_to_rust(
|
||||
}
|
||||
}
|
||||
}
|
||||
ToRust::SetTitle { id, title } => {
|
||||
if let Some(pair) = st.windows.get(&id) {
|
||||
pair.window.set_title(&title);
|
||||
}
|
||||
}
|
||||
ToRust::SetSize { id, width, height } => {
|
||||
if let Some(pair) = st.windows.get(&id) {
|
||||
pair.window.set_inner_size(tao::dpi::LogicalSize::new(
|
||||
width as f64,
|
||||
height as f64,
|
||||
));
|
||||
}
|
||||
}
|
||||
ToRust::SetAppIcon { icon } => {
|
||||
if let Some(decoded) = decode_base64_icon(&icon) {
|
||||
st.app_icon = Some(decoded.clone());
|
||||
apply_dock_icon(&decoded);
|
||||
for pair in st.windows.values() {
|
||||
apply_window_icon(&pair.window, &decoded);
|
||||
}
|
||||
log::info!("Set app icon");
|
||||
} else {
|
||||
log::warn!("SetAppIcon: could not decode icon");
|
||||
}
|
||||
}
|
||||
ToRust::Show { id } => {
|
||||
if let Some(pair) = st.windows.get(&id) {
|
||||
pair.window.set_visible(true);
|
||||
}
|
||||
}
|
||||
ToRust::Hide { id } => {
|
||||
if let Some(pair) = st.windows.get(&id) {
|
||||
pair.window.set_visible(false);
|
||||
}
|
||||
}
|
||||
ToRust::Focus { id } => {
|
||||
if let Some(pair) = st.windows.get(&id) {
|
||||
pair.window.set_focus();
|
||||
}
|
||||
}
|
||||
ToRust::Minimize { id } => {
|
||||
if let Some(pair) = st.windows.get(&id) {
|
||||
pair.window.set_minimized(true);
|
||||
}
|
||||
}
|
||||
ToRust::Maximize { id } => {
|
||||
if let Some(pair) = st.windows.get(&id) {
|
||||
pair.window.set_maximized(true);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1228,3 +1598,73 @@ fn which_node() -> Option<String> {
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ico_32bpp_bmp_decode() {
|
||||
let w = 4usize;
|
||||
let pixels: [[u8; 4]; 4] = [
|
||||
[255, 0, 0, 255],
|
||||
[0, 255, 0, 128],
|
||||
[0, 0, 255, 64],
|
||||
[255, 255, 255, 0],
|
||||
];
|
||||
let mut bmp = vec![0u8; 40];
|
||||
bmp[4..8].copy_from_slice(&(w as i32).to_le_bytes());
|
||||
bmp[8..12].copy_from_slice(&(w as i32).to_le_bytes());
|
||||
bmp[14..16].copy_from_slice(&32u16.to_le_bytes());
|
||||
for y in (0..w).rev() {
|
||||
for x in 0..w {
|
||||
let p = pixels[y];
|
||||
bmp.extend_from_slice(&[p[2], p[1], p[0], p[3]]);
|
||||
}
|
||||
}
|
||||
let mut ico = vec![0u8; 6];
|
||||
ico[2..4].copy_from_slice(&1u16.to_le_bytes());
|
||||
ico.extend_from_slice(&[w as u8, w as u8, 0, 0]);
|
||||
ico.extend_from_slice(&1u16.to_le_bytes());
|
||||
ico.extend_from_slice(&32u16.to_le_bytes());
|
||||
ico.extend_from_slice(&(bmp.len() as u32).to_le_bytes());
|
||||
ico.extend_from_slice(&22u32.to_le_bytes());
|
||||
ico.extend_from_slice(&bmp);
|
||||
|
||||
let icon = decode_ico_icon(&ico).expect("should decode 32bpp BMP-in-ICO");
|
||||
assert_eq!(icon.width, w as u32);
|
||||
assert_eq!(icon.height, w as u32);
|
||||
for y in 0..w {
|
||||
for x in 0..w {
|
||||
let i = (y * w + x) * 4;
|
||||
assert_eq!(&icon.rgba[i..i + 4], &pixels[y]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn png_in_ico_decode() {
|
||||
let mut png = Vec::new();
|
||||
{
|
||||
let mut enc = png::Encoder::new(&mut png, 2, 2);
|
||||
enc.set_color(png::ColorType::Rgba);
|
||||
enc.set_depth(png::BitDepth::Eight);
|
||||
let mut w = enc.write_header().unwrap();
|
||||
w.write_image_data(&[10, 20, 30, 255, 40, 50, 60, 255, 70, 80, 90, 255, 100, 110, 120, 255])
|
||||
.unwrap();
|
||||
}
|
||||
let mut ico = vec![0u8; 6];
|
||||
ico[2..4].copy_from_slice(&1u16.to_le_bytes());
|
||||
ico.extend_from_slice(&[0, 0, 0, 0]);
|
||||
ico.extend_from_slice(&1u16.to_le_bytes());
|
||||
ico.extend_from_slice(&32u16.to_le_bytes());
|
||||
ico.extend_from_slice(&(png.len() as u32).to_le_bytes());
|
||||
ico.extend_from_slice(&22u32.to_le_bytes());
|
||||
ico.extend_from_slice(&png);
|
||||
|
||||
let icon = decode_ico_icon(&ico).expect("should decode PNG-in-ICO");
|
||||
assert_eq!(icon.width, 2);
|
||||
assert_eq!(icon.height, 2);
|
||||
assert_eq!(&icon.rgba[..4], &[10, 20, 30, 255]);
|
||||
}
|
||||
}
|
||||
|
||||
+33
-2
@@ -12,6 +12,8 @@
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
const path = require('path');
|
||||
const { bridge } = require('./native-bridge');
|
||||
const NativeImage = require('./native-image');
|
||||
|
||||
class App extends EventEmitter {
|
||||
constructor() {
|
||||
@@ -24,7 +26,7 @@ class App extends EventEmitter {
|
||||
this._badgeCount = 0;
|
||||
this._secureKeyboardEntryEnabled = false;
|
||||
this._userAgent = null;
|
||||
this._name = 'Gelectron App';
|
||||
this._name = this._detectAppName();
|
||||
this._names = null;
|
||||
|
||||
const os = require('os');
|
||||
@@ -45,7 +47,10 @@ class App extends EventEmitter {
|
||||
|
||||
this._commandLine = new Map();
|
||||
this._dock = process.platform === 'darwin' ? {
|
||||
setIcon: (icon) => {},
|
||||
setIcon: (icon) => {
|
||||
const b64 = this._iconToBase64Png(icon);
|
||||
if (b64) bridge.setAppIcon(b64);
|
||||
},
|
||||
bounce: () => 0,
|
||||
cancelBounce: () => {},
|
||||
setBadge: () => {},
|
||||
@@ -72,6 +77,32 @@ class App extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
_detectAppName() {
|
||||
try {
|
||||
const appDir = process.env.GELECTRON_APP_PATH || process.cwd();
|
||||
const pkg = require(path.join(appDir, 'package.json'));
|
||||
return pkg.productName || pkg.name || 'Gelectron App';
|
||||
} catch (e) {
|
||||
return 'Gelectron App';
|
||||
}
|
||||
}
|
||||
|
||||
_iconToBase64Png(icon) {
|
||||
try {
|
||||
let img = icon;
|
||||
if (typeof icon === 'string') {
|
||||
img = NativeImage.createFromPath(icon);
|
||||
}
|
||||
if (img && typeof img.toPNG === 'function' && !img.isEmpty()) {
|
||||
const png = img.toPNG();
|
||||
if (png && png.length > 0) return png.toString('base64');
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore invalid icon
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── Properties ──────────────────────────────────────────────
|
||||
|
||||
get commandLine() {
|
||||
|
||||
@@ -8,6 +8,7 @@ const { EventEmitter } = require('events');
|
||||
const path = require('path');
|
||||
const { bridge, isNative } = require('./native-bridge');
|
||||
const { app } = require('./app');
|
||||
const NativeImage = require('./native-image');
|
||||
|
||||
class WebContents extends EventEmitter {
|
||||
constructor(id) {
|
||||
@@ -219,6 +220,7 @@ class BrowserWindow extends EventEmitter {
|
||||
resizable: this._options.resizable,
|
||||
alwaysOnTop: this._options.alwaysOnTop,
|
||||
fullscreen: this._options.fullscreen,
|
||||
icon: this._iconToBase64Png(this._options.icon),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -232,6 +234,23 @@ class BrowserWindow extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
_iconToBase64Png(icon) {
|
||||
if (!icon) return null;
|
||||
try {
|
||||
let img = icon;
|
||||
if (typeof icon === 'string') {
|
||||
img = NativeImage.createFromPath(icon);
|
||||
}
|
||||
if (img && typeof img.toPNG === 'function' && !img.isEmpty()) {
|
||||
const png = img.toPNG();
|
||||
if (png && png.length > 0) return png.toString('base64');
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore invalid icon
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static fromWebContents(webContents) {
|
||||
for (const win of BrowserWindow._windows.values()) {
|
||||
if (win.webContents && win.webContents.id === webContents.id) return win;
|
||||
@@ -270,7 +289,6 @@ class BrowserWindow extends EventEmitter {
|
||||
if (isNative) bridge.showWindow(this.id);
|
||||
this.emit('show');
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (this._isDestroyed) return;
|
||||
this._isVisible = false;
|
||||
|
||||
@@ -152,6 +152,10 @@ class NativeBridge extends EventEmitter {
|
||||
quit() {
|
||||
this._send({ type: 'quit' });
|
||||
}
|
||||
|
||||
setAppIcon(base64Png) {
|
||||
this._send({ type: 'set-app-icon', icon: base64Png });
|
||||
}
|
||||
}
|
||||
|
||||
const bridge = new NativeBridge();
|
||||
|
||||
@@ -17,12 +17,22 @@ class NativeImage {
|
||||
try {
|
||||
const fs = require('fs');
|
||||
const data = fs.readFileSync(path);
|
||||
// Basic PNG header parsing for dimensions
|
||||
img._data = data;
|
||||
img._isEmpty = false;
|
||||
if (data.length > 24 && data[0] === 0x89 && data[1] === 0x50) {
|
||||
img._width = data.readUInt32BE(16);
|
||||
img._height = data.readUInt32BE(20);
|
||||
img._data = data;
|
||||
img._isEmpty = false;
|
||||
} else if (data.length > 22 && data[0] === 0x00 && data[1] === 0x00 && data[2] === 0x01 && data[3] === 0x00) {
|
||||
img._width = data[6] || 256;
|
||||
img._height = data[7] || 256;
|
||||
} else if (data.length > 12 && data[0] === 0x69 && data[1] === 0x63 && data[2] === 0x6e && data[3] === 0x73) {
|
||||
const type = data.toString('ascii', 8, 12);
|
||||
const c = type.charCodeAt(1);
|
||||
if (c >= 0x30 && c <= 0x39) {
|
||||
img._width = img._height = 1 << (c - 0x30);
|
||||
} else if (type[0] === 'p' && c >= 0x34 && c <= 0x38) {
|
||||
img._width = img._height = 16 << (c - 0x34);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[gelectron] Failed to load image: ${path}`, err.message);
|
||||
@@ -109,9 +119,6 @@ class NativeImage {
|
||||
isTemplateImage() {
|
||||
return this._isTemplate || false;
|
||||
}
|
||||
|
||||
toPNG(options) { return this.toPNG(options); }
|
||||
toJPEG(quality) { return this.toJPEG(quality); }
|
||||
}
|
||||
|
||||
module.exports = NativeImage;
|
||||
|
||||
Reference in New Issue
Block a user