Chore: adjust architecture

This commit is contained in:
Dreamacro 2018-12-16 00:22:14 +08:00
parent 715827c94d
commit 60b38e7236
18 changed files with 261 additions and 162 deletions

View File

@ -32,8 +32,9 @@ export class ButtonSelect extends React.Component<ButtonSelectProps, {}> {
value={option.value} value={option.value}
key={option.value} key={option.value}
className={classnames('button-select-options', { actived: value === option.value })} className={classnames('button-select-options', { actived: value === option.value })}
onClick={() => onSelect(option.value)} onClick={() => onSelect(option.value)}>
>{option.label}</button> { option.label }
</button>
)) ))
} }
</div> </div>

View File

@ -1,49 +1,32 @@
import * as React from 'react' import * as React from 'react'
import { BaseComponentProps } from '@models/BaseProps' import { BaseComponentProps } from '@models/BaseProps'
import { Icon } from '@components'
import classnames from 'classnames' import classnames from 'classnames'
import './style.scss' import './style.scss'
interface TagsProps extends BaseComponentProps { interface TagsProps extends BaseComponentProps {
data: Set<string> data: Set<string>
showAdd: boolean onClick: (name: string) => void
onDelete: (tag: string) => void selected: string
onAdd: () => void
} }
export class Tags extends React.Component<TagsProps, {}> { export class Tags extends React.Component<TagsProps, {}> {
static defaultProps: TagsProps = {
data: new Set(),
showAdd: true,
onDelete: () => {},
onAdd: () => {}
}
render () { render () {
const { className, data, onDelete, onAdd, showAdd } = this.props const { className, data, onClick, selected } = this.props
const tags = [...data] const tags = [...data]
.sort() .sort()
.map(t => ( .map(t => {
<li> const tagClass = classnames({ 'tags-selected': selected === t })
return (
<li className={tagClass} key={t} onClick={() => onClick(t)}>
{ t } { t }
<Icon
className="tags-delete"
type="plus"
size={12}
style={{ fontWeight: 'bold', color: '#fff' }}
onClick={() => onDelete(t)}/>
</li> </li>
)) )
})
return ( return (
<ul className={classnames('tags', className)}> <ul className={classnames('tags', className)}>
{ tags } { tags }
{
showAdd &&
<li className="tags-add" onClick={onAdd}>
<Icon type="plus" size={12} style={{ fontWeight: 'bold', color: '#fff' }} />
</li>
}
</ul> </ul>
) )
} }

View File

@ -16,46 +16,18 @@ $delete-height: 22px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: 1px solid $color-primary-darken; border: 1px solid $color-primary-dark;
color: $color-primary-darken; color: $color-primary-darken;
height: $height; height: $height;
border-radius: $height / 2; border-radius: $height / 2;
padding: 0 6px; padding: 0 6px;
margin: 3px 4px; margin: 3px 4px;
font-size: 10px; font-size: 10px;
cursor: default;
.tags-delete {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
right: -$delete-height / 2;
top: -$delete-height / 2;
height: $delete-height;
width: $delete-height;
border-radius: 50%;
background-color: $color-red;
transform: rotate(45deg) scale(0.7);
opacity: 0;
transition: opacity 0.2s ease;
cursor: pointer; cursor: pointer;
} }
&:hover .tags-delete { .tags-selected {
opacity: 1; background-color: $color-primary-dark;
} color: #fff;
}
li.tags-add {
height: $add-height;
width: $add-height;
border: none;
text-align: center;
padding: 0;
border-radius: 50%;
background-color: $color-primary-darken;
transform: translateX(-2px) scale(0.7);
cursor: pointer;
} }
} }

View File

@ -0,0 +1,33 @@
import * as React from 'react'
import { inject } from 'mobx-react'
import { BaseComponentProps } from '@models'
import { ConfigStore } from '@stores'
import { changeProxySelected, Group as IGroup } from '@lib/request'
import { storeKeys } from '@lib/createStore'
import { Tags } from '@components'
import './style.scss'
interface GroupProps extends BaseComponentProps {
config: IGroup
store?: ConfigStore
}
@inject(...storeKeys)
export class Group extends React.Component<GroupProps, {}> {
handleChangeProxySelected = async (name: string) => {
await changeProxySelected(this.props.config.name, name)
await this.props.store.fetchData()
}
render () {
const { config } = this.props
const proxies = new Set(config.all)
return (
<div className="proxy-group">
<span className="proxy-group-name">{ config.name }</span>
<span className="proxy-group-type">{ config.type }</span>
<Tags className="proxy-group-tags" data={proxies} onClick={this.handleChangeProxySelected} selected={config.now} />
</div>
)
}
}

View File

@ -0,0 +1,42 @@
@import '~@styles/variables';
.proxy-group {
display: flex;
align-items: center;
font-size: 14px;
padding: 12px 0;
color: $color-black-light;
}
.proxy-group-name {
display: flex;
padding: 0 20px;
width: 120px;
}
.proxy-group-type {
height: 24px;
line-height: 24px;
width: 80px;
text-align: center;
background-color: $color-primary-dark;
color: #fff;
border-radius: 3px;
}
.proxies-group-card {
padding: 0;
}
.proxies-group-item {
border-bottom: 1px solid $color-gray;
&:last-child {
border-bottom: none;
}
}
.proxy-group-tags {
flex: 1;
margin-left: 30px;
}

View File

@ -1,14 +1,14 @@
import * as React from 'react' import * as React from 'react'
import classnames from 'classnames' import classnames from 'classnames'
import { Icon } from '@components' // import { Icon } from '@components'
import { BaseComponentProps, Proxy as IProxy, TagColors } from '@models' import { BaseComponentProps, TagColors } from '@models'
import { getProxyDelay } from '@lib/request' import { getProxyDelay, Proxy as IProxy } from '@lib/request'
import { to, getLocalStorageItem, setLocalStorageItem, sample, noop } from '@lib/helper' import { to, getLocalStorageItem, setLocalStorageItem, sample } from '@lib/helper'
import './style.scss' import './style.scss'
interface ProxyProps extends BaseComponentProps { interface ProxyProps extends BaseComponentProps {
config: IProxy config: IProxy
onEdit?: (e: React.MouseEvent<HTMLElement>) => void // onEdit?: (e: React.MouseEvent<HTMLElement>) => void
} }
interface ProxyState { interface ProxyState {
@ -18,8 +18,6 @@ interface ProxyState {
} }
export class Proxy extends React.Component<ProxyProps , ProxyState> { export class Proxy extends React.Component<ProxyProps , ProxyState> {
private mount = true
constructor (props) { constructor (props) {
super(props) super(props)
@ -49,18 +47,10 @@ export class Proxy extends React.Component<ProxyProps , ProxyState> {
} }
} }
componentWillUnmount () {
this.mount = false
}
async componentDidMount () { async componentDidMount () {
const { config } = this.props const { config } = this.props
const [res, err] = await to(getProxyDelay(config.name)) const [res, err] = await to(getProxyDelay(config.name))
if (!this.mount) {
return
}
if (err) { if (err) {
return this.setState({ hasError: true }) return this.setState({ hasError: true })
} }
@ -70,7 +60,7 @@ export class Proxy extends React.Component<ProxyProps , ProxyState> {
} }
render () { render () {
const { config, className, onEdit = noop } = this.props const { config, className } = this.props
const { delay, color, hasError } = this.state const { delay, color, hasError } = this.state
const backgroundColor = hasError ? undefined : color const backgroundColor = hasError ? undefined : color
@ -79,7 +69,7 @@ export class Proxy extends React.Component<ProxyProps , ProxyState> {
<span className="proxy-type" style={{ backgroundColor }}>{config.type}</span> <span className="proxy-type" style={{ backgroundColor }}>{config.type}</span>
<p className="proxy-name">{config.name}</p> <p className="proxy-name">{config.name}</p>
<p className="proxy-delay">{delay === -1 ? '-' : `${delay}ms`}</p> <p className="proxy-delay">{delay === -1 ? '-' : `${delay}ms`}</p>
<Icon className="proxy-editor" type="setting" onClick={onEdit} /> {/* <Icon className="proxy-editor" type="setting" onClick={onEdit} /> */}
</div> </div>
) )
} }

View File

@ -1,2 +1,3 @@
export * from './Proxy' export * from './Proxy'
export * from './Group'
export * from './ModifyProxyDialog' export * from './ModifyProxyDialog'

View File

@ -2,79 +2,56 @@ import * as React from 'react'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import { inject, observer } from 'mobx-react' import { inject, observer } from 'mobx-react'
import { storeKeys } from '@lib/createStore' import { storeKeys } from '@lib/createStore'
import { Header, Icon } from '@components' import { Card, Header } from '@components'
import { I18nProps, BaseRouterProps, Proxy as IProxy } from '@models' import { I18nProps, BaseRouterProps } from '@models'
import { Proxy, ModifyProxyDialog } from './components' import { Proxy, Group } from './components'
import './style.scss' import './style.scss'
interface ProxiesProps extends BaseRouterProps, I18nProps {} interface ProxiesProps extends BaseRouterProps, I18nProps {}
interface ProxiesState { interface ProxiesState {
showModifyProxyDialog: boolean
activeConfig?: IProxy
activeConfigIndex?: number
} }
@inject(...storeKeys) @inject(...storeKeys)
@observer @observer
class Proxies extends React.Component<ProxiesProps, ProxiesState> { class Proxies extends React.Component<ProxiesProps, ProxiesState> {
state = {
showModifyProxyDialog: false,
activeConfig: null,
activeConfigIndex: -1
}
componentDidMount () { componentDidMount () {
this.props.config.fetchAndParseConfig() this.props.store.fetchData()
}
handleConfigApply = async (config: IProxy) => {
await this.props.config.modifyProxyByIndexAndSave(this.state.activeConfigIndex, config)
this.setState({ showModifyProxyDialog: false, activeConfig: null })
} }
render () { render () {
const { t, config } = this.props const { t, store } = this.props
const { showModifyProxyDialog, activeConfig } = this.state
return ( return (
<>
<div className="page"> <div className="page">
<div className="proxies-container"> <div className="proxies-container">
<Header title={t('title')} > <Header title={t('groupTitle')} />
<Icon type="plus" size={20} style={{ fontWeight: 'bold' }} /> <Card className="proxies-group-card">
</Header> <ul className="proxies-group-list">
{ {
config.config.proxy.length !== 0 && <ul className="proxies-list"> store.data.proxyGroup.map(p => (
{ <li className="proxies-group-item" key={p.name}>
config.config.proxy.map((p, index) => ( <Group config={p} />
<li key={p.name}>
<Proxy config={p} onEdit={() => this.setState({
showModifyProxyDialog: true,
activeConfig: p,
activeConfigIndex: index
})} />
</li> </li>
)) ))
} }
</ul> </ul>
} </Card>
</div> </div>
<div className="proxies-container"> <div className="proxies-container">
<Header title={t('groupTitle')} /> <Header title={t('title')} />
</div> <ul className="proxies-list">
</div>
{ {
showModifyProxyDialog && <ModifyProxyDialog store.data.proxy.map(p => (
config={activeConfig} <li key={p.name}>
onOk={this.handleConfigApply} <Proxy config={p} />
onCancel={() => this.setState({ showModifyProxyDialog: false, activeConfig: null, activeConfigIndex: -1 })} </li>
/> ))
} }
</> </ul>
</div>
</div>
) )
} }
} }

View File

@ -15,3 +15,11 @@
} }
} }
} }
.proxies-group-list {
list-style: none;
}
.proxies-group-card {
margin: 15px 0 20px;
}

View File

@ -13,12 +13,12 @@ interface RulesProps extends BaseRouterProps, I18nProps {}
class Rules extends React.Component<RulesProps, {}> { class Rules extends React.Component<RulesProps, {}> {
async componentDidMount () { async componentDidMount () {
const { config } = this.props const { store } = this.props
await config.fetchAndParseConfig() await store.fetchData()
} }
renderRuleItem = ({ index, key, style }) => { renderRuleItem = ({ index, key, style }) => {
const { rules } = this.props.config.config const { rules } = this.props.store.data
const rule = rules[index] const rule = rules[index]
return ( return (
<li className="rule-item" key={key} style={style}> <li className="rule-item" key={key} style={style}>
@ -39,7 +39,7 @@ class Rules extends React.Component<RulesProps, {}> {
render () { render () {
const { t } = this.props const { t } = this.props
const { rules } = this.props.config.config const { rules } = this.props.store.config
return ( return (
<div className="page"> <div className="page">
<Header title={t('title')} /> <Header title={t('title')} />

View File

@ -79,7 +79,7 @@ class Settings extends React.Component<I18nProps, {}> {
async componentDidMount () { async componentDidMount () {
if (isClashX()) { if (isClashX()) {
await rootStores.config.fetchAndParseConfig() await rootStores.store.fetchAndParseConfig()
const startAtLogin = await jsBridge.getStartAtLogin() const startAtLogin = await jsBridge.getStartAtLogin()
const setAsSystemProxy = await jsBridge.isSystemProxySet() const setAsSystemProxy = await jsBridge.isSystemProxySet()
this.setState({ this.setState({
@ -88,9 +88,9 @@ class Settings extends React.Component<I18nProps, {}> {
isClashX: true isClashX: true
}) })
} else { } else {
await rootStores.config.fetchConfig() await rootStores.store.fetchConfig()
} }
const general = rootStores.config.config.general const general = rootStores.store.config.general
this.setState({ this.setState({
allowConnectFromLan: general.allowLan, allowConnectFromLan: general.allowLan,
proxyMode: general.mode, proxyMode: general.mode,

View File

@ -8,7 +8,7 @@ const history = createHashHistory()
export const rootStores = { export const rootStores = {
router: new RouterStore(history), router: new RouterStore(history),
config: new ConfigStore() store: new ConfigStore()
} }
export const storeKeys = Object.keys(rootStores) export const storeKeys = Object.keys(rootStores)

View File

@ -34,3 +34,12 @@ export async function to <T, E = Error> (promise: Promise<T>): Promise<[T, E]> {
} }
export type Partial<T> = { [P in keyof T]?: T[P] } 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[] = []
for (const item of arr) {
fn(item) ? left.push(item) : right.push(item)
}
return [left, right]
}

View File

@ -17,19 +17,31 @@ export interface Config {
} }
export interface Rules { export interface Rules {
rules: { name: string, payload: string }[] rules: Rule[]
}
export interface Rule {
type: string
payload: string
proxy: string
} }
export interface Proxies { export interface Proxies {
proxies: { proxies: {
[key: string]: Proxy [key: string]: Proxy | Group
} }
} }
export interface Proxy { export interface Proxy {
type: 'Direct' | 'Selector' | 'Reject' | 'URLTest' | 'Shadowsocks' | 'Vmess' | 'Socks' | 'Fallback' name: string
now?: string type: 'Direct' | 'Reject' | 'Shadowsocks' | 'Vmess' | 'Socks' | 'Http'
all?: string[] }
export interface Group {
name: string
type: 'Selector' | 'URLTest' | 'Fallback'
now: string
all: string[]
} }
export async function getConfig () { export async function getConfig () {
@ -66,7 +78,7 @@ export async function getProxyDelay (name: string) {
const req = await getInstance() const req = await getInstance()
return req.get<{ delay: number }>(`proxies/${name}/delay`, { return req.get<{ delay: number }>(`proxies/${name}/delay`, {
params: { params: {
timeout: 20000, timeout: 5000,
url: 'http://www.gstatic.com/generate_204' url: 'http://www.gstatic.com/generate_204'
} }
}) })
@ -74,7 +86,7 @@ export async function getProxyDelay (name: string) {
export async function changeProxySelected (name: string, select: string) { export async function changeProxySelected (name: string, select: string) {
const req = await getInstance() const req = await getInstance()
return req.get<void>(`proxies/${name}`, { data: { name: select } }) return req.put<void>(`proxies/${name}`, { name: select })
} }
export async function getInstance () { export async function getInstance () {
@ -98,8 +110,8 @@ export async function getInstance () {
export async function getExternalControllerConfig () { export async function getExternalControllerConfig () {
if (isClashX()) { if (isClashX()) {
await rootStores.config.fetchAndParseConfig() await rootStores.store.fetchAndParseConfig()
const general = rootStores.config.config.general const general = rootStores.store.config.general
return { return {
hostname: general.externalControllerAddr, hostname: general.externalControllerAddr,

View File

@ -14,7 +14,7 @@ export interface BaseRouterProps extends RouteComponentProps<any>, BaseProps {}
export interface BaseProps extends BaseComponentProps { export interface BaseProps extends BaseComponentProps {
styles?: any styles?: any
router?: RouterStore router?: RouterStore
config?: ConfigStore store?: ConfigStore
} }
export interface BaseComponentProps { export interface BaseComponentProps {

View File

@ -1,5 +1,6 @@
import { Proxy, ProxyGroup } from './Proxy' import { Proxy, ProxyGroup } from './Proxy'
import { Rule } from './Rule' import { Rule } from './Rule'
import * as API from '@lib/request'
/** /**
* clash config * clash config
@ -62,3 +63,45 @@ export interface Config {
rules?: Rule[] rules?: Rule[]
} }
export interface Data {
general?: {
/**
* http proxy port
*/
port?: number
/**
* socks proxy port
*/
socksPort?: number
/**
* redir proxy port
*/
redirPort?: number
/**
* proxy is allow lan
*/
allowLan?: boolean
/**
* clash proxy mode
*/
mode?: string
/**
* clash tty log level
*/
logLevel?: string
}
proxy?: API.Proxy[]
proxyGroup?: API.Group[]
rules?: API.Rule[]
}

View File

@ -2,8 +2,8 @@ import { observable, action, runInAction } from 'mobx'
import * as yaml from 'yaml' import * as yaml from 'yaml'
import * as Models from '@models' import * as Models from '@models'
import { jsBridge } from '@lib/jsBridge' import { jsBridge } from '@lib/jsBridge'
import { getConfig } from '@lib/request' import * as API from '@lib/request'
import { getLocalStorageItem } from '@lib/helper' import { getLocalStorageItem, partition } from '@lib/helper'
export class ConfigStore { export class ConfigStore {
@ -15,19 +15,47 @@ export class ConfigStore {
} }
@observable @observable
public state: 'pending' | 'ok' | 'error' = 'pending' data: Models.Data = {
general: {},
proxy: [],
proxyGroup: [],
rules: []
}
@action
async fetchData () {
const [{ data: general }, rawProxies, rules] = await Promise.all([API.getConfig(), API.getProxies(), API.getRules()])
runInAction(() => {
this.data.general = {
port: general.port,
socksPort: general['socket-port'],
redirPort: general['redir-port'],
mode: general.mode,
logLevel: general['log-level']
}
const policyGroup = new Set(['Selector', 'URLTest', 'Fallback'])
const unUsedProxy = new Set(['DIRECT', 'REJECT', 'GLOBAL'])
const proxies = Object.keys(rawProxies.data.proxies)
.filter(key => !unUsedProxy.has(key))
.map(key => ({ ...rawProxies.data.proxies[key], name: key }))
const [proxy, groups] = partition(proxies, proxy => !policyGroup.has(proxy.type))
this.data.proxy = proxy as API.Proxy[]
this.data.proxyGroup = groups as API.Group[]
this.data.rules = rules.data.rules
})
}
@action @action
async fetchAndParseConfig () { async fetchAndParseConfig () {
this.state = 'pending'
const rawConfig = await jsBridge.readConfigString() const rawConfig = await jsBridge.readConfigString()
runInAction(() => { runInAction(() => {
// emit error when config is empty // emit error when config is empty
// because read config might be error // because read config might be error
if (!rawConfig) { if (!rawConfig) {
this.state = 'error'
return return
} }
@ -65,13 +93,12 @@ export class ConfigStore {
proxyGroup, proxyGroup,
rules: rule || [] rules: rule || []
} }
this.state = 'ok'
}) })
} }
@action @action
async fetchConfig () { async fetchConfig () {
const { data: config } = await getConfig() const { data: config } = await API.getConfig()
this.config = { this.config = {
general: { general: {
port: config.port, port: config.port,

View File

@ -18,4 +18,5 @@ $color-white: #fff;
$color-green: #67c23a; $color-green: #67c23a;
$color-orange: #e6a23c; $color-orange: #e6a23c;
$color-red: #f56c6c; $color-red: #f56c6c;
$color-black-light: #546b87;
$color-black: #000; $color-black: #000;