mirror of
https://github.com/woodchen-ink/clash-and-dashboard.git
synced 2025-07-18 14:01:56 +08:00
304 lines
7.5 KiB
TypeScript
304 lines
7.5 KiB
TypeScript
import { atom, useRecoilState, selector } from 'recoil'
|
|
import get from 'lodash/get'
|
|
import throttle from 'lodash/throttle'
|
|
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, isClashX } 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 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 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 ruleProvider = atom({
|
|
key: 'ruleProvider',
|
|
default: [] as API.RuleProvider[]
|
|
})
|
|
|
|
export function useRuleProviders () {
|
|
const [data, set] = useRecoilState(ruleProvider)
|
|
const [{ premium }] = useRecoilState(version)
|
|
|
|
async function update () {
|
|
if (!premium) {
|
|
return
|
|
}
|
|
|
|
const ruleProviders = await API.getRuleProviders()
|
|
|
|
const providers = Object.keys(ruleProviders.data.providers)
|
|
.map<API.RuleProvider>(name => ruleProviders.data.providers[name])
|
|
|
|
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'],
|
|
mixedPort: data['mixed-port'] ?? 0,
|
|
redirPort: data['redir-port'],
|
|
mode: data.mode.toLowerCase() as Models.Data['general']['mode'],
|
|
logLevel: data['log-level'],
|
|
allowLan: data['allow-lan']
|
|
})
|
|
}
|
|
|
|
return { general: data, update: throttle(update, 50) }
|
|
}
|
|
|
|
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 clashxData = atom({
|
|
key: 'clashxData',
|
|
default: {
|
|
isClashX: false,
|
|
startAtLogin: false,
|
|
systemProxy: false
|
|
}
|
|
})
|
|
|
|
export function useClashXData () {
|
|
const [data, set] = useRecoilState(clashxData)
|
|
|
|
async function update () {
|
|
if (!isClashX()) {
|
|
return
|
|
}
|
|
|
|
const startAtLogin = await jsBridge?.getStartAtLogin() ?? false
|
|
const systemProxy = await jsBridge?.isSystemProxySet() ?? false
|
|
|
|
set({ startAtLogin, systemProxy, isClashX: true })
|
|
}
|
|
|
|
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 }
|
|
}
|