mirror of
https://github.com/woodchen-ink/clash-and-dashboard.git
synced 2025-07-18 14:01:56 +08:00
Migration: HookStore & Logs to hooks component
This commit is contained in:
parent
a92ee4862f
commit
3d16e6e893
3747
package-lock.json
generated
3747
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -77,6 +77,7 @@
|
||||
"eventemitter3": "^4.0.0",
|
||||
"i18next": "^17.0.6",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
"immer": "^3.1.3",
|
||||
"mobx": "^5.10.1",
|
||||
"mobx-react": "^6.1.1",
|
||||
"mobx-react-router": "^4.0.7",
|
||||
@ -88,6 +89,8 @@
|
||||
"react-virtualized": "^9.21.1",
|
||||
"terser-webpack-plugin": "^1.3.0",
|
||||
"typescript": "^3.5.2",
|
||||
"unstated-next": "^1.1.0",
|
||||
"use-immer": "^0.3.2",
|
||||
"yaml": "^1.6.0"
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,6 @@ export function Select (props: SelectProps) {
|
||||
const attachmentRef = useRef<HTMLDivElement>()
|
||||
const targetRef = useRef<HTMLDivElement>()
|
||||
|
||||
|
||||
useLayoutEffect(() => {
|
||||
document.addEventListener('click', handleGlobalClick, true)
|
||||
return () => {
|
||||
|
@ -1,66 +1,58 @@
|
||||
import * as React from 'react'
|
||||
import React, { useLayoutEffect, useEffect, useRef, useState } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import { withTranslation, WithTranslation } from 'react-i18next'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Card, Header } from '@components'
|
||||
import './style.scss'
|
||||
import { getLogsStreamReader } from '@lib/request'
|
||||
import { StreamReader } from '@lib/streamer'
|
||||
import { Log } from '@models/Log'
|
||||
import './style.scss'
|
||||
|
||||
interface Log {
|
||||
type: string
|
||||
payload: string,
|
||||
time: Date
|
||||
}
|
||||
export default function Logs () {
|
||||
const listRef = useRef<HTMLUListElement>()
|
||||
const logsRef = useRef<Log[]>([])
|
||||
const [logs, setLogs] = useState<Log[]>([])
|
||||
const { t } = useTranslation(['Logs'])
|
||||
|
||||
interface LogsProps extends WithTranslation {}
|
||||
|
||||
interface LogsState {
|
||||
logs: Log[]
|
||||
}
|
||||
|
||||
class Logs extends React.Component<LogsProps, LogsState> {
|
||||
state: LogsState = {
|
||||
logs: []
|
||||
}
|
||||
|
||||
private streamReader = null
|
||||
private listRef = React.createRef<HTMLUListElement>()
|
||||
async componentDidMount () {
|
||||
this.streamReader = await getLogsStreamReader()
|
||||
let logs = this.streamReader.buffer()
|
||||
this.setState({ logs }, () => this.scrollToBottom())
|
||||
this.streamReader.subscribe('data', (data) => {
|
||||
logs = [].concat(this.state.logs, data.map(d => ({ ...d, time: new Date() })))
|
||||
this.setState({ logs }, () => this.scrollToBottom())
|
||||
})
|
||||
}
|
||||
|
||||
scrollToBottom = () => {
|
||||
const ul = this.listRef.current
|
||||
useLayoutEffect(() => {
|
||||
const ul = listRef.current
|
||||
ul.scrollTop = ul.scrollHeight
|
||||
}
|
||||
}, [logsRef.current])
|
||||
|
||||
render () {
|
||||
const { t } = this.props
|
||||
return (
|
||||
<div className="page">
|
||||
<Header title={ t('title') } />
|
||||
<Card className="logs-card">
|
||||
<ul className="logs-panel" ref={this.listRef}>
|
||||
{
|
||||
this.state.logs.map(
|
||||
(log, index) => (
|
||||
<li key={index}>
|
||||
<span className="logs-panel-time">{ dayjs(log.time).format('YYYY-MM-DD HH:mm:ss') }</span>
|
||||
<span>[{ log.type }] { log.payload }</span>
|
||||
</li>
|
||||
)
|
||||
useEffect(() => {
|
||||
let streamReader: StreamReader<Log> = null
|
||||
|
||||
function handleLog (newLogs: Log[]) {
|
||||
logsRef.current = logsRef.current.slice().concat(newLogs.map(d => ({ ...d, time: new Date() })))
|
||||
setLogs(logsRef.current)
|
||||
}
|
||||
|
||||
void async function () {
|
||||
const streamReader = await getLogsStreamReader()
|
||||
logsRef.current = streamReader.buffer()
|
||||
setLogs(logsRef.current)
|
||||
streamReader.subscribe<Log[]>('data', handleLog)
|
||||
}()
|
||||
|
||||
return () => streamReader && streamReader.unsubscribe('data', handleLog)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<Header title={ t('title') } />
|
||||
<Card className="logs-card">
|
||||
<ul className="logs-panel" ref={listRef}>
|
||||
{
|
||||
logs.map(
|
||||
(log, index) => (
|
||||
<li key={index}>
|
||||
<span className="logs-panel-time">{ dayjs(log.time).format('YYYY-MM-DD HH:mm:ss') }</span>
|
||||
<span>[{ log.type }] { log.payload }</span>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default withTranslation(['Logs'])(Logs)
|
||||
|
22
src/lib/hook.ts
Normal file
22
src/lib/hook.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Draft } from 'immer'
|
||||
import { useImmer } from 'use-immer'
|
||||
|
||||
export function useObject<T extends object> (initialValue: T) {
|
||||
let [copy, setCopy] = useImmer(initialValue)
|
||||
|
||||
function change<K extends keyof Draft<T>> (key: K, value: Draft<T>[K]) {
|
||||
setCopy(draft => {
|
||||
draft[key] = value
|
||||
})
|
||||
}
|
||||
|
||||
function set<K extends keyof Draft<T>, U extends keyof T> (newValue: T) {
|
||||
setCopy((draft: Draft<T>) => {
|
||||
(draft as any).isTemplate = true
|
||||
for (const key of Object.keys(newValue)) {
|
||||
draft[key as K] = newValue[key as U] as any
|
||||
}
|
||||
})
|
||||
}
|
||||
return { value: copy, change, set }
|
||||
}
|
@ -2,10 +2,11 @@ import axios, { AxiosInstance } from 'axios'
|
||||
import { Partial, getLocalStorageItem } from '@lib/helper'
|
||||
import { isClashX, jsBridge } from '@lib/jsBridge'
|
||||
import { rootStores } from '@lib/createStore'
|
||||
import { Log } from '@models/Log'
|
||||
import { StreamReader } from './streamer'
|
||||
|
||||
let instance: AxiosInstance
|
||||
let logsStreamReader = null
|
||||
let logsStreamReader: StreamReader<Log> = null
|
||||
|
||||
export interface Config {
|
||||
port: number
|
||||
|
5
src/models/Log.ts
Normal file
5
src/models/Log.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface Log {
|
||||
type: string
|
||||
payload: string,
|
||||
time: Date
|
||||
}
|
@ -4,19 +4,37 @@ import { Provider } from 'mobx-react'
|
||||
import { HashRouter } from 'react-router-dom'
|
||||
import { I18nextProvider } from 'react-i18next'
|
||||
import { rootStores } from '@lib/createStore'
|
||||
import { BaseComponentProps } from '@models/BaseProps'
|
||||
import { APIInfo, Data, ClashXData, ExternalControllerModal } from '@stores'
|
||||
import App from '@containers/App'
|
||||
import i18n from '@i18n'
|
||||
|
||||
function Store (props: BaseComponentProps) {
|
||||
return (
|
||||
<APIInfo.Provider>
|
||||
<Data.Provider>
|
||||
<ClashXData.Provider>
|
||||
<ExternalControllerModal.Provider>
|
||||
{ props.children }
|
||||
</ExternalControllerModal.Provider>
|
||||
</ClashXData.Provider>
|
||||
</Data.Provider>
|
||||
</APIInfo.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default function renderApp () {
|
||||
const rootEl = document.getElementById('root')
|
||||
const AppInstance = (
|
||||
<Provider {...rootStores}>
|
||||
<HashRouter>
|
||||
<I18nextProvider i18n={ i18n }>
|
||||
<App />
|
||||
</I18nextProvider>
|
||||
</HashRouter>
|
||||
</Provider>
|
||||
<Store>
|
||||
<Provider {...rootStores}>
|
||||
<HashRouter>
|
||||
<I18nextProvider i18n={ i18n }>
|
||||
<App />
|
||||
</I18nextProvider>
|
||||
</HashRouter>
|
||||
</Provider>
|
||||
</Store>
|
||||
)
|
||||
|
||||
render(AppInstance, rootEl)
|
||||
|
106
src/stores/HookStore.ts
Normal file
106
src/stores/HookStore.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import { useState } from 'react'
|
||||
import * as Models from '@models'
|
||||
import { createContainer } from 'unstated-next'
|
||||
import * as API from '@lib/request'
|
||||
import { useObject } from '@lib/hook'
|
||||
import { jsBridge, isClashX } from '@lib/jsBridge'
|
||||
import { setLocalStorageItem, partition } from '@lib/helper'
|
||||
|
||||
function useData () {
|
||||
const { value: data, change } = useObject<Models.Data>({
|
||||
general: {},
|
||||
proxy: [],
|
||||
proxyGroup: [],
|
||||
rules: []
|
||||
})
|
||||
|
||||
async function fetch () {
|
||||
const [{ data: general }, rawProxies, rules] = await Promise.all([API.getConfig(), API.getProxies(), API.getRules()])
|
||||
|
||||
change('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
|
||||
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))
|
||||
|
||||
change('proxy', proxy as API.Proxy[])
|
||||
change('proxyGroup', groups as API.Group[])
|
||||
change('rules', rules.data.rules)
|
||||
}
|
||||
|
||||
return { data, fetch }
|
||||
}
|
||||
|
||||
function useAPIInfo () {
|
||||
const { value: data, set } = useObject<Models.APIInfo>({
|
||||
hostname: '127.0.0.1',
|
||||
port: '9090',
|
||||
secret: ''
|
||||
})
|
||||
|
||||
async function fetch () {
|
||||
if (isClashX()) {
|
||||
const apiInfo = await jsBridge.getAPIInfo()
|
||||
set({ hostname: apiInfo.host, port: apiInfo.port, secret: apiInfo.secret })
|
||||
return
|
||||
}
|
||||
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 useClashXData () {
|
||||
const { value: 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 }
|
||||
}
|
||||
|
||||
function useExternalControllerModal () {
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
function show () {
|
||||
setVisible(true)
|
||||
}
|
||||
|
||||
function hidden () {
|
||||
setVisible(false)
|
||||
}
|
||||
|
||||
return { visible, show, hidden }
|
||||
}
|
||||
|
||||
export const Data = createContainer(useData)
|
||||
export const APIInfo = createContainer(useAPIInfo)
|
||||
export const ClashXData = createContainer(useClashXData)
|
||||
export const ExternalControllerModal = createContainer(useExternalControllerModal)
|
@ -1,2 +1,3 @@
|
||||
export * from './ConfigStore'
|
||||
export * from './RouterStore'
|
||||
export * from './HookStore'
|
||||
|
Loading…
x
Reference in New Issue
Block a user