334 lines
11 KiB
JavaScript
334 lines
11 KiB
JavaScript
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);
|
|
}
|