Files
moreminimore-vibe/src/main.ts
Mohamed Aziz Mejri ba115605b5 Set minimum window size to prevent UI breakage (#1195)
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. -->
2025-09-05 23:59:07 -07:00

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.