Compare commits

...

3 Commits

Author SHA1 Message Date
Memory
99a5505d16
feat: launch WebUI directly from subscription manager 2025-09-27 10:00:21 +08:00
Memory
76a849e376
i18n: refine Russian (ru-RU) translations 2025-09-27 09:37:35 +08:00
Memory
74b65430be
opt: card size 2025-09-27 09:37:22 +08:00
21 changed files with 125 additions and 21 deletions

View File

@ -183,6 +183,11 @@ export const mihomoUpgrade = async (): Promise<void> => {
return await instance.post('/upgrade') return await instance.post('/upgrade')
} }
export const mihomoUpgradeUI = async (): Promise<void> => {
const instance = await getAxios()
return await instance.post('/upgrade/ui')
}
// Smart 内核 API // Smart 内核 API
export const mihomoSmartGroupWeights = async ( export const mihomoSmartGroupWeights = async (
groupName: string groupName: string

View File

@ -15,6 +15,7 @@ import {
mihomoUpdateRuleProviders, mihomoUpdateRuleProviders,
mihomoUpgrade, mihomoUpgrade,
mihomoUpgradeGeo, mihomoUpgradeGeo,
mihomoUpgradeUI,
mihomoVersion, mihomoVersion,
patchMihomoConfig, patchMihomoConfig,
mihomoSmartGroupWeights, mihomoSmartGroupWeights,
@ -167,6 +168,7 @@ export function registerIpcMainHandlers(): void {
ipcMain.handle('mihomoUnfixedProxy', (_e, group) => ipcErrorWrapper(mihomoUnfixedProxy)(group)) ipcMain.handle('mihomoUnfixedProxy', (_e, group) => ipcErrorWrapper(mihomoUnfixedProxy)(group))
ipcMain.handle('mihomoUpgradeGeo', ipcErrorWrapper(mihomoUpgradeGeo)) ipcMain.handle('mihomoUpgradeGeo', ipcErrorWrapper(mihomoUpgradeGeo))
ipcMain.handle('mihomoUpgrade', ipcErrorWrapper(mihomoUpgrade)) ipcMain.handle('mihomoUpgrade', ipcErrorWrapper(mihomoUpgrade))
ipcMain.handle('mihomoUpgradeUI', ipcErrorWrapper(mihomoUpgradeUI))
ipcMain.handle('mihomoProxyDelay', (_e, proxy, url) => ipcMain.handle('mihomoProxyDelay', (_e, proxy, url) =>
ipcErrorWrapper(mihomoProxyDelay)(proxy, url) ipcErrorWrapper(mihomoProxyDelay)(proxy, url)
) )

View File

@ -138,3 +138,9 @@
background: #c0c1c58f; background: #c0c1c58f;
} }
} }
.sider-card-title {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}

View File

@ -218,7 +218,7 @@ const ConnCard: React.FC<Props> = (props) => {
</CardBody> </CardBody>
<CardFooter className="pt-1"> <CardFooter className="pt-1">
<h3 <h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`} className={`text-md font-bold sider-card-title ${match ? 'text-primary-foreground' : 'text-foreground'}`}
> >
{t('sider.cards.connections')} {t('sider.cards.connections')}
</h3> </h3>

View File

@ -102,7 +102,7 @@ const DNSCard: React.FC<Props> = (props) => {
</CardBody> </CardBody>
<CardFooter className="pt-1"> <CardFooter className="pt-1">
<h3 <h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`} className={`text-md font-bold sider-card-title ${match ? 'text-primary-foreground' : 'text-foreground'}`}
> >
{t('sider.cards.dns')} {t('sider.cards.dns')}
</h3> </h3>

View File

@ -84,7 +84,7 @@ const LogCard: React.FC<Props> = (props) => {
</CardBody> </CardBody>
<CardFooter className="pt-1"> <CardFooter className="pt-1">
<h3 <h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`} className={`text-md font-bold text-ellipsis whitespace-nowrap overflow-hidden ${match ? 'text-primary-foreground' : 'text-foreground'}`}
> >
{t('sider.cards.logs')} {t('sider.cards.logs')}
</h3> </h3>

View File

@ -158,7 +158,7 @@ const MihomoCoreCard: React.FC<Props> = (props) => {
</CardBody> </CardBody>
<CardFooter className="pt-1"> <CardFooter className="pt-1">
<h3 <h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`} className={`text-md font-bold text-ellipsis whitespace-nowrap overflow-hidden ${match ? 'text-primary-foreground' : 'text-foreground'}`}
> >
{t('sider.cards.core')} {t('sider.cards.core')}
</h3> </h3>

View File

@ -83,7 +83,7 @@ const OverrideCard: React.FC<Props> = (props) => {
</CardBody> </CardBody>
<CardFooter className="pt-1"> <CardFooter className="pt-1">
<h3 <h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`} className={`text-md font-bold text-ellipsis whitespace-nowrap overflow-hidden ${match ? 'text-primary-foreground' : 'text-foreground'}`}
> >
{t('sider.cards.override')} {t('sider.cards.override')}
</h3> </h3>

View File

@ -238,7 +238,7 @@ const ProfileCard: React.FC<Props> = (props) => {
</CardBody> </CardBody>
<CardFooter className="pt-1"> <CardFooter className="pt-1">
<h3 <h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`} className={`text-md font-bold sider-card-title ${match ? 'text-primary-foreground' : 'text-foreground'}`}
> >
{t('sider.cards.profiles')} {t('sider.cards.profiles')}
</h3> </h3>

View File

@ -103,7 +103,7 @@ const ProxyCard: React.FC<Props> = (props) => {
</CardBody> </CardBody>
<CardFooter className="pt-1"> <CardFooter className="pt-1">
<h3 <h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`} className={`text-md font-bold sider-card-title ${match ? 'text-primary-foreground' : 'text-foreground'}`}
> >
{t('proxies.card.title')} {t('proxies.card.title')}
</h3> </h3>

View File

@ -84,7 +84,7 @@ const ResourceCard: React.FC<Props> = (props) => {
</CardBody> </CardBody>
<CardFooter className="pt-1"> <CardFooter className="pt-1">
<h3 <h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`} className={`text-md font-bold text-ellipsis whitespace-nowrap overflow-hidden ${match ? 'text-primary-foreground' : 'text-foreground'}`}
> >
{t('sider.cards.resources')} {t('sider.cards.resources')}
</h3> </h3>

View File

@ -104,7 +104,7 @@ const RuleCard: React.FC<Props> = (props) => {
</CardBody> </CardBody>
<CardFooter className="pt-1"> <CardFooter className="pt-1">
<h3 <h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`} className={`text-md font-bold text-ellipsis whitespace-nowrap overflow-hidden ${match ? 'text-primary-foreground' : 'text-foreground'}`}
> >
{t('sider.cards.rules')} {t('sider.cards.rules')}
</h3> </h3>

View File

@ -102,7 +102,7 @@ const SniffCard: React.FC<Props> = (props) => {
</CardBody> </CardBody>
<CardFooter className="pt-1"> <CardFooter className="pt-1">
<h3 <h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`} className={`text-md font-bold text-ellipsis whitespace-nowrap overflow-hidden ${match ? 'text-primary-foreground' : 'text-foreground'}`}
> >
{t('sider.cards.sniff')} {t('sider.cards.sniff')}
</h3> </h3>

View File

@ -84,7 +84,7 @@ const SubStoreCard: React.FC<Props> = (props) => {
</CardBody> </CardBody>
<CardFooter className="pt-1"> <CardFooter className="pt-1">
<h3 <h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`} className={`text-md font-bold text-ellipsis whitespace-nowrap overflow-hidden ${match ? 'text-primary-foreground' : 'text-foreground'}`}
> >
{t('sider.cards.substore')} {t('sider.cards.substore')}
</h3> </h3>

View File

@ -116,7 +116,7 @@ const SysproxySwitcher: React.FC<Props> = (props) => {
</CardBody> </CardBody>
<CardFooter className="pt-1"> <CardFooter className="pt-1">
<h3 <h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`} className={`text-md font-bold sider-card-title ${match ? 'text-primary-foreground' : 'text-foreground'}`}
> >
{t('sider.cards.systemProxy')} {t('sider.cards.systemProxy')}
</h3> </h3>

View File

@ -150,7 +150,7 @@ const TunSwitcher: React.FC<Props> = (props) => {
</CardBody> </CardBody>
<CardFooter className="pt-1"> <CardFooter className="pt-1">
<h3 <h3
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`} className={`text-md font-bold sider-card-title ${match ? 'text-primary-foreground' : 'text-foreground'}`}
> >
{t('sider.cards.tun')} {t('sider.cards.tun')}
</h3> </h3>

View File

@ -400,6 +400,12 @@
"profiles.remote": "Remote", "profiles.remote": "Remote",
"profiles.local": "Local", "profiles.local": "Local",
"profiles.trafficUsage": "Traffic Usage Progress", "profiles.trafficUsage": "Traffic Usage Progress",
"profiles.openWebUI.title": "WebUI Management Panel",
"profiles.openWebUI.description": "Select a WebUI panel to open",
"profiles.openWebUI.local": "Local WebUI",
"profiles.updateWebUI.button": "Update Panel",
"profiles.updateWebUI.success": "WebUI panel updated successfully",
"profiles.updateWebUI.failed": "WebUI panel update failed: {{error}}",
"profiles.editInfo.title": "Edit Information", "profiles.editInfo.title": "Edit Information",
"profiles.editInfo.name": "Name", "profiles.editInfo.name": "Name",
"profiles.editInfo.url": "Subscription URL", "profiles.editInfo.url": "Subscription URL",

View File

@ -25,9 +25,9 @@
"common.notification.systemProxyDisabled": "Системный прокси отключен", "common.notification.systemProxyDisabled": "Системный прокси отключен",
"common.notification.tunEnabled": "TUN включен", "common.notification.tunEnabled": "TUN включен",
"common.notification.tunDisabled": "TUN отключен", "common.notification.tunDisabled": "TUN отключен",
"common.notification.ruleMode": "Режим правил", "common.notification.ruleMode": "Правило",
"common.notification.globalMode": "Глобальный режим", "common.notification.globalMode": "Глобальный",
"common.notification.directMode": "Прямой режим", "common.notification.directMode": "Прямой",
"common.error.appCrash": "Приложение завершилось аварийно :( Пожалуйста, отправьте следующую информацию разработчику для устранения проблемы", "common.error.appCrash": "Приложение завершилось аварийно :( Пожалуйста, отправьте следующую информацию разработчику для устранения проблемы",
"common.error.copyErrorMessage": "Копировать сообщение об ошибке", "common.error.copyErrorMessage": "Копировать сообщение об ошибке",
"common.error.invalidCron": "Неверное выражение Cron", "common.error.invalidCron": "Неверное выражение Cron",
@ -249,7 +249,7 @@
"sider.cards.trafficUsage": "Использование трафика", "sider.cards.trafficUsage": "Использование трафика",
"sider.cards.neverExpire": "Бессрочно", "sider.cards.neverExpire": "Бессрочно",
"sider.cards.outbound.title": "Режим исходящего трафика", "sider.cards.outbound.title": "Режим исходящего трафика",
"sider.cards.outbound.rule": о правилам", "sider.cards.outbound.rule": равило",
"sider.cards.outbound.global": "Глобальный", "sider.cards.outbound.global": "Глобальный",
"sider.cards.outbound.direct": "Прямой", "sider.cards.outbound.direct": "Прямой",
"sider.size.large": "Большой", "sider.size.large": "Большой",
@ -508,9 +508,9 @@
"tray.showWindow": "Показать окно", "tray.showWindow": "Показать окно",
"tray.showFloatingWindow": "Показать плавающее окно", "tray.showFloatingWindow": "Показать плавающее окно",
"tray.hideFloatingWindow": "Скрыть плавающее окно", "tray.hideFloatingWindow": "Скрыть плавающее окно",
"tray.ruleMode": "Режим правил", "tray.ruleMode": "Правило",
"tray.globalMode": "Глобальный режим", "tray.globalMode": "Глобальный",
"tray.directMode": "Прямой режим", "tray.directMode": "Прямой",
"tray.systemProxy": "Системный прокси", "tray.systemProxy": "Системный прокси",
"tray.tun": "TUN", "tray.tun": "TUN",
"tray.profiles": "Профили", "tray.profiles": "Профили",

View File

@ -405,6 +405,12 @@
"profiles.traffic.expired": "已过期", "profiles.traffic.expired": "已过期",
"profiles.traffic.remainingDays": "剩余 {{days}} 天", "profiles.traffic.remainingDays": "剩余 {{days}} 天",
"profiles.traffic.lastUpdate": "最后更新:{{time}}", "profiles.traffic.lastUpdate": "最后更新:{{time}}",
"profiles.openWebUI.title": "WebUI 管理面板",
"profiles.openWebUI.description": "选择一个 WebUI 面板打开",
"profiles.openWebUI.local": "本地 WebUI",
"profiles.updateWebUI.button": "更新面板",
"profiles.updateWebUI.success": "WebUI 面板更新成功",
"profiles.updateWebUI.failed": "WebUI 面板更新失败: {{error}}",
"profiles.editInfo.title": "编辑信息", "profiles.editInfo.title": "编辑信息",
"profiles.editInfo.name": "名称", "profiles.editInfo.name": "名称",
"profiles.editInfo.url": "订阅地址", "profiles.editInfo.url": "订阅地址",

View File

@ -7,12 +7,16 @@ import {
DropdownItem, DropdownItem,
DropdownMenu, DropdownMenu,
DropdownTrigger, DropdownTrigger,
Input Input,
Card,
CardBody,
CardHeader
} from '@heroui/react' } from '@heroui/react'
import BasePage from '@renderer/components/base/base-page' import BasePage from '@renderer/components/base/base-page'
import ProfileItem from '@renderer/components/profiles/profile-item' import ProfileItem from '@renderer/components/profiles/profile-item'
import { useProfileConfig } from '@renderer/hooks/use-profile-config' import { useProfileConfig } from '@renderer/hooks/use-profile-config'
import { useAppConfig } from '@renderer/hooks/use-app-config' import { useAppConfig } from '@renderer/hooks/use-app-config'
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
import { getFilePath, readTextFile, subStoreCollections, subStoreSubs } from '@renderer/utils/ipc' import { getFilePath, readTextFile, subStoreCollections, subStoreSubs } from '@renderer/utils/ipc'
import type { KeyboardEvent } from 'react' import type { KeyboardEvent } from 'react'
import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
@ -32,6 +36,7 @@ import SubStoreIcon from '@renderer/components/base/substore-icon'
import useSWR from 'swr' import useSWR from 'swr'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { mihomoUpgradeUI } from '@renderer/utils/ipc'
const Profiles: React.FC = () => { const Profiles: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
@ -45,6 +50,9 @@ const Profiles: React.FC = () => {
mutateProfileConfig mutateProfileConfig
} = useProfileConfig() } = useProfileConfig()
const { appConfig } = useAppConfig() const { appConfig } = useAppConfig()
const { controledMihomoConfig } = useControledMihomoConfig()
const externalController = controledMihomoConfig?.['external-controller'] || ''
const externalUI = (controledMihomoConfig as any)?.['external-ui']
const { useSubStore = true, useCustomSubStore = false, customSubStoreUrl = '' } = appConfig || {} const { useSubStore = true, useCustomSubStore = false, customSubStoreUrl = '' } = appConfig || {}
const { current, items = [] } = profileConfig || {} const { current, items = [] } = profileConfig || {}
const navigate = useNavigate() const navigate = useNavigate()
@ -195,6 +203,26 @@ const Profiles: React.FC = () => {
setSortedItems(items) setSortedItems(items)
}, [items]) }, [items])
// 获取本地WebUI的URL
const getLocalWebUIUrl = (): string => {
if (externalController) {
// 将地址转换为WebUI URL
// 例如: 127.0.0.1:9090 -> http://127.0.0.1:9090/ui
const controller = externalController.replace('0.0.0.0', '127.0.0.1')
// 如果配置了external-ui使用/ui路径否则可能需要使用不同的路径
const uiPath = externalUI ? '/ui' : '/ui' // 默认使用/ui路径
return `http://${controller}${uiPath}`
}
// 默认URL
return 'http://127.0.0.1:9090/ui'
}
// 检查本地WebUI是否可用
const isLocalWebUIAvailable = (): boolean => {
// 如果有配置的external-controller则认为本地WebUI可用
return !!externalController
}
return ( return (
<BasePage <BasePage
ref={pageRef} ref={pageRef}
@ -378,6 +406,53 @@ const Profiles: React.FC = () => {
</div> </div>
<Divider /> <Divider />
</div> </div>
{/* WebUI Card with Multiple Options */}
<div className="m-2">
<Card>
<CardHeader className="flex gap-3">
<div className="flex flex-col">
<p className="text-md">{t('profiles.openWebUI.title')}</p>
<p className="text-small text-default-500">{t('profiles.openWebUI.description')}</p>
</div>
</CardHeader>
<CardBody>
<div className="flex gap-2 flex-wrap">
<Button
onPress={() => window.open('https://metacubexd.pages.dev/', '_blank')}
>
MetaCubeXD
</Button>
<Button
onPress={() => window.open('https://zashboard.pages.dev/', '_blank')}
>
Zashboard
</Button>
{isLocalWebUIAvailable() && (
<>
<Button
onPress={() => window.open(getLocalWebUIUrl(), '_blank')}
>
{t('profiles.openWebUI.local')}
</Button>
<Button
color="success"
onPress={async () => {
try {
await mihomoUpgradeUI()
new Notification(t('profiles.updateWebUI.success'))
} catch (e) {
new Notification(t('profiles.updateWebUI.failed', { error: String(e) }))
}
}}
>
{t('profiles.updateWebUI.button')}
</Button>
</>
)}
</div>
</CardBody>
</Card>
</div>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}> <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
<div <div
className={`${fileOver ? 'blur-sm' : ''} grid sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2 m-2`} className={`${fileOver ? 'blur-sm' : ''} grid sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2 m-2`}

View File

@ -84,6 +84,10 @@ export async function mihomoUpgrade(): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoUpgrade')) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoUpgrade'))
} }
export async function mihomoUpgradeUI(): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoUpgradeUI'))
}
export async function mihomoProxyDelay(proxy: string, url?: string): Promise<IMihomoDelay> { export async function mihomoProxyDelay(proxy: string, url?: string): Promise<IMihomoDelay> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoProxyDelay', proxy, url)) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoProxyDelay', proxy, url))
} }