diff --git a/src/main/autoRun.ts b/src/main/autoRun.ts index cc1458f..e8d03ba 100644 --- a/src/main/autoRun.ts +++ b/src/main/autoRun.ts @@ -119,6 +119,10 @@ export function disableAutoRun(): void { } if (process.platform === 'linux') { const desktopFilePath = `${app.getPath('home')}/.config/autostart/${appName}.desktop` - fs.rmSync(desktopFilePath) + try { + fs.rmSync(desktopFilePath) + } catch (e) { + console.error(e) + } } } diff --git a/src/main/cmds.ts b/src/main/cmds.ts index 676be57..a0ec5be 100644 --- a/src/main/cmds.ts +++ b/src/main/cmds.ts @@ -1,10 +1,24 @@ import { ipcMain } from 'electron' import { mihomoVersion } from './mihomo-api' import { checkAutoRun, disableAutoRun, enableAutoRun } from './autoRun' +import { + getAppConfig, + setAppConfig, + getControledMihomoConfig, + setControledMihomoConfig +} from './config' export function registerIpcMainHandlers(): void { ipcMain.handle('mihomoVersion', mihomoVersion) ipcMain.handle('checkAutoRun', checkAutoRun) ipcMain.handle('enableAutoRun', enableAutoRun) ipcMain.handle('disableAutoRun', disableAutoRun) + ipcMain.handle('getAppConfig', (_e, force) => getAppConfig(force)) + ipcMain.handle('setAppConfig', (_e, config) => { + setAppConfig(config) + }) + ipcMain.handle('getControledMihomoConfig', (_e, force) => getControledMihomoConfig(force)) + ipcMain.handle('setControledMihomoConfig', (_e, config) => { + setControledMihomoConfig(config) + }) } diff --git a/src/main/config.ts b/src/main/config.ts new file mode 100644 index 0000000..a715961 --- /dev/null +++ b/src/main/config.ts @@ -0,0 +1,50 @@ +import yaml from 'yaml' +import fs from 'fs' +import { app } from 'electron' +import path from 'path' +import { defaultConfig } from './template' + +const dataDir = app.getPath('userData') +const appConfigPath = path.join(dataDir, 'config.yaml') +const controledMihomoConfigPath = path.join(dataDir, 'mihomo.yaml') + +export let appConfig: IAppConfig +export let controledMihomoConfig: Partial + +export function initConfig(): void { + if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir) + } + if (!fs.existsSync(appConfigPath)) { + fs.writeFileSync(appConfigPath, yaml.stringify(defaultConfig)) + } + if (!fs.existsSync(controledMihomoConfigPath)) { + fs.writeFileSync(controledMihomoConfigPath, yaml.stringify({})) + } + getAppConfig(true) + getControledMihomoConfig(true) +} + +export function getAppConfig(force = false): IAppConfig { + if (force || !appConfig) { + appConfig = yaml.parse(fs.readFileSync(appConfigPath, 'utf-8')) + } + return appConfig +} + +export function setAppConfig(patch: Partial): void { + appConfig = Object.assign(appConfig, patch) + fs.writeFileSync(appConfigPath, yaml.stringify(appConfig)) +} + +export function getControledMihomoConfig(force = false): Partial { + if (force || !controledMihomoConfig) { + controledMihomoConfig = yaml.parse(fs.readFileSync(controledMihomoConfigPath, 'utf-8')) + } + return controledMihomoConfig +} + +export function setControledMihomoConfig(patch: Partial): void { + controledMihomoConfig = Object.assign(controledMihomoConfig, patch) + fs.writeFileSync(controledMihomoConfigPath, yaml.stringify(controledMihomoConfig)) +} diff --git a/src/main/index.ts b/src/main/index.ts index a4ecc47..47749fa 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -4,11 +4,14 @@ import { electronApp, optimizer, is } from '@electron-toolkit/utils' import pngIcon from '../../resources/icon.png?asset' import icoIcon from '../../resources/icon.ico?asset' import { registerIpcMainHandlers } from './cmds' +import { initConfig, appConfig } from './config' let window: BrowserWindow | null = null let tray: Tray | null = null let trayContextMenu: Menu | null = null +initConfig() + function createWindow(): void { // Create the browser window. window = new BrowserWindow({ @@ -26,8 +29,10 @@ function createWindow(): void { }) window.on('ready-to-show', () => { - window?.show() - window?.focusOnWebView() + if (!appConfig.silentStart) { + window?.show() + window?.focusOnWebView() + } }) window.on('close', (event) => { diff --git a/src/main/template.ts b/src/main/template.ts new file mode 100644 index 0000000..7c9ee54 --- /dev/null +++ b/src/main/template.ts @@ -0,0 +1,3 @@ +export const defaultConfig: IAppConfig = { + silentStart: false +} diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index e2f9499..4270bbd 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -4,7 +4,7 @@ import { useLocation, useNavigate, useRoutes } from 'react-router-dom' import OutboundModeSwitcher from '@renderer/components/sider/outbound-mode-switcher' import SysproxySwitcher from '@renderer/components/sider/sysproxy-switcher' import TunSwitcher from '@renderer/components/sider/tun-switcher' -import { Button } from '@nextui-org/react' +import { Button, Divider } from '@nextui-org/react' import { IoSettings } from 'react-icons/io5' import routes from '@renderer/routes' import ProfileCard from '@renderer/components/sider/profile-card' @@ -14,7 +14,7 @@ import OverrideCard from '@renderer/components/sider/override-card' import ConnCard from '@renderer/components/sider/conn-card' import LogCard from '@renderer/components/sider/log-card' -function App(): JSX.Element { +const App: React.FC = () => { const { setTheme } = useTheme() const navigate = useNavigate() const location = useLocation() @@ -41,7 +41,7 @@ function App(): JSX.Element { return (
-
+

出站模式

-
{page}
+ +
{page}
) } diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 20a0bcd..4fb681a 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -2,9 +2,31 @@ @tailwind components; @tailwind utilities; -@layer utilities { - /* Hide scrollbar for Chrome, Safari and Opera */ - .no-scrollbar::-webkit-scrollbar { - display: none; +*::-webkit-scrollbar { + width: 8px; + height: 6px; +} + +/* Light mode */ +@media (prefers-color-scheme: light) { + *::-webkit-scrollbar-thumb { + background: #c0c1c58f; + border-radius: 5px; + } + + *::-webkit-scrollbar-thumb:hover { + background: #c0c1c550; + } +} + +/* Dark mode */ +@media (prefers-color-scheme: dark) { + *::-webkit-scrollbar-thumb { + background: #c0c1c550; + border-radius: 5px; + } + + *::-webkit-scrollbar-thumb:hover { + background: #c0c1c58f; } } diff --git a/src/renderer/src/components/base/base-page.tsx b/src/renderer/src/components/base/base-page.tsx new file mode 100644 index 0000000..d4c2649 --- /dev/null +++ b/src/renderer/src/components/base/base-page.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import { Divider } from '@nextui-org/divider' +interface Props { + title?: React.ReactNode + header?: React.ReactNode + children?: React.ReactNode + contentClassName?: string +} + +const BasePage: React.FC = (props) => { + return ( +
+
+
+
{props.title}
+
{props.header}
+
+ +
+
{props.children}
+
+ ) +} + +export default BasePage diff --git a/src/renderer/src/components/settings/setting-card.tsx b/src/renderer/src/components/settings/setting-card.tsx new file mode 100644 index 0000000..d558561 --- /dev/null +++ b/src/renderer/src/components/settings/setting-card.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import { Card, CardBody } from '@nextui-org/react' + +interface Props { + children?: React.ReactNode +} + +const SettingCard: React.FC = (props) => { + return ( + + {props.children} + + ) +} + +export default SettingCard diff --git a/src/renderer/src/components/settings/setting-item.tsx b/src/renderer/src/components/settings/setting-item.tsx new file mode 100644 index 0000000..e6e93d0 --- /dev/null +++ b/src/renderer/src/components/settings/setting-item.tsx @@ -0,0 +1,28 @@ +import { Divider } from '@nextui-org/react' +import React from 'react' + +interface Props { + title: React.ReactNode + actions?: React.ReactNode + children?: React.ReactNode + divider?: boolean +} + +const SettingItem: React.FC = (props) => { + const { divider = false } = props + + return ( + <> +
+
+

{props.title}

+
{props.actions}
+
+ {props.children} +
+ {divider && } + + ) +} + +export default SettingItem diff --git a/src/renderer/src/components/sider/conn-card.tsx b/src/renderer/src/components/sider/conn-card.tsx index 02fb78a..0a4cdc9 100644 --- a/src/renderer/src/components/sider/conn-card.tsx +++ b/src/renderer/src/components/sider/conn-card.tsx @@ -2,7 +2,7 @@ import { Button, Card, CardBody, CardFooter, Chip } from '@nextui-org/react' import { IoLink } from 'react-icons/io5' import { useLocation, useNavigate } from 'react-router-dom' -export default function ConnCard(): JSX.Element { +const ConnCard: React.FC = () => { const navigate = useNavigate() const location = useLocation() @@ -33,3 +33,5 @@ export default function ConnCard(): JSX.Element { ) } + +export default ConnCard diff --git a/src/renderer/src/components/sider/log-card.tsx b/src/renderer/src/components/sider/log-card.tsx index 0182497..8fce847 100644 --- a/src/renderer/src/components/sider/log-card.tsx +++ b/src/renderer/src/components/sider/log-card.tsx @@ -2,7 +2,7 @@ import { Button, Card, CardBody, CardFooter } from '@nextui-org/react' import { IoJournal } from 'react-icons/io5' import { useLocation, useNavigate } from 'react-router-dom' -export default function LogCard(): JSX.Element { +const LogCard: React.FC = () => { const navigate = useNavigate() const location = useLocation() @@ -30,3 +30,5 @@ export default function LogCard(): JSX.Element { ) } + +export default LogCard diff --git a/src/renderer/src/components/sider/outbound-mode-switcher.tsx b/src/renderer/src/components/sider/outbound-mode-switcher.tsx index c7ea3c9..7e3f50a 100644 --- a/src/renderer/src/components/sider/outbound-mode-switcher.tsx +++ b/src/renderer/src/components/sider/outbound-mode-switcher.tsx @@ -1,14 +1,17 @@ import { Tabs, Tab } from '@nextui-org/react' -import { Key, useState } from 'react' +import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config' +import { Key } from 'react' + +const OutboundModeSwitcher: React.FC = () => { + const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig() + const { mode } = controledMihomoConfig || {} -export default function OutboundModeSwitcher(): JSX.Element { - const [mode, setMode] = useState('rule') return ( setMode(key as OutboundMode)} + onSelectionChange={(key: Key) => patchControledMihomoConfig({ mode: key as OutboundMode })} > ) } + +export default OutboundModeSwitcher diff --git a/src/renderer/src/components/sider/override-card.tsx b/src/renderer/src/components/sider/override-card.tsx index f4297a5..829e2ee 100644 --- a/src/renderer/src/components/sider/override-card.tsx +++ b/src/renderer/src/components/sider/override-card.tsx @@ -2,7 +2,7 @@ import { Button, Card, CardBody, CardFooter, Switch } from '@nextui-org/react' import { MdFormatOverline } from 'react-icons/md' import { useLocation, useNavigate } from 'react-router-dom' -export default function OverrideCard(): JSX.Element { +const OverrideCard: React.FC = () => { const navigate = useNavigate() const location = useLocation() @@ -31,3 +31,5 @@ export default function OverrideCard(): JSX.Element { ) } + +export default OverrideCard diff --git a/src/renderer/src/components/sider/profile-card.tsx b/src/renderer/src/components/sider/profile-card.tsx index 8834593..d819516 100644 --- a/src/renderer/src/components/sider/profile-card.tsx +++ b/src/renderer/src/components/sider/profile-card.tsx @@ -2,7 +2,7 @@ import { Button, Card, CardBody, CardFooter, Slider } from '@nextui-org/react' import { IoMdRefresh } from 'react-icons/io' import { useLocation, useNavigate } from 'react-router-dom' -export default function ProfileCard(): JSX.Element { +const ProfileCard: React.FC = () => { const navigate = useNavigate() const location = useLocation() @@ -27,3 +27,5 @@ export default function ProfileCard(): JSX.Element { ) } + +export default ProfileCard diff --git a/src/renderer/src/components/sider/proxy-card.tsx b/src/renderer/src/components/sider/proxy-card.tsx index ee4acde..8beea7d 100644 --- a/src/renderer/src/components/sider/proxy-card.tsx +++ b/src/renderer/src/components/sider/proxy-card.tsx @@ -2,7 +2,7 @@ import { Button, Card, CardBody, CardFooter } from '@nextui-org/react' import { SiSpeedtest } from 'react-icons/si' import { useLocation, useNavigate } from 'react-router-dom' -export default function ProxyCard(): JSX.Element { +const ProxyCard: React.FC = () => { const navigate = useNavigate() const location = useLocation() @@ -27,3 +27,5 @@ export default function ProxyCard(): JSX.Element { ) } + +export default ProxyCard diff --git a/src/renderer/src/components/sider/rule-card.tsx b/src/renderer/src/components/sider/rule-card.tsx index 5bd7ed8..ed9e3f8 100644 --- a/src/renderer/src/components/sider/rule-card.tsx +++ b/src/renderer/src/components/sider/rule-card.tsx @@ -2,7 +2,7 @@ import { Button, Card, CardBody, CardFooter, Chip } from '@nextui-org/react' import { IoGitNetwork } from 'react-icons/io5' import { useLocation, useNavigate } from 'react-router-dom' -export default function RuleCard(): JSX.Element { +const RuleCard: React.FC = () => { const navigate = useNavigate() const location = useLocation() @@ -33,3 +33,5 @@ export default function RuleCard(): JSX.Element { ) } + +export default RuleCard diff --git a/src/renderer/src/components/sider/sysproxy-switcher.tsx b/src/renderer/src/components/sider/sysproxy-switcher.tsx index dbd911f..ff83dcf 100644 --- a/src/renderer/src/components/sider/sysproxy-switcher.tsx +++ b/src/renderer/src/components/sider/sysproxy-switcher.tsx @@ -1,7 +1,7 @@ import { Button, Card, CardBody, CardFooter, Switch } from '@nextui-org/react' import { IoSettings } from 'react-icons/io5' -export default function SysproxySwitcher(): JSX.Element { +const SysproxySwitcher: React.FC = () => { return ( @@ -18,3 +18,5 @@ export default function SysproxySwitcher(): JSX.Element { ) } + +export default SysproxySwitcher diff --git a/src/renderer/src/components/sider/tun-switcher.tsx b/src/renderer/src/components/sider/tun-switcher.tsx index 9780bca..c51c5ad 100644 --- a/src/renderer/src/components/sider/tun-switcher.tsx +++ b/src/renderer/src/components/sider/tun-switcher.tsx @@ -1,7 +1,7 @@ import { Button, Card, CardBody, CardFooter, Switch } from '@nextui-org/react' import { IoSettings } from 'react-icons/io5' -export default function SysproxySwitcher(): JSX.Element { +const TunSwitcher: React.FC = () => { return ( @@ -18,3 +18,5 @@ export default function SysproxySwitcher(): JSX.Element { ) } + +export default TunSwitcher diff --git a/src/renderer/src/hooks/use-config.tsx b/src/renderer/src/hooks/use-config.tsx new file mode 100644 index 0000000..5ebb4bb --- /dev/null +++ b/src/renderer/src/hooks/use-config.tsx @@ -0,0 +1,23 @@ +import useSWR from 'swr' +import { getAppConfig, setAppConfig } from '@renderer/utils/ipc' + +interface RetuenType { + appConfig: IAppConfig | undefined + mutateAppConfig: () => void + patchAppConfig: (value: Partial) => void +} + +export const useAppConfig = (): RetuenType => { + const { data: appConfig, mutate: mutateAppConfig } = useSWR('getConfig', () => getAppConfig()) + + const patchAppConfig = async (value: Partial): Promise => { + await setAppConfig(value) + await mutateAppConfig() + } + + return { + appConfig, + mutateAppConfig, + patchAppConfig + } +} diff --git a/src/renderer/src/hooks/use-controled-mihomo-config.tsx b/src/renderer/src/hooks/use-controled-mihomo-config.tsx new file mode 100644 index 0000000..d7aafa8 --- /dev/null +++ b/src/renderer/src/hooks/use-controled-mihomo-config.tsx @@ -0,0 +1,26 @@ +import useSWR from 'swr' +import { getControledMihomoConfig, setControledMihomoConfig } from '@renderer/utils/ipc' + +interface RetuenType { + controledMihomoConfig: Partial | undefined + mutateControledMihomoConfig: () => void + patchControledMihomoConfig: (value: Partial) => void +} + +export const useControledMihomoConfig = (): RetuenType => { + const { data: controledMihomoConfig, mutate: mutateControledMihomoConfig } = useSWR( + 'getControledMihomoConfig', + () => getControledMihomoConfig() + ) + + const patchControledMihomoConfig = async (value: Partial): Promise => { + await setControledMihomoConfig(value) + await mutateControledMihomoConfig() + } + + return { + controledMihomoConfig, + mutateControledMihomoConfig, + patchControledMihomoConfig + } +} diff --git a/src/renderer/src/pages/connections.tsx b/src/renderer/src/pages/connections.tsx index 103701d..69e1e0d 100644 --- a/src/renderer/src/pages/connections.tsx +++ b/src/renderer/src/pages/connections.tsx @@ -1,3 +1,5 @@ -export default function Connections(): JSX.Element { +const Connections: React.FC = () => { return
Connections
} + +export default Connections diff --git a/src/renderer/src/pages/logs.tsx b/src/renderer/src/pages/logs.tsx index 0de2ec7..8d56605 100644 --- a/src/renderer/src/pages/logs.tsx +++ b/src/renderer/src/pages/logs.tsx @@ -1,3 +1,5 @@ -export default function Logs(): JSX.Element { +const Logs: React.FC = () => { return
Logs
} + +export default Logs diff --git a/src/renderer/src/pages/override.tsx b/src/renderer/src/pages/override.tsx index 3bbdbfa..2f5a3f0 100644 --- a/src/renderer/src/pages/override.tsx +++ b/src/renderer/src/pages/override.tsx @@ -1,3 +1,5 @@ -export default function Override(): JSX.Element { +const Override: React.FC = () => { return
Override
} + +export default Override diff --git a/src/renderer/src/pages/profiles.tsx b/src/renderer/src/pages/profiles.tsx index d31690b..a56cf58 100644 --- a/src/renderer/src/pages/profiles.tsx +++ b/src/renderer/src/pages/profiles.tsx @@ -1,3 +1,5 @@ -export default function Profiles(): JSX.Element { +const Profiles: React.FC = () => { return
Profiles
} + +export default Profiles diff --git a/src/renderer/src/pages/proxies.tsx b/src/renderer/src/pages/proxies.tsx index df7545f..b8aad65 100644 --- a/src/renderer/src/pages/proxies.tsx +++ b/src/renderer/src/pages/proxies.tsx @@ -1,3 +1,5 @@ -export default function Proxies(): JSX.Element { +const Proxies: React.FC = () => { return
Proxies
} + +export default Proxies diff --git a/src/renderer/src/pages/rules.tsx b/src/renderer/src/pages/rules.tsx index 7f91eaa..b3324b8 100644 --- a/src/renderer/src/pages/rules.tsx +++ b/src/renderer/src/pages/rules.tsx @@ -1,3 +1,5 @@ -export default function Rules(): JSX.Element { +const Rules: React.FC = () => { return
Rules
} + +export default Rules diff --git a/src/renderer/src/pages/settings.tsx b/src/renderer/src/pages/settings.tsx index 3f40133..74df5fb 100644 --- a/src/renderer/src/pages/settings.tsx +++ b/src/renderer/src/pages/settings.tsx @@ -1,35 +1,65 @@ -import { Button } from '@nextui-org/react' -import { checkAutoRun, enableAutoRun, disableAutoRun } from '@renderer/utils/api' +import { Button, Switch } from '@nextui-org/react' +import BasePage from '@renderer/components/base/base-page' +import SettingCard from '@renderer/components/settings/setting-card' +import SettingItem from '@renderer/components/settings/setting-item' +import { useAppConfig } from '@renderer/hooks/use-config' +import { checkAutoRun, enableAutoRun, disableAutoRun } from '@renderer/utils/ipc' +import { IoLogoGithub } from 'react-icons/io5' import useSWR from 'swr' -export default function Settings(): JSX.Element { - const { data, error, isLoading, mutate } = useSWR('checkAutoRun', checkAutoRun, { +const Settings: React.FC = () => { + const { data: enable, mutate } = useSWR('checkAutoRun', checkAutoRun, { errorRetryCount: 5, errorRetryInterval: 200 }) - if (error) return
failed to load
- if (isLoading) return
loading...
+ const { appConfig, patchAppConfig } = useAppConfig() + const { silentStart = false } = appConfig || {} + return ( -
- {`${data}`} - - -
+ { + window.open('https://github.com/pompurin404/mihomo-party') + }} + > + + + } + > + + + { + if (v) { + enableAutoRun() + } else { + disableAutoRun() + } + mutate(v) + }} + /> + + + { + patchAppConfig({ silentStart: v }) + mutate() + }} + /> + + + ) } + +export default Settings diff --git a/src/renderer/src/utils/api.ts b/src/renderer/src/utils/api.ts deleted file mode 100644 index d86a63b..0000000 --- a/src/renderer/src/utils/api.ts +++ /dev/null @@ -1,15 +0,0 @@ -export async function mihomoVersion(): Promise { - return await window.electron.ipcRenderer.invoke('mihomoVersion') -} - -export async function checkAutoRun(): Promise { - return await window.electron.ipcRenderer.invoke('checkAutoRun') -} - -export async function enableAutoRun(): Promise { - await window.electron.ipcRenderer.invoke('enableAutoRun') -} - -export async function disableAutoRun(): Promise { - await window.electron.ipcRenderer.invoke('disableAutoRun') -} diff --git a/src/renderer/src/utils/ipc.ts b/src/renderer/src/utils/ipc.ts new file mode 100644 index 0000000..eeefef5 --- /dev/null +++ b/src/renderer/src/utils/ipc.ts @@ -0,0 +1,31 @@ +export async function mihomoVersion(): Promise { + return await window.electron.ipcRenderer.invoke('mihomoVersion') +} + +export async function checkAutoRun(): Promise { + return await window.electron.ipcRenderer.invoke('checkAutoRun') +} + +export async function enableAutoRun(): Promise { + await window.electron.ipcRenderer.invoke('enableAutoRun') +} + +export async function disableAutoRun(): Promise { + await window.electron.ipcRenderer.invoke('disableAutoRun') +} + +export async function getAppConfig(force = false): Promise { + return await window.electron.ipcRenderer.invoke('getAppConfig', force) +} + +export async function setAppConfig(patch: Partial): Promise { + await window.electron.ipcRenderer.invoke('setAppConfig', patch) +} + +export async function getControledMihomoConfig(force = false): Promise> { + return await window.electron.ipcRenderer.invoke('getControledMihomoConfig', force) +} + +export async function setControledMihomoConfig(patch: Partial): Promise { + await window.electron.ipcRenderer.invoke('setControledMihomoConfig', patch) +} diff --git a/src/shared/types.d.ts b/src/shared/types.d.ts index fe7fe01..0345c22 100644 --- a/src/shared/types.d.ts +++ b/src/shared/types.d.ts @@ -4,3 +4,14 @@ interface IMihomoVersion { version: string meta: boolean } + +interface IAppConfig { + silentStart: boolean +} + +interface IMihomoConfig { + mode: OutboundMode + 'mixed-port': number + 'socks-port'?: number + port?: number +} diff --git a/tsconfig.web.json b/tsconfig.web.json index 311e44e..f8ae6ae 100644 --- a/tsconfig.web.json +++ b/tsconfig.web.json @@ -5,7 +5,7 @@ "src/renderer/src/**/*", "src/renderer/src/**/*.tsx", "src/preload/*.d.ts", - "src/shared/**/*.d.ts" + "src/shared/*.d.ts" ], "compilerOptions": { "composite": true,