This PR solves issue #1194 by setting a minimum height and width <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Set a minimum window size to prevent UI breakage when the app is resized too small. The BrowserWindow now enforces minWidth 800 and minHeight 500 so layouts stay stable and controls remain accessible. <!-- End of auto-generated description by cubic. -->
288 lines
8.5 KiB
TypeScript
288 lines
8.5 KiB
TypeScript
import { app, BrowserWindow, dialog } from "electron";
|
|
import * as path from "node:path";
|
|
import { registerIpcHandlers } from "./ipc/ipc_host";
|
|
import dotenv from "dotenv";
|
|
// @ts-ignore
|
|
import started from "electron-squirrel-startup";
|
|
import { updateElectronApp, UpdateSourceType } from "update-electron-app";
|
|
import log from "electron-log";
|
|
import {
|
|
getSettingsFilePath,
|
|
readSettings,
|
|
writeSettings,
|
|
} from "./main/settings";
|
|
import { handleSupabaseOAuthReturn } from "./supabase_admin/supabase_return_handler";
|
|
import { handleDyadProReturn } from "./main/pro";
|
|
import { IS_TEST_BUILD } from "./ipc/utils/test_utils";
|
|
import { BackupManager } from "./backup_manager";
|
|
import { getDatabasePath, initializeDatabase } from "./db";
|
|
import { UserSettings } from "./lib/schemas";
|
|
import { handleNeonOAuthReturn } from "./neon_admin/neon_return_handler";
|
|
|
|
log.errorHandler.startCatching();
|
|
log.eventLogger.startLogging();
|
|
log.scope.labelPadding = false;
|
|
|
|
const logger = log.scope("main");
|
|
|
|
// Load environment variables from .env file
|
|
dotenv.config();
|
|
|
|
// Register IPC handlers before app is ready
|
|
registerIpcHandlers();
|
|
|
|
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
|
if (started) {
|
|
app.quit();
|
|
}
|
|
|
|
// https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app#main-process-mainjs
|
|
if (process.defaultApp) {
|
|
if (process.argv.length >= 2) {
|
|
app.setAsDefaultProtocolClient("dyad", process.execPath, [
|
|
path.resolve(process.argv[1]),
|
|
]);
|
|
}
|
|
} else {
|
|
app.setAsDefaultProtocolClient("dyad");
|
|
}
|
|
|
|
export async function onReady() {
|
|
try {
|
|
const backupManager = new BackupManager({
|
|
settingsFile: getSettingsFilePath(),
|
|
dbFile: getDatabasePath(),
|
|
});
|
|
await backupManager.initialize();
|
|
} catch (e) {
|
|
logger.error("Error initializing backup manager", e);
|
|
}
|
|
initializeDatabase();
|
|
const settings = readSettings();
|
|
await onFirstRunMaybe(settings);
|
|
createWindow();
|
|
|
|
logger.info("Auto-update enabled=", settings.enableAutoUpdate);
|
|
if (settings.enableAutoUpdate) {
|
|
// Technically we could just pass the releaseChannel directly to the host,
|
|
// but this is more explicit and falls back to stable if there's an unknown
|
|
// release channel.
|
|
const postfix = settings.releaseChannel === "beta" ? "beta" : "stable";
|
|
const host = `https://api.dyad.sh/v1/update/${postfix}`;
|
|
logger.info("Auto-update release channel=", postfix);
|
|
updateElectronApp({
|
|
logger,
|
|
updateSource: {
|
|
type: UpdateSourceType.ElectronPublicUpdateService,
|
|
repo: "dyad-sh/dyad",
|
|
host,
|
|
},
|
|
}); // additional configuration options available
|
|
}
|
|
}
|
|
|
|
export async function onFirstRunMaybe(settings: UserSettings) {
|
|
if (!settings.hasRunBefore) {
|
|
await promptMoveToApplicationsFolder();
|
|
writeSettings({
|
|
hasRunBefore: true,
|
|
});
|
|
}
|
|
if (IS_TEST_BUILD) {
|
|
writeSettings({
|
|
isTestMode: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ask the user if the app should be moved to the
|
|
* applications folder.
|
|
*/
|
|
async function promptMoveToApplicationsFolder(): Promise<void> {
|
|
// Why not in e2e tests?
|
|
// There's no way to stub this dialog in time, so we just skip it
|
|
// in e2e testing mode.
|
|
if (IS_TEST_BUILD) return;
|
|
if (process.platform !== "darwin") return;
|
|
if (app.isInApplicationsFolder()) return;
|
|
logger.log("Prompting user to move to applications folder");
|
|
|
|
const { response } = await dialog.showMessageBox({
|
|
type: "question",
|
|
buttons: ["Move to Applications Folder", "Do Not Move"],
|
|
defaultId: 0,
|
|
message: "Move to Applications Folder? (required for auto-update)",
|
|
});
|
|
|
|
if (response === 0) {
|
|
logger.log("User chose to move to applications folder");
|
|
app.moveToApplicationsFolder();
|
|
} else {
|
|
logger.log("User chose not to move to applications folder");
|
|
}
|
|
}
|
|
|
|
declare global {
|
|
const MAIN_WINDOW_VITE_DEV_SERVER_URL: string;
|
|
}
|
|
|
|
let mainWindow: BrowserWindow | null = null;
|
|
|
|
const createWindow = () => {
|
|
// Create the browser window.
|
|
mainWindow = new BrowserWindow({
|
|
width: process.env.NODE_ENV === "development" ? 1280 : 960,
|
|
minWidth: 800,
|
|
height: 700,
|
|
minHeight: 500,
|
|
titleBarStyle: "hidden",
|
|
titleBarOverlay: false,
|
|
trafficLightPosition: {
|
|
x: 10,
|
|
y: 8,
|
|
},
|
|
webPreferences: {
|
|
nodeIntegration: false,
|
|
contextIsolation: true,
|
|
preload: path.join(__dirname, "preload.js"),
|
|
// transparent: true,
|
|
},
|
|
// backgroundColor: "#00000001",
|
|
// frame: false,
|
|
});
|
|
// and load the index.html of the app.
|
|
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
|
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
|
|
} else {
|
|
mainWindow.loadFile(
|
|
path.join(__dirname, "../renderer/main_window/index.html"),
|
|
);
|
|
}
|
|
if (process.env.NODE_ENV === "development") {
|
|
// Open the DevTools.
|
|
mainWindow.webContents.openDevTools();
|
|
}
|
|
};
|
|
|
|
const gotTheLock = app.requestSingleInstanceLock();
|
|
|
|
if (!gotTheLock) {
|
|
app.quit();
|
|
} else {
|
|
app.on("second-instance", (_event, commandLine, _workingDirectory) => {
|
|
// Someone tried to run a second instance, we should focus our window.
|
|
if (mainWindow) {
|
|
if (mainWindow.isMinimized()) mainWindow.restore();
|
|
mainWindow.focus();
|
|
}
|
|
// the commandLine is array of strings in which last element is deep link url
|
|
handleDeepLinkReturn(commandLine.pop()!);
|
|
});
|
|
app.whenReady().then(onReady);
|
|
}
|
|
|
|
// Handle the protocol. In this case, we choose to show an Error Box.
|
|
app.on("open-url", (event, url) => {
|
|
handleDeepLinkReturn(url);
|
|
});
|
|
|
|
function handleDeepLinkReturn(url: string) {
|
|
// example url: "dyad://supabase-oauth-return?token=a&refreshToken=b"
|
|
let parsed: URL;
|
|
try {
|
|
parsed = new URL(url);
|
|
} catch {
|
|
log.info("Invalid deep link URL", url);
|
|
return;
|
|
}
|
|
|
|
// Intentionally do NOT log the full URL which may contain sensitive tokens.
|
|
log.log(
|
|
"Handling deep link: protocol",
|
|
parsed.protocol,
|
|
"hostname",
|
|
parsed.hostname,
|
|
);
|
|
if (parsed.protocol !== "dyad:") {
|
|
dialog.showErrorBox(
|
|
"Invalid Protocol",
|
|
`Expected dyad://, got ${parsed.protocol}. Full URL: ${url}`,
|
|
);
|
|
return;
|
|
}
|
|
if (parsed.hostname === "neon-oauth-return") {
|
|
const token = parsed.searchParams.get("token");
|
|
const refreshToken = parsed.searchParams.get("refreshToken");
|
|
const expiresIn = Number(parsed.searchParams.get("expiresIn"));
|
|
if (!token || !refreshToken || !expiresIn) {
|
|
dialog.showErrorBox(
|
|
"Invalid URL",
|
|
"Expected token, refreshToken, and expiresIn",
|
|
);
|
|
return;
|
|
}
|
|
handleNeonOAuthReturn({ token, refreshToken, expiresIn });
|
|
// Send message to renderer to trigger re-render
|
|
mainWindow?.webContents.send("deep-link-received", {
|
|
type: parsed.hostname,
|
|
});
|
|
return;
|
|
}
|
|
if (parsed.hostname === "supabase-oauth-return") {
|
|
const token = parsed.searchParams.get("token");
|
|
const refreshToken = parsed.searchParams.get("refreshToken");
|
|
const expiresIn = Number(parsed.searchParams.get("expiresIn"));
|
|
if (!token || !refreshToken || !expiresIn) {
|
|
dialog.showErrorBox(
|
|
"Invalid URL",
|
|
"Expected token, refreshToken, and expiresIn",
|
|
);
|
|
return;
|
|
}
|
|
handleSupabaseOAuthReturn({ token, refreshToken, expiresIn });
|
|
// Send message to renderer to trigger re-render
|
|
mainWindow?.webContents.send("deep-link-received", {
|
|
type: parsed.hostname,
|
|
});
|
|
return;
|
|
}
|
|
// dyad://dyad-pro-return?key=123&budget_reset_at=2025-05-26T16:31:13.492000Z&max_budget=100
|
|
if (parsed.hostname === "dyad-pro-return") {
|
|
const apiKey = parsed.searchParams.get("key");
|
|
if (!apiKey) {
|
|
dialog.showErrorBox("Invalid URL", "Expected key");
|
|
return;
|
|
}
|
|
handleDyadProReturn({
|
|
apiKey,
|
|
});
|
|
// Send message to renderer to trigger re-render
|
|
mainWindow?.webContents.send("deep-link-received", {
|
|
type: parsed.hostname,
|
|
});
|
|
return;
|
|
}
|
|
dialog.showErrorBox("Invalid deep link URL", url);
|
|
}
|
|
|
|
// Quit when all windows are closed, except on macOS. There, it's common
|
|
// for applications and their menu bar to stay active until the user quits
|
|
// explicitly with Cmd + Q.
|
|
app.on("window-all-closed", () => {
|
|
if (process.platform !== "darwin") {
|
|
app.quit();
|
|
}
|
|
});
|
|
|
|
app.on("activate", () => {
|
|
// On OS X it's common to re-create a window in the app when the
|
|
// dock icon is clicked and there are no other windows open.
|
|
if (BrowserWindow.getAllWindows().length === 0) {
|
|
createWindow();
|
|
}
|
|
});
|
|
|
|
// In this file you can include the rest of your app's specific main process
|
|
// code. You can also put them in separate files and import them here.
|