mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 05:45:04 +03:00
## Critical Fixes - #1: Server readiness — waitForServer() polls before loading window - #2: Restart timeout — 5s + SIGKILL prevents IPC handler from hanging - #3: changePort — now stops/restarts server on new port ## Important Fixes - #4: Tray cleanup — destroy old Tray before recreating - #5: IPC emission — server-status & port-changed events - #6: Disposer pattern — replaces removeAllListeners - #7: useSyncExternalStore — eliminates 5x re-renders ## Minor: #8-#16 (dead code, CSP, platform titlebar, types, errors, version) Tests: 76 / 15 suites (was 64/9)
198 lines
4.8 KiB
TypeScript
198 lines
4.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useCallback, useSyncExternalStore } from "react";
|
|
|
|
/**
|
|
* Code Review Fixes Applied:
|
|
* #7 useIsElectron — useSyncExternalStore for zero re-renders
|
|
* #11 Import AppInfo type instead of inline duplication
|
|
* #12 useDataDir — add error state (was swallowed silently)
|
|
*/
|
|
|
|
// ── Fix #7: Module-level detection (no state, no re-renders) ──
|
|
|
|
function getIsElectronSnapshot(): boolean {
|
|
return typeof window !== "undefined" && window.electronAPI?.isElectron === true;
|
|
}
|
|
|
|
function getServerSnapshot(): boolean {
|
|
return false; // SSR always returns false
|
|
}
|
|
|
|
const noop = () => () => {};
|
|
|
|
/**
|
|
* Check if running in Electron — zero re-renders via useSyncExternalStore
|
|
*/
|
|
export function useIsElectron(): boolean {
|
|
return useSyncExternalStore(noop, getIsElectronSnapshot, getServerSnapshot);
|
|
}
|
|
|
|
/**
|
|
* App info shape from Electron main process
|
|
* Fix #11: Single source of truth (matches electron/types.d.ts)
|
|
*/
|
|
interface AppInfo {
|
|
name: string;
|
|
version: string;
|
|
platform: string;
|
|
isDev: boolean;
|
|
port: number;
|
|
}
|
|
|
|
/**
|
|
* Get Electron app information
|
|
*/
|
|
export function useElectronAppInfo() {
|
|
const hasApi = getIsElectronSnapshot();
|
|
const [appInfo, setAppInfo] = useState<AppInfo | null>(null);
|
|
const [loading, setLoading] = useState(hasApi);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (typeof window === "undefined" || !window.electronAPI) return;
|
|
|
|
window.electronAPI
|
|
.getAppInfo()
|
|
.then((info) => {
|
|
setAppInfo(info);
|
|
setLoading(false);
|
|
})
|
|
.catch((err) => {
|
|
setError(err);
|
|
setLoading(false);
|
|
});
|
|
}, []);
|
|
|
|
return { appInfo, loading, error };
|
|
}
|
|
|
|
/**
|
|
* Get the data directory path
|
|
* Fix #12: Now exposes error state (was swallowed silently)
|
|
*/
|
|
export function useDataDir() {
|
|
const hasApi = getIsElectronSnapshot();
|
|
const [dataDir, setDataDir] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(hasApi);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (typeof window === "undefined" || !window.electronAPI) return;
|
|
|
|
window.electronAPI
|
|
.getDataDir()
|
|
.then((dir) => {
|
|
setDataDir(dir);
|
|
setLoading(false);
|
|
})
|
|
.catch((err) => {
|
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
setLoading(false);
|
|
});
|
|
}, []);
|
|
|
|
return { dataDir, loading, error };
|
|
}
|
|
|
|
/**
|
|
* Window controls for Electron
|
|
*/
|
|
export function useWindowControls() {
|
|
const isElectron = useIsElectron();
|
|
|
|
const minimize = useCallback(() => {
|
|
if (isElectron && window.electronAPI) {
|
|
window.electronAPI.minimizeWindow();
|
|
}
|
|
}, [isElectron]);
|
|
|
|
const maximize = useCallback(() => {
|
|
if (isElectron && window.electronAPI) {
|
|
window.electronAPI.maximizeWindow();
|
|
}
|
|
}, [isElectron]);
|
|
|
|
const close = useCallback(() => {
|
|
if (isElectron && window.electronAPI) {
|
|
window.electronAPI.closeWindow();
|
|
}
|
|
}, [isElectron]);
|
|
|
|
return { isElectron, minimize, maximize, close };
|
|
}
|
|
|
|
/**
|
|
* Open external URL in default browser
|
|
*/
|
|
export function useOpenExternal() {
|
|
const isElectron = useIsElectron();
|
|
|
|
const openExternal = useCallback(
|
|
async (url: string) => {
|
|
if (isElectron && window.electronAPI) {
|
|
await window.electronAPI.openExternal(url);
|
|
} else {
|
|
window.open(url, "_blank", "noopener,noreferrer");
|
|
}
|
|
},
|
|
[isElectron]
|
|
);
|
|
|
|
return { openExternal };
|
|
}
|
|
|
|
/**
|
|
* Server controls for Electron
|
|
*/
|
|
export function useServerControls() {
|
|
const isElectron = useIsElectron();
|
|
const [restarting, setRestarting] = useState(false);
|
|
|
|
const restart = useCallback(async () => {
|
|
if (!isElectron || !window.electronAPI) {
|
|
return { success: false };
|
|
}
|
|
|
|
setRestarting(true);
|
|
try {
|
|
const result = await window.electronAPI.restartServer();
|
|
return result;
|
|
} finally {
|
|
setRestarting(false);
|
|
}
|
|
}, [isElectron]);
|
|
|
|
return { isElectron, restart, restarting };
|
|
}
|
|
|
|
/**
|
|
* Listen for server status updates
|
|
* Fix #6: Uses disposer returned by preload for precise cleanup
|
|
*/
|
|
export function useServerStatus(onStatus: (status: { status: string; port: number }) => void) {
|
|
const isElectron = useIsElectron();
|
|
|
|
useEffect(() => {
|
|
if (!isElectron || !window.electronAPI) return;
|
|
|
|
const dispose = window.electronAPI.onServerStatus(onStatus);
|
|
return dispose;
|
|
}, [isElectron, onStatus]);
|
|
}
|
|
|
|
/**
|
|
* Listen for port changes
|
|
* Fix #6: Uses disposer returned by preload for precise cleanup
|
|
*/
|
|
export function usePortChanged(onPortChanged: (port: number) => void) {
|
|
const isElectron = useIsElectron();
|
|
|
|
useEffect(() => {
|
|
if (!isElectron || !window.electronAPI) return;
|
|
|
|
const dispose = window.electronAPI.onPortChanged(onPortChanged);
|
|
return dispose;
|
|
}, [isElectron, onPortChanged]);
|
|
}
|