setup config

This commit is contained in:
pompurin404 2024-07-31 18:58:46 +08:00
parent 35a9dcca4f
commit 4c851ef97a
No known key found for this signature in database
32 changed files with 377 additions and 70 deletions

View File

@ -119,6 +119,10 @@ export function disableAutoRun(): void {
} }
if (process.platform === 'linux') { if (process.platform === 'linux') {
const desktopFilePath = `${app.getPath('home')}/.config/autostart/${appName}.desktop` const desktopFilePath = `${app.getPath('home')}/.config/autostart/${appName}.desktop`
fs.rmSync(desktopFilePath) try {
fs.rmSync(desktopFilePath)
} catch (e) {
console.error(e)
}
} }
} }

View File

@ -1,10 +1,24 @@
import { ipcMain } from 'electron' import { ipcMain } from 'electron'
import { mihomoVersion } from './mihomo-api' import { mihomoVersion } from './mihomo-api'
import { checkAutoRun, disableAutoRun, enableAutoRun } from './autoRun' import { checkAutoRun, disableAutoRun, enableAutoRun } from './autoRun'
import {
getAppConfig,
setAppConfig,
getControledMihomoConfig,
setControledMihomoConfig
} from './config'
export function registerIpcMainHandlers(): void { export function registerIpcMainHandlers(): void {
ipcMain.handle('mihomoVersion', mihomoVersion) ipcMain.handle('mihomoVersion', mihomoVersion)
ipcMain.handle('checkAutoRun', checkAutoRun) ipcMain.handle('checkAutoRun', checkAutoRun)
ipcMain.handle('enableAutoRun', enableAutoRun) ipcMain.handle('enableAutoRun', enableAutoRun)
ipcMain.handle('disableAutoRun', disableAutoRun) 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)
})
} }

50
src/main/config.ts Normal file
View File

@ -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<IMihomoConfig>
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<IAppConfig>): void {
appConfig = Object.assign(appConfig, patch)
fs.writeFileSync(appConfigPath, yaml.stringify(appConfig))
}
export function getControledMihomoConfig(force = false): Partial<IMihomoConfig> {
if (force || !controledMihomoConfig) {
controledMihomoConfig = yaml.parse(fs.readFileSync(controledMihomoConfigPath, 'utf-8'))
}
return controledMihomoConfig
}
export function setControledMihomoConfig(patch: Partial<IMihomoConfig>): void {
controledMihomoConfig = Object.assign(controledMihomoConfig, patch)
fs.writeFileSync(controledMihomoConfigPath, yaml.stringify(controledMihomoConfig))
}

View File

@ -4,11 +4,14 @@ import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import pngIcon from '../../resources/icon.png?asset' import pngIcon from '../../resources/icon.png?asset'
import icoIcon from '../../resources/icon.ico?asset' import icoIcon from '../../resources/icon.ico?asset'
import { registerIpcMainHandlers } from './cmds' import { registerIpcMainHandlers } from './cmds'
import { initConfig, appConfig } from './config'
let window: BrowserWindow | null = null let window: BrowserWindow | null = null
let tray: Tray | null = null let tray: Tray | null = null
let trayContextMenu: Menu | null = null let trayContextMenu: Menu | null = null
initConfig()
function createWindow(): void { function createWindow(): void {
// Create the browser window. // Create the browser window.
window = new BrowserWindow({ window = new BrowserWindow({
@ -26,8 +29,10 @@ function createWindow(): void {
}) })
window.on('ready-to-show', () => { window.on('ready-to-show', () => {
window?.show() if (!appConfig.silentStart) {
window?.focusOnWebView() window?.show()
window?.focusOnWebView()
}
}) })
window.on('close', (event) => { window.on('close', (event) => {

3
src/main/template.ts Normal file
View File

@ -0,0 +1,3 @@
export const defaultConfig: IAppConfig = {
silentStart: false
}

View File

@ -4,7 +4,7 @@ import { useLocation, useNavigate, useRoutes } from 'react-router-dom'
import OutboundModeSwitcher from '@renderer/components/sider/outbound-mode-switcher' import OutboundModeSwitcher from '@renderer/components/sider/outbound-mode-switcher'
import SysproxySwitcher from '@renderer/components/sider/sysproxy-switcher' import SysproxySwitcher from '@renderer/components/sider/sysproxy-switcher'
import TunSwitcher from '@renderer/components/sider/tun-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 { IoSettings } from 'react-icons/io5'
import routes from '@renderer/routes' import routes from '@renderer/routes'
import ProfileCard from '@renderer/components/sider/profile-card' 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 ConnCard from '@renderer/components/sider/conn-card'
import LogCard from '@renderer/components/sider/log-card' import LogCard from '@renderer/components/sider/log-card'
function App(): JSX.Element { const App: React.FC = () => {
const { setTheme } = useTheme() const { setTheme } = useTheme()
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
@ -41,7 +41,7 @@ function App(): JSX.Element {
return ( return (
<div className="w-full h-[100vh] flex"> <div className="w-full h-[100vh] flex">
<div className="side w-[250px] h-full border-r border-default-200"> <div className="side w-[250px] h-full">
<div className="flex justify-between h-[32px] m-2"> <div className="flex justify-between h-[32px] m-2">
<h3 className="select-none text-lg font-bold leading-[32px]"></h3> <h3 className="select-none text-lg font-bold leading-[32px]"></h3>
<Button <Button
@ -81,7 +81,8 @@ function App(): JSX.Element {
</div> </div>
</div> </div>
</div> </div>
<div className="main w-[calc(100%-250px)] h-full overflow-y-auto">{page}</div> <Divider orientation="vertical" />
<div className="main w-[calc(100%-251px)] h-full overflow-y-auto">{page}</div>
</div> </div>
) )
} }

View File

@ -2,9 +2,31 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer utilities { *::-webkit-scrollbar {
/* Hide scrollbar for Chrome, Safari and Opera */ width: 8px;
.no-scrollbar::-webkit-scrollbar { height: 6px;
display: none; }
/* 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;
} }
} }

View File

@ -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> = (props) => {
return (
<div className="w-full h-full overflow-y-auto custom-scrollbar">
<div className="sticky top-0 z-40 h-[48px] w-full backdrop-blur">
<div className="p-2 flex justify-between">
<div className="select-none title h-full text-lg leading-[32px]">{props.title}</div>
<div className="header h-full">{props.header}</div>
</div>
<Divider />
</div>
<div className="content">{props.children}</div>
</div>
)
}
export default BasePage

View File

@ -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> = (props) => {
return (
<Card className="m-2">
<CardBody>{props.children}</CardBody>
</Card>
)
}
export default SettingCard

View File

@ -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> = (props) => {
const { divider = false } = props
return (
<>
<div className="h-[32px] w-full flex justify-between">
<div className="h-full flex items-center">
<h4 className="h-full select-none text-md leading-[32px]">{props.title}</h4>
<div>{props.actions}</div>
</div>
{props.children}
</div>
{divider && <Divider className="my-2" />}
</>
)
}
export default SettingItem

View File

@ -2,7 +2,7 @@ import { Button, Card, CardBody, CardFooter, Chip } from '@nextui-org/react'
import { IoLink } from 'react-icons/io5' import { IoLink } from 'react-icons/io5'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
export default function ConnCard(): JSX.Element { const ConnCard: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
@ -33,3 +33,5 @@ export default function ConnCard(): JSX.Element {
</Card> </Card>
) )
} }
export default ConnCard

View File

@ -2,7 +2,7 @@ import { Button, Card, CardBody, CardFooter } from '@nextui-org/react'
import { IoJournal } from 'react-icons/io5' import { IoJournal } from 'react-icons/io5'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
export default function LogCard(): JSX.Element { const LogCard: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
@ -30,3 +30,5 @@ export default function LogCard(): JSX.Element {
</Card> </Card>
) )
} }
export default LogCard

View File

@ -1,14 +1,17 @@
import { Tabs, Tab } from '@nextui-org/react' 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<OutboundMode>('rule')
return ( return (
<Tabs <Tabs
fullWidth fullWidth
color="primary" color="primary"
selectedKey={mode} selectedKey={mode}
onSelectionChange={(key: Key) => setMode(key as OutboundMode)} onSelectionChange={(key: Key) => patchControledMihomoConfig({ mode: key as OutboundMode })}
> >
<Tab <Tab
className={`select-none ${mode === 'rule' ? 'font-bold' : ''}`} className={`select-none ${mode === 'rule' ? 'font-bold' : ''}`}
@ -28,3 +31,5 @@ export default function OutboundModeSwitcher(): JSX.Element {
</Tabs> </Tabs>
) )
} }
export default OutboundModeSwitcher

View File

@ -2,7 +2,7 @@ import { Button, Card, CardBody, CardFooter, Switch } from '@nextui-org/react'
import { MdFormatOverline } from 'react-icons/md' import { MdFormatOverline } from 'react-icons/md'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
export default function OverrideCard(): JSX.Element { const OverrideCard: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
@ -31,3 +31,5 @@ export default function OverrideCard(): JSX.Element {
</Card> </Card>
) )
} }
export default OverrideCard

View File

@ -2,7 +2,7 @@ import { Button, Card, CardBody, CardFooter, Slider } from '@nextui-org/react'
import { IoMdRefresh } from 'react-icons/io' import { IoMdRefresh } from 'react-icons/io'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
export default function ProfileCard(): JSX.Element { const ProfileCard: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
@ -27,3 +27,5 @@ export default function ProfileCard(): JSX.Element {
</Card> </Card>
) )
} }
export default ProfileCard

View File

@ -2,7 +2,7 @@ import { Button, Card, CardBody, CardFooter } from '@nextui-org/react'
import { SiSpeedtest } from 'react-icons/si' import { SiSpeedtest } from 'react-icons/si'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
export default function ProxyCard(): JSX.Element { const ProxyCard: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
@ -27,3 +27,5 @@ export default function ProxyCard(): JSX.Element {
</Card> </Card>
) )
} }
export default ProxyCard

View File

@ -2,7 +2,7 @@ import { Button, Card, CardBody, CardFooter, Chip } from '@nextui-org/react'
import { IoGitNetwork } from 'react-icons/io5' import { IoGitNetwork } from 'react-icons/io5'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
export default function RuleCard(): JSX.Element { const RuleCard: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
@ -33,3 +33,5 @@ export default function RuleCard(): JSX.Element {
</Card> </Card>
) )
} }
export default RuleCard

View File

@ -1,7 +1,7 @@
import { Button, Card, CardBody, CardFooter, Switch } from '@nextui-org/react' import { Button, Card, CardBody, CardFooter, Switch } from '@nextui-org/react'
import { IoSettings } from 'react-icons/io5' import { IoSettings } from 'react-icons/io5'
export default function SysproxySwitcher(): JSX.Element { const SysproxySwitcher: React.FC = () => {
return ( return (
<Card className="w-[50%] mr-1"> <Card className="w-[50%] mr-1">
<CardBody className="pb-1 pt-0 px-0"> <CardBody className="pb-1 pt-0 px-0">
@ -18,3 +18,5 @@ export default function SysproxySwitcher(): JSX.Element {
</Card> </Card>
) )
} }
export default SysproxySwitcher

View File

@ -1,7 +1,7 @@
import { Button, Card, CardBody, CardFooter, Switch } from '@nextui-org/react' import { Button, Card, CardBody, CardFooter, Switch } from '@nextui-org/react'
import { IoSettings } from 'react-icons/io5' import { IoSettings } from 'react-icons/io5'
export default function SysproxySwitcher(): JSX.Element { const TunSwitcher: React.FC = () => {
return ( return (
<Card className="w-[50%] ml-1"> <Card className="w-[50%] ml-1">
<CardBody className="pb-1 pt-0 px-0"> <CardBody className="pb-1 pt-0 px-0">
@ -18,3 +18,5 @@ export default function SysproxySwitcher(): JSX.Element {
</Card> </Card>
) )
} }
export default TunSwitcher

View File

@ -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<IAppConfig>) => void
}
export const useAppConfig = (): RetuenType => {
const { data: appConfig, mutate: mutateAppConfig } = useSWR('getConfig', () => getAppConfig())
const patchAppConfig = async (value: Partial<IAppConfig>): Promise<void> => {
await setAppConfig(value)
await mutateAppConfig()
}
return {
appConfig,
mutateAppConfig,
patchAppConfig
}
}

View File

@ -0,0 +1,26 @@
import useSWR from 'swr'
import { getControledMihomoConfig, setControledMihomoConfig } from '@renderer/utils/ipc'
interface RetuenType {
controledMihomoConfig: Partial<IMihomoConfig> | undefined
mutateControledMihomoConfig: () => void
patchControledMihomoConfig: (value: Partial<IMihomoConfig>) => void
}
export const useControledMihomoConfig = (): RetuenType => {
const { data: controledMihomoConfig, mutate: mutateControledMihomoConfig } = useSWR(
'getControledMihomoConfig',
() => getControledMihomoConfig()
)
const patchControledMihomoConfig = async (value: Partial<IMihomoConfig>): Promise<void> => {
await setControledMihomoConfig(value)
await mutateControledMihomoConfig()
}
return {
controledMihomoConfig,
mutateControledMihomoConfig,
patchControledMihomoConfig
}
}

View File

@ -1,3 +1,5 @@
export default function Connections(): JSX.Element { const Connections: React.FC = () => {
return <div>Connections</div> return <div>Connections</div>
} }
export default Connections

View File

@ -1,3 +1,5 @@
export default function Logs(): JSX.Element { const Logs: React.FC = () => {
return <div>Logs</div> return <div>Logs</div>
} }
export default Logs

View File

@ -1,3 +1,5 @@
export default function Override(): JSX.Element { const Override: React.FC = () => {
return <div>Override</div> return <div>Override</div>
} }
export default Override

View File

@ -1,3 +1,5 @@
export default function Profiles(): JSX.Element { const Profiles: React.FC = () => {
return <div>Profiles</div> return <div>Profiles</div>
} }
export default Profiles

View File

@ -1,3 +1,5 @@
export default function Proxies(): JSX.Element { const Proxies: React.FC = () => {
return <div>Proxies</div> return <div>Proxies</div>
} }
export default Proxies

View File

@ -1,3 +1,5 @@
export default function Rules(): JSX.Element { const Rules: React.FC = () => {
return <div>Rules</div> return <div>Rules</div>
} }
export default Rules

View File

@ -1,35 +1,65 @@
import { Button } from '@nextui-org/react' import { Button, Switch } from '@nextui-org/react'
import { checkAutoRun, enableAutoRun, disableAutoRun } from '@renderer/utils/api' 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' import useSWR from 'swr'
export default function Settings(): JSX.Element { const Settings: React.FC = () => {
const { data, error, isLoading, mutate } = useSWR('checkAutoRun', checkAutoRun, { const { data: enable, mutate } = useSWR('checkAutoRun', checkAutoRun, {
errorRetryCount: 5, errorRetryCount: 5,
errorRetryInterval: 200 errorRetryInterval: 200
}) })
if (error) return <div>failed to load</div> const { appConfig, patchAppConfig } = useAppConfig()
if (isLoading) return <div>loading...</div> const { silentStart = false } = appConfig || {}
return ( return (
<div> <BasePage
{`${data}`} title="设置"
<Button header={
onPress={async () => { <Button
await enableAutoRun() isIconOnly
await mutate() size="sm"
}} onPress={() => {
> window.open('https://github.com/pompurin404/mihomo-party')
enable }}
</Button> >
<Button <IoLogoGithub className="text-lg" />
onPress={async () => { </Button>
await disableAutoRun() }
await mutate() >
}} <SettingCard>
> <SettingItem title="开机自启" divider>
disable <Switch
</Button> size="sm"
</div> isSelected={enable}
onValueChange={(v) => {
if (v) {
enableAutoRun()
} else {
disableAutoRun()
}
mutate(v)
}}
/>
</SettingItem>
<SettingItem title="静默启动">
<Switch
size="sm"
isSelected={silentStart}
onValueChange={(v) => {
patchAppConfig({ silentStart: v })
mutate()
}}
/>
</SettingItem>
</SettingCard>
</BasePage>
) )
} }
export default Settings

View File

@ -1,15 +0,0 @@
export async function mihomoVersion(): Promise<IMihomoVersion> {
return await window.electron.ipcRenderer.invoke('mihomoVersion')
}
export async function checkAutoRun(): Promise<boolean> {
return await window.electron.ipcRenderer.invoke('checkAutoRun')
}
export async function enableAutoRun(): Promise<void> {
await window.electron.ipcRenderer.invoke('enableAutoRun')
}
export async function disableAutoRun(): Promise<void> {
await window.electron.ipcRenderer.invoke('disableAutoRun')
}

View File

@ -0,0 +1,31 @@
export async function mihomoVersion(): Promise<IMihomoVersion> {
return await window.electron.ipcRenderer.invoke('mihomoVersion')
}
export async function checkAutoRun(): Promise<boolean> {
return await window.electron.ipcRenderer.invoke('checkAutoRun')
}
export async function enableAutoRun(): Promise<void> {
await window.electron.ipcRenderer.invoke('enableAutoRun')
}
export async function disableAutoRun(): Promise<void> {
await window.electron.ipcRenderer.invoke('disableAutoRun')
}
export async function getAppConfig(force = false): Promise<IAppConfig> {
return await window.electron.ipcRenderer.invoke('getAppConfig', force)
}
export async function setAppConfig(patch: Partial<IAppConfig>): Promise<void> {
await window.electron.ipcRenderer.invoke('setAppConfig', patch)
}
export async function getControledMihomoConfig(force = false): Promise<Partial<IMihomoConfig>> {
return await window.electron.ipcRenderer.invoke('getControledMihomoConfig', force)
}
export async function setControledMihomoConfig(patch: Partial<IMihomoConfig>): Promise<void> {
await window.electron.ipcRenderer.invoke('setControledMihomoConfig', patch)
}

11
src/shared/types.d.ts vendored
View File

@ -4,3 +4,14 @@ interface IMihomoVersion {
version: string version: string
meta: boolean meta: boolean
} }
interface IAppConfig {
silentStart: boolean
}
interface IMihomoConfig {
mode: OutboundMode
'mixed-port': number
'socks-port'?: number
port?: number
}

View File

@ -5,7 +5,7 @@
"src/renderer/src/**/*", "src/renderer/src/**/*",
"src/renderer/src/**/*.tsx", "src/renderer/src/**/*.tsx",
"src/preload/*.d.ts", "src/preload/*.d.ts",
"src/shared/**/*.d.ts" "src/shared/*.d.ts"
], ],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,