Migration: HookStore & Logs to hooks component

This commit is contained in:
Dreamacro 2019-07-03 19:19:12 +08:00
parent a92ee4862f
commit 3d16e6e893
10 changed files with 2013 additions and 2013 deletions

3747
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -77,6 +77,7 @@
"eventemitter3": "^4.0.0", "eventemitter3": "^4.0.0",
"i18next": "^17.0.6", "i18next": "^17.0.6",
"i18next-browser-languagedetector": "^3.0.1", "i18next-browser-languagedetector": "^3.0.1",
"immer": "^3.1.3",
"mobx": "^5.10.1", "mobx": "^5.10.1",
"mobx-react": "^6.1.1", "mobx-react": "^6.1.1",
"mobx-react-router": "^4.0.7", "mobx-react-router": "^4.0.7",
@ -88,6 +89,8 @@
"react-virtualized": "^9.21.1", "react-virtualized": "^9.21.1",
"terser-webpack-plugin": "^1.3.0", "terser-webpack-plugin": "^1.3.0",
"typescript": "^3.5.2", "typescript": "^3.5.2",
"unstated-next": "^1.1.0",
"use-immer": "^0.3.2",
"yaml": "^1.6.0" "yaml": "^1.6.0"
} }
} }

View File

@ -24,7 +24,6 @@ export function Select (props: SelectProps) {
const attachmentRef = useRef<HTMLDivElement>() const attachmentRef = useRef<HTMLDivElement>()
const targetRef = useRef<HTMLDivElement>() const targetRef = useRef<HTMLDivElement>()
useLayoutEffect(() => { useLayoutEffect(() => {
document.addEventListener('click', handleGlobalClick, true) document.addEventListener('click', handleGlobalClick, true)
return () => { return () => {

View File

@ -1,66 +1,58 @@
import * as React from 'react' import React, { useLayoutEffect, useEffect, useRef, useState } from 'react'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { withTranslation, WithTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Card, Header } from '@components' import { Card, Header } from '@components'
import './style.scss'
import { getLogsStreamReader } from '@lib/request' import { getLogsStreamReader } from '@lib/request'
import { StreamReader } from '@lib/streamer'
import { Log } from '@models/Log'
import './style.scss'
interface Log { export default function Logs () {
type: string const listRef = useRef<HTMLUListElement>()
payload: string, const logsRef = useRef<Log[]>([])
time: Date const [logs, setLogs] = useState<Log[]>([])
} const { t } = useTranslation(['Logs'])
interface LogsProps extends WithTranslation {} useLayoutEffect(() => {
const ul = listRef.current
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
ul.scrollTop = ul.scrollHeight ul.scrollTop = ul.scrollHeight
} }, [logsRef.current])
render () { useEffect(() => {
const { t } = this.props let streamReader: StreamReader<Log> = null
return (
<div className="page"> function handleLog (newLogs: Log[]) {
<Header title={ t('title') } /> logsRef.current = logsRef.current.slice().concat(newLogs.map(d => ({ ...d, time: new Date() })))
<Card className="logs-card"> setLogs(logsRef.current)
<ul className="logs-panel" ref={this.listRef}> }
{
this.state.logs.map( void async function () {
(log, index) => ( const streamReader = await getLogsStreamReader()
<li key={index}> logsRef.current = streamReader.buffer()
<span className="logs-panel-time">{ dayjs(log.time).format('YYYY-MM-DD HH:mm:ss') }</span> setLogs(logsRef.current)
<span>[{ log.type }] { log.payload }</span> streamReader.subscribe<Log[]>('data', handleLog)
</li> }()
)
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> </ul>
</div> </Card>
) </div>
} )
} }
export default withTranslation(['Logs'])(Logs)

22
src/lib/hook.ts Normal file
View 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 }
}

View File

@ -2,10 +2,11 @@ import axios, { AxiosInstance } from 'axios'
import { Partial, getLocalStorageItem } from '@lib/helper' import { Partial, getLocalStorageItem } from '@lib/helper'
import { isClashX, jsBridge } from '@lib/jsBridge' import { isClashX, jsBridge } from '@lib/jsBridge'
import { rootStores } from '@lib/createStore' import { rootStores } from '@lib/createStore'
import { Log } from '@models/Log'
import { StreamReader } from './streamer' import { StreamReader } from './streamer'
let instance: AxiosInstance let instance: AxiosInstance
let logsStreamReader = null let logsStreamReader: StreamReader<Log> = null
export interface Config { export interface Config {
port: number port: number

5
src/models/Log.ts Normal file
View File

@ -0,0 +1,5 @@
export interface Log {
type: string
payload: string,
time: Date
}

View File

@ -4,19 +4,37 @@ import { Provider } from 'mobx-react'
import { HashRouter } from 'react-router-dom' import { HashRouter } from 'react-router-dom'
import { I18nextProvider } from 'react-i18next' import { I18nextProvider } from 'react-i18next'
import { rootStores } from '@lib/createStore' import { rootStores } from '@lib/createStore'
import { BaseComponentProps } from '@models/BaseProps'
import { APIInfo, Data, ClashXData, ExternalControllerModal } from '@stores'
import App from '@containers/App' import App from '@containers/App'
import i18n from '@i18n' 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 () { export default function renderApp () {
const rootEl = document.getElementById('root') const rootEl = document.getElementById('root')
const AppInstance = ( const AppInstance = (
<Provider {...rootStores}> <Store>
<HashRouter> <Provider {...rootStores}>
<I18nextProvider i18n={ i18n }> <HashRouter>
<App /> <I18nextProvider i18n={ i18n }>
</I18nextProvider> <App />
</HashRouter> </I18nextProvider>
</Provider> </HashRouter>
</Provider>
</Store>
) )
render(AppInstance, rootEl) render(AppInstance, rootEl)

106
src/stores/HookStore.ts Normal file
View 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)

View File

@ -1,2 +1,3 @@
export * from './ConfigStore' export * from './ConfigStore'
export * from './RouterStore' export * from './RouterStore'
export * from './HookStore'