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}
key={option.value}
className={classnames('button-select-options', { actived: value === option.value })}
onClick={() => onSelect(option.value)}
>{option.label}</button>
onClick={() => onSelect(option.value)}>
{ option.label }
</button>
))
}
</div>

View File

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

View File

@ -16,46 +16,18 @@ $delete-height: 22px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid $color-primary-darken;
border: 1px solid $color-primary-dark;
color: $color-primary-darken;
height: $height;
border-radius: $height / 2;
padding: 0 6px;
margin: 3px 4px;
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;
}
&:hover .tags-delete {
opacity: 1;
}
}
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;
}
.tags-selected {
background-color: $color-primary-dark;
color: #fff;
}
}

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ const history = createHashHistory()
export const rootStores = {
router: new RouterStore(history),
config: new ConfigStore()
store: new ConfigStore()
}
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 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 {
rules: { name: string, payload: string }[]
rules: Rule[]
}
export interface Rule {
type: string
payload: string
proxy: string
}
export interface Proxies {
proxies: {
[key: string]: Proxy
[key: string]: Proxy | Group
}
}
export interface Proxy {
type: 'Direct' | 'Selector' | 'Reject' | 'URLTest' | 'Shadowsocks' | 'Vmess' | 'Socks' | 'Fallback'
now?: string
all?: string[]
name: string
type: 'Direct' | 'Reject' | 'Shadowsocks' | 'Vmess' | 'Socks' | 'Http'
}
export interface Group {
name: string
type: 'Selector' | 'URLTest' | 'Fallback'
now: string
all: string[]
}
export async function getConfig () {
@ -66,7 +78,7 @@ export async function getProxyDelay (name: string) {
const req = await getInstance()
return req.get<{ delay: number }>(`proxies/${name}/delay`, {
params: {
timeout: 20000,
timeout: 5000,
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) {
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 () {
@ -98,8 +110,8 @@ export async function getInstance () {
export async function getExternalControllerConfig () {
if (isClashX()) {
await rootStores.config.fetchAndParseConfig()
const general = rootStores.config.config.general
await rootStores.store.fetchAndParseConfig()
const general = rootStores.store.config.general
return {
hostname: general.externalControllerAddr,

View File

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

View File

@ -1,5 +1,6 @@
import { Proxy, ProxyGroup } from './Proxy'
import { Rule } from './Rule'
import * as API from '@lib/request'
/**
* clash config
@ -62,3 +63,45 @@ export interface Config {
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 Models from '@models'
import { jsBridge } from '@lib/jsBridge'
import { getConfig } from '@lib/request'
import { getLocalStorageItem } from '@lib/helper'
import * as API from '@lib/request'
import { getLocalStorageItem, partition } from '@lib/helper'
export class ConfigStore {
@ -15,19 +15,47 @@ export class ConfigStore {
}
@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
async fetchAndParseConfig () {
this.state = 'pending'
const rawConfig = await jsBridge.readConfigString()
runInAction(() => {
// emit error when config is empty
// because read config might be error
if (!rawConfig) {
this.state = 'error'
return
}
@ -65,13 +93,12 @@ export class ConfigStore {
proxyGroup,
rules: rule || []
}
this.state = 'ok'
})
}
@action
async fetchConfig () {
const { data: config } = await getConfig()
const { data: config } = await API.getConfig()
this.config = {
general: {
port: config.port,

View File

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