Initial commit.

This commit is contained in:
Cameron Redmore 2025-05-19 11:40:56 +01:00
commit b022bca449
20 changed files with 7405 additions and 0 deletions

29
.github/copilot-instructions.md vendored Normal file
View file

@ -0,0 +1,29 @@
## Project Overview
This is an Electron application designed to open Google Gemini
Key features:
- Opens google.com on launch.
- Registers a global keyboard shortcut (WIN+SHIFT+G) to show/focus the application window.
- Minimizes to the system tray when the window is "closed" instead of quitting. Clicking the tray icon restores the window.
## Tech Stack
- Electron
- Node.js
- HTML/CSS/JavaScript (though primarily interacting with an external site)
## Development Environment
- Package Manager: pnpm
- Main process logic: `main.js`
- Preload script: `preload.js` (if needed for future enhancements)
- Initial HTML: `index.html` (serves as a placeholder before loading Google)
## Important Notes for Copilot
- When modifying `main.js`, ensure that the tray functionality and global shortcut registration are preserved.
- The application's primary purpose is to display gemini.google.com
- Use `pnpm` for any package management commands.
- The main window should be created with `nodeIntegration: true` and `contextIsolation: false` if direct DOM manipulation of loaded pages is ever required (though not for the current google.com use case). For security, these are typically `false` and `true` respectively. Given we are loading an external site, we should stick to more secure defaults unless a specific feature requires otherwise.
- Ensure that paths in `main.js` (e.g., for `index.html` or icons) are correctly resolved using `path.join(__dirname, ...)`.

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
node_modules
settings.jsonG
out/
cert/

18
.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,18 @@
{
"version": "2.0.0",
"tasks": [
{
"args": [
"start"
],
"command": "pnpm",
"group": "build",
"isBackground": true,
"label": "Run Electron App",
"problemMatcher": [
"$electron"
],
"type": "shell"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
assets/SampleAppx.44x44.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
assets/SampleAppx.50x50.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
assets/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

48
changeVersion.js Normal file
View file

@ -0,0 +1,48 @@
const fs = require('fs');
const path = require('path');
function changeVersion(newVersion) {
const packageJsonPath = path.join(__dirname, 'package.json');
// Read the current package.json
fs.readFile(packageJsonPath, 'utf8', (err, data) => {
if (err) {
console.error('Error reading package.json:', err);
return;
}
let packageJson;
try {
packageJson = JSON.parse(data);
} catch (parseError) {
console.error('Error parsing package.json:', parseError);
return;
}
// Update the version
packageJson.version = newVersion;
// Write the updated package.json back to file
fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8', (writeErr) => {
if (writeErr) {
console.error('Error writing package.json:', writeErr);
return;
}
console.log(`Version updated to ${newVersion}`);
});
}
);
}
//Semver based on current date and time
const currentDate = new Date();
const year = currentDate.getFullYear();
const month = String(currentDate.getMonth() + 1).padStart(2, '0');
const day = String(currentDate.getDate()).padStart(2, '0');
const hours = String(currentDate.getHours()).padStart(2, '0');
const minutes = String(currentDate.getMinutes()).padStart(2, '0');
const seconds = String(currentDate.getSeconds()).padStart(2, '0');
const newVersion = `1.${year}${month}${day}.${hours}${minutes}${seconds}`;
changeVersion(newVersion);

122
firstrun.html Normal file
View file

@ -0,0 +1,122 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gemini Native</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: #f0f2f5;
color: #333;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
text-align: center;
padding: 20px;
box-sizing: border-box;
}
.container {
background-color: #ffffff;
padding: 40px;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
max-width: 600px;
width: 100%;
}
h1 {
color: #1a73e8; /* Google Blue */
font-size: 28px;
margin-bottom: 20px;
}
p {
font-size: 16px;
line-height: 1.6;
margin-bottom: 15px;
}
.shortcut {
background-color: #e8f0fe;
color: #1a73e8;
padding: 3px 8px;
border-radius: 6px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 0.9em;
border: 1px solid #d2e3fc;
}
.footer-note {
font-size: 0.9em;
color: #5f6368;
margin-top: 30px;
}
.logo {
display: flex;
justify-content: center;
margin-bottom: 20px;
padding: 10px;
}
.logo img {
width: 128px;
height: 128px;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
}
.close-button {
background-color: #1a73e8;
color: white;
border: none;
padding: 10px 20px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin-top: 20px;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.close-button:hover {
background-color: #1558b0; /* Darker Google Blue */
}
</style>
</head>
<body>
<div class="container">
<div class="logo">
<img src="assets/icon.png" alt="Gemini Native Logo" width="64" height="64" style="margin-bottom: 20px;">
</div>
<h1>Welcome to Gemini Native!</h1>
<p>Gemini Native has successfully installed and is now running in the system tray.</p>
<p>You can quickly access Gemini using these keyboard shortcuts:</p>
<p>
Press <span class="shortcut">Super + Shift + G</span> to open and focus the Gemini window.
</p>
<p>
Press <span class="shortcut">Super + Ctrl + G</span> to open the app and paste a screenshot of your current screen directly into Gemini.
</p>
<p class="footer-note">
Click the tray icon to show or hide the main window.
</p>
<button id="closeWindowButton" class="close-button">Got it, close this window</button>
</div>
<script>
document.getElementById('closeWindowButton').addEventListener('click', () => {
window.close();
});
</script>
</body>
</html>

38
forge.config.js Normal file
View file

@ -0,0 +1,38 @@
const { FusesPlugin } = require('@electron-forge/plugin-fuses');
const { FuseV1Options, FuseVersion } = require('@electron/fuses');
module.exports = {
packagerConfig: {
asar: true,
icon: './assets/icon'
},
rebuildConfig: {},
makers: [
{
name: '@electron-forge/maker-squirrel',
config: {
name: 'GeminiNative',
authors: 'Cameron Redmore',
description: 'A native Gemini client for Windows.',
setupIcon: './assets/icon.ico'
}
}
],
plugins: [
{
name: '@electron-forge/plugin-auto-unpack-natives',
config: {},
},
// Fuses are used to enable/disable various Electron functionality
// at package time, before code signing the application
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
}),
],
};

17
index.html Normal file
View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'">
<title>Gemini Opener</title>
</head>
<body>
<h1>Loading Gemini...</h1>
<!-- Renderer process messages (if any) will go here -->
<p id="info"></p>
<!-- You can also require other files to run in this process -->
<!-- <script src="./renderer.js"></script> -->
</body>
</html>

334
main.js Normal file
View file

@ -0,0 +1,334 @@
if (require('electron-squirrel-startup')) return;
const { app, BrowserWindow, globalShortcut, Tray, Menu, nativeImage, screen, clipboard, desktopCapturer } = require('electron');
const path = require('path');
const settingsManager = require('./src/settingsManager');
const { handleSquirrelEvent } = require('./src/squirrelEvents');
const { createWindow, getMainWindow, toggleWindowVisibility, positionWindowAtBottomCenter, createFirstRunWindow } = require('./src/windowManager');
const fs = require('fs'); // Ensure fs is required
// Handle Squirrel events for Windows installers
if (handleSquirrelEvent()) {
// Squirrel event handled (e.g., app installed, updated, or uninstalled), quit the app
return;
}
let tray = null;
let isQuitting = false; // Flag to differentiate between closing the window and quitting the app
async function captureAndPaste() {
const mainWindow = getMainWindow();
try {
const cursorPoint = screen.getCursorScreenPoint();
const display = screen.getDisplayNearestPoint(cursorPoint);
const sources = await desktopCapturer.getSources({ types: ['screen'], thumbnailSize: display.size });
const primaryDisplaySource = sources.find(source => source.display_id === display.id.toString());
if (primaryDisplaySource) {
const screenshot = primaryDisplaySource.thumbnail;
clipboard.writeImage(screenshot);
} else {
// TODO: Handle case where primary display source is not found
}
} catch (error) {
// TODO: Handle screenshot error
}
toggleWindowVisibility(); // Show the window after capturing
if (mainWindow && mainWindow.isVisible()) {
// Optional: Reload the page before pasting if the setting is enabled
if (settingsManager.getSetting('reloadOnPasteEnabled')) {
await mainWindow.webContents.reload();
await mainWindow.webContents.executeJavaScript('document.readyState === "complete"');
await new Promise(resolve => setTimeout(resolve, 250));
} else {
await mainWindow.webContents.executeJavaScript('document.readyState === "complete"');
await new Promise(resolve => setTimeout(resolve, 100));
}
}
if (mainWindow) {
// Simulate a paste action (Ctrl+V or Cmd+V)
mainWindow.webContents.sendInputEvent({
type: 'keyDown',
keyCode: 'v',
modifiers: process.platform === 'darwin' ? ['meta'] : ['control']
});
mainWindow.webContents.sendInputEvent({
type: 'keyUp',
keyCode: 'v',
modifiers: process.platform === 'darwin' ? ['meta'] : ['control']
});
}
}
function createAppWindow() {
const mainWindow = createWindow(
(event) => { // on-close handler
if (!isQuitting) {
event.preventDefault(); // Prevent the window from actually closing
getMainWindow().hide(); // Hide the window instead
if (!tray) { // Create tray icon if it doesn't exist
createTray();
}
}
},
() => { // on-blur handler
const mw = getMainWindow();
if (mw && mw.isVisible() && !isQuitting && settingsManager.getSetting('autoHideEnabled')) {
mw.hide();
if (!tray) {
createTray();
}
}
}
);
// Listen for 'before-input-event' to handle Escape key
if (mainWindow && mainWindow.webContents) {
mainWindow.webContents.on('before-input-event', (event, input) => {
if (input.key === 'Escape' && input.type === 'keyDown' && mainWindow.isFocused()) {
mainWindow.hide();
event.preventDefault(); // Prevent default Escape behavior (like closing modals)
}
});
}
}
function createTray() {
let iconPath;
try {
iconPath = path.join(__dirname, 'assets', 'icon.png');
const image = nativeImage.createFromPath(iconPath);
// Fallback to generic or minimal icon if the custom icon fails to load
if (image.isEmpty() && process.platform === 'win32') {
const genericIcon = nativeImage.createFromNamedImage('application-icon', [32, 32]);
tray = new Tray(genericIcon);
} else if (image.isEmpty()) {
const minimalIcon = nativeImage.createFromDataURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAE0lEQVR42mNkIAAYRxWAAQAG9AAFdG1U2AAAAABJRU5ErkJggg==');
tray = new Tray(minimalIcon);
}
else {
tray = new Tray(image);
}
} catch (e) {
const fallbackImage = nativeImage.createFromDataURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAE0lEQVR42mNkIAAYRxWAAQAG9AAFdG1U2AAAAABJRU5ErkJggg==');
tray = new Tray(fallbackImage);
}
// Read package.json to get the version
let buildTime = 'N/A';
try {
const packageJsonPath = path.join(app.getAppPath(), 'package.json');
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf-8');
const packageData = JSON.parse(packageJsonContent);
const version = packageData.version; // e.g., "1.20250517.172559"
if (version) {
const parts = version.split('.');
if (parts.length === 3) {
const datePart = parts[1]; // "YYYYMMDD"
const timePart = parts[2]; // "HHMMSS"
const year = datePart.substring(0, 4);
const month = datePart.substring(4, 6);
const day = datePart.substring(6, 8);
const hour = timePart.substring(0, 2);
const minute = timePart.substring(2, 4);
const second = timePart.substring(4, 6);
buildTime = `Build: ${year}-${month}-${day} ${hour}:${minute}:${second}`;
}
}
} catch (error) {
console.error('Failed to read or parse package.json for build time:', error);
// Keep buildTime as 'N/A' or some other default
}
const contextMenu = Menu.buildFromTemplate([
{ label: "Toggles", enabled: false },
{
label: 'Auto-Hide on Focus Loss',
type: 'checkbox',
checked: settingsManager.getSetting('autoHideEnabled'),
click: () => {
const newValue = !settingsManager.getSetting('autoHideEnabled');
settingsManager.updateSetting('autoHideEnabled', newValue);
},
},
{
label: 'Reload on Screenshot',
type: 'checkbox',
checked: settingsManager.getSetting('reloadOnPasteEnabled'),
click: () => {
const newValue = !settingsManager.getSetting('reloadOnPasteEnabled');
settingsManager.updateSetting('reloadOnPasteEnabled', newValue);
},
},
{
label: 'Always on Top',
type: 'checkbox',
checked: settingsManager.getSetting('isAlwaysOnTop'),
click: () => {
const newValue = !settingsManager.getSetting('isAlwaysOnTop');
settingsManager.updateSetting('isAlwaysOnTop', newValue);
if (mainWindow) {
mainWindow.setAlwaysOnTop(newValue);
}
},
},
{
label: 'Title Bar Visible',
type: 'checkbox',
checked: settingsManager.getSetting('isFrameVisible'),
click: () => {
const newValue = !settingsManager.getSetting('isFrameVisible');
settingsManager.updateSetting('isFrameVisible', newValue);
const mainWindow = getMainWindow();
if (mainWindow) {
mainWindow.destroy();
createAppWindow();
}
},
},
{
label: 'Launch on Startup',
type: 'checkbox',
checked: settingsManager.getSetting('launchOnStartup'),
click: () => {
const newValue = !settingsManager.getSetting('launchOnStartup');
settingsManager.updateSetting('launchOnStartup', newValue);
app.setLoginItemSettings({
openAtLogin: newValue,
path: app.getPath('exe'),
});
},
},
{ type: 'separator' }, // Optional: adds a line before the build time and Quit
{ label: buildTime, enabled: false },
{
label: 'Quit',
click: () => {
isQuitting = true;
app.quit();
},
},
]);
tray.setToolTip('Gemini');
tray.setContextMenu(contextMenu);
// Handle tray icon click: show/hide window or create if it doesn't exist
tray.on('click', () => {
const mainWindow = getMainWindow();
if (mainWindow) {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
} else {
createAppWindow();
}
});
}
Menu.setApplicationMenu(null)
app.on('ready', () => {
console.log(process.argv);
let firstRunWindow = null;
// Check for Squirrel events (like first run)
if (process.argv.includes('--squirrel-firstrun')) {
firstRunWindow = createFirstRunWindow(app);
}
createAppWindow();
// Register a global shortcut (Super+Shift+G) to toggle window visibility.
// 'Super' is the Windows key or Command key on macOS.
const retShowHide = globalShortcut.register('Super+Shift+G', async () => {
console.log('Toggle window visibility triggered');
toggleWindowVisibility();
if (firstRunWindow) {
firstRunWindow.close(); // Close the first run window if it exists
firstRunWindow = null; // Clear the reference
}
});
if (!retShowHide) {
// TODO: Handle failure to register shortcut
} else {
// Successfully registered
}
// Register a global shortcut (Super+Control+G) to capture screen and paste.
const retCapturePaste = globalShortcut.register('Super+Control+G', () => {
console.log('Capture and paste triggered');
captureAndPaste();
if (firstRunWindow) {
firstRunWindow.close(); // Close the first run window if it exists
firstRunWindow = null; // Clear the reference
}
});
if (!retCapturePaste) {
// TODO: Handle failure to register shortcut
} else {
// Successfully registered
}
// Set login item settings based on user preference (launch on startup)
app.setLoginItemSettings({
openAtLogin: settingsManager.getSetting('launchOnStartup'),
path: app.getPath('exe'),
});
createTray();
});
app.on('will-quit', () => {
// Unregister all global shortcuts when the application is about to quit
globalShortcut.unregisterAll();
});
app.on('window-all-closed', () => {
// Quit the app if isQuitting is true (user explicitly chose to quit)
// Otherwise, the app continues to run in the tray
if (isQuitting) {
app.quit();
}
});
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
const mainWindow = getMainWindow();
if (!mainWindow) {
createAppWindow();
} else {
mainWindow.show();
}
});
app.on('web-contents-created', (event, webContents) => {
webContents.on('will-navigate', (event) => {
// Only allow navigation to `google.com` or subdomains
const url = new URL(webContents.getURL());
if (url.hostname !== 'google.com' && !url.hostname.endsWith('.google.com')) {
event.preventDefault();
}
});
});
const assetsDir = path.join(__dirname, 'assets');
// Ensure assets directory exists
if (!fs.existsSync(assetsDir)) {
fs.mkdirSync(assetsDir);
}
const iconPath = path.join(assetsDir, 'icon.png');
// Create a default icon if it doesn't exist
if (!fs.existsSync(iconPath)) {
const base64Icon = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; // 1x1 transparent pixel
const buffer = Buffer.from(base64Icon, 'base64');
fs.writeFileSync(iconPath, buffer);
}

6500
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

35
package.json Normal file
View file

@ -0,0 +1,35 @@
{
"name": "gemini-native",
"version": "1.20250517.174518",
"description": "A native Electron application for Google Gemini.",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "electron-forge start",
"watch": "nodemon --watch . --exec \"electron .\"",
"changeVersion": "node changeversion.js",
"make": "npm run changeVersion && npm run make-internal",
"make-internal": "electron-forge make",
"firstrun": "nodemon --watch . --exec \"electron .\" --squirrel-firstrun"
},
"keywords": [],
"author": "Cameron Redmore",
"productName": "Gemini",
"license": "GPL-3.0-only",
"devDependencies": {
"@electron-forge/cli": "^7.8.1",
"@electron-forge/maker-appx": "^7.8.1",
"@electron-forge/maker-deb": "7.8.1",
"@electron-forge/maker-rpm": "7.8.1",
"@electron-forge/maker-squirrel": "7.8.1",
"@electron-forge/maker-zip": "7.8.1",
"@electron-forge/plugin-auto-unpack-natives": "7.8.1",
"@electron-forge/plugin-fuses": "7.8.1",
"@electron/fuses": "^1.8.0",
"electron": "^36.2.1",
"nodemon": "^3.1.10"
},
"dependencies": {
"electron-squirrel-startup": "^1.0.1"
}
}

16
preload.js Normal file
View file

@ -0,0 +1,16 @@
// Preload (Isolated World)
const { contextBridge, ipcRenderer } = require('electron');
// We are not exposing anything to the renderer process for this simple app.
// If we needed to, it would look something like this:
/*
contextBridge.exposeInMainWorld('electronAPI', {
// Example: expose a function to send a message to the main process
// sendMessage: (message) => ipcRenderer.send('some-channel', message),
// Example: expose a function to receive messages from the main process
// onMessage: (callback) => ipcRenderer.on('other-channel', (_event, ...args) => callback(...args)),
});
*/
console.log('Preload script loaded. Context isolation is on.');

67
src/settingsManager.js Normal file
View file

@ -0,0 +1,67 @@
const fs = require('fs');
const path = require('path');
const settingsFilePath = path.join(process.env.APPDATA || (process.platform === 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME + "/.config"), 'GeminiNative', 'settings.json');
const settingsDirPath = path.dirname(settingsFilePath);
const defaultSettings = {
autoHideEnabled: true,
reloadOnPasteEnabled: true,
isAlwaysOnTop: true,
isFrameVisible: false,
launchOnStartup: false, // Added: Setting for launch on startup
};
let currentSettings = { ...defaultSettings };
function ensureSettingsDirExists() {
if (!fs.existsSync(settingsDirPath)) {
fs.mkdirSync(settingsDirPath, { recursive: true });
}
}
function loadSettings() {
ensureSettingsDirExists();
try {
if (fs.existsSync(settingsFilePath)) {
const data = fs.readFileSync(settingsFilePath, 'utf-8');
const loadedSettings = JSON.parse(data);
currentSettings = { ...defaultSettings, ...loadedSettings };
} else {
// Save default settings if file doesn't exist
saveSettings();
}
} catch (error) {
console.error('Error loading settings, using defaults:', error);
currentSettings = { ...defaultSettings };
}
return currentSettings;
}
function saveSettings() {
ensureSettingsDirExists();
try {
const data = JSON.stringify(currentSettings, null, 2);
fs.writeFileSync(settingsFilePath, data, 'utf-8');
} catch (error) {
console.error('Error saving settings:', error);
}
}
// Initialize settings on load
loadSettings();
module.exports = {
getSetting: (key) => {
return currentSettings[key];
},
updateSetting: (key, value) => {
if (currentSettings[key] !== value) {
currentSettings[key] = value;
saveSettings();
}
},
getAllSettings: () => {
return { ...currentSettings };
}
};

54
src/squirrelEvents.js Normal file
View file

@ -0,0 +1,54 @@
const { app } = require('electron');
const ChildProcess = require('child_process');
const path = require('path');
const fs = require('fs');
function handleSquirrelEvent() {
if (process.argv.length === 1) {
return false;
}
const appFolder = path.resolve(process.execPath, '..');
const rootAtomFolder = path.resolve(appFolder, '..');
const updateDotExe = path.resolve(path.join(rootAtomFolder, 'Update.exe'));
const exeName = path.basename(process.execPath);
const spawn = function (command, args) {
let spawnedProcess;
try {
spawnedProcess = ChildProcess.spawn(command, args, { detached: true });
} catch (error) {
// Log error or handle appropriately
}
return spawnedProcess;
};
const spawnUpdate = function (args) {
return spawn(updateDotExe, args);
};
const squirrelEvent = process.argv[1];
switch (squirrelEvent) {
case '--squirrel-install':
case '--squirrel-updated':
spawnUpdate(['--createShortcut', exeName]);
const desktopShortcutPath = path.join(app.getPath('desktop'), `${exeName}.lnk`);
if (fs.existsSync(desktopShortcutPath)) {
fs.unlinkSync(desktopShortcutPath);
}
setTimeout(app.quit, 1000);
return true;
case '--squirrel-uninstall':
spawnUpdate(['--removeShortcut', exeName]);
setTimeout(app.quit, 1000);
return true;
case '--squirrel-obsolete':
app.quit();
return true;
}
return false; // Ensure a boolean is always returned
}
module.exports = { handleSquirrelEvent };

123
src/windowManager.js Normal file
View file

@ -0,0 +1,123 @@
const { BrowserWindow, screen } = require('electron');
const path = require('path');
const settingsManager = require('./settingsManager');
let mainWindow;
let firstRunWindow;
function positionWindowAtBottomCenter(window) {
const cursorPoint = screen.getCursorScreenPoint();
const display = screen.getDisplayNearestPoint(cursorPoint);
const { workArea } = display;
const [windowWidth, windowHeight] = window.getSize();
const x = Math.round(workArea.x + (workArea.width - windowWidth) / 2);
const y = Math.round(workArea.y + workArea.height - windowHeight - 8);
window.setPosition(x, y);
}
function createWindow(onCloseCallback, onBlurCallback) {
const initialWidth = 800;
const initialHeight = 900;
mainWindow = new BrowserWindow({
width: initialWidth,
height: initialHeight,
frame: settingsManager.getSetting('isFrameVisible'),
resizable: false,
alwaysOnTop: settingsManager.getSetting('isAlwaysOnTop'),
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
},
title: 'Gemini',
icon: path.join(__dirname, 'assets', 'icon.png'),
skipTaskbar: true,
show: false
});
positionWindowAtBottomCenter(mainWindow);
mainWindow.loadURL('https://gemini.google.com');
mainWindow.setMenuBarVisibility(false);
mainWindow.on('close', onCloseCallback);
mainWindow.on('closed', () => {
mainWindow = null;
});
mainWindow.on('blur', onBlurCallback);
return mainWindow;
}
function createFirstRunWindow(appInstance) {
firstRunWindow = new BrowserWindow({
width: 800,
height: 800,
frame: true,
resizable: false,
alwaysOnTop: true,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
},
show: false,
skipTaskbar: false,
autoHideMenuBar: true,
frame: false,
icon: path.join(__dirname, '..', 'assets', 'icon.png'), // Adjusted path for assets
});
firstRunWindow.loadFile(path.join(__dirname, '..', 'firstrun.html')); // Adjusted path for firstrun.html
firstRunWindow.once('ready-to-show', () => {
positionWindowAtBottomCenter(firstRunWindow);
firstRunWindow.show();
});
firstRunWindow.on('closed', () => {
firstRunWindow = null;
});
return firstRunWindow;
}
function getMainWindow() {
return mainWindow;
}
function toggleWindowVisibility() {
if (mainWindow) {
const cursorPoint = screen.getCursorScreenPoint();
const cursorDisplay = screen.getDisplayNearestPoint(cursorPoint);
const windowBounds = mainWindow.getBounds();
const windowDisplay = screen.getDisplayMatching(windowBounds);
if (mainWindow.isVisible() && mainWindow.isFocused() && windowDisplay.id === cursorDisplay.id) {
console.log("Hiding main window");
mainWindow.hide();
} else {
console.log("Showing main window");
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
mainWindow.show();
mainWindow.focus();
positionWindowAtBottomCenter(mainWindow);
}
} else {
// This case should ideally be handled by the caller,
// perhaps by calling createWindow if mainWindow is null.
// For now, let's log or decide on a consistent behavior.
console.log("Tried to toggle visibility but mainWindow is not defined.");
}
}
module.exports = {
createWindow,
getMainWindow,
toggleWindowVisibility,
positionWindowAtBottomCenter,
createFirstRunWindow
};