mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-27 21:20: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 = {
|
const newItem = {
|
||||||
id,
|
id,
|
||||||
name: item.name || (item.type === 'remote' ? 'Remote File' : 'Local File'),
|
name: item.name || (item.type === 'remote' ? 'Remote File' : 'Local File'),
|
||||||
type: item.type || 'local',
|
type: item.type,
|
||||||
url: item.url,
|
url: item.url,
|
||||||
interval: item.interval || 0,
|
interval: item.interval || 0,
|
||||||
updated: new Date().getTime()
|
updated: new Date().getTime()
|
||||||
|
|||||||
@ -94,14 +94,14 @@ async function setupFirewall(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
execSync(removeCommand, { shell: 'powershell' })
|
execSync(removeCommand, { shell: 'powershell' })
|
||||||
} catch {
|
} catch {
|
||||||
console.log('Remove-NetFirewallRule Failed')
|
console.error('Remove-NetFirewallRule Failed')
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
execSync(createCommand, { shell: 'powershell' })
|
execSync(createCommand, { shell: 'powershell' })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dialog.showErrorBox('防火墙设置失败', `${e}`)
|
dialog.showErrorBox('防火墙设置失败', `${e}`)
|
||||||
reject(e)
|
reject(e)
|
||||||
console.log('New-NetFirewallRule Failed')
|
console.error('New-NetFirewallRule Failed')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resolve()
|
resolve()
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Divider } from '@nextui-org/react'
|
import { Divider } from '@nextui-org/react'
|
||||||
import React from 'react'
|
import React, { forwardRef, useImperativeHandle, useRef } from 'react'
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: React.ReactNode
|
title?: React.ReactNode
|
||||||
header?: React.ReactNode
|
header?: React.ReactNode
|
||||||
@ -7,9 +7,14 @@ interface Props {
|
|||||||
contentClassName?: string
|
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 (
|
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="sticky top-0 z-40 h-[48px] w-full backdrop-blur bg-background/40">
|
||||||
<div className="p-2 flex justify-between">
|
<div className="p-2 flex justify-between">
|
||||||
<div className="select-none title h-full text-lg leading-[32px]">{props.title}</div>
|
<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 className="content">{props.children}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
|
BasePage.displayName = 'BasePage'
|
||||||
export default BasePage
|
export default BasePage
|
||||||
|
|||||||
@ -50,18 +50,19 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
)}
|
)}
|
||||||
|
{values.type === 'remote' && (
|
||||||
<SettingItem title="更新间隔(分钟)">
|
<SettingItem title="更新间隔(分钟)">
|
||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
type="number"
|
type="number"
|
||||||
className="w-[200px]"
|
className="w-[200px]"
|
||||||
value={values.interval?.toString() ?? ''}
|
value={values.interval?.toString() ?? ''}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
setValues({ ...values, interval: parseInt(v) })
|
setValues({ ...values, interval: parseInt(v) })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
)}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button variant="light" onPress={onClose}>
|
<Button variant="light" onPress={onClose}>
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
CardBody,
|
CardBody,
|
||||||
CardFooter,
|
CardFooter,
|
||||||
|
Chip,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@ -139,24 +140,27 @@ const ProfileItem: React.FC<Props> = (props) => {
|
|||||||
{info?.name}
|
{info?.name}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<Button
|
{info.type === 'remote' && (
|
||||||
isIconOnly
|
<Button
|
||||||
size="sm"
|
isIconOnly
|
||||||
variant="light"
|
size="sm"
|
||||||
color="default"
|
variant="light"
|
||||||
disabled={updating}
|
|
||||||
onPress={() => {
|
|
||||||
setUpdating(true)
|
|
||||||
addProfileItem(info).finally(() => {
|
|
||||||
setUpdating(false)
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IoMdRefresh
|
|
||||||
color="default"
|
color="default"
|
||||||
className={`${isCurrent ? 'text-white' : 'text-foreground'} text-[24px] ${updating ? 'animate-spin' : ''}`}
|
disabled={updating}
|
||||||
/>
|
onPress={() => {
|
||||||
</Button>
|
setUpdating(true)
|
||||||
|
addProfileItem(info).finally(() => {
|
||||||
|
setUpdating(false)
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IoMdRefresh
|
||||||
|
color="default"
|
||||||
|
className={`${isCurrent ? 'text-white' : 'text-foreground'} text-[24px] ${updating ? 'animate-spin' : ''}`}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Button isIconOnly size="sm" variant="light" color="default">
|
<Button isIconOnly size="sm" variant="light" color="default">
|
||||||
@ -181,12 +185,27 @@ const ProfileItem: React.FC<Props> = (props) => {
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
{info.type === 'remote' && (
|
||||||
className={`mt-2 flex select-none justify-between ${isCurrent ? 'text-white' : 'text-foreground'}`}
|
<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>
|
<small>{extra ? `${calcTraffic(usage)}/${calcTraffic(total)}` : undefined}</small>
|
||||||
</div>
|
<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>
|
</CardBody>
|
||||||
<CardFooter className="pt-0">
|
<CardFooter className="pt-0">
|
||||||
{extra && (
|
{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 { 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'
|
||||||
@ -42,30 +42,47 @@ const ProfileCard: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{info?.name}
|
{info?.name}
|
||||||
</h3>
|
</h3>
|
||||||
<Button
|
{info.type === 'remote' && (
|
||||||
isIconOnly
|
<Button
|
||||||
size="sm"
|
isIconOnly
|
||||||
disabled={updating}
|
size="sm"
|
||||||
variant="light"
|
disabled={updating}
|
||||||
color="default"
|
variant="light"
|
||||||
onPress={() => {
|
color="default"
|
||||||
setUpdating(true)
|
onPress={() => {
|
||||||
addProfileItem(info).finally(() => {
|
setUpdating(true)
|
||||||
setUpdating(false)
|
addProfileItem(info).finally(() => {
|
||||||
})
|
setUpdating(false)
|
||||||
}}
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IoMdRefresh
|
||||||
|
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'} `}
|
||||||
>
|
>
|
||||||
<IoMdRefresh
|
<small>{extra ? `${calcTraffic(usage)}/${calcTraffic(total)}` : undefined}</small>
|
||||||
className={`text-[24px] ${match ? 'text-white' : 'text-foreground'} ${updating ? 'animate-spin' : ''}`}
|
<small>{dayjs(info.updated).fromNow()}</small>
|
||||||
/>
|
</div>
|
||||||
</Button>
|
)}
|
||||||
</div>
|
{info.type === 'local' && (
|
||||||
<div
|
<div
|
||||||
className={`mt-2 flex select-none justify-between ${match ? 'text-white' : 'text-foreground'} `}
|
className={`mt-2 flex select-none justify-between ${match ? 'text-white' : 'text-foreground'}`}
|
||||||
>
|
>
|
||||||
<small>{extra ? `${calcTraffic(usage)}/${calcTraffic(total)}` : undefined}</small>
|
<Chip
|
||||||
<small>{dayjs(info.updated).fromNow()}</small>
|
size="sm"
|
||||||
</div>
|
variant="bordered"
|
||||||
|
className={`${match ? 'text-white border-white' : 'border-primary text-primary'}`}
|
||||||
|
>
|
||||||
|
本地
|
||||||
|
</Chip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
<CardFooter className="pt-0">
|
<CardFooter className="pt-0">
|
||||||
{extra && (
|
{extra && (
|
||||||
|
|||||||
@ -19,7 +19,7 @@ const SysproxySwitcher: React.FC = () => {
|
|||||||
await triggerSysProxy(enable)
|
await triggerSysProxy(enable)
|
||||||
await patchAppConfig({ sysProxy: { enable } })
|
await patchAppConfig({ sysProxy: { enable } })
|
||||||
} catch (e) {
|
} 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 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 { useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { MdContentPaste } from 'react-icons/md'
|
import { MdContentPaste } from 'react-icons/md'
|
||||||
|
|
||||||
const Profiles: React.FC = () => {
|
const Profiles: React.FC = () => {
|
||||||
@ -16,6 +16,7 @@ const Profiles: React.FC = () => {
|
|||||||
} = useProfileConfig()
|
} = useProfileConfig()
|
||||||
const { current, items } = profileConfig || {}
|
const { current, items } = profileConfig || {}
|
||||||
const [importing, setImporting] = useState(false)
|
const [importing, setImporting] = useState(false)
|
||||||
|
const [fileOver, setFileOver] = useState(false)
|
||||||
const [url, setUrl] = useState('')
|
const [url, setUrl] = useState('')
|
||||||
|
|
||||||
const handleImport = async (): Promise<void> => {
|
const handleImport = async (): Promise<void> => {
|
||||||
@ -26,9 +27,50 @@ const Profiles: React.FC = () => {
|
|||||||
setImporting(false)
|
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 (
|
return (
|
||||||
<BasePage title="订阅管理">
|
<BasePage ref={pageRef} title="订阅管理">
|
||||||
<div className="sticky top-[48px] z-40 backdrop-blur bg-background/40 flex p-2">
|
<div className="sticky top-[48px] z-40 backdrop-blur bg-background/40 flex p-2">
|
||||||
<Input
|
<Input
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
@ -61,7 +103,9 @@ const Profiles: React.FC = () => {
|
|||||||
导入
|
导入
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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) => (
|
{items?.map((item) => (
|
||||||
<ProfileItem
|
<ProfileItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user