import { useDispatch, useSelector } from 'react-redux'
import { resourceDir, join } from '@tauri-apps/api/path'
import { convertFileSrc } from '@tauri-apps/api/core'
import { v4 as uuidv4 } from 'uuid'
import { cn } from '@/lib/utils'
import { bytesToMbps } from '@/utils/tools'
import { errorToast } from '@/components/GlobalToast'
import { toast } from '@/components/ui/use-toast'
import { commands } from '@/bindings'
import {
// installService,
// uninstallService,
// startCore,
// stopCore,
enableProxy,
disableProxy,
} from '@/store/serviceSlice'
import { WebSocketClient } from '@/utils/webSocketClient'
import { ServiceControlPanel } from '@/components/ServiceControlPanel'
import { useCoreConfig } from '@/hooks/useCoreConfig'
import LineChart from '@/pages/home/components/LineChart'
import Links from '@/pages/home/components/Links'
import UnionSvg from '@/assets/svg/home/union.svg?react'
import ArrowDownSvg from '@/assets/svg/home/arrow-down.svg?react'
import ArrowUpSvg from '@/assets/svg/home/arrow-up.svg?react'
import type { AppDispatch, RootState } from '@/store'
import './index.scss'
const AgentOpenBtn = () => {
return (
)
}
const AgentCloseBtn = ({ className }: { className?: string }) => {
return (
)
}
const AgentInBtn = ({
className,
isProxyEnabled,
}: { className?: string; isProxyEnabled: boolean }) => {
let timer: NodeJS.Timeout | null = null
let index = 0
let [opacityList, setOpacityList] = useState([
'opacity-100',
'opacity-50',
'opacity-25',
])
// todo 后期使用gasp 或者 formwork 替代定时器
useEffect(() => {
timer = setInterval(() => {
switch (index) {
case 0:
opacityList = ['opacity-50', 'opacity-100', 'opacity-50']
break
case 1:
opacityList = ['opacity-25', 'opacity-50', 'opacity-100']
break
case 2:
opacityList = ['opacity-100', 'opacity-50', 'opacity-25']
break
default:
}
index++
setOpacityList(opacityList)
if (index === 3) {
index = 0
}
}, 300)
return () => {
timer && clearInterval(timer)
}
}, [])
return (
{isProxyEnabled ? '关闭中' : '代理中'}
)
}
function Home() {
let trafficWs: WebSocketClient | null = null
const dispatch = useDispatch()
const { loadCoreConfig } = useCoreConfig()
const [videoPath, setVideoPath] = useState('')
const { countries, exitCountries } = useSelector(
(state: RootState) => state.nodesReducer,
)
const { circuits, fallbackCircuit } = useSelector(
(state: RootState) => state.circuitReducer,
)
const { isProxyEnabled, isCoreRunning } = useSelector(
(state: RootState) => state.serviceReducer,
)
const [isProxyLoading, setIsProxyLoading] = useState(false)
const [traffic, setTraffic] = useState<{
send_bytes: number
recv_bytes: number
}>({ recv_bytes: 0, send_bytes: 0 })
const isCircuitReady = useMemo(() => {
// 判断是否有有链路并且至少有一个能用
const circuitReady = circuits.some((circuit) => circuit?.state !== 'failed')
if (
circuitReady ||
(fallbackCircuit && fallbackCircuit?.state !== 'failed')
) {
return true
} else {
return false
}
}, [circuits, fallbackCircuit])
// 处理代理开关
const handleProxyToggle = async (
isProxyEnabled: boolean,
isCircuitReady: boolean,
isCoreRunning: boolean,
) => {
try {
// 如果核心未运行,先启动核心
if (!isCoreRunning) {
await commands.startCore()
}
// 这里要先轮询去判断是否启动成功,如果启动失败,则抛出异常
let isGetNodesResult = await commands.getNodes()
let count = 0
while (isGetNodesResult.status !== 'ok') {
isGetNodesResult = await commands.getNodes()
count++
// 如果50次都没有开启成功核心那么启动失败
if (count > 50) {
throw new Error('启动核心失败')
}
}
// 如果代理未启用且链路未就绪,创建默认链路
// if (!isProxyEnabled && !isCircuitReady) {
// await dispatch(
// createCircuit({
// uid: uuidv4(),
// name: '系统默认链路',
// inbound: countries[0],
// outbound: exitCountries[exitCountries.length - 1],
// multi_hop: 3,
// fallback: true,
// rule_path: null,
// is_prefix: false,
// }),
// ).unwrap()
// }
setIsProxyLoading(true)
// 切换代理状态
await dispatch(isProxyEnabled ? disableProxy() : enableProxy()).unwrap()
} catch (error) {
console.log(error, 'error')
const errorMessage = isProxyEnabled
? '关闭代理失败!'
: '开启代理失败,请检查节点配置、或重新尝试开启'
errorToast(errorMessage, toast)
console.error('Proxy toggle failed:', error)
} finally {
setIsProxyLoading(false)
}
}
const openWsTraffic = async () => {
if (trafficWs) return
const { api_port } = await loadCoreConfig()
// todo! 后面会把二级制文件启动的参数作为配置项,这里暂时写死
trafficWs = new WebSocketClient(`ws://127.0.0.1:${api_port}/traffic`, {
headers: {
Authorization: 'Bearer secret',
},
})
// 执行 WebSocket 操作
await trafficWs.connect()
trafficWs.addListener((msg) => {
if (msg.type === 'Text') {
const result = JSON.parse(msg.data)
setTraffic({
recv_bytes: bytesToMbps(result.data.recv_bytes),
send_bytes: bytesToMbps(result.data.send_bytes),
})
}
})
}
const closeWsTraffic = async () => {
if (trafficWs) {
await trafficWs.disconnect()
trafficWs = null
}
}
const getVideoPath = async () => {
const resourceDirPath = await resourceDir()
// tauri 读取视频和音频不能直接像前端那样import video from 'xxx.mp4' 需要用convertFileSrc并且开启相应的权限,不然打包出来的引用会非常卡媒体文件越大越卡
// https://v2.tauri.app/zh-cn/reference/javascript/api/namespacecore/#convertfilesrc
const filePath = await join(
resourceDirPath,
'assets/video/crystal-ball.mp4',
)
const assetUrl = convertFileSrc(filePath)
setVideoPath(assetUrl)
}
useEffect(() => {
getVideoPath()
}, [])
useEffect(() => {
if (isProxyEnabled) {
openWsTraffic()
} else {
closeWsTraffic()
}
return () => {
closeWsTraffic()
}
}, [isProxyEnabled])
return (
{/*
*/}
您好呀,欢迎使用匿名反溯源网络系统!
当前链路稳定整体稳定,服务器处于低负载
handleProxyToggle(isProxyEnabled, isCircuitReady, isCoreRunning)
}
>
{isProxyLoading ? (
) : isProxyEnabled ? (
) : (
)}
{isProxyEnabled && (
{traffic.send_bytes === 0
? '0.000'
: traffic.send_bytes.toFixed(3)}
{traffic.recv_bytes === 0
? '0.000'
: traffic.recv_bytes.toFixed(3)}
)}
{import.meta.env.DEV && }
)
}
export default memo(Home)