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": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.8.4",
|
"@babel/cli": "^7.8.4",
|
||||||
"@babel/core": "^7.9.0",
|
"@babel/core": "^7.9.6",
|
||||||
"@babel/preset-env": "^7.9.5",
|
"@babel/preset-env": "^7.9.6",
|
||||||
"@babel/preset-react": "^7.9.4",
|
"@babel/preset-react": "^7.9.4",
|
||||||
"@hot-loader/react-dom": "^16.13.0",
|
"@hot-loader/react-dom": "^16.13.0",
|
||||||
"@types/classnames": "^2.2.10",
|
"@types/classnames": "^2.2.10",
|
||||||
"@types/lodash-es": "^4.17.3",
|
"@types/lodash-es": "^4.17.3",
|
||||||
"@types/node": "^13.13.2",
|
"@types/node": "^14.0.6",
|
||||||
"@types/react": "^16.9.34",
|
"@types/react": "^16.9.35",
|
||||||
"@types/react-dom": "^16.9.6",
|
"@types/react-dom": "^16.9.8",
|
||||||
"@types/react-router-dom": "^5.1.4",
|
"@types/react-router-dom": "^5.1.5",
|
||||||
"@types/react-table": "^7.0.16",
|
"@types/react-table": "^7.0.18",
|
||||||
"@types/react-virtualized-auto-sizer": "^1.0.0",
|
"@types/react-virtualized-auto-sizer": "^1.0.0",
|
||||||
"@types/react-window": "^1.8.2",
|
"@types/react-window": "^1.8.2",
|
||||||
|
"@types/recoil": "0.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.29.0",
|
"@typescript-eslint/eslint-plugin": "^2.29.0",
|
||||||
"@typescript-eslint/parser": "^2.29.0",
|
"@typescript-eslint/parser": "^2.29.0",
|
||||||
"autoprefixer": "^9.7.6",
|
"autoprefixer": "^9.8.0",
|
||||||
"awesome-typescript-loader": "^5.2.1",
|
"awesome-typescript-loader": "^5.2.1",
|
||||||
"babel-loader": "^8.1.0",
|
"babel-loader": "^8.1.0",
|
||||||
"babel-preset-minify": "^0.5.1",
|
"babel-preset-minify": "^0.5.1",
|
||||||
"css-loader": "^3.5.3",
|
"css-loader": "^3.5.3",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^6.8.0",
|
||||||
"eslint-config-standard": "^14.1.1",
|
"eslint-config-standard": "^14.1.1",
|
||||||
"eslint-loader": "^4.0.0",
|
"eslint-loader": "^4.0.2",
|
||||||
"eslint-plugin-import": "^2.20.2",
|
"eslint-plugin-import": "^2.20.2",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-promise": "^4.2.1",
|
"eslint-plugin-promise": "^4.2.1",
|
||||||
"eslint-plugin-react": "^7.19.0",
|
"eslint-plugin-react": "^7.20.0",
|
||||||
"eslint-plugin-standard": "^4.0.1",
|
"eslint-plugin-standard": "^4.0.1",
|
||||||
"file-loader": "^6.0.0",
|
"file-loader": "^6.0.0",
|
||||||
"html-webpack-plugin": "^4.2.0",
|
"html-webpack-plugin": "^4.3.0",
|
||||||
"image-webpack-loader": "^6.0.0",
|
"image-webpack-loader": "^6.0.0",
|
||||||
"mini-css-extract-plugin": "^0.9.0",
|
"mini-css-extract-plugin": "^0.9.0",
|
||||||
"offline-plugin": "^5.0.7",
|
"offline-plugin": "^5.0.7",
|
||||||
"postcss-loader": "^3.0.0",
|
"postcss-loader": "^3.0.0",
|
||||||
"react-hot-loader": "^4.12.20",
|
"react-hot-loader": "^4.12.21",
|
||||||
"sass": "^1.26.5",
|
"sass": "^1.26.7",
|
||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^8.0.2",
|
||||||
"style-loader": "^1.2.0",
|
"style-loader": "^1.2.1",
|
||||||
"stylelint": "^13.3.3",
|
"stylelint": "^13.5.0",
|
||||||
"stylelint-config-standard": "^20.0.0",
|
"stylelint-config-standard": "^20.0.0",
|
||||||
"stylelint-webpack-plugin": "^1.2.3",
|
"stylelint-webpack-plugin": "^1.2.3",
|
||||||
"terser-webpack-plugin": "^2.3.5",
|
"terser-webpack-plugin": "^2.3.5",
|
||||||
"typescript": "^3.8.3",
|
"typescript": "^3.9.3",
|
||||||
"webpack": "^4.43.0",
|
"webpack": "^4.43.0",
|
||||||
"webpack-cli": "^3.3.11",
|
"webpack-cli": "^3.3.11",
|
||||||
"webpack-dev-middleware": "^3.7.2",
|
"webpack-dev-middleware": "^3.7.2",
|
||||||
"webpack-dev-server": "^3.10.3",
|
"webpack-dev-server": "^3.11.0",
|
||||||
"webpack-merge": "^4.2.2",
|
"webpack-merge": "^4.2.2",
|
||||||
"webpack-pwa-manifest": "^4.2.0"
|
"webpack-pwa-manifest": "^4.2.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.19.2",
|
"axios": "^0.19.2",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"dayjs": "^1.8.25",
|
"dayjs": "^1.8.28",
|
||||||
"eventemitter3": "^4.0.0",
|
"eventemitter3": "^4.0.4",
|
||||||
"immer": "^6.0.3",
|
"immer": "^6.0.9",
|
||||||
"lodash-es": "^4.17.15",
|
"lodash-es": "^4.17.15",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-table": "^7.0.4",
|
"react-table": "^7.1.0",
|
||||||
"react-virtualized-auto-sizer": "^1.0.2",
|
"react-virtualized-auto-sizer": "^1.0.2",
|
||||||
"react-window": "^1.8.5",
|
"react-window": "^1.8.5",
|
||||||
"swr": "^0.2.0",
|
"recoil": "0.0.7",
|
||||||
"unstated-next": "^1.1.0",
|
"swr": "^0.2.2",
|
||||||
"use-immer": "^0.4.0"
|
"use-immer": "^0.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useRef, useLayoutEffect } from 'react'
|
import React, { useState, useRef, useLayoutEffect } from 'react'
|
||||||
import { containers } from '@stores'
|
import { useI18n } from '@stores'
|
||||||
import { BaseComponentProps } from '@models'
|
import { BaseComponentProps } from '@models'
|
||||||
import { noop } from '@lib/helper'
|
import { noop } from '@lib/helper'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
@ -8,16 +8,16 @@ import './style.scss'
|
|||||||
interface TagsProps extends BaseComponentProps {
|
interface TagsProps extends BaseComponentProps {
|
||||||
data: string[]
|
data: string[]
|
||||||
onClick: (name: string) => void
|
onClick: (name: string) => void
|
||||||
shouldError?: (name: string) => boolean
|
errSet?: Set<string>
|
||||||
select: string
|
select: string
|
||||||
rowHeight: number
|
rowHeight: number
|
||||||
canClick: boolean
|
canClick: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Tags (props: TagsProps) {
|
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 { t } = useTranslation('Proxies')
|
||||||
const [expand, setExpand] = useState(false)
|
const [expand, setExpand] = useState(false)
|
||||||
const [showExtend, setShowExtend] = useState(false)
|
const [showExtend, setShowExtend] = useState(false)
|
||||||
@ -36,7 +36,7 @@ export function Tags (props: TagsProps) {
|
|||||||
|
|
||||||
const tags = data
|
const tags = data
|
||||||
.map(t => {
|
.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 (
|
return (
|
||||||
<li className={tagClass} key={t} onClick={() => handleClick(t)}>
|
<li className={tagClass} key={t} onClick={() => handleClick(t)}>
|
||||||
{ t }
|
{ t }
|
||||||
|
@ -2,7 +2,7 @@ import React, { useMemo, useLayoutEffect } from 'react'
|
|||||||
import { useBlockLayout, useResizeColumns, useTable } from 'react-table'
|
import { useBlockLayout, useResizeColumns, useTable } from 'react-table'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import { Header, Card, Checkbox, Modal, Icon } from '@components'
|
import { Header, Card, Checkbox, Modal, Icon } from '@components'
|
||||||
import { containers } from '@stores'
|
import { useI18n } from '@stores'
|
||||||
import * as API from '@lib/request'
|
import * as API from '@lib/request'
|
||||||
import { StreamReader } from '@lib/streamer'
|
import { StreamReader } from '@lib/streamer'
|
||||||
import { useObject, useVisible } from '@lib/hook'
|
import { useObject, useVisible } from '@lib/hook'
|
||||||
@ -62,7 +62,7 @@ function formatSpeed (upload: number, download: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Connections () {
|
export default function Connections () {
|
||||||
const { useTranslation, lang } = containers.useI18n()
|
const { useTranslation, lang } = useI18n()
|
||||||
const t = useMemo(() => useTranslation('Connections').t, [useTranslation])
|
const t = useMemo(() => useTranslation('Connections').t, [useTranslation])
|
||||||
|
|
||||||
// total
|
// total
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { useObject } from '@lib/hook'
|
import { useObject } from '@lib/hook'
|
||||||
import { Modal, Input, Row, Col, Alert } from '@components'
|
import { Modal, Input, Row, Col, Alert } from '@components'
|
||||||
import { containers } from '@stores'
|
import { useI18n, useAPIInfo, useIdentity } from '@stores'
|
||||||
import './style.scss'
|
import './style.scss'
|
||||||
|
|
||||||
export default function ExternalController () {
|
export default function ExternalController () {
|
||||||
const { useTranslation } = containers.useI18n()
|
const { useTranslation } = useI18n()
|
||||||
const { t } = useTranslation('Settings')
|
const { t } = useTranslation('Settings')
|
||||||
const { data: info, update, fetch } = containers.useAPIInfo()
|
const { data: info, update, fetch } = useAPIInfo()
|
||||||
const { unauthorized: { hide, visible } } = containers.useData()
|
const { identity, set: setIdentity } = useIdentity()
|
||||||
const [value, set] = useObject({
|
const [value, set] = useObject({
|
||||||
hostname: '',
|
hostname: '',
|
||||||
port: '',
|
port: '',
|
||||||
@ -30,10 +30,10 @@ export default function ExternalController () {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
show={visible}
|
show={!identity}
|
||||||
title={t('externalControllerSetting.title')}
|
title={t('externalControllerSetting.title')}
|
||||||
bodyClassName="external-controller"
|
bodyClassName="external-controller"
|
||||||
onClose={hide}
|
onClose={() => setIdentity(true)}
|
||||||
onOk={handleOk}
|
onOk={handleOk}
|
||||||
>
|
>
|
||||||
<Alert type="info" inside={true}>
|
<Alert type="info" inside={true}>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useLayoutEffect, useEffect, useRef, useState } from 'react'
|
import React, { useLayoutEffect, useEffect, useRef, useState } from 'react'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { containers } from '@stores'
|
import { useI18n } from '@stores'
|
||||||
import { Card, Header } from '@components'
|
import { Card, Header } from '@components'
|
||||||
import { getLogsStreamReader } from '@lib/request'
|
import { getLogsStreamReader } from '@lib/request'
|
||||||
import { StreamReader } from '@lib/streamer'
|
import { StreamReader } from '@lib/streamer'
|
||||||
@ -11,7 +11,7 @@ export default function Logs () {
|
|||||||
const listRef = useRef<HTMLUListElement>()
|
const listRef = useRef<HTMLUListElement>()
|
||||||
const logsRef = useRef<Log[]>([])
|
const logsRef = useRef<Log[]>([])
|
||||||
const [logs, setLogs] = useState<Log[]>([])
|
const [logs, setLogs] = useState<Log[]>([])
|
||||||
const { useTranslation } = containers.useI18n()
|
const { useTranslation } = useI18n()
|
||||||
const { t } = useTranslation('Logs')
|
const { t } = useTranslation('Logs')
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import * as React from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { containers } from '@stores'
|
import { useRecoilValue } from 'recoil'
|
||||||
|
import { useProxy, useConfig, proxyMapping } from '@stores'
|
||||||
import { changeProxySelected, Group as IGroup, getConnections, closeConnection } from '@lib/request'
|
import { changeProxySelected, Group as IGroup, getConnections, closeConnection } from '@lib/request'
|
||||||
import { Tags, Tag } from '@components'
|
import { Tags, Tag } from '@components'
|
||||||
import './style.scss'
|
import './style.scss'
|
||||||
@ -9,13 +10,14 @@ interface GroupProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Group (props: GroupProps) {
|
export function Group (props: GroupProps) {
|
||||||
const { fetch, data: Data } = containers.useData()
|
const { update } = useProxy()
|
||||||
const { data: Config } = containers.useConfig()
|
const proxyMap = useRecoilValue(proxyMapping)
|
||||||
|
const { data: Config } = useConfig()
|
||||||
const { config } = props
|
const { config } = props
|
||||||
|
|
||||||
async function handleChangeProxySelected (name: string) {
|
async function handleChangeProxySelected (name: string) {
|
||||||
await changeProxySelected(props.config.name, name)
|
await changeProxySelected(props.config.name, name)
|
||||||
await fetch()
|
await update()
|
||||||
if (Config.breakConnections) {
|
if (Config.breakConnections) {
|
||||||
const list: string[] = []
|
const list: string[] = []
|
||||||
const snapshot = await getConnections()
|
const snapshot = await getConnections()
|
||||||
@ -31,14 +33,17 @@ export function Group (props: GroupProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldError (name: string) {
|
const errSet = useMemo(() => {
|
||||||
const history = Data.proxyMap.get(name)?.history
|
const set = new Set<string>()
|
||||||
if (history?.length) {
|
for (const proxy of config.all) {
|
||||||
return !history.slice(-1)[0].delay
|
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'
|
const canClick = config.type === 'Selector'
|
||||||
return (
|
return (
|
||||||
@ -52,7 +57,7 @@ export function Group (props: GroupProps) {
|
|||||||
className="proxy-group-tags"
|
className="proxy-group-tags"
|
||||||
data={config.all}
|
data={config.all}
|
||||||
onClick={handleChangeProxySelected}
|
onClick={handleChangeProxySelected}
|
||||||
shouldError={shouldError}
|
errSet={errSet}
|
||||||
select={config.now}
|
select={config.now}
|
||||||
canClick={canClick}
|
canClick={canClick}
|
||||||
rowHeight={30} />
|
rowHeight={30} />
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { Card, Tag, Icon, Loading } from '@components'
|
import { Card, Tag, Icon, Loading } from '@components'
|
||||||
import { containers } from '@stores'
|
import { useI18n, useProxyProviders } from '@stores'
|
||||||
import { fromNow } from '@lib/date'
|
import { fromNow } from '@lib/date'
|
||||||
import { Provider as IProvider, Proxy as IProxy, updateProvider, healthCheckProvider } from '@lib/request'
|
import { Provider as IProvider, Proxy as IProxy, updateProvider, healthCheckProvider } from '@lib/request'
|
||||||
import { useVisible } from '@lib/hook'
|
import { useVisible } from '@lib/hook'
|
||||||
@ -14,22 +14,22 @@ interface ProvidersProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Provider (props: ProvidersProps) {
|
export function Provider (props: ProvidersProps) {
|
||||||
const { fetch } = containers.useData()
|
const { update } = useProxyProviders()
|
||||||
const { useTranslation, lang } = containers.useI18n()
|
const { useTranslation, lang } = useI18n()
|
||||||
const { provider } = props
|
|
||||||
|
|
||||||
|
const { provider } = props
|
||||||
const { t } = useTranslation('Proxies')
|
const { t } = useTranslation('Proxies')
|
||||||
|
|
||||||
const { visible, hide, show } = useVisible()
|
const { visible, hide, show } = useVisible()
|
||||||
|
|
||||||
function handleHealthChech () {
|
function handleHealthChech () {
|
||||||
show()
|
show()
|
||||||
healthCheckProvider(provider.name).then(() => fetch()).finally(() => hide())
|
healthCheckProvider(provider.name).then(() => update()).finally(() => hide())
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleUpdate () {
|
function handleUpdate () {
|
||||||
show()
|
show()
|
||||||
updateProvider(provider.name).then(() => fetch()).finally(() => hide())
|
updateProvider(provider.name).then(() => update()).finally(() => hide())
|
||||||
}
|
}
|
||||||
|
|
||||||
const proxies = useMemo(() => {
|
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 classnames from 'classnames'
|
||||||
import { BaseComponentProps } from '@models'
|
import { BaseComponentProps } from '@models'
|
||||||
import { containers } from '@stores'
|
import { useProxy } from '@stores'
|
||||||
import { getProxyDelay, Proxy as IProxy } from '@lib/request'
|
import { getProxyDelay, Proxy as IProxy } from '@lib/request'
|
||||||
import EE, { Action } from '@lib/event'
|
import EE, { Action } from '@lib/event'
|
||||||
import { isClashX, jsBridge } from '@lib/jsBridge'
|
import { isClashX, jsBridge } from '@lib/jsBridge'
|
||||||
@ -14,8 +14,8 @@ interface ProxyProps extends BaseComponentProps {
|
|||||||
|
|
||||||
const TagColors = {
|
const TagColors = {
|
||||||
'#909399': 0,
|
'#909399': 0,
|
||||||
'#00c520': 150,
|
'#00c520': 260,
|
||||||
'#ff9a28': 500,
|
'#ff9a28': 600,
|
||||||
'#ff3e5e': Infinity
|
'#ff3e5e': Infinity
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,20 +31,24 @@ async function getDelay (name: string) {
|
|||||||
|
|
||||||
export function Proxy (props: ProxyProps) {
|
export function Proxy (props: ProxyProps) {
|
||||||
const { config, className } = props
|
const { config, className } = props
|
||||||
const [delay, setDelay] = useState(0)
|
const { set } = useProxy()
|
||||||
const { updateDelay } = containers.useData()
|
|
||||||
|
|
||||||
async function speedTest () {
|
async function speedTest () {
|
||||||
const [delay, err] = await to(getDelay(config.name))
|
const [delay, err] = await to(getDelay(config.name))
|
||||||
|
|
||||||
const validDelay = err ? 0 : delay
|
const validDelay = err ? 0 : delay
|
||||||
setDelay(validDelay)
|
set(draft => {
|
||||||
updateDelay(config.name, validDelay)
|
const proxy = draft.proxies.find(p => p.name === proxy)
|
||||||
|
if (proxy) {
|
||||||
|
proxy.history.push({ time: Date.now().toString(), delay: validDelay })
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const delay = useMemo(
|
||||||
setDelay(config.history && config.history.length ? config.history.slice(-1)[0].delay : 0)
|
() => config.history?.length ? config.history.slice(-1)[0].delay : 0,
|
||||||
}, [config])
|
[config]
|
||||||
|
)
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
EE.subscribe(Action.SPEED_NOTIFY, speedTest)
|
EE.subscribe(Action.SPEED_NOTIFY, speedTest)
|
||||||
|
@ -3,7 +3,7 @@ import useSWR from 'swr'
|
|||||||
import EE from '@lib/event'
|
import EE from '@lib/event'
|
||||||
import { useRound } from '@lib/hook'
|
import { useRound } from '@lib/hook'
|
||||||
import { Card, Header, Icon, Checkbox } from '@components'
|
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 * as API from '@lib/request'
|
||||||
|
|
||||||
import { Proxy, Group, Provider } from './components'
|
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)
|
return (lastDelayB || Number.MAX_SAFE_INTEGER) - (lastDelayA || Number.MAX_SAFE_INTEGER)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Proxies () {
|
function ProxyGroups () {
|
||||||
const { data, fetch } = containers.useData()
|
const { groups } = useProxy()
|
||||||
const { useTranslation } = containers.useI18n()
|
const { data: config, set: setConfig } = useConfig()
|
||||||
const { data: config, set: setConfig } = containers.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')
|
const { t } = useTranslation('Proxies')
|
||||||
useSWR('data', fetch)
|
|
||||||
|
|
||||||
function handleNotitySpeedTest () {
|
function handleNotitySpeedTest () {
|
||||||
EE.notifySpeedTest()
|
EE.notifySpeedTest()
|
||||||
@ -41,78 +97,53 @@ export default function Proxies () {
|
|||||||
const { current: sort, next } = useRound(
|
const { current: sort, next } = useRound(
|
||||||
[sortType.Asc, sortType.Desc, sortType.None]
|
[sortType.Asc, sortType.Desc, sortType.None]
|
||||||
)
|
)
|
||||||
const proxies = useMemo(() => {
|
const sortedProxies = useMemo(() => {
|
||||||
switch (sort) {
|
switch (sort) {
|
||||||
case sortType.Desc:
|
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:
|
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:
|
default:
|
||||||
return data.proxy.slice()
|
return proxies.slice()
|
||||||
}
|
}
|
||||||
}, [sort, data])
|
}, [sort, proxies])
|
||||||
const handleSort = next
|
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 (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
{
|
<ProxyGroups />
|
||||||
data.proxyGroup.length !== 0 &&
|
<ProxyProviders />
|
||||||
<div className="proxies-container">
|
<Proxies />
|
||||||
<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>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
import React, { useEffect } from 'react'
|
import React from 'react'
|
||||||
import { Header, Card, Row, Col } from '@components'
|
import { Header, Card, Row, Col } from '@components'
|
||||||
import { containers } from '@stores'
|
import { useI18n, useRule } from '@stores'
|
||||||
import { FixedSizeList as List } from 'react-window'
|
import { FixedSizeList as List } from 'react-window'
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer'
|
import AutoSizer from 'react-virtualized-auto-sizer'
|
||||||
|
import useSWR from 'swr'
|
||||||
import './style.scss'
|
import './style.scss'
|
||||||
|
|
||||||
export default function Rules () {
|
export default function Rules () {
|
||||||
const { data, fetch } = containers.useData()
|
const { rules, update } = useRule()
|
||||||
const { useTranslation } = containers.useI18n()
|
const { useTranslation } = useI18n()
|
||||||
const { t } = useTranslation('Rules')
|
const { t } = useTranslation('Rules')
|
||||||
const { rules } = data
|
|
||||||
|
|
||||||
useEffect(() => {
|
useSWR('rules', update)
|
||||||
fetch()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
function renderRuleItem ({ index, style }: { index: number, style: React.CSSProperties }) {
|
function renderRuleItem ({ index, style }: { index: number, style: React.CSSProperties }) {
|
||||||
const rule = rules[index]
|
const rule = rules[index]
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { Header, Card, Row, Col, Switch, ButtonSelect, ButtonSelectOptions, Input, Icon } from '@components'
|
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 { updateConfig } from '@lib/request'
|
||||||
import { useObject } from '@lib/hook'
|
import { useObject } from '@lib/hook'
|
||||||
import { to } from '@lib/helper'
|
|
||||||
import { isClashX, jsBridge } from '@lib/jsBridge'
|
import { isClashX, jsBridge } from '@lib/jsBridge'
|
||||||
import { Lang } from '@i18n'
|
import { Lang } from '@i18n'
|
||||||
import './style.scss'
|
import './style.scss'
|
||||||
@ -11,10 +10,11 @@ import './style.scss'
|
|||||||
const languageOptions: ButtonSelectOptions[] = [{ label: '中文', value: 'zh_CN' }, { label: 'English', value: 'en_US' }]
|
const languageOptions: ButtonSelectOptions[] = [{ label: '中文', value: 'zh_CN' }, { label: 'English', value: 'en_US' }]
|
||||||
|
|
||||||
export default function Settings () {
|
export default function Settings () {
|
||||||
const { data: clashXData, fetch: fetchClashXData } = containers.useClashXData()
|
const { data: clashXData, update: fetchClashXData } = useClashXData()
|
||||||
const { data, fetch, unauthorized: { show } } = containers.useData()
|
const { general, update: fetchGeneral } = useGeneral()
|
||||||
const { data: apiInfo } = containers.useAPIInfo()
|
const { set: setIdentity } = useIdentity()
|
||||||
const { useTranslation, setLang, lang } = containers.useI18n()
|
const { data: apiInfo } = useAPIInfo()
|
||||||
|
const { useTranslation, setLang, lang } = useI18n()
|
||||||
const { t } = useTranslation('Settings')
|
const { t } = useTranslation('Settings')
|
||||||
const [info, set] = useObject({
|
const [info, set] = useObject({
|
||||||
socks5ProxyPort: 7891,
|
socks5ProxyPort: 7891,
|
||||||
@ -23,22 +23,20 @@ export default function Settings () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch()
|
fetchGeneral()
|
||||||
if (isClashX()) {
|
if (isClashX()) {
|
||||||
fetchClashXData().then(() => set('isClashX', true))
|
fetchClashXData().then(() => set('isClashX', true))
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
set('socks5ProxyPort', data.general.socksPort)
|
set('socks5ProxyPort', general.socksPort)
|
||||||
set('httpProxyPort', data.general.port)
|
set('httpProxyPort', general.port)
|
||||||
}, [data])
|
}, [general])
|
||||||
|
|
||||||
async function handleProxyModeChange (mode: string) {
|
async function handleProxyModeChange (mode: string) {
|
||||||
const [, err] = await to(updateConfig({ mode }))
|
await updateConfig({ mode })
|
||||||
if (!err) {
|
await fetchGeneral()
|
||||||
fetch()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleStartAtLoginChange (state: boolean) {
|
async function handleStartAtLoginChange (state: boolean) {
|
||||||
@ -56,26 +54,18 @@ export default function Settings () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleHttpPortSave () {
|
async function handleHttpPortSave () {
|
||||||
const [, err] = await to(updateConfig({ port: info.httpProxyPort }))
|
await updateConfig({ port: info.httpProxyPort })
|
||||||
if (!err) {
|
await fetchGeneral()
|
||||||
await fetch()
|
|
||||||
set('httpProxyPort', data.general.port)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSocksPortSave () {
|
async function handleSocksPortSave () {
|
||||||
const [, err] = await to(updateConfig({ 'socks-port': info.socks5ProxyPort }))
|
await updateConfig({ 'socks-port': info.socks5ProxyPort })
|
||||||
if (!err) {
|
await fetchGeneral()
|
||||||
await fetch()
|
|
||||||
set('socks5ProxyPort', data.general.socksPort)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleAllowLanChange (state: boolean) {
|
async function handleAllowLanChange (state: boolean) {
|
||||||
const [, err] = await to(updateConfig({ 'allow-lan': state }))
|
await updateConfig({ 'allow-lan': state })
|
||||||
if (!err) {
|
await fetchGeneral()
|
||||||
await fetch()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -83,7 +73,7 @@ export default function Settings () {
|
|||||||
port: externalControllerPort
|
port: externalControllerPort
|
||||||
} = apiInfo
|
} = apiInfo
|
||||||
|
|
||||||
const { allowLan, mode } = data.general
|
const { allowLan, mode } = general
|
||||||
const {
|
const {
|
||||||
startAtLogin,
|
startAtLogin,
|
||||||
systemProxy
|
systemProxy
|
||||||
@ -189,7 +179,7 @@ export default function Settings () {
|
|||||||
<span className="label">{t('labels.externalController')}</span>
|
<span className="label">{t('labels.externalController')}</span>
|
||||||
</Col>
|
</Col>
|
||||||
<Col className="external-controller" span={10}>
|
<Col className="external-controller" span={10}>
|
||||||
<span className="modify-btn" onClick={show}>
|
<span className="modify-btn" onClick={() => setIdentity(false)}>
|
||||||
{`${externalControllerHost}:${externalControllerPort}`}
|
{`${externalControllerHost}:${externalControllerPort}`}
|
||||||
</span>
|
</span>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { NavLink } from 'react-router-dom'
|
import { NavLink } from 'react-router-dom'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import { containers } from '@stores'
|
import { useI18n, useVersion } from '@stores'
|
||||||
|
|
||||||
import './style.scss'
|
import './style.scss'
|
||||||
import logo from '@assets/logo.png'
|
import logo from '@assets/logo.png'
|
||||||
|
import useSWR from 'swr'
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
routes: {
|
routes: {
|
||||||
@ -17,9 +18,12 @@ interface SidebarProps {
|
|||||||
|
|
||||||
export default function Sidebar (props: SidebarProps) {
|
export default function Sidebar (props: SidebarProps) {
|
||||||
const { routes } = props
|
const { routes } = props
|
||||||
const { useTranslation } = containers.useI18n()
|
const { useTranslation } = useI18n()
|
||||||
|
const { version, premium, update } = useVersion()
|
||||||
const { t } = useTranslation('SideBar')
|
const { t } = useTranslation('SideBar')
|
||||||
|
|
||||||
|
useSWR('version', update)
|
||||||
|
|
||||||
const navlinks = routes.map(
|
const navlinks = routes.map(
|
||||||
({ path, name, exact, noMobile }) => (
|
({ path, name, exact, noMobile }) => (
|
||||||
<li className={classnames('item', { 'no-mobile': noMobile })} key={name}>
|
<li className={classnames('item', { 'no-mobile': noMobile })} key={name}>
|
||||||
@ -34,6 +38,11 @@ export default function Sidebar (props: SidebarProps) {
|
|||||||
<ul className="sidebar-menu">
|
<ul className="sidebar-menu">
|
||||||
{ navlinks }
|
{ navlinks }
|
||||||
</ul>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
.sidebar {
|
.sidebar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -21,6 +22,7 @@
|
|||||||
.sidebar-menu {
|
.sidebar-menu {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
|
|
||||||
.item {
|
.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) {
|
@media (max-width: 768px) {
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -5,7 +5,8 @@ export default {
|
|||||||
Logs: 'Logs',
|
Logs: 'Logs',
|
||||||
Rules: 'Rules',
|
Rules: 'Rules',
|
||||||
Settings: 'Setting',
|
Settings: 'Setting',
|
||||||
Connections: 'Connections'
|
Connections: 'Connections',
|
||||||
|
Version: 'Version'
|
||||||
},
|
},
|
||||||
Settings: {
|
Settings: {
|
||||||
title: 'Settings',
|
title: 'Settings',
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
/* eslint-disable @typescript-eslint/camelcase */
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
import { useState, useCallback } from 'react'
|
|
||||||
import get from 'lodash/get'
|
|
||||||
import { getLocalStorageItem, setLocalStorageItem } from '@lib/helper'
|
import { getLocalStorageItem, setLocalStorageItem } from '@lib/helper'
|
||||||
|
|
||||||
import en_US from './en_US'
|
import en_US from './en_US'
|
||||||
import zh_CN from './zh_CN'
|
import zh_CN from './zh_CN'
|
||||||
|
|
||||||
const Language = {
|
export const Language = {
|
||||||
en_US,
|
en_US,
|
||||||
zh_CN
|
zh_CN
|
||||||
}
|
}
|
||||||
@ -16,7 +14,7 @@ export type Lang = keyof typeof Language
|
|||||||
|
|
||||||
const languageKey = 'language'
|
const languageKey = 'language'
|
||||||
|
|
||||||
const locales = Object.keys(Language)
|
export const locales = Object.keys(Language)
|
||||||
|
|
||||||
function getNavigatorLanguage (): string[] {
|
function getNavigatorLanguage (): string[] {
|
||||||
const found: string[] = []
|
const found: string[] = []
|
||||||
@ -34,7 +32,7 @@ function getNavigatorLanguage (): string[] {
|
|||||||
return found
|
return found
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLanguage (): Lang {
|
export function getLanguage (): Lang {
|
||||||
const localLanguage = getLocalStorageItem(languageKey)
|
const localLanguage = getLocalStorageItem(languageKey)
|
||||||
if (localLanguage && locales.includes(localLanguage)) {
|
if (localLanguage && locales.includes(localLanguage)) {
|
||||||
return localLanguage as Lang
|
return localLanguage as Lang
|
||||||
@ -52,23 +50,6 @@ function getLanguage (): Lang {
|
|||||||
return 'en_US'
|
return 'en_US'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useI18n () {
|
export function setLanguage (lang: Lang) {
|
||||||
const [lang, set] = useState(getLanguage())
|
setLocalStorageItem(languageKey, lang)
|
||||||
|
|
||||||
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 }
|
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,8 @@ export default {
|
|||||||
Logs: '日志',
|
Logs: '日志',
|
||||||
Rules: '规则',
|
Rules: '规则',
|
||||||
Settings: '设置',
|
Settings: '设置',
|
||||||
Connections: '连接'
|
Connections: '连接',
|
||||||
|
Version: '版本'
|
||||||
},
|
},
|
||||||
Settings: {
|
Settings: {
|
||||||
title: '设置',
|
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[]] {
|
export function partition<T> (arr: T[], fn: (T) => boolean): [T[], T[]] {
|
||||||
const left: T[] = []
|
const left: T[] = []
|
||||||
const right: T[] = []
|
const right: T[] = []
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Draft } from 'immer'
|
import { Draft } from 'immer'
|
||||||
import { useImmer } from 'use-immer'
|
import { useImmer } from 'use-immer'
|
||||||
import { createContainer } from 'unstated-next'
|
|
||||||
import { useRef, useEffect, useState, useMemo } from 'react'
|
import { useRef, useEffect, useState, useMemo } from 'react'
|
||||||
|
|
||||||
import { noop } from '@lib/helper'
|
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) {
|
export function useRound<T> (list: T[], defidx = 0) {
|
||||||
if (list.length < 2) {
|
if (list.length < 2) {
|
||||||
throw new Error('List requires at least two elements')
|
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 axios from 'axios'
|
||||||
import { Partial, getLocalStorageItem, to } from '@lib/helper'
|
import { getLocalStorageItem, to } from '@lib/helper'
|
||||||
import { isClashX, jsBridge } from '@lib/jsBridge'
|
import { isClashX, jsBridge } from '@lib/jsBridge'
|
||||||
import { createAsyncSingleton } from '@lib/asyncSingleton'
|
import { createAsyncSingleton } from '@lib/asyncSingleton'
|
||||||
import { Log } from '@models/Log'
|
import { Log } from '@models/Log'
|
||||||
@ -181,7 +181,7 @@ export async function getProxy (name: string) {
|
|||||||
|
|
||||||
export async function getVersion () {
|
export async function getVersion () {
|
||||||
const req = await getInstance()
|
const req = await getInstance()
|
||||||
return req.get<{ version: string }>('version')
|
return req.get<{ version: string, premium?: boolean }>('version')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getProxyDelay (name: string) {
|
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 {
|
export interface Data {
|
||||||
version?: string
|
version?: string
|
||||||
|
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { render } from 'react-dom'
|
import { render } from 'react-dom'
|
||||||
import { HashRouter } from 'react-router-dom'
|
import { HashRouter } from 'react-router-dom'
|
||||||
import { Provider as Global } from '@stores'
|
import { RecoilRoot } from 'recoil'
|
||||||
import App from '@containers/App'
|
import App from '@containers/App'
|
||||||
|
|
||||||
export default function renderApp () {
|
export default function renderApp () {
|
||||||
const rootEl = document.getElementById('root')
|
const rootEl = document.getElementById('root')
|
||||||
const AppInstance = (
|
const AppInstance = (
|
||||||
<Global>
|
<RecoilRoot>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<App />
|
<App />
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</Global>
|
</RecoilRoot>
|
||||||
)
|
)
|
||||||
|
|
||||||
render(AppInstance, rootEl)
|
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