mirror of
https://github.com/woodchen-ink/clash-and-dashboard.git
synced 2025-07-18 05:51:56 +08:00
Chore: clear request client
This commit is contained in:
parent
53f4aa4e32
commit
7e999dfdca
1
src/assets/index.d.ts
vendored
Normal file
1
src/assets/index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module '*.png'
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import React from 'react'
|
||||
import { Route, Redirect, Switch } from 'react-router-dom'
|
||||
import classnames from 'classnames'
|
||||
import { isClashX } from '@lib/jsBridge'
|
||||
@ -11,15 +11,13 @@ import Settings from '@containers/Settings'
|
||||
import SlideBar from '@containers/Sidebar'
|
||||
import Connections from '@containers/Connections'
|
||||
import ExternalControllerModal from '@containers/ExternalControllerDrawer'
|
||||
import { getLogsStreamReader } from '@lib/request'
|
||||
import { useLogsStreamReader } from '@stores'
|
||||
|
||||
import '../styles/common.scss'
|
||||
import '../styles/iconfont.scss'
|
||||
|
||||
export default function App () {
|
||||
useEffect(() => {
|
||||
getLogsStreamReader()
|
||||
}, [])
|
||||
useLogsStreamReader()
|
||||
|
||||
const routes = [
|
||||
// { path: '/', name: 'Overview', component: Overview, exact: true },
|
||||
|
@ -4,9 +4,8 @@ import classnames from 'classnames'
|
||||
import { useScroll } from 'react-use'
|
||||
import { groupBy } from 'lodash-es'
|
||||
import { Header, Card, Checkbox, Modal, Icon } from '@components'
|
||||
import { useI18n } from '@stores'
|
||||
import { useClient, useConnectionStreamReader, useI18n } from '@stores'
|
||||
import * as API from '@lib/request'
|
||||
import { StreamReader } from '@lib/streamer'
|
||||
import { useObject, useVisible } from '@lib/hook'
|
||||
import { fromNow } from '@lib/date'
|
||||
import { RuleType } from '@models'
|
||||
@ -93,6 +92,8 @@ interface formatConnection {
|
||||
export default function Connections() {
|
||||
const { translation, lang } = useI18n()
|
||||
const t = useMemo(() => translation('Connections').t, [translation])
|
||||
const connStreamReader = useConnectionStreamReader()
|
||||
const client = useClient()
|
||||
|
||||
// total
|
||||
const [traffic, setTraffic] = useObject({
|
||||
@ -103,7 +104,7 @@ export default function Connections() {
|
||||
// close all connections
|
||||
const { visible, show, hide } = useVisible()
|
||||
function handleCloseConnections() {
|
||||
API.closeAllConnections().finally(() => hide())
|
||||
client.closeAllConnections().finally(() => hide())
|
||||
}
|
||||
|
||||
// connections
|
||||
@ -161,8 +162,6 @@ export default function Connections() {
|
||||
] as TableColumnOption<formatConnection>[], [t])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
let streamReader: StreamReader<API.Snapshot> | null = null
|
||||
|
||||
function handleConnection(snapshots: API.Snapshot[]) {
|
||||
for (const snapshot of snapshots) {
|
||||
setTraffic({
|
||||
@ -174,18 +173,12 @@ export default function Connections() {
|
||||
}
|
||||
}
|
||||
|
||||
(async function () {
|
||||
streamReader = await API.getConnectionStreamReader()
|
||||
streamReader.subscribe('data', handleConnection)
|
||||
}())
|
||||
|
||||
connStreamReader?.subscribe('data', handleConnection)
|
||||
return () => {
|
||||
if (streamReader) {
|
||||
streamReader.unsubscribe('data', handleConnection)
|
||||
streamReader.destory()
|
||||
}
|
||||
connStreamReader?.unsubscribe('data', handleConnection)
|
||||
connStreamReader?.destory()
|
||||
}
|
||||
}, [feed, setTraffic])
|
||||
}, [connStreamReader, feed, setTraffic])
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
|
@ -1,14 +1,17 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useUpdateAtom } from 'jotai/utils'
|
||||
import { useObject } from '@lib/hook'
|
||||
import { Modal, Input, Alert } from '@components'
|
||||
import { useI18n, useAPIInfo, useIdentity } from '@stores'
|
||||
import { useI18n, useAPIInfo, identityAtom } from '@stores'
|
||||
import { localStorageAtom } from '@stores/request'
|
||||
import './style.scss'
|
||||
|
||||
export default function ExternalController () {
|
||||
const { translation } = useI18n()
|
||||
const { t } = translation('Settings')
|
||||
const { data: info, update, fetch } = useAPIInfo()
|
||||
const { identity, set: setIdentity } = useIdentity()
|
||||
const { hostname, port, secret } = useAPIInfo()
|
||||
const [identity, setIdentity] = useAtom(identityAtom)
|
||||
const [value, set] = useObject({
|
||||
hostname: '',
|
||||
port: '',
|
||||
@ -16,16 +19,14 @@ export default function ExternalController () {
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
fetch()
|
||||
}, [fetch])
|
||||
set({ hostname, port, secret })
|
||||
}, [hostname, port, secret, set])
|
||||
|
||||
useEffect(() => {
|
||||
set({ hostname: info.hostname, port: info.port, secret: info.secret })
|
||||
}, [info, set])
|
||||
const setter = useUpdateAtom(localStorageAtom)
|
||||
|
||||
function handleOk () {
|
||||
const { hostname, port, secret } = value
|
||||
update({ hostname, port, secret })
|
||||
setter([{ hostname, port, secret }])
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -1,9 +1,7 @@
|
||||
import React, { useLayoutEffect, useEffect, useRef, useState } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import { useI18n } from '@stores'
|
||||
import { useI18n, useLogsStreamReader } from '@stores'
|
||||
import { Card, Header } from '@components'
|
||||
import { getLogsStreamReader } from '@lib/request'
|
||||
import { StreamReader } from '@lib/streamer'
|
||||
import { Log } from '@models/Log'
|
||||
import './style.scss'
|
||||
|
||||
@ -13,6 +11,7 @@ export default function Logs () {
|
||||
const [logs, setLogs] = useState<Log[]>([])
|
||||
const { translation } = useI18n()
|
||||
const { t } = translation('Logs')
|
||||
const logsStreamReader = useLogsStreamReader()
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const ul = listRef.current
|
||||
@ -22,22 +21,19 @@ export default function Logs () {
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
let streamReader: StreamReader<Log> | null = null
|
||||
|
||||
function handleLog (newLogs: Log[]) {
|
||||
logsRef.current = logsRef.current.slice().concat(newLogs.map(d => ({ ...d, time: new Date() })))
|
||||
setLogs(logsRef.current)
|
||||
}
|
||||
|
||||
(async function () {
|
||||
streamReader = await getLogsStreamReader()
|
||||
logsRef.current = streamReader.buffer()
|
||||
if (logsStreamReader) {
|
||||
logsStreamReader.subscribe('data', handleLog)
|
||||
logsRef.current = logsStreamReader.buffer()
|
||||
setLogs(logsRef.current)
|
||||
streamReader.subscribe('data', handleLog)
|
||||
}())
|
||||
}
|
||||
|
||||
return () => streamReader?.unsubscribe('data', handleLog)
|
||||
}, [])
|
||||
return () => logsStreamReader?.unsubscribe('data', handleLog)
|
||||
}, [logsStreamReader])
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useProxy, useConfig, proxyMapping } from '@stores'
|
||||
import { changeProxySelected, Group as IGroup, getConnections, closeConnection } from '@lib/request'
|
||||
import { useProxy, useConfig, proxyMapping, useClient } from '@stores'
|
||||
import { Group as IGroup } from '@lib/request'
|
||||
import { Tags, Tag } from '@components'
|
||||
import './style.scss'
|
||||
|
||||
@ -13,14 +13,15 @@ export function Group (props: GroupProps) {
|
||||
const { markProxySelected } = useProxy()
|
||||
const [proxyMap] = useAtom(proxyMapping)
|
||||
const { data: Config } = useConfig()
|
||||
const client = useClient()
|
||||
const { config } = props
|
||||
|
||||
async function handleChangeProxySelected (name: string) {
|
||||
await changeProxySelected(props.config.name, name)
|
||||
await client.changeProxySelected(props.config.name, name)
|
||||
markProxySelected(props.config.name, name)
|
||||
if (Config.breakConnections) {
|
||||
const list: string[] = []
|
||||
const snapshot = await getConnections()
|
||||
const snapshot = await client.getConnections()
|
||||
for (const connection of snapshot.data.connections) {
|
||||
if (connection.chains.includes(props.config.name)) {
|
||||
list.push(connection.id)
|
||||
@ -28,7 +29,7 @@ export function Group (props: GroupProps) {
|
||||
}
|
||||
|
||||
for (const id of list) {
|
||||
closeConnection(id)
|
||||
client.closeConnection(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { Card, Tag, Icon, Loading } from '@components'
|
||||
import { useI18n, useProxyProviders } from '@stores'
|
||||
import { useClient, useI18n, useProxyProviders } from '@stores'
|
||||
import { fromNow } from '@lib/date'
|
||||
import { Provider as IProvider, Proxy as IProxy, updateProvider, healthCheckProvider } from '@lib/request'
|
||||
import { Provider as IProvider, Proxy as IProxy } from '@lib/request'
|
||||
import { useVisible } from '@lib/hook'
|
||||
import { compareDesc } from '@containers/Proxies'
|
||||
import { Proxy } from '@containers/Proxies/components/Proxy'
|
||||
@ -15,6 +15,7 @@ interface ProvidersProps {
|
||||
export function Provider (props: ProvidersProps) {
|
||||
const { update } = useProxyProviders()
|
||||
const { translation, lang } = useI18n()
|
||||
const client = useClient()
|
||||
|
||||
const { provider } = props
|
||||
const { t } = translation('Proxies')
|
||||
@ -23,12 +24,12 @@ export function Provider (props: ProvidersProps) {
|
||||
|
||||
function handleHealthChech () {
|
||||
show()
|
||||
healthCheckProvider(provider.name).then(() => update()).finally(() => hide())
|
||||
client.healthCheckProvider(provider.name).then(() => update()).finally(() => hide())
|
||||
}
|
||||
|
||||
function handleUpdate () {
|
||||
show()
|
||||
updateProvider(provider.name).then(() => update()).finally(() => hide())
|
||||
client.updateProvider(provider.name).then(() => update()).finally(() => hide())
|
||||
}
|
||||
|
||||
const proxies = useMemo(() => {
|
||||
|
@ -3,8 +3,8 @@ import { ResultAsync } from 'neverthrow'
|
||||
import type{ AxiosError } from 'axios'
|
||||
import classnames from 'classnames'
|
||||
import { BaseComponentProps } from '@models'
|
||||
import { useProxy } from '@stores'
|
||||
import { getProxyDelay, Proxy as IProxy } from '@lib/request'
|
||||
import { useClient, useProxy } from '@stores'
|
||||
import { Proxy as IProxy } from '@lib/request'
|
||||
import EE, { Action } from '@lib/event'
|
||||
import { isClashX, jsBridge } from '@lib/jsBridge'
|
||||
|
||||
@ -21,19 +21,20 @@ const TagColors = {
|
||||
'#ff3e5e': Infinity
|
||||
}
|
||||
|
||||
async function getDelay (name: string) {
|
||||
if (isClashX()) {
|
||||
const delay = await jsBridge?.getProxyDelay(name) ?? 0
|
||||
return delay
|
||||
}
|
||||
|
||||
const { data: { delay } } = await getProxyDelay(name)
|
||||
return delay
|
||||
}
|
||||
|
||||
export function Proxy (props: ProxyProps) {
|
||||
const { config, className } = props
|
||||
const { set } = useProxy()
|
||||
const client = useClient()
|
||||
|
||||
const getDelay = useCallback(async (name: string) => {
|
||||
if (isClashX()) {
|
||||
const delay = await jsBridge?.getProxyDelay(name) ?? 0
|
||||
return delay
|
||||
}
|
||||
|
||||
const { data: { delay } } = await client.getProxyDelay(name)
|
||||
return delay
|
||||
}, [client])
|
||||
|
||||
const speedTest = useCallback(async function () {
|
||||
const result = await ResultAsync.fromPromise(getDelay(config.name), e => e as AxiosError)
|
||||
@ -45,7 +46,7 @@ export function Proxy (props: ProxyProps) {
|
||||
proxy.history.push({ time: Date.now().toString(), delay: validDelay })
|
||||
}
|
||||
})
|
||||
}, [config.name, set])
|
||||
}, [config.name, getDelay, set])
|
||||
|
||||
const delay = useMemo(
|
||||
() => config.history?.length ? config.history.slice(-1)[0].delay : 0,
|
||||
|
@ -1,9 +1,9 @@
|
||||
import * as React from 'react'
|
||||
import classnames from 'classnames'
|
||||
import { Card, Tag, Icon } from '@components'
|
||||
import { useI18n, useRuleProviders } from '@stores'
|
||||
import { useClient, useI18n, useRuleProviders } from '@stores'
|
||||
import { fromNow } from '@lib/date'
|
||||
import { RuleProvider, updateRuleProvider } from '@lib/request'
|
||||
import { RuleProvider } from '@lib/request'
|
||||
import { useVisible } from '@lib/hook'
|
||||
import './style.scss'
|
||||
|
||||
@ -14,6 +14,7 @@ interface ProvidersProps {
|
||||
export function Provider (props: ProvidersProps) {
|
||||
const { update } = useRuleProviders()
|
||||
const { translation, lang } = useI18n()
|
||||
const client = useClient()
|
||||
|
||||
const { provider } = props
|
||||
const { t } = translation('Rules')
|
||||
@ -22,7 +23,7 @@ export function Provider (props: ProvidersProps) {
|
||||
|
||||
function handleUpdate () {
|
||||
show()
|
||||
updateRuleProvider(provider.name).then(() => update()).finally(() => hide())
|
||||
client.updateRuleProvider(provider.name).then(() => update()).finally(() => hide())
|
||||
}
|
||||
|
||||
const updateClassnames = classnames('rule-provider-icon', { 'rule-provider-loading': visible })
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
import classnames from 'classnames'
|
||||
import { useUpdateAtom } from 'jotai/utils'
|
||||
import { capitalize } from 'lodash-es'
|
||||
import { Header, Card, Switch, ButtonSelect, ButtonSelectOptions, Input } from '@components'
|
||||
import { useI18n, useClashXData, useAPIInfo, useGeneral, useIdentity, useVersion } from '@stores'
|
||||
import { updateConfig } from '@lib/request'
|
||||
import { useI18n, useClashXData, useAPIInfo, useGeneral, useVersion, useClient, identityAtom } from '@stores'
|
||||
import { useObject } from '@lib/hook'
|
||||
import { jsBridge } from '@lib/jsBridge'
|
||||
import { Lang } from '@i18n'
|
||||
@ -15,10 +15,11 @@ export default function Settings () {
|
||||
const { premium } = useVersion()
|
||||
const { data: clashXData, update: fetchClashXData } = useClashXData()
|
||||
const { general, update: fetchGeneral } = useGeneral()
|
||||
const { set: setIdentity } = useIdentity()
|
||||
const { data: apiInfo } = useAPIInfo()
|
||||
const setIdentity = useUpdateAtom(identityAtom)
|
||||
const apiInfo = useAPIInfo()
|
||||
const { translation, setLang, lang } = useI18n()
|
||||
const { t } = translation('Settings')
|
||||
const client = useClient()
|
||||
const [info, set] = useObject({
|
||||
socks5ProxyPort: 7891,
|
||||
httpProxyPort: 7890,
|
||||
@ -32,7 +33,7 @@ export default function Settings () {
|
||||
}, [general, set])
|
||||
|
||||
async function handleProxyModeChange (mode: string) {
|
||||
await updateConfig({ mode })
|
||||
await client.updateConfig({ mode })
|
||||
await fetchGeneral()
|
||||
}
|
||||
|
||||
@ -51,22 +52,22 @@ export default function Settings () {
|
||||
}
|
||||
|
||||
async function handleHttpPortSave () {
|
||||
await updateConfig({ port: info.httpProxyPort })
|
||||
await client.updateConfig({ port: info.httpProxyPort })
|
||||
await fetchGeneral()
|
||||
}
|
||||
|
||||
async function handleSocksPortSave () {
|
||||
await updateConfig({ 'socks-port': info.socks5ProxyPort })
|
||||
await client.updateConfig({ 'socks-port': info.socks5ProxyPort })
|
||||
await fetchGeneral()
|
||||
}
|
||||
|
||||
async function handleMixedPortSave () {
|
||||
await updateConfig({ 'mixed-port': info.mixedProxyPort })
|
||||
await client.updateConfig({ 'mixed-port': info.mixedProxyPort })
|
||||
await fetchGeneral()
|
||||
}
|
||||
|
||||
async function handleAllowLanChange (state: boolean) {
|
||||
await updateConfig({ 'allow-lan': state })
|
||||
await client.updateConfig({ 'allow-lan': state })
|
||||
await fetchGeneral()
|
||||
}
|
||||
|
||||
|
@ -3,9 +3,8 @@ import { NavLink } from 'react-router-dom'
|
||||
import classnames from 'classnames'
|
||||
import { useI18n, useVersion, useClashXData } from '@stores'
|
||||
|
||||
import './style.scss'
|
||||
import logo from '@assets/logo.png'
|
||||
import useSWR from 'swr'
|
||||
import './style.scss'
|
||||
|
||||
interface SidebarProps {
|
||||
routes: {
|
||||
@ -19,12 +18,10 @@ interface SidebarProps {
|
||||
export default function Sidebar (props: SidebarProps) {
|
||||
const { routes } = props
|
||||
const { translation } = useI18n()
|
||||
const { version, premium, update } = useVersion()
|
||||
const { version, premium } = useVersion()
|
||||
const { data } = useClashXData()
|
||||
const { t } = translation('SideBar')
|
||||
|
||||
useSWR('version', update)
|
||||
|
||||
const navlinks = routes.map(
|
||||
({ path, name, exact, noMobile }) => (
|
||||
<li className={classnames('item', { 'no-mobile': noMobile })} key={name}>
|
||||
|
@ -1,17 +1,5 @@
|
||||
export function getLocalStorageItem (key: string, defaultValue = '') {
|
||||
return window.localStorage.getItem(key) || defaultValue
|
||||
}
|
||||
|
||||
export function setLocalStorageItem (key: string, value: string) {
|
||||
return window.localStorage.setItem(key, value)
|
||||
}
|
||||
|
||||
export function noop () {}
|
||||
|
||||
export function getSearchParam(key: string) {
|
||||
return new URLSearchParams(window.location.search).get(key)
|
||||
}
|
||||
|
||||
export function partition<T> (arr: T[], fn: (arg: T) => boolean): [T[], T[]] {
|
||||
const left: T[] = []
|
||||
const right: T[] = []
|
||||
|
@ -1,10 +1,4 @@
|
||||
import axios, { AxiosError } from 'axios'
|
||||
import { ResultAsync } from 'neverthrow'
|
||||
import { getLocalStorageItem, getSearchParam } from '@lib/helper'
|
||||
import { isClashX, jsBridge } from '@lib/jsBridge'
|
||||
import { createAsyncSingleton } from '@lib/asyncSingleton'
|
||||
import { Log } from '@models/Log'
|
||||
import { StreamReader } from './streamer'
|
||||
import axios, { AxiosInstance } from 'axios'
|
||||
|
||||
export interface Config {
|
||||
port: number
|
||||
@ -99,172 +93,90 @@ export interface Connections {
|
||||
rulePayload: string
|
||||
}
|
||||
|
||||
export async function getExternalControllerConfig () {
|
||||
if (isClashX()) {
|
||||
const info = await jsBridge!.getAPIInfo()
|
||||
|
||||
return {
|
||||
hostname: info.host,
|
||||
port: info.port,
|
||||
secret: info.secret,
|
||||
protocol: 'http:'
|
||||
}
|
||||
}
|
||||
|
||||
let url: URL | undefined;
|
||||
{
|
||||
const meta = document.querySelector<HTMLMetaElement>('meta[name="external-controller"]')
|
||||
if (meta?.content?.match(/^https?:/)) {
|
||||
// [protocol]://[secret]@[hostname]:[port]
|
||||
url = new URL(meta.content)
|
||||
}
|
||||
}
|
||||
|
||||
const hostname = getSearchParam('host') ?? getLocalStorageItem('externalControllerAddr', url?.hostname ?? '127.0.0.1')
|
||||
const port = getSearchParam('port') ?? getLocalStorageItem('externalControllerPort', url?.port ?? '9090')
|
||||
const secret = getSearchParam('secret') ?? getLocalStorageItem('secret', url?.username ?? '')
|
||||
const protocol = getSearchParam('protocol') ?? hostname === '127.0.0.1' ? 'http:' : (url?.protocol ?? window.location.protocol)
|
||||
|
||||
if (!hostname || !port) {
|
||||
throw new Error('can\'t get hostname or port')
|
||||
}
|
||||
|
||||
return { hostname, port, secret, protocol }
|
||||
}
|
||||
|
||||
export const getInstance = createAsyncSingleton(async () => {
|
||||
const {
|
||||
hostname,
|
||||
port,
|
||||
secret,
|
||||
protocol
|
||||
} = await getExternalControllerConfig()
|
||||
|
||||
return axios.create({
|
||||
baseURL: `${protocol}//${hostname}:${port}`,
|
||||
headers: secret ? { Authorization: `Bearer ${secret}` } : {}
|
||||
})
|
||||
})
|
||||
|
||||
export async function getConfig () {
|
||||
const req = await getInstance()
|
||||
return req.get<Config>('configs')
|
||||
}
|
||||
|
||||
export async function updateConfig (config: Partial<Config>) {
|
||||
const req = await getInstance()
|
||||
return req.patch<void>('configs', config)
|
||||
}
|
||||
|
||||
export async function getRules () {
|
||||
const req = await getInstance()
|
||||
return req.get<Rules>('rules')
|
||||
}
|
||||
|
||||
export async function updateRules () {
|
||||
const req = await getInstance()
|
||||
return req.put<void>('rules')
|
||||
}
|
||||
|
||||
export async function getProxyProviders () {
|
||||
const req = await getInstance()
|
||||
return req.get<ProxyProviders>('providers/proxies', {
|
||||
validateStatus (status) {
|
||||
// compatible old version
|
||||
return (status >= 200 && status < 300) || status === 404
|
||||
}
|
||||
})
|
||||
// compatible old version
|
||||
.then(resp => {
|
||||
if (resp.status === 404) {
|
||||
resp.data = { providers: {} }
|
||||
}
|
||||
return resp
|
||||
export class Client {
|
||||
private axiosClient: AxiosInstance
|
||||
constructor(url: string, secret?: string) {
|
||||
this.axiosClient = axios.create({
|
||||
baseURL: url,
|
||||
headers: secret ? { Authorization: `Bearer ${secret}` } : {}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRuleProviders () {
|
||||
const req = await getInstance()
|
||||
return req.get<RuleProviders>('providers/rules')
|
||||
}
|
||||
getConfig() {
|
||||
return this.axiosClient.get<Config>('configs')
|
||||
}
|
||||
|
||||
export async function updateProvider (name: string) {
|
||||
const req = await getInstance()
|
||||
return req.put<void>(`providers/proxies/${encodeURIComponent(name)}`)
|
||||
}
|
||||
updateConfig(config: Partial<Config>) {
|
||||
return this.axiosClient.patch<void>('configs', config)
|
||||
}
|
||||
|
||||
export async function updateRuleProvider (name: string) {
|
||||
const req = await getInstance()
|
||||
return req.put<void>(`providers/rules/${encodeURIComponent(name)}`)
|
||||
}
|
||||
getRules() {
|
||||
return this.axiosClient.get<Rules>('rules')
|
||||
}
|
||||
|
||||
export async function healthCheckProvider (name: string) {
|
||||
const req = await getInstance()
|
||||
return req.get<void>(`providers/proxies/${encodeURIComponent(name)}/healthcheck`)
|
||||
}
|
||||
|
||||
export async function getProxies () {
|
||||
const req = await getInstance()
|
||||
return req.get<Proxies>('proxies')
|
||||
}
|
||||
|
||||
export async function getProxy (name: string) {
|
||||
const req = await getInstance()
|
||||
return req.get<Proxy>(`proxies/${encodeURIComponent(name)}`)
|
||||
}
|
||||
|
||||
export async function getVersion () {
|
||||
const req = await getInstance()
|
||||
return req.get<{ version: string, premium?: boolean }>('version')
|
||||
}
|
||||
|
||||
export async function getProxyDelay (name: string) {
|
||||
const req = await getInstance()
|
||||
return req.get<{ delay: number }>(`proxies/${encodeURIComponent(name)}/delay`, {
|
||||
params: {
|
||||
timeout: 5000,
|
||||
url: 'http://www.gstatic.com/generate_204'
|
||||
async getProxyProviders () {
|
||||
const resp = await this.axiosClient.get<ProxyProviders>('providers/proxies', {
|
||||
validateStatus(status) {
|
||||
// compatible old version
|
||||
return (status >= 200 && status < 300) || status === 404
|
||||
}
|
||||
})
|
||||
if (resp.status === 404) {
|
||||
resp.data = { providers: {} }
|
||||
}
|
||||
})
|
||||
return resp
|
||||
}
|
||||
|
||||
getRuleProviders () {
|
||||
return this.axiosClient.get<RuleProviders>('providers/rules')
|
||||
}
|
||||
|
||||
updateProvider (name: string) {
|
||||
return this.axiosClient.put<void>(`providers/proxies/${encodeURIComponent(name)}`)
|
||||
}
|
||||
|
||||
updateRuleProvider (name: string) {
|
||||
return this.axiosClient.put<void>(`providers/rules/${encodeURIComponent(name)}`)
|
||||
}
|
||||
|
||||
healthCheckProvider (name: string) {
|
||||
return this.axiosClient.get<void>(`providers/proxies/${encodeURIComponent(name)}/healthcheck`)
|
||||
}
|
||||
|
||||
getProxies () {
|
||||
return this.axiosClient.get<Proxies>('proxies')
|
||||
}
|
||||
|
||||
getProxy (name: string) {
|
||||
return this.axiosClient.get<Proxy>(`proxies/${encodeURIComponent(name)}`)
|
||||
}
|
||||
|
||||
getVersion () {
|
||||
return this.axiosClient.get<{ version: string, premium?: boolean }>('version')
|
||||
}
|
||||
|
||||
getProxyDelay (name: string) {
|
||||
return this.axiosClient.get<{ delay: number }>(`proxies/${encodeURIComponent(name)}/delay`, {
|
||||
params: {
|
||||
timeout: 5000,
|
||||
url: 'http://www.gstatic.com/generate_204'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
closeAllConnections () {
|
||||
return this.axiosClient.delete('connections')
|
||||
}
|
||||
|
||||
closeConnection (id: string) {
|
||||
return this.axiosClient.delete(`connections/${id}`)
|
||||
}
|
||||
|
||||
getConnections () {
|
||||
return this.axiosClient.get<Snapshot>('connections')
|
||||
}
|
||||
|
||||
changeProxySelected (name: string, select: string) {
|
||||
return this.axiosClient.put<void>(`proxies/${encodeURIComponent(name)}`, { name: select })
|
||||
}
|
||||
}
|
||||
|
||||
export async function closeAllConnections () {
|
||||
const req = await getInstance()
|
||||
return req.delete('connections')
|
||||
}
|
||||
|
||||
export async function closeConnection (id: string) {
|
||||
const req = await getInstance()
|
||||
return req.delete(`connections/${id}`)
|
||||
}
|
||||
|
||||
export async function getConnections () {
|
||||
const req = await getInstance()
|
||||
return req.get<Snapshot>('connections')
|
||||
}
|
||||
|
||||
export async function changeProxySelected (name: string, select: string) {
|
||||
const req = await getInstance()
|
||||
return req.put<void>(`proxies/${encodeURIComponent(name)}`, { name: select })
|
||||
}
|
||||
|
||||
export const getLogsStreamReader = createAsyncSingleton(async function () {
|
||||
const externalController = await getExternalControllerConfig()
|
||||
const { data: config } = await getConfig()
|
||||
const result = await ResultAsync.fromPromise(getVersion(), err => err as AxiosError)
|
||||
const version = result.isErr() ? 'unkonwn version' : result.value.data.version
|
||||
const useWebsocket = !!version || true
|
||||
|
||||
const logUrl = `${externalController.protocol}//${externalController.hostname}:${externalController.port}/logs?level=${config['log-level']}`
|
||||
return new StreamReader<Log>({ url: logUrl, bufferLength: 200, token: externalController.secret, useWebsocket })
|
||||
})
|
||||
|
||||
export const getConnectionStreamReader = createAsyncSingleton(async function () {
|
||||
const externalController = await getExternalControllerConfig()
|
||||
const result = await ResultAsync.fromPromise(getVersion(), err => err as AxiosError)
|
||||
const version = result.isErr() ? 'unkonwn version' : result.value.data.version
|
||||
|
||||
const useWebsocket = !!version || true
|
||||
const logUrl = `${externalController.protocol}//${externalController.hostname}:${externalController.port}/connections`
|
||||
return new StreamReader<Snapshot>({ url: logUrl, bufferLength: 200, token: externalController.secret, useWebsocket })
|
||||
})
|
||||
|
@ -1,14 +1,17 @@
|
||||
import React from 'react'
|
||||
import React, { Suspense } from 'react'
|
||||
import { render } from 'react-dom'
|
||||
import { HashRouter } from 'react-router-dom'
|
||||
import App from '@containers/App'
|
||||
import { Loading } from '@components'
|
||||
import 'virtual:windi.css'
|
||||
|
||||
export default function renderApp () {
|
||||
const rootEl = document.getElementById('root')
|
||||
const AppInstance = (
|
||||
<HashRouter>
|
||||
<App />
|
||||
<Suspense fallback={<Loading visible />}>
|
||||
<App />
|
||||
</Suspense>
|
||||
</HashRouter>
|
||||
)
|
||||
|
||||
|
@ -1 +1,2 @@
|
||||
export * from './jotai'
|
||||
export * from './request'
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ResultAsync } from 'neverthrow'
|
||||
import { AxiosError } from 'axios'
|
||||
import { atom, useAtom } from 'jotai'
|
||||
import { atomWithStorage } from 'jotai/utils'
|
||||
import { atomWithStorage, useUpdateAtom } from 'jotai/utils'
|
||||
import { atomWithImmer } from 'jotai/immer'
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { get } from 'lodash-es'
|
||||
@ -12,33 +12,14 @@ import { Language, locales, Lang, getDefaultLanguage } from '@i18n'
|
||||
import { useWarpImmerSetter, WritableDraft } from '@lib/jotai'
|
||||
import * as API from '@lib/request'
|
||||
import * as Models from '@models'
|
||||
import { partition, setLocalStorageItem } from '@lib/helper'
|
||||
import { partition } from '@lib/helper'
|
||||
import { isClashX, jsBridge } from '@lib/jsBridge'
|
||||
import { useAPIInfo, useClient } from './request'
|
||||
import { StreamReader } from '@lib/streamer'
|
||||
import { Log } from '@models/Log'
|
||||
import { Snapshot } from '@lib/request'
|
||||
|
||||
const identity = atom(true)
|
||||
|
||||
type AsyncFunction<A, O> = (...args: A[]) => Promise<O>
|
||||
|
||||
export function useIdentity () {
|
||||
const [id, set] = useAtom(identity)
|
||||
|
||||
function wrapFetcher<A, O> (fn: AsyncFunction<A, O>) {
|
||||
return async function (...args: A[]) {
|
||||
const result = await ResultAsync.fromPromise(fn(...args), e => e as AxiosError)
|
||||
if (result.isErr()) {
|
||||
if (result.error.response?.status === 401) {
|
||||
set(false)
|
||||
}
|
||||
throw result.error
|
||||
}
|
||||
|
||||
set(true)
|
||||
return result.value
|
||||
}
|
||||
}
|
||||
|
||||
return { identity: id, wrapFetcher, set }
|
||||
}
|
||||
export const identityAtom = atom(true)
|
||||
|
||||
export const languageAtom = atomWithStorage<Lang | undefined>('language', undefined)
|
||||
|
||||
@ -66,10 +47,11 @@ export const version = atom({
|
||||
|
||||
export function useVersion () {
|
||||
const [data, set] = useAtom(version)
|
||||
const { set: setIdentity } = useIdentity()
|
||||
const client = useClient()
|
||||
const setIdentity = useUpdateAtom(identityAtom)
|
||||
|
||||
async function update () {
|
||||
const result = await ResultAsync.fromPromise(API.getVersion(), e => e as AxiosError)
|
||||
useSWR([client], async function () {
|
||||
const result = await ResultAsync.fromPromise(client.getVersion(), e => e as AxiosError)
|
||||
setIdentity(result.isOk())
|
||||
|
||||
set(
|
||||
@ -77,20 +59,21 @@ export function useVersion () {
|
||||
? { version: '', premium: false }
|
||||
: { version: result.value.data.version, premium: !!result.value.data.premium }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return { version: data.version, premium: data.premium, update }
|
||||
return { version: data.version, premium: data.premium }
|
||||
}
|
||||
|
||||
export function useRuleProviders () {
|
||||
const [{ premium }] = useAtom(version)
|
||||
const client = useClient()
|
||||
|
||||
const { data, mutate } = useSWR('/providers/rule', async () => {
|
||||
const { data, mutate } = useSWR(['/providers/rule', client], async () => {
|
||||
if (!premium) {
|
||||
return []
|
||||
}
|
||||
|
||||
const ruleProviders = await API.getRuleProviders()
|
||||
const ruleProviders = await client.getRuleProviders()
|
||||
|
||||
return Object.keys(ruleProviders.data.providers)
|
||||
.map<API.RuleProvider>(name => ruleProviders.data.providers[name])
|
||||
@ -117,9 +100,10 @@ export const proxyProvider = atom([] as API.Provider[])
|
||||
|
||||
export function useProxyProviders () {
|
||||
const [providers, set] = useAtom(proxyProvider)
|
||||
const client = useClient()
|
||||
|
||||
const { data, mutate } = useSWR('/providers/proxy', async () => {
|
||||
const proxyProviders = await API.getProxyProviders()
|
||||
const { data, mutate } = useSWR(['/providers/proxy', client], async () => {
|
||||
const proxyProviders = await client.getProxyProviders()
|
||||
|
||||
return Object.keys(proxyProviders.data.providers)
|
||||
.map<API.Provider>(name => proxyProviders.data.providers[name])
|
||||
@ -132,8 +116,10 @@ export function useProxyProviders () {
|
||||
}
|
||||
|
||||
export function useGeneral () {
|
||||
const { data, mutate } = useSWR('/config', async () => {
|
||||
const resp = await API.getConfig()
|
||||
const client = useClient()
|
||||
|
||||
const { data, mutate } = useSWR(['/config', client], async () => {
|
||||
const resp = await client.getConfig()
|
||||
const data = resp.data
|
||||
return {
|
||||
port: data.port,
|
||||
@ -164,9 +150,10 @@ export const proxies = atomWithImmer({
|
||||
export function useProxy () {
|
||||
const [allProxy, rawSet] = useAtom(proxies)
|
||||
const set = useWarpImmerSetter(rawSet)
|
||||
const client = useClient()
|
||||
|
||||
const { mutate } = useSWR('/proxies', async () => {
|
||||
const allProxies = await API.getProxies()
|
||||
const { mutate } = useSWR(['/proxies', client], async () => {
|
||||
const allProxies = await client.getProxies()
|
||||
|
||||
const global = allProxies.data.proxies.GLOBAL as API.Group
|
||||
// fix missing name
|
||||
@ -240,42 +227,64 @@ export function useClashXData () {
|
||||
return { data, update: mutate }
|
||||
}
|
||||
|
||||
export const apiData = atom({
|
||||
hostname: '127.0.0.1',
|
||||
port: '9090',
|
||||
secret: ''
|
||||
})
|
||||
|
||||
export function useAPIInfo () {
|
||||
const [data, set] = useAtom(apiData)
|
||||
|
||||
const fetch = useCallback(async function fetch () {
|
||||
const info = await API.getExternalControllerConfig()
|
||||
set({ ...info })
|
||||
}, [set])
|
||||
|
||||
async function update (info: typeof data) {
|
||||
const { hostname, port, secret } = info
|
||||
setLocalStorageItem('externalControllerAddr', hostname)
|
||||
setLocalStorageItem('externalControllerPort', port)
|
||||
setLocalStorageItem('secret', secret)
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
return { data, fetch, update }
|
||||
}
|
||||
|
||||
export const rules = atomWithImmer([] as API.Rule[])
|
||||
|
||||
export function useRule () {
|
||||
const [data, rawSet] = useAtom(rules)
|
||||
const set = useWarpImmerSetter(rawSet)
|
||||
const client = useClient()
|
||||
|
||||
async function update () {
|
||||
const resp = await API.getRules()
|
||||
const resp = await client.getRules()
|
||||
set(resp.data.rules)
|
||||
}
|
||||
|
||||
return { rules: data, update }
|
||||
}
|
||||
|
||||
const logsAtom = atom({
|
||||
key: '',
|
||||
instance: null as StreamReader<Log> | null
|
||||
})
|
||||
|
||||
export function useLogsStreamReader () {
|
||||
const apiInfo = useAPIInfo()
|
||||
const { general } = useGeneral()
|
||||
const version = useVersion()
|
||||
const [item, setItem] = useAtom(logsAtom)
|
||||
|
||||
if (!version.version) {
|
||||
return null
|
||||
}
|
||||
|
||||
const useWebsocket = !!version.version || true
|
||||
const key = `${apiInfo.protocol}//${apiInfo.hostname}:${apiInfo.port}/logs?level=${general.logLevel ?? ''}&useWebsocket=${useWebsocket}&secret=${apiInfo.secret}`
|
||||
if (item.key === key) {
|
||||
return item.instance!
|
||||
}
|
||||
|
||||
const oldInstance = item.instance
|
||||
|
||||
const logUrl = `${apiInfo.protocol}//${apiInfo.hostname}:${apiInfo.port}/logs?level=${general.logLevel ?? ''}`
|
||||
const instance = new StreamReader<Log>({ url: logUrl, bufferLength: 200, token: apiInfo.secret, useWebsocket })
|
||||
setItem({ key, instance })
|
||||
|
||||
if (oldInstance) {
|
||||
oldInstance.destory()
|
||||
}
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
export function useConnectionStreamReader () {
|
||||
const apiInfo = useAPIInfo()
|
||||
const version = useVersion()
|
||||
|
||||
const useWebsocket = !!version.version || true
|
||||
|
||||
const url = `${apiInfo.protocol}//${apiInfo.hostname}:${apiInfo.port}/connections`
|
||||
return useMemo(
|
||||
() => version.version ? new StreamReader<Snapshot>({ url, bufferLength: 200, token: apiInfo.secret, useWebsocket }) : null,
|
||||
[apiInfo.secret, url, useWebsocket, version.version]
|
||||
)
|
||||
}
|
||||
|
78
src/stores/request.ts
Normal file
78
src/stores/request.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { isClashX, jsBridge } from "@lib/jsBridge";
|
||||
import { atomWithStorage, useAtomValue } from "jotai/utils";
|
||||
import { useLocation } from "react-use";
|
||||
import { Client } from "@lib/request";
|
||||
|
||||
const clashxConfigAtom = atom(async () => {
|
||||
if (!isClashX()) {
|
||||
return null
|
||||
}
|
||||
|
||||
const info = await jsBridge!.getAPIInfo()
|
||||
return {
|
||||
hostname: info.host,
|
||||
port: info.port,
|
||||
secret: info.secret,
|
||||
protocol: 'http:'
|
||||
}
|
||||
})
|
||||
|
||||
export const localStorageAtom = atomWithStorage<{
|
||||
hostname: string;
|
||||
port: string;
|
||||
secret: string;
|
||||
}[]>('externalControllers', [])
|
||||
|
||||
export function useAPIInfo() {
|
||||
const clashx = useAtomValue(clashxConfigAtom)
|
||||
const location = useLocation()
|
||||
const localStorage = useAtomValue(localStorageAtom)
|
||||
|
||||
if (clashx) {
|
||||
return clashx
|
||||
}
|
||||
|
||||
let url: URL | undefined;
|
||||
{
|
||||
const meta = document.querySelector<HTMLMetaElement>('meta[name="external-controller"]')
|
||||
if (meta?.content?.match(/^https?:/)) {
|
||||
// [protocol]://[secret]@[hostname]:[port]
|
||||
url = new URL(meta.content)
|
||||
}
|
||||
}
|
||||
|
||||
const qs = new URLSearchParams(location.search)
|
||||
|
||||
const hostname = qs.get('host') ?? localStorage?.[0]?.hostname ?? url?.hostname ?? '127.0.0.1'
|
||||
const port = qs.get('port') ?? localStorage?.[0]?.port ?? url?.port ?? '9090'
|
||||
const secret = qs.get('secret') ?? localStorage?.[0]?.secret ?? url?.username ?? ''
|
||||
const protocol = qs.get('protocol') ?? hostname === '127.0.0.1' ? 'http:' : (url?.protocol ?? window.location.protocol)
|
||||
|
||||
return { hostname, port, secret, protocol }
|
||||
}
|
||||
|
||||
const clientAtom = atom({
|
||||
key: '',
|
||||
instance: null as Client | null
|
||||
})
|
||||
|
||||
export function useClient() {
|
||||
const {
|
||||
hostname,
|
||||
port,
|
||||
secret,
|
||||
protocol
|
||||
} = useAPIInfo()
|
||||
|
||||
const [item, setItem] = useAtom(clientAtom)
|
||||
const key = `${protocol}//${hostname}:${port}?secret=${secret}`
|
||||
if (item.key === key) {
|
||||
return item.instance!
|
||||
}
|
||||
|
||||
const client = new Client(`${protocol}//${hostname}:${port}`, secret)
|
||||
setItem({ key, instance: client })
|
||||
|
||||
return client
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user