Added settings page and alternate provider support.

This commit is contained in:
Cameron Redmore 2025-05-21 10:51:53 +01:00
parent b022bca449
commit bc62e0d059
6 changed files with 364 additions and 12 deletions

135
main.js
View file

@ -1,6 +1,6 @@
if (require('electron-squirrel-startup')) return;
const { app, BrowserWindow, globalShortcut, Tray, Menu, nativeImage, screen, clipboard, desktopCapturer } = require('electron');
const { app, BrowserWindow, globalShortcut, Tray, Menu, nativeImage, screen, clipboard, desktopCapturer, ipcMain } = require('electron');
const path = require('path');
const settingsManager = require('./src/settingsManager');
const { handleSquirrelEvent } = require('./src/squirrelEvents');
@ -15,6 +15,7 @@ if (handleSquirrelEvent()) {
let tray = null;
let isQuitting = false; // Flag to differentiate between closing the window and quitting the app
let settingsWindow = null;
async function captureAndPaste() {
const mainWindow = getMainWindow();
@ -95,6 +96,33 @@ function createAppWindow() {
}
});
}
return mainWindow; // Ensure mainWindow is returned
}
function createSettingsWindow() {
if (settingsWindow) {
settingsWindow.focus();
return;
}
settingsWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'), // Optional: if you need preload for settings
nodeIntegration: true, // Required for ipcRenderer in settingsRenderer.js
contextIsolation: false, // Required for ipcRenderer in settingsRenderer.js
},
parent: getMainWindow(), // Optional: makes it a child of the main window
modal: false, // Set to true if you want it to be a modal dialog
autoHideMenuBar: true,
});
settingsWindow.loadFile(path.join(__dirname, 'settings.html'));
settingsWindow.on('closed', () => {
settingsWindow = null;
});
}
function createTray() {
@ -203,7 +231,34 @@ function createTray() {
});
},
},
{ type: 'separator' }, // Optional: adds a line before the build time and Quit
{ type: 'separator' },
{
label: 'Select Provider',
submenu: settingsManager.getAllSettings().providers.map(provider => ({
label: provider.name,
type: 'radio',
checked: settingsManager.getSetting('currentProvider') === provider.name,
click: () => {
settingsManager.updateSetting('currentProvider', provider.name);
const mainWindow = getMainWindow();
if (mainWindow) {
mainWindow.loadURL(provider.url);
}
// Rebuild tray to reflect change (important for radio button state)
if (tray) {
tray.destroy();
createTray();
}
}
}))
},
{ type: 'separator' },
{
label: 'Settings',
click: () => {
createSettingsWindow();
}
},
{ label: buildTime, enabled: false },
{
label: 'Quit',
@ -233,6 +288,66 @@ Menu.setApplicationMenu(null)
app.on('ready', () => {
console.log(process.argv);
// IPC Handlers for settings
ipcMain.on('get-all-settings', (event) => {
event.returnValue = settingsManager.getAllSettings();
});
ipcMain.on('update-setting', (event, { key, value }) => {
settingsManager.updateSetting(key, value);
// Optionally, notify other windows if needed, e.g., main window for 'isAlwaysOnTop'
if (key === 'isAlwaysOnTop') {
const mainWindow = getMainWindow();
if (mainWindow) {
mainWindow.setAlwaysOnTop(value);
}
}
if (key === 'launchOnStartup') {
app.setLoginItemSettings({
openAtLogin: value,
path: app.getPath('exe'),
});
}
// If currentProvider changes, update the main window URL
if (key === 'currentProvider') {
const mainWindow = getMainWindow();
if (mainWindow) {
const providerUrl = settingsManager.getCurrentProviderUrl();
mainWindow.loadURL(providerUrl);
}
// Rebuild tray to reflect change in provider selection
if (tray) {
tray.destroy();
createTray(); // This will rebuild the menu with the correct checked state
}
}
// Notify settings window to reload if changes can affect it directly
if (settingsWindow) {
settingsWindow.webContents.send('settings-updated');
}
});
ipcMain.on('add-provider', (event, provider) => {
settingsManager.addProvider(provider);
if (settingsWindow) {
settingsWindow.webContents.send('settings-updated'); // Refresh provider list
}
});
ipcMain.on('delete-provider', (event, index) => {
settingsManager.deleteProvider(index);
if (settingsWindow) {
settingsWindow.webContents.send('settings-updated'); // Refresh provider list
}
});
ipcMain.on('update-provider', (event, { index, name, url }) => {
settingsManager.updateProvider(index, { name, url });
if (settingsWindow) {
settingsWindow.webContents.send('settings-updated'); // Refresh provider list
}
});
let firstRunWindow = null;
// Check for Squirrel events (like first run)
@ -284,6 +399,15 @@ app.on('ready', () => {
});
createTray();
// Listen for current-provider-changed event to update the main window URL
ipcMain.on('current-provider-changed', () => {
const mainWindow = getMainWindow();
if (mainWindow) {
const providerUrl = settingsManager.getCurrentProviderUrl();
mainWindow.loadURL(providerUrl);
}
});
});
app.on('will-quit', () => {
@ -311,13 +435,6 @@ app.on('activate', () => {
});
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');

View file

@ -1,6 +1,6 @@
{
"name": "gemini-native",
"version": "1.20250517.174518",
"version": "1.20250519.193208",
"description": "A native Electron application for Google Gemini.",
"main": "main.js",
"scripts": {

84
settings.html Normal file
View file

@ -0,0 +1,84 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Settings</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self';">
<style>
body { font-family: sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
.container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
h1 { color: #333; }
.setting { margin-bottom: 15px; display: flex; align-items: center; }
.setting label { margin-left: 10px; flex-grow: 1;}
.setting input[type="checkbox"] { width: 18px; height: 18px; }
.section { margin-top: 20px; border-top: 1px solid #eee; padding-top: 20px; }
h2 { color: #555; margin-bottom: 10px;}
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f0f0f0; }
.actions button { margin-right: 5px; padding: 5px 10px; cursor: pointer; }
.add-provider-form input[type="text"] { padding: 8px; margin-right: 10px; border: 1px solid #ccc; border-radius: 4px; }
.add-provider-form button { padding: 8px 15px; }
</style>
</head>
<body>
<div class="container">
<h1>Settings</h1>
<div class="section">
<h2>General</h2>
<div class="setting">
<input type="checkbox" id="autoHideEnabled">
<label for="autoHideEnabled">Auto-Hide on Focus Loss</label>
</div>
<div class="setting">
<input type="checkbox" id="reloadOnPasteEnabled">
<label for="reloadOnPasteEnabled">Reload on Screenshot/Paste</label>
</div>
<div class="setting">
<input type="checkbox" id="isAlwaysOnTop">
<label for="isAlwaysOnTop">Always on Top</label>
</div>
<div class="setting">
<input type="checkbox" id="isFrameVisible">
<label for="isFrameVisible">Show Title Bar (Requires Restart)</label>
</div>
<div class="setting">
<input type="checkbox" id="launchOnStartup">
<label for="launchOnStartup">Launch on Startup</label>
</div>
</div>
<div class="section">
<h2>Current Provider</h2>
<div class="setting">
<select id="currentProviderSelect" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc; width: 100%;">
<!-- Options will be populated by settingsRenderer.js -->
</select>
</div>
</div>
<div class="section">
<h2>Providers</h2>
<table id="providersTable">
<thead>
<tr>
<th>Name</th>
<th>URL</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- Provider rows will be inserted here -->
</tbody>
</table>
<div class="add-provider-form" style="margin-top: 15px;">
<input type="text" id="newProviderName" placeholder="Provider Name">
<input type="text" id="newProviderUrl" placeholder="Provider URL">
<button id="addProviderBtn">Add Provider</button>
</div>
</div>
</div>
<script src="./src/settingsRenderer.js"></script>
</body>
</html>

View file

@ -9,7 +9,18 @@ const defaultSettings = {
reloadOnPasteEnabled: true,
isAlwaysOnTop: true,
isFrameVisible: false,
launchOnStartup: false, // Added: Setting for launch on startup
launchOnStartup: false,
currentProvider: "Gemini",
providers: [
{name: "Gemini", url: "https://gemini.google.com"},
{name: "ChatGPT", url: "https://chat.openai.com"},
{name: "Bing Copilot", url: "https://www.bing.com/chat"},
{name: "Claude", url: "https://claude.ai"},
{name: "Qwen", url: "https://chat.qwen.ai/"},
{name: "Perplexity", url: "https://www.perplexity.ai"},
]
};
let currentSettings = { ...defaultSettings };
@ -63,5 +74,26 @@ module.exports = {
},
getAllSettings: () => {
return { ...currentSettings };
},
addProvider: (provider) => {
currentSettings.providers.push(provider);
saveSettings();
},
deleteProvider: (index) => {
if (index >= 0 && index < currentSettings.providers.length) {
currentSettings.providers.splice(index, 1);
saveSettings();
}
},
updateProvider: (index, provider) => {
if (index >= 0 && index < currentSettings.providers.length) {
currentSettings.providers[index] = provider;
saveSettings();
}
},
getCurrentProviderUrl: () => {
const currentProviderName = currentSettings.currentProvider;
const provider = currentSettings.providers.find(p => p.name === currentProviderName);
return provider ? provider.url : (currentSettings.providers[0] ? currentSettings.providers[0].url : 'https://gemini.google.com'); // Fallback
}
};

117
src/settingsRenderer.js Normal file
View file

@ -0,0 +1,117 @@
// This script will handle the renderer process for the settings.html page.
const { ipcRenderer } = require('electron');
document.addEventListener('DOMContentLoaded', () => {
loadSettings();
setupEventListeners();
});
function loadSettings() {
const settings = ipcRenderer.sendSync('get-all-settings'); // Synchronous call to get settings
// General settings
document.getElementById('autoHideEnabled').checked = settings.autoHideEnabled;
document.getElementById('reloadOnPasteEnabled').checked = settings.reloadOnPasteEnabled;
document.getElementById('isAlwaysOnTop').checked = settings.isAlwaysOnTop;
document.getElementById('isFrameVisible').checked = settings.isFrameVisible;
document.getElementById('launchOnStartup').checked = settings.launchOnStartup;
// Current Provider Dropdown
const currentProviderSelect = document.getElementById('currentProviderSelect');
currentProviderSelect.innerHTML = ''; // Clear existing options
settings.providers.forEach(provider => {
const option = document.createElement('option');
option.value = provider.name;
option.textContent = provider.name;
if (provider.name === settings.currentProvider) {
option.selected = true;
}
currentProviderSelect.appendChild(option);
});
// Providers Table
const providersTableBody = document.getElementById('providersTable').getElementsByTagName('tbody')[0];
providersTableBody.innerHTML = ''; // Clear existing rows
settings.providers.forEach((provider, index) => {
const row = providersTableBody.insertRow();
row.insertCell().textContent = provider.name;
row.insertCell().textContent = provider.url;
const actionsCell = row.insertCell();
const deleteButton = document.createElement('button');
deleteButton.textContent = 'Delete';
deleteButton.onclick = () => deleteProvider(index);
actionsCell.appendChild(deleteButton);
// Add an edit button (optional, for more advanced CRUD)
// const editButton = document.createElement('button');
// editButton.textContent = 'Edit';
// editButton.onclick = () => editProvider(index);
// actionsCell.appendChild(editButton);
});
}
function setupEventListeners() {
// General settings listeners
document.getElementById('autoHideEnabled').addEventListener('change', (event) => {
ipcRenderer.send('update-setting', { key: 'autoHideEnabled', value: event.target.checked });
});
document.getElementById('reloadOnPasteEnabled').addEventListener('change', (event) => {
ipcRenderer.send('update-setting', { key: 'reloadOnPasteEnabled', value: event.target.checked });
});
document.getElementById('isAlwaysOnTop').addEventListener('change', (event) => {
ipcRenderer.send('update-setting', { key: 'isAlwaysOnTop', value: event.target.checked });
});
document.getElementById('isFrameVisible').addEventListener('change', (event) => {
ipcRenderer.send('update-setting', { key: 'isFrameVisible', value: event.target.checked });
});
document.getElementById('launchOnStartup').addEventListener('change', (event) => {
ipcRenderer.send('update-setting', { key: 'launchOnStartup', value: event.target.checked });
});
// Current provider select listener
document.getElementById('currentProviderSelect').addEventListener('change', (event) => {
ipcRenderer.send('update-setting', { key: 'currentProvider', value: event.target.value });
// Optionally, send a specific event if the main window needs to react immediately without a full settings reload signal
ipcRenderer.send('current-provider-changed');
});
// Provider table listeners
document.getElementById('addProviderBtn').addEventListener('click', () => {
const nameInput = document.getElementById('newProviderName');
const urlInput = document.getElementById('newProviderUrl');
const name = nameInput.value.trim();
const url = urlInput.value.trim();
if (name && url) {
ipcRenderer.send('add-provider', { name, url });
nameInput.value = ''; // Clear input
urlInput.value = ''; // Clear input
loadSettings(); // Reload to show the new provider
}
});
}
function deleteProvider(index) {
ipcRenderer.send('delete-provider', index);
loadSettings(); // Reload to reflect deletion
}
// Example of editProvider (requires more UI/logic for editing)
/*
function editProvider(index) {
const provider = ipcRenderer.sendSync('get-all-settings').providers[index];
const newName = prompt('Enter new name:', provider.name);
const newUrl = prompt('Enter new URL:', provider.url);
if (newName !== null && newUrl !== null) {
ipcRenderer.send('update-provider', { index, name: newName, url: newUrl });
loadSettings();
}
}
*/
// Listen for settings-updated from main process if other windows can change settings
ipcRenderer.on('settings-updated', () => {
loadSettings();
});

View file

@ -39,7 +39,9 @@ function createWindow(onCloseCallback, onBlurCallback) {
});
positionWindowAtBottomCenter(mainWindow);
mainWindow.loadURL('https://gemini.google.com');
// Load the URL of the current provider
const providerUrl = settingsManager.getCurrentProviderUrl();
mainWindow.loadURL(providerUrl);
mainWindow.setMenuBarVisibility(false);
mainWindow.on('close', onCloseCallback);