mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-28 05:30:29 +08:00
support local profile
This commit is contained in:
parent
80020eadc8
commit
fd86c0dba4
@ -105,7 +105,7 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
|
||||
const newItem = {
|
||||
id,
|
||||
name: item.name || (item.type === 'remote' ? 'Remote File' : 'Local File'),
|
||||
type: item.type || 'local',
|
||||
type: item.type,
|
||||
url: item.url,
|
||||
interval: item.interval || 0,
|
||||
updated: new Date().getTime()
|
||||
|
||||
@ -94,14 +94,14 @@ async function setupFirewall(): Promise<void> {
|
||||
try {
|
||||
execSync(removeCommand, { shell: 'powershell' })
|
||||
} catch {
|
||||
console.log('Remove-NetFirewallRule Failed')
|
||||
console.error('Remove-NetFirewallRule Failed')
|
||||
}
|
||||
try {
|
||||
execSync(createCommand, { shell: 'powershell' })
|
||||
} catch (e) {
|
||||
dialog.showErrorBox('防火墙设置失败', `${e}`)
|
||||
reject(e)
|
||||
console.log('New-NetFirewallRule Failed')
|
||||
console.error('New-NetFirewallRule Failed')
|
||||
}
|
||||
}
|
||||
resolve()
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Divider } from '@nextui-org/react'
|
||||
import React from 'react'
|
||||
import React, { forwardRef, useImperativeHandle, useRef } from 'react'
|
||||
interface Props {
|
||||
title?: React.ReactNode
|
||||
header?: React.ReactNode
|
||||
@ -7,9 +7,14 @@ interface Props {
|
||||
contentClassName?: string
|
||||
}
|
||||
|
||||
const BasePage: React.FC<Props> = (props) => {
|
||||
const BasePage = forwardRef<HTMLDivElement, Props>((props, ref) => {
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
useImperativeHandle(ref, () => {
|
||||
return contentRef.current as HTMLDivElement
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="w-full h-full overflow-y-auto custom-scrollbar">
|
||||
<div ref={contentRef} className="w-full h-full overflow-y-auto custom-scrollbar">
|
||||
<div className="sticky top-0 z-40 h-[48px] w-full backdrop-blur bg-background/40">
|
||||
<div className="p-2 flex justify-between">
|
||||
<div className="select-none title h-full text-lg leading-[32px]">{props.title}</div>
|
||||
@ -20,6 +25,7 @@ const BasePage: React.FC<Props> = (props) => {
|
||||
<div className="content">{props.children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
BasePage.displayName = 'BasePage'
|
||||
export default BasePage
|
||||
|
||||
@ -50,7 +50,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
||||
/>
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
{values.type === 'remote' && (
|
||||
<SettingItem title="更新间隔(分钟)">
|
||||
<Input
|
||||
size="sm"
|
||||
@ -62,6 +62,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="light" onPress={onClose}>
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
Card,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
Chip,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
@ -139,6 +140,7 @@ const ProfileItem: React.FC<Props> = (props) => {
|
||||
{info?.name}
|
||||
</h3>
|
||||
<div className="flex">
|
||||
{info.type === 'remote' && (
|
||||
<Button
|
||||
isIconOnly
|
||||
size="sm"
|
||||
@ -157,6 +159,8 @@ const ProfileItem: React.FC<Props> = (props) => {
|
||||
className={`${isCurrent ? 'text-white' : 'text-foreground'} text-[24px] ${updating ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button isIconOnly size="sm" variant="light" color="default">
|
||||
@ -181,12 +185,27 @@ const ProfileItem: React.FC<Props> = (props) => {
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
{info.type === 'remote' && (
|
||||
<div
|
||||
className={`mt-2 flex select-none justify-between ${isCurrent ? 'text-white' : 'text-foreground'}`}
|
||||
>
|
||||
<small>{extra ? `${calcTraffic(usage)}/${calcTraffic(total)}` : undefined}</small>
|
||||
<small>{dayjs(info.updated).fromNow()}</small>
|
||||
</div>
|
||||
)}
|
||||
{info.type === 'local' && (
|
||||
<div
|
||||
className={`mt-2 flex select-none justify-between ${isCurrent ? 'text-white' : 'text-foreground'}`}
|
||||
>
|
||||
<Chip
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
className={`${isCurrent ? 'text-white border-white' : 'border-primary text-primary'}`}
|
||||
>
|
||||
本地
|
||||
</Chip>
|
||||
</div>
|
||||
)}
|
||||
</CardBody>
|
||||
<CardFooter className="pt-0">
|
||||
{extra && (
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Button, Card, CardBody, CardFooter, Progress } from '@nextui-org/react'
|
||||
import { Button, Card, CardBody, CardFooter, Chip, Progress } from '@nextui-org/react'
|
||||
import { useProfileConfig } from '@renderer/hooks/use-profile-config'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { calcTraffic, calcPercent } from '@renderer/utils/calc'
|
||||
@ -42,6 +42,7 @@ const ProfileCard: React.FC = () => {
|
||||
>
|
||||
{info?.name}
|
||||
</h3>
|
||||
{info.type === 'remote' && (
|
||||
<Button
|
||||
isIconOnly
|
||||
size="sm"
|
||||
@ -59,13 +60,29 @@ const ProfileCard: React.FC = () => {
|
||||
className={`text-[24px] ${match ? 'text-white' : 'text-foreground'} ${updating ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{info.type === 'remote' && (
|
||||
<div
|
||||
className={`mt-2 flex select-none justify-between ${match ? 'text-white' : 'text-foreground'} `}
|
||||
>
|
||||
<small>{extra ? `${calcTraffic(usage)}/${calcTraffic(total)}` : undefined}</small>
|
||||
<small>{dayjs(info.updated).fromNow()}</small>
|
||||
</div>
|
||||
)}
|
||||
{info.type === 'local' && (
|
||||
<div
|
||||
className={`mt-2 flex select-none justify-between ${match ? 'text-white' : 'text-foreground'}`}
|
||||
>
|
||||
<Chip
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
className={`${match ? 'text-white border-white' : 'border-primary text-primary'}`}
|
||||
>
|
||||
本地
|
||||
</Chip>
|
||||
</div>
|
||||
)}
|
||||
</CardBody>
|
||||
<CardFooter className="pt-0">
|
||||
{extra && (
|
||||
|
||||
@ -19,7 +19,7 @@ const SysproxySwitcher: React.FC = () => {
|
||||
await triggerSysProxy(enable)
|
||||
await patchAppConfig({ sysProxy: { enable } })
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import { Button, Input } from '@nextui-org/react'
|
||||
import BasePage from '@renderer/components/base/base-page'
|
||||
import ProfileItem from '@renderer/components/profiles/profile-item'
|
||||
import { useProfileConfig } from '@renderer/hooks/use-profile-config'
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { MdContentPaste } from 'react-icons/md'
|
||||
|
||||
const Profiles: React.FC = () => {
|
||||
@ -16,6 +16,7 @@ const Profiles: React.FC = () => {
|
||||
} = useProfileConfig()
|
||||
const { current, items } = profileConfig || {}
|
||||
const [importing, setImporting] = useState(false)
|
||||
const [fileOver, setFileOver] = useState(false)
|
||||
const [url, setUrl] = useState('')
|
||||
|
||||
const handleImport = async (): Promise<void> => {
|
||||
@ -26,9 +27,50 @@ const Profiles: React.FC = () => {
|
||||
setImporting(false)
|
||||
}
|
||||
}
|
||||
const pageRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
pageRef.current?.addEventListener('dragover', (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setFileOver(true)
|
||||
})
|
||||
pageRef.current?.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setFileOver(false)
|
||||
})
|
||||
pageRef.current?.addEventListener('drop', (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (event.dataTransfer?.files) {
|
||||
const file = event.dataTransfer.files[0]
|
||||
if (file.name.endsWith('.yml') || file.name.endsWith('.yaml')) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = async (e): Promise<void> => {
|
||||
const content = e.target?.result as string
|
||||
try {
|
||||
await addProfileItem({ name: file.name, type: 'local', file: content })
|
||||
} finally {
|
||||
setFileOver(false)
|
||||
}
|
||||
}
|
||||
reader.readAsText(file)
|
||||
} else {
|
||||
alert('不支持的文件类型')
|
||||
}
|
||||
}
|
||||
setFileOver(false)
|
||||
})
|
||||
return (): void => {
|
||||
pageRef.current?.removeEventListener('dragover', () => {})
|
||||
pageRef.current?.removeEventListener('dragleave', () => {})
|
||||
pageRef.current?.removeEventListener('drop', () => {})
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<BasePage title="订阅管理">
|
||||
<BasePage ref={pageRef} title="订阅管理">
|
||||
<div className="sticky top-[48px] z-40 backdrop-blur bg-background/40 flex p-2">
|
||||
<Input
|
||||
variant="bordered"
|
||||
@ -61,7 +103,9 @@ const Profiles: React.FC = () => {
|
||||
导入
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2 mx-2">
|
||||
<div
|
||||
className={`${fileOver ? 'blur-sm' : ''} grid sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2 mx-2`}
|
||||
>
|
||||
{items?.map((item) => (
|
||||
<ProfileItem
|
||||
key={item.id}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user