mirror of
https://github.com/woodchen-ink/clash-and-dashboard.git
synced 2025-07-18 14:01:56 +08:00
Feature: replace unstated-next with recoil
This commit is contained in:
parent
45d1473adc
commit
e4da18d01a
1373
package-lock.json
generated
1373
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
49
package.json
49
package.json
@ -29,71 +29,72 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.8.4",
|
||||
"@babel/core": "^7.9.0",
|
||||
"@babel/preset-env": "^7.9.5",
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/preset-env": "^7.9.6",
|
||||
"@babel/preset-react": "^7.9.4",
|
||||
"@hot-loader/react-dom": "^16.13.0",
|
||||
"@types/classnames": "^2.2.10",
|
||||
"@types/lodash-es": "^4.17.3",
|
||||
"@types/node": "^13.13.2",
|
||||
"@types/react": "^16.9.34",
|
||||
"@types/react-dom": "^16.9.6",
|
||||
"@types/react-router-dom": "^5.1.4",
|
||||
"@types/react-table": "^7.0.16",
|
||||
"@types/node": "^14.0.6",
|
||||
"@types/react": "^16.9.35",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@types/react-router-dom": "^5.1.5",
|
||||
"@types/react-table": "^7.0.18",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.0",
|
||||
"@types/react-window": "^1.8.2",
|
||||
"@types/recoil": "0.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^2.29.0",
|
||||
"@typescript-eslint/parser": "^2.29.0",
|
||||
"autoprefixer": "^9.7.6",
|
||||
"autoprefixer": "^9.8.0",
|
||||
"awesome-typescript-loader": "^5.2.1",
|
||||
"babel-loader": "^8.1.0",
|
||||
"babel-preset-minify": "^0.5.1",
|
||||
"css-loader": "^3.5.3",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-standard": "^14.1.1",
|
||||
"eslint-loader": "^4.0.0",
|
||||
"eslint-loader": "^4.0.2",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"eslint-plugin-react": "^7.20.0",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
"file-loader": "^6.0.0",
|
||||
"html-webpack-plugin": "^4.2.0",
|
||||
"html-webpack-plugin": "^4.3.0",
|
||||
"image-webpack-loader": "^6.0.0",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"offline-plugin": "^5.0.7",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"react-hot-loader": "^4.12.20",
|
||||
"sass": "^1.26.5",
|
||||
"react-hot-loader": "^4.12.21",
|
||||
"sass": "^1.26.7",
|
||||
"sass-loader": "^8.0.2",
|
||||
"style-loader": "^1.2.0",
|
||||
"stylelint": "^13.3.3",
|
||||
"style-loader": "^1.2.1",
|
||||
"stylelint": "^13.5.0",
|
||||
"stylelint-config-standard": "^20.0.0",
|
||||
"stylelint-webpack-plugin": "^1.2.3",
|
||||
"terser-webpack-plugin": "^2.3.5",
|
||||
"typescript": "^3.8.3",
|
||||
"typescript": "^3.9.3",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-dev-middleware": "^3.7.2",
|
||||
"webpack-dev-server": "^3.10.3",
|
||||
"webpack-dev-server": "^3.11.0",
|
||||
"webpack-merge": "^4.2.2",
|
||||
"webpack-pwa-manifest": "^4.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.19.2",
|
||||
"classnames": "^2.2.6",
|
||||
"dayjs": "^1.8.25",
|
||||
"eventemitter3": "^4.0.0",
|
||||
"immer": "^6.0.3",
|
||||
"dayjs": "^1.8.28",
|
||||
"eventemitter3": "^4.0.4",
|
||||
"immer": "^6.0.9",
|
||||
"lodash-es": "^4.17.15",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-table": "^7.0.4",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-table": "^7.1.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
"react-window": "^1.8.5",
|
||||
"swr": "^0.2.0",
|
||||
"unstated-next": "^1.1.0",
|
||||
"recoil": "0.0.7",
|
||||
"swr": "^0.2.2",
|
||||
"use-immer": "^0.4.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useState, useRef, useLayoutEffect } from 'react'
|
||||
import { containers } from '@stores'
|
||||
import { useI18n } from '@stores'
|
||||
import { BaseComponentProps } from '@models'
|
||||
import { noop } from '@lib/helper'
|
||||
import classnames from 'classnames'
|
||||
@ -8,16 +8,16 @@ import './style.scss'
|
||||
interface TagsProps extends BaseComponentProps {
|
||||
data: string[]
|
||||
onClick: (name: string) => void
|
||||
shouldError?: (name: string) => boolean
|
||||
errSet?: Set<string>
|
||||
select: string
|
||||
rowHeight: number
|
||||
canClick: boolean
|
||||
}
|
||||
|
||||
export function Tags (props: TagsProps) {
|
||||
const { className, data, onClick, select, canClick, shouldError, rowHeight: rawHeight } = props
|
||||
const { className, data, onClick, select, canClick, errSet, rowHeight: rawHeight } = props
|
||||
|
||||
const { useTranslation } = containers.useI18n()
|
||||
const { useTranslation } = useI18n()
|
||||
const { t } = useTranslation('Proxies')
|
||||
const [expand, setExpand] = useState(false)
|
||||
const [showExtend, setShowExtend] = useState(false)
|
||||
@ -36,7 +36,7 @@ export function Tags (props: TagsProps) {
|
||||
|
||||
const tags = data
|
||||
.map(t => {
|
||||
const tagClass = classnames({ 'tags-selected': select === t, 'can-click': canClick, error: shouldError && shouldError(t) })
|
||||
const tagClass = classnames({ 'tags-selected': select === t, 'can-click': canClick, error: errSet?.has(t) })
|
||||
return (
|
||||
<li className={tagClass} key={t} onClick={() => handleClick(t)}>
|
||||
{ t }
|
||||
|
@ -2,7 +2,7 @@ import React, { useMemo, useLayoutEffect } from 'react'
|
||||
import { useBlockLayout, useResizeColumns, useTable } from 'react-table'
|
||||
import classnames from 'classnames'
|
||||
import { Header, Card, Checkbox, Modal, Icon } from '@components'
|
||||
import { containers } from '@stores'
|
||||
import { useI18n } from '@stores'
|
||||
import * as API from '@lib/request'
|
||||
import { StreamReader } from '@lib/streamer'
|
||||
import { useObject, useVisible } from '@lib/hook'
|
||||
@ -62,7 +62,7 @@ function formatSpeed (upload: number, download: number) {
|
||||
}
|
||||
|
||||
export default function Connections () {
|
||||
const { useTranslation, lang } = containers.useI18n()
|
||||
const { useTranslation, lang } = useI18n()
|
||||
const t = useMemo(() => useTranslation('Connections').t, [useTranslation])
|
||||
|
||||
// total
|
||||
|
@ -1,14 +1,14 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { useObject } from '@lib/hook'
|
||||
import { Modal, Input, Row, Col, Alert } from '@components'
|
||||
import { containers } from '@stores'
|
||||
import { useI18n, useAPIInfo, useIdentity } from '@stores'
|
||||
import './style.scss'
|
||||
|
||||
export default function ExternalController () {
|
||||
const { useTranslation } = containers.useI18n()
|
||||
const { useTranslation } = useI18n()
|
||||
const { t } = useTranslation('Settings')
|
||||
const { data: info, update, fetch } = containers.useAPIInfo()
|
||||
const { unauthorized: { hide, visible } } = containers.useData()
|
||||
const { data: info, update, fetch } = useAPIInfo()
|
||||
const { identity, set: setIdentity } = useIdentity()
|
||||
const [value, set] = useObject({
|
||||
hostname: '',
|
||||
port: '',
|
||||
@ -30,10 +30,10 @@ export default function ExternalController () {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={visible}
|
||||
show={!identity}
|
||||
title={t('externalControllerSetting.title')}
|
||||
bodyClassName="external-controller"
|
||||
onClose={hide}
|
||||
onClose={() => setIdentity(true)}
|
||||
onOk={handleOk}
|
||||
>
|
||||
<Alert type="info" inside={true}>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useLayoutEffect, useEffect, useRef, useState } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import { containers } from '@stores'
|
||||
import { useI18n } from '@stores'
|
||||
import { Card, Header } from '@components'
|
||||
import { getLogsStreamReader } from '@lib/request'
|
||||
import { StreamReader } from '@lib/streamer'
|
||||
@ -11,7 +11,7 @@ export default function Logs () {
|
||||
const listRef = useRef<HTMLUListElement>()
|
||||
const logsRef = useRef<Log[]>([])
|
||||
const [logs, setLogs] = useState<Log[]>([])
|
||||
const { useTranslation } = containers.useI18n()
|
||||
const { useTranslation } = useI18n()
|
||||
const { t } = useTranslation('Logs')
|
||||
|
||||
useLayoutEffect(() => {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import * as React from 'react'
|
||||
import { containers } from '@stores'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useRecoilValue } from 'recoil'
|
||||
import { useProxy, useConfig, proxyMapping } from '@stores'
|
||||
import { changeProxySelected, Group as IGroup, getConnections, closeConnection } from '@lib/request'
|
||||
import { Tags, Tag } from '@components'
|
||||
import './style.scss'
|
||||
@ -9,13 +10,14 @@ interface GroupProps {
|
||||
}
|
||||
|
||||
export function Group (props: GroupProps) {
|
||||
const { fetch, data: Data } = containers.useData()
|
||||
const { data: Config } = containers.useConfig()
|
||||
const { update } = useProxy()
|
||||
const proxyMap = useRecoilValue(proxyMapping)
|
||||
const { data: Config } = useConfig()
|
||||
const { config } = props
|
||||
|
||||
async function handleChangeProxySelected (name: string) {
|
||||
await changeProxySelected(props.config.name, name)
|
||||
await fetch()
|
||||
await update()
|
||||
if (Config.breakConnections) {
|
||||
const list: string[] = []
|
||||
const snapshot = await getConnections()
|
||||
@ -31,14 +33,17 @@ export function Group (props: GroupProps) {
|
||||
}
|
||||
}
|
||||
|
||||
function shouldError (name: string) {
|
||||
const history = Data.proxyMap.get(name)?.history
|
||||
if (history?.length) {
|
||||
return !history.slice(-1)[0].delay
|
||||
const errSet = useMemo(() => {
|
||||
const set = new Set<string>()
|
||||
for (const proxy of config.all) {
|
||||
const history = proxyMap.get(proxy)?.history
|
||||
if (history?.length && history.slice(-1)[0].delay !== 0) {
|
||||
set.add(proxy)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
return set
|
||||
}, [proxyMap])
|
||||
|
||||
const canClick = config.type === 'Selector'
|
||||
return (
|
||||
@ -52,7 +57,7 @@ export function Group (props: GroupProps) {
|
||||
className="proxy-group-tags"
|
||||
data={config.all}
|
||||
onClick={handleChangeProxySelected}
|
||||
shouldError={shouldError}
|
||||
errSet={errSet}
|
||||
select={config.now}
|
||||
canClick={canClick}
|
||||
rowHeight={30} />
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { Card, Tag, Icon, Loading } from '@components'
|
||||
import { containers } from '@stores'
|
||||
import { useI18n, useProxyProviders } from '@stores'
|
||||
import { fromNow } from '@lib/date'
|
||||
import { Provider as IProvider, Proxy as IProxy, updateProvider, healthCheckProvider } from '@lib/request'
|
||||
import { useVisible } from '@lib/hook'
|
||||
@ -14,22 +14,22 @@ interface ProvidersProps {
|
||||
}
|
||||
|
||||
export function Provider (props: ProvidersProps) {
|
||||
const { fetch } = containers.useData()
|
||||
const { useTranslation, lang } = containers.useI18n()
|
||||
const { provider } = props
|
||||
const { update } = useProxyProviders()
|
||||
const { useTranslation, lang } = useI18n()
|
||||
|
||||
const { provider } = props
|
||||
const { t } = useTranslation('Proxies')
|
||||
|
||||
const { visible, hide, show } = useVisible()
|
||||
|
||||
function handleHealthChech () {
|
||||
show()
|
||||
healthCheckProvider(provider.name).then(() => fetch()).finally(() => hide())
|
||||
healthCheckProvider(provider.name).then(() => update()).finally(() => hide())
|
||||
}
|
||||
|
||||
function handleUpdate () {
|
||||
show()
|
||||
updateProvider(provider.name).then(() => fetch()).finally(() => hide())
|
||||
updateProvider(provider.name).then(() => update()).finally(() => hide())
|
||||
}
|
||||
|
||||
const proxies = useMemo(() => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useState, useMemo, useLayoutEffect, useEffect } from 'react'
|
||||
import React, { useMemo, useLayoutEffect } from 'react'
|
||||
import classnames from 'classnames'
|
||||
import { BaseComponentProps } from '@models'
|
||||
import { containers } from '@stores'
|
||||
import { useProxy } from '@stores'
|
||||
import { getProxyDelay, Proxy as IProxy } from '@lib/request'
|
||||
import EE, { Action } from '@lib/event'
|
||||
import { isClashX, jsBridge } from '@lib/jsBridge'
|
||||
@ -14,8 +14,8 @@ interface ProxyProps extends BaseComponentProps {
|
||||
|
||||
const TagColors = {
|
||||
'#909399': 0,
|
||||
'#00c520': 150,
|
||||
'#ff9a28': 500,
|
||||
'#00c520': 260,
|
||||
'#ff9a28': 600,
|
||||
'#ff3e5e': Infinity
|
||||
}
|
||||
|
||||
@ -31,20 +31,24 @@ async function getDelay (name: string) {
|
||||
|
||||
export function Proxy (props: ProxyProps) {
|
||||
const { config, className } = props
|
||||
const [delay, setDelay] = useState(0)
|
||||
const { updateDelay } = containers.useData()
|
||||
const { set } = useProxy()
|
||||
|
||||
async function speedTest () {
|
||||
const [delay, err] = await to(getDelay(config.name))
|
||||
|
||||
const validDelay = err ? 0 : delay
|
||||
setDelay(validDelay)
|
||||
updateDelay(config.name, validDelay)
|
||||
set(draft => {
|
||||
const proxy = draft.proxies.find(p => p.name === proxy)
|
||||
if (proxy) {
|
||||
proxy.history.push({ time: Date.now().toString(), delay: validDelay })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setDelay(config.history && config.history.length ? config.history.slice(-1)[0].delay : 0)
|
||||
}, [config])
|
||||
const delay = useMemo(
|
||||
() => config.history?.length ? config.history.slice(-1)[0].delay : 0,
|
||||
[config]
|
||||
)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
EE.subscribe(Action.SPEED_NOTIFY, speedTest)
|
||||
|
@ -3,7 +3,7 @@ import useSWR from 'swr'
|
||||
import EE from '@lib/event'
|
||||
import { useRound } from '@lib/hook'
|
||||
import { Card, Header, Icon, Checkbox } from '@components'
|
||||
import { containers } from '@stores'
|
||||
import { useI18n, useConfig, useProxy, useProxyProviders } from '@stores'
|
||||
import * as API from '@lib/request'
|
||||
|
||||
import { Proxy, Group, Provider } from './components'
|
||||
@ -27,12 +27,68 @@ export function compareDesc (a: API.Proxy, b: API.Proxy) {
|
||||
return (lastDelayB || Number.MAX_SAFE_INTEGER) - (lastDelayA || Number.MAX_SAFE_INTEGER)
|
||||
}
|
||||
|
||||
export default function Proxies () {
|
||||
const { data, fetch } = containers.useData()
|
||||
const { useTranslation } = containers.useI18n()
|
||||
const { data: config, set: setConfig } = containers.useConfig()
|
||||
function ProxyGroups () {
|
||||
const { groups } = useProxy()
|
||||
const { data: config, set: setConfig } = useConfig()
|
||||
const { useTranslation } = useI18n()
|
||||
const { t } = useTranslation('Proxies')
|
||||
|
||||
return <>
|
||||
{
|
||||
groups.length !== 0 &&
|
||||
<div className="proxies-container">
|
||||
<Header title={t('groupTitle')}>
|
||||
<Checkbox
|
||||
className="connections-filter"
|
||||
checked={config.breakConnections}
|
||||
onChange={value => setConfig('breakConnections', value)}>
|
||||
{t('breakConnectionsText')}
|
||||
</Checkbox>
|
||||
</Header>
|
||||
<Card className="proxies-group-card">
|
||||
<ul className="proxies-group-list">
|
||||
{
|
||||
groups.map(p => (
|
||||
<li className="proxies-group-item" key={p.name}>
|
||||
<Group config={p} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
function ProxyProviders () {
|
||||
const { providers } = useProxyProviders()
|
||||
const { useTranslation } = useI18n()
|
||||
const { t } = useTranslation('Proxies')
|
||||
|
||||
return <>
|
||||
{
|
||||
providers.length !== 0 &&
|
||||
<div className="proxies-container">
|
||||
<Header title={t('providerTitle')} />
|
||||
<ul className="proxies-providers-list">
|
||||
{
|
||||
providers.map(p => (
|
||||
<li className="proxies-providers-item" key={p.name}>
|
||||
<Provider provider={p} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
function Proxies () {
|
||||
const { proxies } = useProxy()
|
||||
const { useTranslation } = useI18n()
|
||||
const { t } = useTranslation('Proxies')
|
||||
useSWR('data', fetch)
|
||||
|
||||
function handleNotitySpeedTest () {
|
||||
EE.notifySpeedTest()
|
||||
@ -41,78 +97,53 @@ export default function Proxies () {
|
||||
const { current: sort, next } = useRound(
|
||||
[sortType.Asc, sortType.Desc, sortType.None]
|
||||
)
|
||||
const proxies = useMemo(() => {
|
||||
const sortedProxies = useMemo(() => {
|
||||
switch (sort) {
|
||||
case sortType.Desc:
|
||||
return data.proxy.slice().sort((a, b) => compareDesc(a, b))
|
||||
return proxies.slice().sort((a, b) => compareDesc(a, b))
|
||||
case sortType.Asc:
|
||||
return data.proxy.slice().sort((a, b) => -1 * compareDesc(a, b))
|
||||
return proxies.slice().sort((a, b) => -1 * compareDesc(a, b))
|
||||
default:
|
||||
return data.proxy.slice()
|
||||
return proxies.slice()
|
||||
}
|
||||
}, [sort, data])
|
||||
}, [sort, proxies])
|
||||
const handleSort = next
|
||||
|
||||
return <>
|
||||
{
|
||||
sortedProxies.length !== 0 &&
|
||||
<div className="proxies-container">
|
||||
<Header title={t('title')}>
|
||||
<Icon className="proxies-action-icon" type={sortMap[sort]} onClick={handleSort} size={20} />
|
||||
<Icon className="proxies-action-icon" type="speed" size={20} />
|
||||
<span className="proxies-speed-test" onClick={handleNotitySpeedTest}>{t('speedTestText')}</span>
|
||||
</Header>
|
||||
<ul className="proxies-list">
|
||||
{
|
||||
sortedProxies.map(p => (
|
||||
<li key={p.name}>
|
||||
<Proxy config={p} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
export default function ProxyContainer () {
|
||||
const { update: updateProxy } = useProxy()
|
||||
const { update: updateProvider } = useProxyProviders()
|
||||
|
||||
useSWR('proxies', updateProxy)
|
||||
useSWR('providers', updateProvider)
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
{
|
||||
data.proxyGroup.length !== 0 &&
|
||||
<div className="proxies-container">
|
||||
<Header title={t('groupTitle')}>
|
||||
<Checkbox
|
||||
className="connections-filter"
|
||||
checked={config.breakConnections}
|
||||
onChange={value => setConfig('breakConnections', value)}>
|
||||
{t('breakConnectionsText')}
|
||||
</Checkbox>
|
||||
</Header>
|
||||
<Card className="proxies-group-card">
|
||||
<ul className="proxies-group-list">
|
||||
{
|
||||
data.proxyGroup.map(p => (
|
||||
<li className="proxies-group-item" key={p.name}>
|
||||
<Group config={p} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
data.proxyProviders.length !== 0 &&
|
||||
<div className="proxies-container">
|
||||
<Header title={t('providerTitle')} />
|
||||
<ul className="proxies-providers-list">
|
||||
{
|
||||
data.proxyProviders.map(p => (
|
||||
<li className="proxies-providers-item" key={p.name}>
|
||||
<Provider provider={p} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
proxies.length !== 0 &&
|
||||
<div className="proxies-container">
|
||||
<Header title={t('title')}>
|
||||
<Icon className="proxies-action-icon" type={sortMap[sort]} onClick={handleSort} size={20} />
|
||||
<Icon className="proxies-action-icon" type="speed" size={20} />
|
||||
<span className="proxies-speed-test" onClick={handleNotitySpeedTest}>{t('speedTestText')}</span>
|
||||
</Header>
|
||||
<ul className="proxies-list">
|
||||
{
|
||||
proxies.map(p => (
|
||||
<li key={p.name}>
|
||||
<Proxy config={p} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
<ProxyGroups />
|
||||
<ProxyProviders />
|
||||
<Proxies />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,19 +1,17 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import React from 'react'
|
||||
import { Header, Card, Row, Col } from '@components'
|
||||
import { containers } from '@stores'
|
||||
import { useI18n, useRule } from '@stores'
|
||||
import { FixedSizeList as List } from 'react-window'
|
||||
import AutoSizer from 'react-virtualized-auto-sizer'
|
||||
import useSWR from 'swr'
|
||||
import './style.scss'
|
||||
|
||||
export default function Rules () {
|
||||
const { data, fetch } = containers.useData()
|
||||
const { useTranslation } = containers.useI18n()
|
||||
const { rules, update } = useRule()
|
||||
const { useTranslation } = useI18n()
|
||||
const { t } = useTranslation('Rules')
|
||||
const { rules } = data
|
||||
|
||||
useEffect(() => {
|
||||
fetch()
|
||||
}, [])
|
||||
useSWR('rules', update)
|
||||
|
||||
function renderRuleItem ({ index, style }: { index: number, style: React.CSSProperties }) {
|
||||
const rule = rules[index]
|
||||
|
@ -1,9 +1,8 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { Header, Card, Row, Col, Switch, ButtonSelect, ButtonSelectOptions, Input, Icon } from '@components'
|
||||
import { containers } from '@stores'
|
||||
import { useI18n, useClashXData, useAPIInfo, useGeneral, useIdentity } from '@stores'
|
||||
import { updateConfig } from '@lib/request'
|
||||
import { useObject } from '@lib/hook'
|
||||
import { to } from '@lib/helper'
|
||||
import { isClashX, jsBridge } from '@lib/jsBridge'
|
||||
import { Lang } from '@i18n'
|
||||
import './style.scss'
|
||||
@ -11,10 +10,11 @@ import './style.scss'
|
||||
const languageOptions: ButtonSelectOptions[] = [{ label: '中文', value: 'zh_CN' }, { label: 'English', value: 'en_US' }]
|
||||
|
||||
export default function Settings () {
|
||||
const { data: clashXData, fetch: fetchClashXData } = containers.useClashXData()
|
||||
const { data, fetch, unauthorized: { show } } = containers.useData()
|
||||
const { data: apiInfo } = containers.useAPIInfo()
|
||||
const { useTranslation, setLang, lang } = containers.useI18n()
|
||||
const { data: clashXData, update: fetchClashXData } = useClashXData()
|
||||
const { general, update: fetchGeneral } = useGeneral()
|
||||
const { set: setIdentity } = useIdentity()
|
||||
const { data: apiInfo } = useAPIInfo()
|
||||
const { useTranslation, setLang, lang } = useI18n()
|
||||
const { t } = useTranslation('Settings')
|
||||
const [info, set] = useObject({
|
||||
socks5ProxyPort: 7891,
|
||||
@ -23,22 +23,20 @@ export default function Settings () {
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
fetch()
|
||||
fetchGeneral()
|
||||
if (isClashX()) {
|
||||
fetchClashXData().then(() => set('isClashX', true))
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
set('socks5ProxyPort', data.general.socksPort)
|
||||
set('httpProxyPort', data.general.port)
|
||||
}, [data])
|
||||
set('socks5ProxyPort', general.socksPort)
|
||||
set('httpProxyPort', general.port)
|
||||
}, [general])
|
||||
|
||||
async function handleProxyModeChange (mode: string) {
|
||||
const [, err] = await to(updateConfig({ mode }))
|
||||
if (!err) {
|
||||
fetch()
|
||||
}
|
||||
await updateConfig({ mode })
|
||||
await fetchGeneral()
|
||||
}
|
||||
|
||||
async function handleStartAtLoginChange (state: boolean) {
|
||||
@ -56,26 +54,18 @@ export default function Settings () {
|
||||
}
|
||||
|
||||
async function handleHttpPortSave () {
|
||||
const [, err] = await to(updateConfig({ port: info.httpProxyPort }))
|
||||
if (!err) {
|
||||
await fetch()
|
||||
set('httpProxyPort', data.general.port)
|
||||
}
|
||||
await updateConfig({ port: info.httpProxyPort })
|
||||
await fetchGeneral()
|
||||
}
|
||||
|
||||
async function handleSocksPortSave () {
|
||||
const [, err] = await to(updateConfig({ 'socks-port': info.socks5ProxyPort }))
|
||||
if (!err) {
|
||||
await fetch()
|
||||
set('socks5ProxyPort', data.general.socksPort)
|
||||
}
|
||||
await updateConfig({ 'socks-port': info.socks5ProxyPort })
|
||||
await fetchGeneral()
|
||||
}
|
||||
|
||||
async function handleAllowLanChange (state: boolean) {
|
||||
const [, err] = await to(updateConfig({ 'allow-lan': state }))
|
||||
if (!err) {
|
||||
await fetch()
|
||||
}
|
||||
await updateConfig({ 'allow-lan': state })
|
||||
await fetchGeneral()
|
||||
}
|
||||
|
||||
const {
|
||||
@ -83,7 +73,7 @@ export default function Settings () {
|
||||
port: externalControllerPort
|
||||
} = apiInfo
|
||||
|
||||
const { allowLan, mode } = data.general
|
||||
const { allowLan, mode } = general
|
||||
const {
|
||||
startAtLogin,
|
||||
systemProxy
|
||||
@ -189,7 +179,7 @@ export default function Settings () {
|
||||
<span className="label">{t('labels.externalController')}</span>
|
||||
</Col>
|
||||
<Col className="external-controller" span={10}>
|
||||
<span className="modify-btn" onClick={show}>
|
||||
<span className="modify-btn" onClick={() => setIdentity(false)}>
|
||||
{`${externalControllerHost}:${externalControllerPort}`}
|
||||
</span>
|
||||
</Col>
|
||||
|
@ -1,10 +1,11 @@
|
||||
import * as React from 'react'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import classnames from 'classnames'
|
||||
import { containers } from '@stores'
|
||||
import { useI18n, useVersion } from '@stores'
|
||||
|
||||
import './style.scss'
|
||||
import logo from '@assets/logo.png'
|
||||
import useSWR from 'swr'
|
||||
|
||||
interface SidebarProps {
|
||||
routes: {
|
||||
@ -17,9 +18,12 @@ interface SidebarProps {
|
||||
|
||||
export default function Sidebar (props: SidebarProps) {
|
||||
const { routes } = props
|
||||
const { useTranslation } = containers.useI18n()
|
||||
const { useTranslation } = useI18n()
|
||||
const { version, premium, update } = useVersion()
|
||||
const { t } = useTranslation('SideBar')
|
||||
|
||||
useSWR('version', update)
|
||||
|
||||
const navlinks = routes.map(
|
||||
({ path, name, exact, noMobile }) => (
|
||||
<li className={classnames('item', { 'no-mobile': noMobile })} key={name}>
|
||||
@ -34,6 +38,11 @@ export default function Sidebar (props: SidebarProps) {
|
||||
<ul className="sidebar-menu">
|
||||
{ navlinks }
|
||||
</ul>
|
||||
<div className="sidebar-version">
|
||||
<span className="sidebar-version-label">Clash { t('Version') }</span>
|
||||
<span className="sidebar-version-text">{ version }</span>
|
||||
{ premium && <span className="sidebar-version-label">Premium</span> }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -21,6 +22,7 @@
|
||||
.sidebar-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
margin-top: 12px;
|
||||
|
||||
.item {
|
||||
@ -52,6 +54,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-version {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.sidebar-version-label {
|
||||
font-size: 14px;
|
||||
color: $color-primary-dark;
|
||||
text-shadow: 0 2px 6px rgba($color: $color-primary-dark, $alpha: 0.4);
|
||||
}
|
||||
|
||||
.sidebar-version-text {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
margin: 8px 0;
|
||||
color: $color-primary-darken;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
|
@ -5,7 +5,8 @@ export default {
|
||||
Logs: 'Logs',
|
||||
Rules: 'Rules',
|
||||
Settings: 'Setting',
|
||||
Connections: 'Connections'
|
||||
Connections: 'Connections',
|
||||
Version: 'Version'
|
||||
},
|
||||
Settings: {
|
||||
title: 'Settings',
|
||||
|
@ -1,13 +1,11 @@
|
||||
/* eslint-disable camelcase */
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import { useState, useCallback } from 'react'
|
||||
import get from 'lodash/get'
|
||||
import { getLocalStorageItem, setLocalStorageItem } from '@lib/helper'
|
||||
|
||||
import en_US from './en_US'
|
||||
import zh_CN from './zh_CN'
|
||||
|
||||
const Language = {
|
||||
export const Language = {
|
||||
en_US,
|
||||
zh_CN
|
||||
}
|
||||
@ -16,7 +14,7 @@ export type Lang = keyof typeof Language
|
||||
|
||||
const languageKey = 'language'
|
||||
|
||||
const locales = Object.keys(Language)
|
||||
export const locales = Object.keys(Language)
|
||||
|
||||
function getNavigatorLanguage (): string[] {
|
||||
const found: string[] = []
|
||||
@ -34,7 +32,7 @@ function getNavigatorLanguage (): string[] {
|
||||
return found
|
||||
}
|
||||
|
||||
function getLanguage (): Lang {
|
||||
export function getLanguage (): Lang {
|
||||
const localLanguage = getLocalStorageItem(languageKey)
|
||||
if (localLanguage && locales.includes(localLanguage)) {
|
||||
return localLanguage as Lang
|
||||
@ -52,23 +50,6 @@ function getLanguage (): Lang {
|
||||
return 'en_US'
|
||||
}
|
||||
|
||||
export function useI18n () {
|
||||
const [lang, set] = useState(getLanguage())
|
||||
|
||||
function setLang (lang: Lang) {
|
||||
set(lang)
|
||||
setLocalStorageItem(languageKey, lang)
|
||||
}
|
||||
|
||||
const useTranslation = useCallback(
|
||||
function (namespace: string) {
|
||||
function t (path: string) {
|
||||
return get(Language[lang][namespace], path) as string
|
||||
}
|
||||
return { t }
|
||||
},
|
||||
[lang]
|
||||
)
|
||||
|
||||
return { lang, locales, setLang, useTranslation }
|
||||
export function setLanguage (lang: Lang) {
|
||||
setLocalStorageItem(languageKey, lang)
|
||||
}
|
||||
|
@ -5,7 +5,8 @@ export default {
|
||||
Logs: '日志',
|
||||
Rules: '规则',
|
||||
Settings: '设置',
|
||||
Connections: '连接'
|
||||
Connections: '连接',
|
||||
Version: '版本'
|
||||
},
|
||||
Settings: {
|
||||
title: '设置',
|
||||
|
@ -26,8 +26,6 @@ export async function to <T, E = Error> (promise: Promise<T>): Promise<[T, E]> {
|
||||
}
|
||||
}
|
||||
|
||||
export type Partial<T> = { [P in keyof T]?: T[P] }
|
||||
|
||||
export function partition<T> (arr: T[], fn: (T) => boolean): [T[], T[]] {
|
||||
const left: T[] = []
|
||||
const right: T[] = []
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Draft } from 'immer'
|
||||
import { useImmer } from 'use-immer'
|
||||
import { createContainer } from 'unstated-next'
|
||||
import { useRef, useEffect, useState, useMemo } from 'react'
|
||||
|
||||
import { noop } from '@lib/helper'
|
||||
@ -54,26 +53,6 @@ export function useInterval (callback: () => void, delay: number) {
|
||||
)
|
||||
}
|
||||
|
||||
type containerFn<Value, State = void> = (initialState?: State) => Value
|
||||
|
||||
export function composeContainer<T, C extends containerFn<T>, U extends { [key: string]: C }, K extends keyof U> (mapping: U) {
|
||||
function Global () {
|
||||
return Object.keys(mapping).reduce((obj, key) => {
|
||||
obj[key as K] = mapping[key]()
|
||||
return obj
|
||||
}, {} as { [K in keyof U]: T })
|
||||
}
|
||||
|
||||
const allContainer = createContainer(Global)
|
||||
return {
|
||||
Provider: allContainer.Provider,
|
||||
containers: Object.keys(mapping).reduce((obj, key) => {
|
||||
obj[key as K] = (() => allContainer.useContainer()[key]) as U[K]
|
||||
return obj
|
||||
}, {} as { [K in keyof U]: U[K] })
|
||||
}
|
||||
}
|
||||
|
||||
export function useRound<T> (list: T[], defidx = 0) {
|
||||
if (list.length < 2) {
|
||||
throw new Error('List requires at least two elements')
|
||||
|
33
src/lib/recoil.ts
Normal file
33
src/lib/recoil.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { useRecoilState, RecoilState } from 'recoil'
|
||||
import produce, { Draft } from 'immer'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
export function useRecoilObjectWithImmer<T> (value: RecoilState<T>) {
|
||||
const [copy, rawSet] = useRecoilState(value)
|
||||
|
||||
function set<K extends keyof Draft<T>> (key: K, value: Draft<T>[K]): void
|
||||
function set<K extends keyof Draft<T>> (data: Partial<T>): void
|
||||
function set<K extends keyof Draft<T>> (f: (draft: Draft<T>) => void | T): void
|
||||
function set<K extends keyof Draft<T>> (data: any, value?: Draft<T>[K]): void {
|
||||
if (typeof data === 'string') {
|
||||
rawSet(produce(copy, (draft: Draft<T>) => {
|
||||
const key = data as K
|
||||
const v = value
|
||||
draft[key] = v
|
||||
}))
|
||||
} else if (typeof data === 'function') {
|
||||
const fn = data as (draft: Draft<T>) => void | T
|
||||
rawSet(produce(copy, fn) as T)
|
||||
} else if (typeof data === 'object') {
|
||||
rawSet(produce(copy, (draft: Draft<T>) => {
|
||||
const obj = data as Draft<T>
|
||||
for (const key of Object.keys(obj)) {
|
||||
const k = key as keyof Draft<T>
|
||||
draft[k] = obj[k]
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
return [copy, useCallback(set, [copy])] as [T, typeof set]
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import axios from 'axios'
|
||||
import { Partial, getLocalStorageItem, to } from '@lib/helper'
|
||||
import { getLocalStorageItem, to } from '@lib/helper'
|
||||
import { isClashX, jsBridge } from '@lib/jsBridge'
|
||||
import { createAsyncSingleton } from '@lib/asyncSingleton'
|
||||
import { Log } from '@models/Log'
|
||||
@ -181,7 +181,7 @@ export async function getProxy (name: string) {
|
||||
|
||||
export async function getVersion () {
|
||||
const req = await getInstance()
|
||||
return req.get<{ version: string }>('version')
|
||||
return req.get<{ version: string, premium?: boolean }>('version')
|
||||
}
|
||||
|
||||
export async function getProxyDelay (name: string) {
|
||||
|
@ -64,17 +64,6 @@ export interface Config {
|
||||
|
||||
}
|
||||
|
||||
export interface ClashXData {
|
||||
startAtLogin: boolean
|
||||
systemProxy: boolean
|
||||
}
|
||||
|
||||
export interface APIInfo {
|
||||
hostname: string
|
||||
port: string
|
||||
secret?: string
|
||||
}
|
||||
|
||||
export interface Data {
|
||||
version?: string
|
||||
|
||||
|
@ -1,17 +1,17 @@
|
||||
import * as React from 'react'
|
||||
import { render } from 'react-dom'
|
||||
import { HashRouter } from 'react-router-dom'
|
||||
import { Provider as Global } from '@stores'
|
||||
import { RecoilRoot } from 'recoil'
|
||||
import App from '@containers/App'
|
||||
|
||||
export default function renderApp () {
|
||||
const rootEl = document.getElementById('root')
|
||||
const AppInstance = (
|
||||
<Global>
|
||||
<RecoilRoot>
|
||||
<HashRouter>
|
||||
<App />
|
||||
</HashRouter>
|
||||
</Global>
|
||||
</RecoilRoot>
|
||||
)
|
||||
|
||||
render(AppInstance, rootEl)
|
||||
|
@ -1,149 +0,0 @@
|
||||
import * as Models from '@models'
|
||||
import * as API from '@lib/request'
|
||||
import { useObject, composeContainer, useVisible } from '@lib/hook'
|
||||
import { jsBridge } from '@lib/jsBridge'
|
||||
import { setLocalStorageItem, partition, to } from '@lib/helper'
|
||||
import { useI18n } from '@i18n'
|
||||
import { AxiosError } from 'axios'
|
||||
|
||||
function useData () {
|
||||
const [data, set] = useObject<Models.Data>({
|
||||
version: '',
|
||||
general: {},
|
||||
proxy: [],
|
||||
proxyGroup: [],
|
||||
proxyProviders: [],
|
||||
rules: [],
|
||||
proxyMap: new Map<string, API.Proxy>()
|
||||
})
|
||||
|
||||
const { visible, show, hide } = useVisible()
|
||||
|
||||
async function fetch () {
|
||||
const [resp, err] = await to(Promise.all([API.getConfig(), API.getProxies(), API.getRules(), API.getProxyProviders()]))
|
||||
const rErr = err as AxiosError
|
||||
if (rErr && (!rErr.response || rErr.response.status === 401)) {
|
||||
show()
|
||||
}
|
||||
|
||||
const [{ data: general }, rawProxies, rules, proxyProviders] = resp
|
||||
|
||||
set('general', {
|
||||
port: general.port,
|
||||
socksPort: general['socks-port'],
|
||||
redirPort: general['redir-port'],
|
||||
mode: general.mode,
|
||||
logLevel: general['log-level'],
|
||||
allowLan: general['allow-lan']
|
||||
})
|
||||
|
||||
const policyGroup = new Set(['Selector', 'URLTest', 'Fallback', 'LoadBalance'])
|
||||
const unUsedProxy = new Set(['DIRECT', 'REJECT', 'GLOBAL'])
|
||||
const proxyList = rawProxies.data.proxies.GLOBAL as API.Group
|
||||
// fix missing name
|
||||
proxyList.name = 'GLOBAL'
|
||||
const proxies = proxyList.all
|
||||
.filter(key => !unUsedProxy.has(key))
|
||||
.map(key => ({ ...rawProxies.data.proxies[key], name: key }))
|
||||
const [proxy, groups] = partition(proxies, proxy => !policyGroup.has(proxy.type))
|
||||
|
||||
const providers = Object.keys(proxyProviders.data.providers)
|
||||
.map<API.Provider>(name => proxyProviders.data.providers[name])
|
||||
.filter(pd => pd.name !== 'default')
|
||||
.filter(pd => pd.vehicleType !== 'Compatible')
|
||||
|
||||
const proxyMap = new Map<string, API.Proxy>()
|
||||
for (const p of proxy) {
|
||||
proxyMap.set(p.name, p as API.Proxy)
|
||||
}
|
||||
|
||||
for (const provider of providers) {
|
||||
for (const p of provider.proxies) {
|
||||
proxyMap.set(p.name, p as API.Proxy)
|
||||
}
|
||||
}
|
||||
|
||||
set({
|
||||
proxy: proxy as API.Proxy[],
|
||||
proxyGroup: general.mode === 'Global' ? [proxyList] : groups as API.Group[],
|
||||
proxyProviders: providers,
|
||||
rules: rules.data.rules,
|
||||
proxyMap
|
||||
})
|
||||
|
||||
const [version, vErr] = await to(API.getVersion())
|
||||
if (vErr) {
|
||||
return
|
||||
}
|
||||
|
||||
set('version', version.data.version)
|
||||
}
|
||||
|
||||
function updateDelay (proxy: string, delay: number) {
|
||||
set(draft => {
|
||||
const p = draft.proxy.find(p => p.name === proxy)
|
||||
if (p) {
|
||||
p.history.push({ time: Date.now().toString(), delay })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return { data, fetch, unauthorized: { visible, show, hide }, updateDelay }
|
||||
}
|
||||
|
||||
function useAPIInfo () {
|
||||
const [data, set] = useObject<Models.APIInfo>({
|
||||
hostname: '127.0.0.1',
|
||||
port: '9090',
|
||||
secret: ''
|
||||
})
|
||||
|
||||
async function fetch () {
|
||||
const info = await API.getExternalControllerConfig()
|
||||
set({ ...info })
|
||||
}
|
||||
|
||||
async function update (info: Models.APIInfo) {
|
||||
const { hostname, port, secret } = info
|
||||
setLocalStorageItem('externalControllerAddr', hostname)
|
||||
setLocalStorageItem('externalControllerPort', port)
|
||||
setLocalStorageItem('secret', secret)
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
return { data, fetch, update }
|
||||
}
|
||||
|
||||
function useConfig () {
|
||||
const [data, set] = useObject({
|
||||
breakConnections: false
|
||||
})
|
||||
|
||||
return { data, set }
|
||||
}
|
||||
|
||||
function useClashXData () {
|
||||
const [data, set] = useObject<Models.ClashXData>({
|
||||
startAtLogin: false,
|
||||
systemProxy: false
|
||||
})
|
||||
|
||||
async function fetch () {
|
||||
const startAtLogin = await jsBridge.getStartAtLogin()
|
||||
const systemProxy = await jsBridge.isSystemProxySet()
|
||||
|
||||
set({ startAtLogin, systemProxy })
|
||||
}
|
||||
|
||||
return { data, fetch }
|
||||
}
|
||||
|
||||
const { Provider, containers } = composeContainer({
|
||||
useData,
|
||||
useAPIInfo,
|
||||
useClashXData,
|
||||
useI18n,
|
||||
useConfig
|
||||
})
|
||||
|
||||
export { Provider, containers }
|
@ -1 +1 @@
|
||||
export * from './HookStore'
|
||||
export * from './recoil'
|
||||
|
271
src/stores/recoil.ts
Normal file
271
src/stores/recoil.ts
Normal file
@ -0,0 +1,271 @@
|
||||
import { atom, useRecoilState, selector } from 'recoil'
|
||||
import get from 'lodash/get'
|
||||
import { useCallback } from 'react'
|
||||
import { AxiosError } from 'axios'
|
||||
|
||||
import { getLanguage, setLanguage, Lang, locales, Language } from '@i18n'
|
||||
import { useRecoilObjectWithImmer } from '@lib/recoil'
|
||||
import * as API from '@lib/request'
|
||||
import { setLocalStorageItem, partition, to } from '@lib/helper'
|
||||
import { jsBridge } from '@lib/jsBridge'
|
||||
import * as Models from '@models'
|
||||
|
||||
const identity = atom({
|
||||
key: 'identity',
|
||||
default: true
|
||||
})
|
||||
|
||||
type AsyncFunction<A, O> = (...args: A[]) => Promise<O>
|
||||
|
||||
export function useIdentity () {
|
||||
const [id, set] = useRecoilState(identity)
|
||||
|
||||
function wrapFetcher<A, O> (fn: AsyncFunction<A, O>) {
|
||||
return async function (...args: A[]) {
|
||||
const [resp, err] = await to(fn(...args))
|
||||
const rErr = err as AxiosError
|
||||
if (rErr && (!rErr.response || rErr.response.status === 401)) {
|
||||
set(false)
|
||||
throw err
|
||||
}
|
||||
set(true)
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
return { identity: id, wrapFetcher, set }
|
||||
}
|
||||
|
||||
const language = atom({
|
||||
key: 'i18n',
|
||||
default: getLanguage()
|
||||
})
|
||||
|
||||
export function useI18n () {
|
||||
const [lang, set] = useRecoilState(language)
|
||||
|
||||
function setLang (lang: Lang) {
|
||||
set(lang)
|
||||
setLanguage(lang)
|
||||
}
|
||||
|
||||
const useTranslation = useCallback(
|
||||
function (namespace: string) {
|
||||
function t (path: string) {
|
||||
return get(Language[lang][namespace], path) as string
|
||||
}
|
||||
return { t }
|
||||
},
|
||||
[lang]
|
||||
)
|
||||
|
||||
return { lang, locales, setLang, useTranslation }
|
||||
}
|
||||
|
||||
export const config = atom({
|
||||
key: 'config',
|
||||
default: {
|
||||
breakConnections: false
|
||||
}
|
||||
})
|
||||
|
||||
export function useConfig () {
|
||||
const [data, set] = useRecoilObjectWithImmer(config)
|
||||
|
||||
return { data, set }
|
||||
}
|
||||
|
||||
export const proxyProvider = atom({
|
||||
key: 'proxyProvider',
|
||||
default: [] as API.Provider[]
|
||||
})
|
||||
|
||||
export function useProxyProviders () {
|
||||
const [data, set] = useRecoilState(proxyProvider)
|
||||
|
||||
async function update () {
|
||||
const proxyProviders = await API.getProxyProviders()
|
||||
|
||||
const providers = Object.keys(proxyProviders.data.providers)
|
||||
.map<API.Provider>(name => proxyProviders.data.providers[name])
|
||||
.filter(pd => pd.name !== 'default')
|
||||
.filter(pd => pd.vehicleType !== 'Compatible')
|
||||
|
||||
set(providers)
|
||||
}
|
||||
|
||||
return { providers: data, update }
|
||||
}
|
||||
|
||||
export const general = atom({
|
||||
key: 'general',
|
||||
default: {} as Models.Data['general']
|
||||
})
|
||||
|
||||
export function useGeneral () {
|
||||
const [data, set] = useRecoilState(general)
|
||||
|
||||
async function update () {
|
||||
const resp = await API.getConfig()
|
||||
const data = resp.data
|
||||
set({
|
||||
port: data.port,
|
||||
socksPort: data['socks-port'],
|
||||
redirPort: data['redir-port'],
|
||||
mode: data.mode,
|
||||
logLevel: data['log-level'],
|
||||
allowLan: data['allow-lan']
|
||||
})
|
||||
}
|
||||
|
||||
return { general: data, update }
|
||||
}
|
||||
|
||||
export const proxies = atom({
|
||||
key: 'proxies',
|
||||
default: {
|
||||
proxies: [] as API.Proxy[],
|
||||
groups: [] as API.Group[],
|
||||
global: {} as API.Group
|
||||
}
|
||||
})
|
||||
|
||||
export function useProxy () {
|
||||
const [data, set] = useRecoilObjectWithImmer(proxies)
|
||||
|
||||
async function update () {
|
||||
const allProxies = await API.getProxies()
|
||||
|
||||
const global = allProxies.data.proxies.GLOBAL as API.Group
|
||||
// fix missing name
|
||||
global.name = 'GLOBAL'
|
||||
|
||||
const policyGroup = new Set(['Selector', 'URLTest', 'Fallback', 'LoadBalance'])
|
||||
const unUsedProxy = new Set(['DIRECT', 'REJECT', 'GLOBAL'])
|
||||
const proxies = global.all
|
||||
.filter(key => !unUsedProxy.has(key))
|
||||
.map(key => ({ ...allProxies.data.proxies[key], name: key }))
|
||||
const [proxy, groups] = partition(proxies, proxy => !policyGroup.has(proxy.type))
|
||||
|
||||
set({ proxies: proxy as API.Proxy[], groups: groups as API.Group[], global: global })
|
||||
}
|
||||
|
||||
return {
|
||||
proxies: data.proxies,
|
||||
groups: data.groups,
|
||||
global: data.global,
|
||||
update,
|
||||
set
|
||||
}
|
||||
}
|
||||
|
||||
export const proxyMapping = selector({
|
||||
key: 'proxyMapping',
|
||||
get: ({ get }) => {
|
||||
const ps = get(proxies)
|
||||
const providers = get(proxyProvider)
|
||||
const proxyMap = new Map<string, API.Proxy>()
|
||||
for (const p of ps.proxies) {
|
||||
proxyMap.set(p.name, p as API.Proxy)
|
||||
}
|
||||
|
||||
for (const provider of providers) {
|
||||
for (const p of provider.proxies) {
|
||||
proxyMap.set(p.name, p as API.Proxy)
|
||||
}
|
||||
}
|
||||
|
||||
return proxyMap
|
||||
}
|
||||
})
|
||||
|
||||
export const version = atom({
|
||||
key: 'version',
|
||||
default: {
|
||||
version: '',
|
||||
premium: false
|
||||
}
|
||||
})
|
||||
|
||||
export function useVersion () {
|
||||
const [data, set] = useRecoilState(version)
|
||||
const { set: setIdentity } = useIdentity()
|
||||
|
||||
async function update () {
|
||||
const [resp, err] = await to(API.getVersion())
|
||||
setIdentity(!err)
|
||||
|
||||
set(
|
||||
err
|
||||
? { version: '', premium: false }
|
||||
: { version: resp.data.version, premium: !!resp.data.premium }
|
||||
)
|
||||
}
|
||||
|
||||
return { version: data.version, premium: data.premium, update }
|
||||
}
|
||||
|
||||
export const clashxData = atom({
|
||||
key: 'clashxData',
|
||||
default: {
|
||||
startAtLogin: false,
|
||||
systemProxy: false
|
||||
}
|
||||
})
|
||||
|
||||
export function useClashXData () {
|
||||
const [data, set] = useRecoilState(clashxData)
|
||||
|
||||
async function update () {
|
||||
const startAtLogin = await jsBridge.getStartAtLogin()
|
||||
const systemProxy = await jsBridge.isSystemProxySet()
|
||||
|
||||
set({ startAtLogin, systemProxy })
|
||||
}
|
||||
|
||||
return { data, update }
|
||||
}
|
||||
|
||||
export const apiData = atom({
|
||||
key: 'apiData',
|
||||
default: {
|
||||
hostname: '127.0.0.1',
|
||||
port: '9090',
|
||||
secret: ''
|
||||
}
|
||||
})
|
||||
|
||||
export function useAPIInfo () {
|
||||
const [data, set] = useRecoilState(apiData)
|
||||
|
||||
async function fetch () {
|
||||
const info = await API.getExternalControllerConfig()
|
||||
set({ ...info })
|
||||
}
|
||||
|
||||
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 = atom({
|
||||
key: 'rules',
|
||||
default: [] as API.Rule[]
|
||||
})
|
||||
|
||||
export function useRule () {
|
||||
const [data, set] = useRecoilObjectWithImmer(rules)
|
||||
|
||||
async function update () {
|
||||
const resp = await API.getRules()
|
||||
set(resp.data.rules)
|
||||
}
|
||||
|
||||
return { rules: data, update }
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user