support local profile

This commit is contained in:
pompurin404 2024-08-07 21:21:04 +08:00
parent 80020eadc8
commit fd86c0dba4
No known key found for this signature in database
8 changed files with 157 additions and 70 deletions

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -50,7 +50,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
/> />
</SettingItem> </SettingItem>
)} )}
{values.type === 'remote' && (
<SettingItem title="更新间隔(分钟)"> <SettingItem title="更新间隔(分钟)">
<Input <Input
size="sm" size="sm"
@ -62,6 +62,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
}} }}
/> />
</SettingItem> </SettingItem>
)}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button variant="light" onPress={onClose}> <Button variant="light" onPress={onClose}>

View File

@ -3,6 +3,7 @@ import {
Card, Card,
CardBody, CardBody,
CardFooter, CardFooter,
Chip,
Dropdown, Dropdown,
DropdownItem, DropdownItem,
DropdownMenu, DropdownMenu,
@ -139,6 +140,7 @@ const ProfileItem: React.FC<Props> = (props) => {
{info?.name} {info?.name}
</h3> </h3>
<div className="flex"> <div className="flex">
{info.type === 'remote' && (
<Button <Button
isIconOnly isIconOnly
size="sm" size="sm"
@ -157,6 +159,8 @@ const ProfileItem: React.FC<Props> = (props) => {
className={`${isCurrent ? 'text-white' : 'text-foreground'} text-[24px] ${updating ? 'animate-spin' : ''}`} className={`${isCurrent ? 'text-white' : 'text-foreground'} text-[24px] ${updating ? 'animate-spin' : ''}`}
/> />
</Button> </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>
{info.type === 'remote' && (
<div <div
className={`mt-2 flex select-none justify-between ${isCurrent ? 'text-white' : 'text-foreground'}`} className={`mt-2 flex select-none justify-between ${isCurrent ? 'text-white' : 'text-foreground'}`}
> >
<small>{extra ? `${calcTraffic(usage)}/${calcTraffic(total)}` : undefined}</small> <small>{extra ? `${calcTraffic(usage)}/${calcTraffic(total)}` : undefined}</small>
<small>{dayjs(info.updated).fromNow()}</small> <small>{dayjs(info.updated).fromNow()}</small>
</div> </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 && (

View File

@ -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,6 +42,7 @@ const ProfileCard: React.FC = () => {
> >
{info?.name} {info?.name}
</h3> </h3>
{info.type === 'remote' && (
<Button <Button
isIconOnly isIconOnly
size="sm" size="sm"
@ -59,13 +60,29 @@ const ProfileCard: React.FC = () => {
className={`text-[24px] ${match ? 'text-white' : 'text-foreground'} ${updating ? 'animate-spin' : ''}`} className={`text-[24px] ${match ? 'text-white' : 'text-foreground'} ${updating ? 'animate-spin' : ''}`}
/> />
</Button> </Button>
)}
</div> </div>
{info.type === 'remote' && (
<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> <small>{extra ? `${calcTraffic(usage)}/${calcTraffic(total)}` : undefined}</small>
<small>{dayjs(info.updated).fromNow()}</small> <small>{dayjs(info.updated).fromNow()}</small>
</div> </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> </CardBody>
<CardFooter className="pt-0"> <CardFooter className="pt-0">
{extra && ( {extra && (

View File

@ -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)
} }
} }

View File

@ -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}