diff --git a/apps/main/src/index.ts b/apps/main/src/index.ts index ffb734568..037fdf658 100644 --- a/apps/main/src/index.ts +++ b/apps/main/src/index.ts @@ -7,6 +7,7 @@ import squirrelStartup from "electron-squirrel-startup" import { isDev, isMacOS } from "./env" import { initializeAppStage0, initializeAppStage1 } from "./init" +import { updateProxy } from "./lib/proxy" import { setAuthSessionToken } from "./lib/user" import { registerUpdater } from "./updater" import { createMainWindow, createWindow } from "./window" @@ -52,6 +53,7 @@ function bootstrap() { mainWindow = createMainWindow() + updateProxy() registerUpdater() //remove Electron, Follow from user agent diff --git a/apps/main/src/lib/proxy.ts b/apps/main/src/lib/proxy.ts new file mode 100644 index 000000000..b33058fd9 --- /dev/null +++ b/apps/main/src/lib/proxy.ts @@ -0,0 +1,76 @@ +import { session } from "electron" + +import { logger } from "../logger" +import { store } from "./store" + +// Sets up the proxy configuration for the app. +// +// See https://www.electronjs.org/docs/latest/api/session#sessetproxyconfig +// for more information about the proxy API. +// +// The open-source project [poooi/poi](https://github.com/poooi/poi) is doing well in proxy configuration +// refer the following files for more details: +// +// https://github.com/poooi/poi/blob/5741d0d02c0a08626dd53196b094223457014491/lib/proxy.ts#L36 +// https://github.com/poooi/poi/blob/5741d0d02c0a08626dd53196b094223457014491/views/components/settings/network/index.es + +export const setProxyConfig = (inputProxy: string) => { + const proxyUri = normalizeProxyUri(inputProxy) + if (!proxyUri) { + return false + } + store.set("proxy", inputProxy) + return true +} + +export const getProxyConfig = () => { + const proxyConfig = store.get("proxy") as string | undefined + if (!proxyConfig) { + return + } + const proxyUri = normalizeProxyUri(proxyConfig) + return proxyUri +} + +const URL_SCHEME = new Set(["http:", "https:", "ftp:", "socks:", "socks4:", "socks5:"]) + +const normalizeProxyUri = (userProxy: string) => { + if (!userProxy) { + return + } + try { + const proxyUrl = new URL(userProxy) + if (!URL_SCHEME.has(proxyUrl.protocol) || !proxyUrl.hostname || !proxyUrl.port) { + return + } + // There are multiple ways to specify a proxy in Electron, + // but for security reasons, we only support simple proxy URLs for now. + return [ + `${proxyUrl.protocol}//${proxyUrl.hostname}:${proxyUrl.port}`, + // Failing over to using no proxy if the proxy is unavailable + "direct://", + ].join(",") + } catch { + return + } +} + +const BYPASS_RULES = [""].join(";") + +export const updateProxy = () => { + const proxyUri = getProxyConfig() + if (!proxyUri) { + session.defaultSession.setProxy({ + // Note that the system mode is different from setting no proxy configuration. + // In the latter case, Electron falls back to the system settings only if no command-line options influence the proxy configuration. + mode: "system", + }) + return + } + + logger.log(`Loading proxy: ${proxyUri}`) + session.defaultSession.setProxy({ + proxyRules: proxyUri, + proxyBypassRules: BYPASS_RULES, + }) +} diff --git a/apps/main/src/tipc/setting.ts b/apps/main/src/tipc/setting.ts index b96e473b4..7bce61971 100644 --- a/apps/main/src/tipc/setting.ts +++ b/apps/main/src/tipc/setting.ts @@ -3,6 +3,7 @@ import { createRequire } from "node:module" import { app, nativeTheme } from "electron" import { setDockCount } from "../lib/dock" +import { setProxyConfig, updateProxy } from "../lib/proxy" import { store } from "../lib/store" import { createSettingWindow } from "../window" import { t } from "./_instance" @@ -39,4 +40,10 @@ export const settingRoute = { setDockBadge: t.procedure.input().action(async ({ input }) => { setDockCount(input) }), + getProxyConfig: t.procedure.action(async () => store.get("proxy")), + setProxyConfig: t.procedure.input().action(async ({ input }) => { + const result = setProxyConfig(input) + updateProxy() + return result + }), } diff --git a/apps/renderer/src/hooks/common/useProxySetting.ts b/apps/renderer/src/hooks/common/useProxySetting.ts new file mode 100644 index 000000000..ad8bd9b43 --- /dev/null +++ b/apps/renderer/src/hooks/common/useProxySetting.ts @@ -0,0 +1,28 @@ +import { atom, useAtomValue, useSetAtom } from "jotai" +import { useCallback } from "react" + +import { tipcClient } from "~/lib/client" + +const proxyAtom = atom("") + +proxyAtom.onMount = (setAtom) => { + tipcClient?.getProxyConfig().then((proxy: string) => { + setAtom(proxy) + }) +} + +export const useProxyValue = () => useAtomValue(proxyAtom) + +export const useSetProxy = () => { + const setProxy = useSetAtom(proxyAtom) + return useCallback( + (proxyString: string) => { + if (!window.electron) { + return + } + setProxy(proxyString) + tipcClient?.setProxyConfig(proxyString) + }, + [setProxy], + ) +} diff --git a/apps/renderer/src/modules/settings/tabs/general.tsx b/apps/renderer/src/modules/settings/tabs/general.tsx index b4560e308..4679c046d 100644 --- a/apps/renderer/src/modules/settings/tabs/general.tsx +++ b/apps/renderer/src/modules/settings/tabs/general.tsx @@ -22,12 +22,15 @@ import { SelectTrigger, SelectValue, } from "~/components/ui/select" +import { useProxyValue, useSetProxy } from "~/hooks/common/useProxySetting" import { fallbackLanguage } from "~/i18n" import { initPostHog } from "~/initialize/posthog" import { tipcClient } from "~/lib/client" import { cn } from "~/lib/utils" import { clearLocalPersistStoreData } from "~/store/utils/clear" +import { SettingDescription, SettingInput } from "../control" +import { SettingItemGroup } from "../section" import { SettingsTitle } from "../title" const { defineSettingItem, SettingBuilder } = createSetting( @@ -158,6 +161,9 @@ export const SettingGeneral = () => { description: t("general.rebuild_database.description"), buttonText: t("general.rebuild_database.button"), }, + + { type: "title", value: t("general.network"), disabled: !window.electron }, + window.electron && NettingSetting, ]} /> @@ -244,3 +250,22 @@ export const LanguageSelector = () => { ) } + +const NettingSetting = () => { + const { t } = useTranslation("settings") + const proxyConfig = useProxyValue() + const setProxyConfig = useSetProxy() + + return ( + + setProxyConfig(event.target.value.trim())} + /> + {t("general.proxy.description")} + + ) +} diff --git a/locales/settings/en.json b/locales/settings/en.json index 5333abf93..902631945 100644 --- a/locales/settings/en.json +++ b/locales/settings/en.json @@ -92,7 +92,10 @@ "general.mark_as_read.render.label": "Mark as read when in the view", "general.mark_as_read.scroll.description": "Automatically mark entries as read when scrolled out of the view.", "general.mark_as_read.scroll.label": "Mark as read when scrolling", + "general.network": "Network", "general.privacy_data": "Privacy & Data", + "general.proxy": "Proxy", + "general.proxy.description": "Set proxy for network traffic routing, e.g., socks://proxy.example.com:1080", "general.rebuild_database.button": "Rebuild", "general.rebuild_database.description": "If you are experiencing rendering issues, rebuilding the database may solve them.", "general.rebuild_database.label": "Rebuild Database", diff --git a/locales/settings/zh-CN.json b/locales/settings/zh-CN.json index 0bd7a04dd..87a68c574 100644 --- a/locales/settings/zh-CN.json +++ b/locales/settings/zh-CN.json @@ -92,7 +92,10 @@ "general.mark_as_read.render.label": "在窗口中时标记为已读", "general.mark_as_read.scroll.description": "当条目滚动出窗口时自动标记为已读", "general.mark_as_read.scroll.label": "滚动时标记为已读", + "general.network": "网络", "general.privacy_data": "隐私与数据", + "general.proxy": "代理", + "general.proxy.description": "代理网络请求,示例:socks://proxy.example.com:1080", "general.rebuild_database.button": "重建", "general.rebuild_database.description": "尝试重建数据库可以解决部分渲染或其他类型问题", "general.rebuild_database.label": "重建数据库",