diff --git a/components/settings-dialog.tsx b/components/settings-dialog.tsx index 0dae69d..1c8fbab 100644 --- a/components/settings-dialog.tsx +++ b/components/settings-dialog.tsx @@ -3,6 +3,7 @@ import { Github, Info, Moon, Sun, Tag } from "lucide-react" import { usePathname, useRouter, useSearchParams } from "next/navigation" import { Suspense, useEffect, useState } from "react" +import { toast } from "sonner" import { Button } from "@/components/ui/button" import { Dialog, @@ -103,6 +104,11 @@ function SettingsContent({ ) const [currentLang, setCurrentLang] = useState("en") + // Proxy settings state (Electron only) + const [httpProxy, setHttpProxy] = useState("") + const [httpsProxy, setHttpsProxy] = useState("") + const [isApplyingProxy, setIsApplyingProxy] = useState(false) + useEffect(() => { // Only fetch if not cached in localStorage if (getStoredAccessCodeRequired() !== null) return @@ -150,6 +156,14 @@ function SettingsContent({ setCloseProtection(storedCloseProtection !== "false") setError("") + + // Load proxy settings (Electron only) + if (window.electronAPI?.getProxy) { + window.electronAPI.getProxy().then((config) => { + setHttpProxy(config.httpProxy || "") + setHttpsProxy(config.httpsProxy || "") + }) + } } }, [open]) @@ -208,6 +222,46 @@ function SettingsContent({ } } + const handleApplyProxy = async () => { + if (!window.electronAPI?.setProxy) return + + // Validate proxy URLs (must start with http:// or https://) + const validateProxyUrl = (url: string): boolean => { + if (!url) return true // Empty is OK + return url.startsWith("http://") || url.startsWith("https://") + } + + const trimmedHttp = httpProxy.trim() + const trimmedHttps = httpsProxy.trim() + + if (trimmedHttp && !validateProxyUrl(trimmedHttp)) { + toast.error("HTTP Proxy must start with http:// or https://") + return + } + if (trimmedHttps && !validateProxyUrl(trimmedHttps)) { + toast.error("HTTPS Proxy must start with http:// or https://") + return + } + + setIsApplyingProxy(true) + try { + const result = await window.electronAPI.setProxy({ + httpProxy: trimmedHttp || undefined, + httpsProxy: trimmedHttps || undefined, + }) + + if (result.success) { + toast.success(dict.settings.proxyApplied) + } else { + toast.error(result.error || "Failed to apply proxy settings") + } + } catch { + toast.error("Failed to apply proxy settings") + } finally { + setIsApplyingProxy(false) + } + } + return ( {/* Header */} @@ -370,6 +424,54 @@ function SettingsContent({ + + {/* Proxy Settings - Electron only */} + {typeof window !== "undefined" && + window.electronAPI?.isElectron && ( +
+
+ +

+ {dict.settings.proxyDescription} +

+
+ +
+ + setHttpProxy(e.target.value) + } + placeholder={`${dict.settings.httpProxy}: http://proxy:8080`} + className="h-9" + /> + + setHttpsProxy(e.target.value) + } + placeholder={`${dict.settings.httpsProxy}: http://proxy:8080`} + className="h-9" + /> +
+ + +
+ )} diff --git a/electron/electron.d.ts b/electron/electron.d.ts index a629e9b..7a219e8 100644 --- a/electron/electron.d.ts +++ b/electron/electron.d.ts @@ -25,6 +25,19 @@ interface ApplyPresetResult { env?: Record } +/** Proxy configuration interface */ +interface ProxyConfig { + httpProxy?: string + httpsProxy?: string +} + +/** Result of setting proxy */ +interface SetProxyResult { + success: boolean + error?: string + devMode?: boolean +} + declare global { interface Window { /** Main window Electron API */ @@ -45,6 +58,10 @@ declare global { openFile: () => Promise /** Save data to file via save dialog */ saveFile: (data: string) => Promise + /** Get proxy configuration */ + getProxy: () => Promise + /** Set proxy configuration (saves and restarts server) */ + setProxy: (config: ProxyConfig) => Promise } /** Settings window Electron API */ @@ -71,4 +88,4 @@ declare global { } } -export { ConfigPreset, ApplyPresetResult } +export { ConfigPreset, ApplyPresetResult, ProxyConfig, SetProxyResult } diff --git a/electron/main/index.ts b/electron/main/index.ts index 60254de..7c29146 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -4,6 +4,7 @@ import { getCurrentPresetEnv } from "./config-manager" import { loadEnvFile } from "./env-loader" import { registerIpcHandlers } from "./ipc-handlers" import { startNextServer, stopNextServer } from "./next-server" +import { applyProxyToEnv } from "./proxy-manager" import { registerSettingsWindowHandlers } from "./settings-window" import { createWindow, getMainWindow } from "./window-manager" @@ -24,6 +25,9 @@ if (!gotTheLock) { // Load environment variables from .env files loadEnvFile() + // Apply proxy settings from saved config + applyProxyToEnv() + // Apply saved preset environment variables (overrides .env) const presetEnv = getCurrentPresetEnv() for (const [key, value] of Object.entries(presetEnv)) { diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index 7e801d1..dc4bc96 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -11,6 +11,12 @@ import { updatePreset, } from "./config-manager" import { restartNextServer } from "./next-server" +import { + applyProxyToEnv, + getProxyConfig, + type ProxyConfig, + saveProxyConfig, +} from "./proxy-manager" /** * Allowed configuration keys for presets @@ -209,4 +215,40 @@ export function registerIpcHandlers(): void { return setCurrentPreset(id) }, ) + + // ==================== Proxy Settings ==================== + + ipcMain.handle("get-proxy", () => { + return getProxyConfig() + }) + + ipcMain.handle("set-proxy", async (_event, config: ProxyConfig) => { + try { + // Save config to file + saveProxyConfig(config) + + // Apply to current process environment + applyProxyToEnv() + + const isDev = process.env.NODE_ENV === "development" + + if (isDev) { + // In development, env vars are already applied + // Next.js dev server may need manual restart + return { success: true, devMode: true } + } + + // Production: restart Next.js server to pick up new env vars + await restartNextServer() + return { success: true } + } catch (error) { + return { + success: false, + error: + error instanceof Error + ? error.message + : "Failed to apply proxy settings", + } + } + }) } diff --git a/electron/main/next-server.ts b/electron/main/next-server.ts index fce07ec..c6dd7df 100644 --- a/electron/main/next-server.ts +++ b/electron/main/next-server.ts @@ -69,6 +69,8 @@ export async function startNextServer(): Promise { NODE_ENV: "production", PORT: String(port), HOSTNAME: "localhost", + // Enable Node.js built-in proxy support for fetch (Node.js 24+) + NODE_USE_ENV_PROXY: "1", } // Set cache directory to a writable location (user's app data folder) @@ -85,6 +87,13 @@ export async function startNextServer(): Promise { } } + // Debug: log proxy-related env vars + console.log("Proxy env vars being passed to server:", { + HTTP_PROXY: env.HTTP_PROXY || env.http_proxy || "not set", + HTTPS_PROXY: env.HTTPS_PROXY || env.https_proxy || "not set", + NODE_USE_ENV_PROXY: env.NODE_USE_ENV_PROXY || "not set", + }) + // Use Electron's utilityProcess API for running Node.js in background // This is the recommended way to run Node.js code in Electron serverProcess = utilityProcess.fork(serverPath, [], { @@ -114,13 +123,41 @@ export async function startNextServer(): Promise { } /** - * Stop the Next.js server process + * Stop the Next.js server process and wait for it to exit */ -export function stopNextServer(): void { +export async function stopNextServer(): Promise { if (serverProcess) { console.log("Stopping Next.js server...") + + // Create a promise that resolves when the process exits + const exitPromise = new Promise((resolve) => { + const proc = serverProcess + if (!proc) { + resolve() + return + } + + const onExit = () => { + resolve() + } + + proc.once("exit", onExit) + + // Timeout after 5 seconds + setTimeout(() => { + proc.removeListener("exit", onExit) + resolve() + }, 5000) + }) + serverProcess.kill() serverProcess = null + + // Wait for process to exit + await exitPromise + + // Additional wait for OS to release port + await new Promise((resolve) => setTimeout(resolve, 500)) } } @@ -150,8 +187,8 @@ async function waitForServerStop(timeout = 5000): Promise { export async function restartNextServer(): Promise { console.log("Restarting Next.js server...") - // Stop the current server - stopNextServer() + // Stop the current server and wait for it to exit + await stopNextServer() // Wait for the port to be released await waitForServerStop() diff --git a/electron/main/proxy-manager.ts b/electron/main/proxy-manager.ts new file mode 100644 index 0000000..cb41de5 --- /dev/null +++ b/electron/main/proxy-manager.ts @@ -0,0 +1,75 @@ +import { app } from "electron" +import * as fs from "fs" +import * as path from "path" +import type { ProxyConfig } from "../electron.d" + +export type { ProxyConfig } + +const CONFIG_FILE = "proxy-config.json" + +function getConfigPath(): string { + return path.join(app.getPath("userData"), CONFIG_FILE) +} + +/** + * Load proxy configuration from JSON file + */ +export function loadProxyConfig(): ProxyConfig { + try { + const configPath = getConfigPath() + if (fs.existsSync(configPath)) { + const data = fs.readFileSync(configPath, "utf-8") + return JSON.parse(data) as ProxyConfig + } + } catch (error) { + console.error("Failed to load proxy config:", error) + } + return {} +} + +/** + * Save proxy configuration to JSON file + */ +export function saveProxyConfig(config: ProxyConfig): void { + try { + const configPath = getConfigPath() + fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8") + } catch (error) { + console.error("Failed to save proxy config:", error) + throw error + } +} + +/** + * Apply proxy configuration to process.env + * Must be called BEFORE starting the Next.js server + */ +export function applyProxyToEnv(): void { + const config = loadProxyConfig() + + if (config.httpProxy) { + process.env.HTTP_PROXY = config.httpProxy + process.env.http_proxy = config.httpProxy + } else { + delete process.env.HTTP_PROXY + delete process.env.http_proxy + } + + if (config.httpsProxy) { + process.env.HTTPS_PROXY = config.httpsProxy + process.env.https_proxy = config.httpsProxy + } else { + delete process.env.HTTPS_PROXY + delete process.env.https_proxy + } +} + +/** + * Get current proxy configuration (from process.env) + */ +export function getProxyConfig(): ProxyConfig { + return { + httpProxy: process.env.HTTP_PROXY || process.env.http_proxy || "", + httpsProxy: process.env.HTTPS_PROXY || process.env.https_proxy || "", + } +} diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 2b460c7..7cbef9c 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -21,4 +21,9 @@ contextBridge.exposeInMainWorld("electronAPI", { // File operations openFile: () => ipcRenderer.invoke("dialog-open-file"), saveFile: (data: string) => ipcRenderer.invoke("dialog-save-file", data), + + // Proxy settings + getProxy: () => ipcRenderer.invoke("get-proxy"), + setProxy: (config: { httpProxy?: string; httpsProxy?: string }) => + ipcRenderer.invoke("set-proxy", config), }) diff --git a/lib/i18n/dictionaries/en.json b/lib/i18n/dictionaries/en.json index 027690a..b544ff2 100644 --- a/lib/i18n/dictionaries/en.json +++ b/lib/i18n/dictionaries/en.json @@ -107,7 +107,13 @@ "diagramActions": "Diagram Actions", "diagramActionsDescription": "Manage diagram history and exports", "history": "History", - "download": "Download" + "download": "Download", + "proxy": "Proxy Settings", + "proxyDescription": "Configure HTTP/HTTPS proxy for API requests (Desktop only)", + "httpProxy": "HTTP Proxy", + "httpsProxy": "HTTPS Proxy", + "applyProxy": "Apply", + "proxyApplied": "Proxy settings applied" }, "save": { "title": "Save Diagram", diff --git a/lib/i18n/dictionaries/ja.json b/lib/i18n/dictionaries/ja.json index 522d979..5a0cada 100644 --- a/lib/i18n/dictionaries/ja.json +++ b/lib/i18n/dictionaries/ja.json @@ -107,7 +107,13 @@ "diagramActions": "ダイアグラム操作", "diagramActionsDescription": "ダイアグラムの履歴とエクスポートを管理", "history": "履歴", - "download": "ダウンロード" + "download": "ダウンロード", + "proxy": "プロキシ設定", + "proxyDescription": "API リクエスト用の HTTP/HTTPS プロキシを設定(デスクトップ版のみ)", + "httpProxy": "HTTP プロキシ", + "httpsProxy": "HTTPS プロキシ", + "applyProxy": "適用", + "proxyApplied": "プロキシ設定が適用されました" }, "save": { "title": "ダイアグラムを保存", diff --git a/lib/i18n/dictionaries/zh.json b/lib/i18n/dictionaries/zh.json index 026e1f5..03a3f17 100644 --- a/lib/i18n/dictionaries/zh.json +++ b/lib/i18n/dictionaries/zh.json @@ -107,7 +107,13 @@ "diagramActions": "图表操作", "diagramActionsDescription": "管理图表历史记录和导出", "history": "历史记录", - "download": "下载" + "download": "下载", + "proxy": "代理设置", + "proxyDescription": "配置 API 请求的 HTTP/HTTPS 代理(仅桌面版)", + "httpProxy": "HTTP 代理", + "httpsProxy": "HTTPS 代理", + "applyProxy": "应用", + "proxyApplied": "代理设置已应用" }, "save": { "title": "保存图表",