mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-27 13:10:30 +08:00
Initialization constant
This commit is contained in:
parent
ae5939d2aa
commit
279f8fe1d3
@ -51,9 +51,14 @@ export async function addProfileItem(item: Partial<IProfileItem>): Promise<void>
|
|||||||
export function removeProfileItem(id: string): void {
|
export function removeProfileItem(id: string): void {
|
||||||
profileConfig.items = profileConfig.items?.filter((item) => item.id !== id)
|
profileConfig.items = profileConfig.items?.filter((item) => item.id !== id)
|
||||||
if (profileConfig.current === id) {
|
if (profileConfig.current === id) {
|
||||||
profileConfig.current = profileConfig.items[0]?.id
|
if (profileConfig.items.length > 0) {
|
||||||
|
profileConfig.current = profileConfig.items[0]?.id
|
||||||
|
} else {
|
||||||
|
profileConfig.current = undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig))
|
fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig))
|
||||||
|
fs.rmSync(profilePath(id))
|
||||||
window?.webContents.send('profileConfigUpdated')
|
window?.webContents.send('profileConfigUpdated')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" lang="zh" />
|
||||||
<title>Mihomo Party</title>
|
<title>Mihomo Party</title>
|
||||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||||
<meta
|
<meta
|
||||||
|
|||||||
@ -1,39 +1,131 @@
|
|||||||
import { Button, Card, CardBody, CardFooter, Progress } from '@nextui-org/react'
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
CardFooter,
|
||||||
|
Dropdown,
|
||||||
|
DropdownItem,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownTrigger,
|
||||||
|
Progress
|
||||||
|
} from '@nextui-org/react'
|
||||||
import { calcPercent, calcTraffic } from '@renderer/utils/calc'
|
import { calcPercent, calcTraffic } from '@renderer/utils/calc'
|
||||||
import React from 'react'
|
import { IoMdMore, IoMdRefresh } from 'react-icons/io'
|
||||||
import { IoMdRefresh } from 'react-icons/io'
|
import dayjs from 'dayjs'
|
||||||
|
import React, { Key, useMemo } from 'react'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
info: IProfileItem
|
info: IProfileItem
|
||||||
isCurrent: boolean
|
isCurrent: boolean
|
||||||
|
removeProfileItem: (id: string) => Promise<void>
|
||||||
|
mutateProfileConfig: () => void
|
||||||
onClick: () => Promise<void>
|
onClick: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MenuItem {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
showDivider: boolean
|
||||||
|
color: 'default' | 'danger'
|
||||||
|
className: string
|
||||||
|
}
|
||||||
const ProfileItem: React.FC<Props> = (props) => {
|
const ProfileItem: React.FC<Props> = (props) => {
|
||||||
const { info, onClick, isCurrent } = props
|
const { info, removeProfileItem, mutateProfileConfig, onClick, isCurrent } = props
|
||||||
const extra = info?.extra
|
const extra = info?.extra
|
||||||
const usage = (extra?.upload ?? 0) + (extra?.download ?? 0)
|
const usage = (extra?.upload ?? 0) + (extra?.download ?? 0)
|
||||||
const total = extra?.total ?? 0
|
const total = extra?.total ?? 0
|
||||||
|
|
||||||
|
const menuItems: MenuItem[] = useMemo(() => {
|
||||||
|
const list = [
|
||||||
|
{
|
||||||
|
key: 'edit',
|
||||||
|
label: '编辑文件',
|
||||||
|
showDivider: true,
|
||||||
|
color: 'default',
|
||||||
|
className: ''
|
||||||
|
} as MenuItem,
|
||||||
|
{
|
||||||
|
key: 'delete',
|
||||||
|
label: '删除',
|
||||||
|
showDivider: false,
|
||||||
|
color: 'danger',
|
||||||
|
className: 'text-danger'
|
||||||
|
} as MenuItem
|
||||||
|
]
|
||||||
|
if (info.home) {
|
||||||
|
list.unshift({
|
||||||
|
key: 'home',
|
||||||
|
label: '主页',
|
||||||
|
showDivider: false,
|
||||||
|
color: 'default',
|
||||||
|
className: ''
|
||||||
|
} as MenuItem)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}, [info])
|
||||||
|
|
||||||
|
const onMenuAction = (key: Key): void => {
|
||||||
|
switch (key) {
|
||||||
|
case 'edit':
|
||||||
|
break
|
||||||
|
case 'delete': {
|
||||||
|
removeProfileItem(info.id)
|
||||||
|
mutateProfileConfig()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'home': {
|
||||||
|
open(info.home)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card fullWidth isPressable onPress={onClick} className={isCurrent ? 'bg-primary' : ''}>
|
<Card fullWidth isPressable onPress={onClick} className={isCurrent ? 'bg-primary' : ''}>
|
||||||
<CardBody>
|
<CardBody className="pb-1">
|
||||||
<div className="flex justify-between h-[32px]">
|
<div className="flex justify-between h-[32px]">
|
||||||
<h3 className="select-none text-ellipsis whitespace-nowrap overflow-hidden text-md font-bold leading-[32px]">
|
<h3 className="select-none text-ellipsis whitespace-nowrap overflow-hidden text-md font-bold leading-[32px]">
|
||||||
{info?.name}
|
{info?.name}
|
||||||
</h3>
|
</h3>
|
||||||
<Button isIconOnly size="sm" variant="light" color="default">
|
<div className="flex">
|
||||||
<IoMdRefresh color="default" className="text-[24px]" />
|
<Button isIconOnly size="sm" variant="light" color="default">
|
||||||
</Button>
|
<IoMdRefresh color="default" className="text-[24px]" />
|
||||||
|
</Button>
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownTrigger>
|
||||||
|
<Button isIconOnly size="sm" variant="light" color="default">
|
||||||
|
<IoMdMore color="default" className="text-[24px]" />
|
||||||
|
</Button>
|
||||||
|
</DropdownTrigger>
|
||||||
|
<DropdownMenu onAction={onMenuAction}>
|
||||||
|
{menuItems.map((item) => (
|
||||||
|
<DropdownItem
|
||||||
|
showDivider={item.showDivider}
|
||||||
|
key={item.key}
|
||||||
|
color={item.color}
|
||||||
|
className={item.className}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</DropdownItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex justify-between">
|
||||||
|
<small>{extra ? `${calcTraffic(usage)}/${calcTraffic(total)}` : undefined}</small>
|
||||||
|
<small>{dayjs(info.updated).fromNow()}</small>
|
||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
<CardFooter className="pt-1">
|
<CardFooter className="pt-0">
|
||||||
<Progress
|
{extra && (
|
||||||
classNames={{ indicator: 'bg-foreground', label: 'select-none' }}
|
<Progress
|
||||||
label={extra ? `${calcTraffic(usage)}/${calcTraffic(total)}` : undefined}
|
className="w-full"
|
||||||
value={calcPercent(extra?.upload, extra?.download, extra?.total)}
|
classNames={{ indicator: 'bg-foreground', label: 'select-none' }}
|
||||||
className="max-w-md"
|
value={calcPercent(extra?.upload, extra?.download, extra?.total)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -3,6 +3,12 @@ import { useProfileConfig } from '@renderer/hooks/use-profile-config'
|
|||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
import { calcTraffic, calcPercent } from '@renderer/utils/calc'
|
import { calcTraffic, calcPercent } from '@renderer/utils/calc'
|
||||||
import { IoMdRefresh } from 'react-icons/io'
|
import { IoMdRefresh } from 'react-icons/io'
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
|
import 'dayjs/locale/zh-cn'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime)
|
||||||
|
dayjs.locale('zh-cn')
|
||||||
|
|
||||||
const ProfileCard: React.FC = () => {
|
const ProfileCard: React.FC = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@ -16,6 +22,7 @@ const ProfileCard: React.FC = () => {
|
|||||||
type: 'local',
|
type: 'local',
|
||||||
name: '空白订阅'
|
name: '空白订阅'
|
||||||
}
|
}
|
||||||
|
|
||||||
const extra = info?.extra
|
const extra = info?.extra
|
||||||
const usage = (extra?.upload ?? 0) + (extra?.download ?? 0)
|
const usage = (extra?.upload ?? 0) + (extra?.download ?? 0)
|
||||||
const total = extra?.total ?? 0
|
const total = extra?.total ?? 0
|
||||||
@ -27,7 +34,7 @@ const ProfileCard: React.FC = () => {
|
|||||||
isPressable
|
isPressable
|
||||||
onPress={() => navigate('/profiles')}
|
onPress={() => navigate('/profiles')}
|
||||||
>
|
>
|
||||||
<CardBody>
|
<CardBody className="pb-1">
|
||||||
<div className="flex justify-between h-[32px]">
|
<div className="flex justify-between h-[32px]">
|
||||||
<h3 className="select-none text-ellipsis whitespace-nowrap overflow-hidden text-md font-bold leading-[32px]">
|
<h3 className="select-none text-ellipsis whitespace-nowrap overflow-hidden text-md font-bold leading-[32px]">
|
||||||
{info?.name}
|
{info?.name}
|
||||||
@ -36,14 +43,19 @@ const ProfileCard: React.FC = () => {
|
|||||||
<IoMdRefresh color="default" className="text-[24px]" />
|
<IoMdRefresh color="default" className="text-[24px]" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-2 flex justify-between">
|
||||||
|
<small>{extra ? `${calcTraffic(usage)}/${calcTraffic(total)}` : undefined}</small>
|
||||||
|
<small>{dayjs(info.updated).fromNow()}</small>
|
||||||
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
<CardFooter className="pt-1">
|
<CardFooter className="pt-0">
|
||||||
<Progress
|
{extra && (
|
||||||
classNames={{ indicator: 'bg-foreground', label: 'select-none' }}
|
<Progress
|
||||||
label={extra ? `${calcTraffic(usage)}/${calcTraffic(total)}` : undefined}
|
className="w-full"
|
||||||
value={calcPercent(extra?.upload, extra?.download, extra?.total)}
|
classNames={{ indicator: 'bg-foreground', label: 'select-none' }}
|
||||||
className="max-w-md"
|
value={calcPercent(extra?.upload, extra?.download, extra?.total)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -3,12 +3,8 @@ import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-c
|
|||||||
import BorderSwitch from '@renderer/components/base/border-swtich'
|
import BorderSwitch from '@renderer/components/base/border-swtich'
|
||||||
import { TbDeviceIpadHorizontalBolt } from 'react-icons/tb'
|
import { TbDeviceIpadHorizontalBolt } from 'react-icons/tb'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
import {
|
import { encryptString, patchMihomoConfig, isEncryptionAvailable } from '@renderer/utils/ipc'
|
||||||
platform,
|
import { platform } from '@renderer/utils/init'
|
||||||
encryptString,
|
|
||||||
patchMihomoConfig,
|
|
||||||
isEncryptionAvailable
|
|
||||||
} from '@renderer/utils/ipc'
|
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||||
import BasePasswordModal from '../base/base-password-modal'
|
import BasePasswordModal from '../base/base-password-modal'
|
||||||
@ -24,7 +20,7 @@ const TunSwitcher: React.FC = () => {
|
|||||||
const { enable } = tun || {}
|
const { enable } = tun || {}
|
||||||
|
|
||||||
const onChange = async (enable: boolean): Promise<void> => {
|
const onChange = async (enable: boolean): Promise<void> => {
|
||||||
if (enable && (await platform()) !== 'win32') {
|
if (enable && platform !== 'win32') {
|
||||||
const encryptionAvailable = await isEncryptionAvailable()
|
const encryptionAvailable = await isEncryptionAvailable()
|
||||||
if (!appConfig?.encryptedPassword && encryptionAvailable) {
|
if (!appConfig?.encryptedPassword && encryptionAvailable) {
|
||||||
setOpenPasswordModal(true)
|
setOpenPasswordModal(true)
|
||||||
|
|||||||
@ -3,18 +3,20 @@ import ReactDOM from 'react-dom/client'
|
|||||||
import { HashRouter } from 'react-router-dom'
|
import { HashRouter } from 'react-router-dom'
|
||||||
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
||||||
import { NextUIProvider } from '@nextui-org/react'
|
import { NextUIProvider } from '@nextui-org/react'
|
||||||
import '@renderer/utils/init'
|
import { init } from '@renderer/utils/init'
|
||||||
import '@renderer/assets/main.css'
|
import '@renderer/assets/main.css'
|
||||||
import App from '@renderer/App'
|
import App from '@renderer/App'
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
init().then(() => {
|
||||||
<React.StrictMode>
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
<NextUIProvider>
|
<React.StrictMode>
|
||||||
<NextThemesProvider attribute="class" defaultTheme="dark">
|
<NextUIProvider>
|
||||||
<HashRouter>
|
<NextThemesProvider attribute="class" defaultTheme="dark">
|
||||||
<App />
|
<HashRouter>
|
||||||
</HashRouter>
|
<App />
|
||||||
</NextThemesProvider>
|
</HashRouter>
|
||||||
</NextUIProvider>
|
</NextThemesProvider>
|
||||||
</React.StrictMode>
|
</NextUIProvider>
|
||||||
)
|
</React.StrictMode>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|||||||
@ -6,7 +6,13 @@ import { useState } from 'react'
|
|||||||
import { MdContentPaste } from 'react-icons/md'
|
import { MdContentPaste } from 'react-icons/md'
|
||||||
|
|
||||||
const Profiles: React.FC = () => {
|
const Profiles: React.FC = () => {
|
||||||
const { profileConfig, addProfileItem, changeCurrentProfile } = useProfileConfig()
|
const {
|
||||||
|
profileConfig,
|
||||||
|
addProfileItem,
|
||||||
|
removeProfileItem,
|
||||||
|
changeCurrentProfile,
|
||||||
|
mutateProfileConfig
|
||||||
|
} = useProfileConfig()
|
||||||
const { current, items } = profileConfig || {}
|
const { current, items } = profileConfig || {}
|
||||||
const [importing, setImporting] = useState(false)
|
const [importing, setImporting] = useState(false)
|
||||||
const [url, setUrl] = useState('')
|
const [url, setUrl] = useState('')
|
||||||
@ -55,6 +61,8 @@ const Profiles: React.FC = () => {
|
|||||||
<ProfileItem
|
<ProfileItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
isCurrent={item.id === current}
|
isCurrent={item.id === current}
|
||||||
|
removeProfileItem={removeProfileItem}
|
||||||
|
mutateProfileConfig={mutateProfileConfig}
|
||||||
info={item}
|
info={item}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await changeCurrentProfile(item.id)
|
await changeCurrentProfile(item.id)
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
import { getPlatform } from './ipc'
|
||||||
const originError = console.error
|
const originError = console.error
|
||||||
const originWarn = console.warn
|
const originWarn = console.warn
|
||||||
console.error = function (...args: any[]): void {
|
console.error = function (...args: any[]): void {
|
||||||
@ -13,3 +15,9 @@ console.warn = function (...args): void {
|
|||||||
}
|
}
|
||||||
originWarn.call(console, args)
|
originWarn.call(console, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export let platform: NodeJS.Platform
|
||||||
|
|
||||||
|
export async function init(): Promise<void> {
|
||||||
|
platform = await getPlatform()
|
||||||
|
}
|
||||||
|
|||||||
@ -115,7 +115,7 @@ export async function encryptString(str: string): Promise<Buffer> {
|
|||||||
return await window.electron.ipcRenderer.invoke('encryptString', str)
|
return await window.electron.ipcRenderer.invoke('encryptString', str)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function platform(): Promise<NodeJS.Platform> {
|
export async function getPlatform(): Promise<NodeJS.Platform> {
|
||||||
return await window.electron.ipcRenderer.invoke('platform')
|
return await window.electron.ipcRenderer.invoke('platform')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user