Add: proxy modify

This commit is contained in:
jas0ncn 2018-11-17 19:11:12 +08:00
parent 49d256a51f
commit aec5b72987
15 changed files with 457 additions and 50 deletions

View File

@ -9,6 +9,7 @@ interface InputProps extends BaseComponentProps {
align?: 'left' | 'center' | 'right'
inside?: boolean
autoFocus?: boolean
type?: string
onChange?: (value: string, event?: React.ChangeEvent<HTMLInputElement>) => void
onBlur?: (event?: React.FocusEvent<HTMLInputElement>) => void
}
@ -20,6 +21,7 @@ export class Input extends React.Component<InputProps, {}> {
align: 'center',
inside: false,
autoFocus: false,
type: 'text',
onChange: () => {},
onBlur: () => {}
}
@ -32,6 +34,7 @@ export class Input extends React.Component<InputProps, {}> {
align,
inside,
autoFocus,
type,
onChange,
onBlur
} = this.props
@ -42,6 +45,7 @@ export class Input extends React.Component<InputProps, {}> {
style={style}
value={value}
autoFocus={autoFocus}
type={type}
onChange={event => onChange(event.target.value, event)}
onBlur={onBlur}
/>

View File

@ -49,6 +49,8 @@ export class Modal extends React.Component<ModalProps, {}> {
$modal = React.createRef<HTMLDivElement>()
$mask = React.createRef<HTMLDivElement>()
constructor (props) {
super(props)
@ -64,9 +66,8 @@ export class Modal extends React.Component<ModalProps, {}> {
private handleMaskClick = (e) => {
const { onClose } = this.props
const el = this.$modal.current
if (el && !el.contains(e.target)) {
if (e.target === this.$mask) {
onClose()
}
}
@ -76,6 +77,7 @@ export class Modal extends React.Component<ModalProps, {}> {
const modal = (
<div
className={classnames('modal-mask', { 'modal-show': show })}
ref={this.$mask}
onClick={this.handleMaskClick}
>
<div

View File

@ -44,12 +44,7 @@ export class Select extends React.Component<SelectProps, SelectState> {
super(props)
}
componentDidUpdate () {
console.log('update')
}
componentDidMount () {
document.addEventListener('click', this.handleGlobalClick, true)
this.setState({ dropdownListStyles: this.calculateAttachmentPosition() })
}
@ -60,6 +55,7 @@ export class Select extends React.Component<SelectProps, SelectState> {
}
document.removeEventListener('click', this.handleGlobalClick, true)
}
shouldComponentUpdate (nextProps, nextState) {
if (nextProps.value === this.props.value && nextState.showDropDownList === this.state.showDropDownList) {
return false
@ -96,7 +92,7 @@ export class Select extends React.Component<SelectProps, SelectState> {
return {
top: Math.floor(targetRectInfo.top) - 10,
left: Math.floor(targetRectInfo.left) - 10,
width: Math.floor(targetRectInfo.width)
width: Math.floor(targetRectInfo.width) + 10
}
}
@ -163,7 +159,7 @@ export class Select extends React.Component<SelectProps, SelectState> {
ref={this.$target}
onClick={this.handleShowDropList}
>
{matchChild.props.children}
{matchChild && matchChild.props && matchChild.props.children}
<Icon type="triangle-down" />
</div>
{hasCreateDropList && createPortal(dropDownList, this.$container)}

View File

@ -1,7 +1,6 @@
@import '~@styles/variables';
.select {
flex: 1;
cursor: pointer;
font-size: 14px;
line-height: 30px;

View File

@ -0,0 +1,129 @@
import * as React from 'react'
import classnames from 'classnames'
import { Row, Col, Input, Icon, Select, Option } from '@components'
import { noop } from '@lib/helper'
// type selector
export function ProxyTypeSelector ({ types, label, value, onSelect = noop }: {
types: { [key: string]: string },
label: string,
value: string,
onSelect?: (type: string) => void
}) {
return (
<Row gutter={24} className="proxy-editor-row">
<Col span={6} className="proxy-editor-label">{label}</Col>
<Col span={18}>
<Select value={value} onSelect={onSelect}>
{
Object.keys(types)
.map(typeName => {
const type = types[typeName]
return (
<Option value={type} key={type}>{typeName}</Option>
)
})
}
</Select>
</Col>
</Row>
)
}
// color selector
export function ProxyColorSelector ({ colors, value, onSelect = noop }: {
colors: string[],
value: string,
onSelect?: (color: string) => void
}) {
return (
<Row gutter={24} style={{ padding: '12px 0' }}>
<div className="proxy-editor-color-selector">
{
colors.map(color => (
<span
className={classnames('color-item', {
'color-item-active': value === color
})}
key={color}
style={{ background: color }}
onClick={() => onSelect(color)}
/>
))
}
</div>
</Row>
)
}
// input form
export function ProxyInputForm ({ label, value, onChange = noop }: {
label: string,
value: string,
onChange?: (value: string) => void
}) {
return (
<Row gutter={24} className="proxy-editor-row">
<Col span={6} className="proxy-editor-label">{label}</Col>
<Col span={18}>
<Input value={value} onChange={onChange} align="left"/>
</Col>
</Row>
)
}
// password form
export class ProxyPasswordForm extends React.Component<{
label: string,
value: string,
onChange?: (value: string) => void
}, { showPassword: boolean }> {
state = {
showPassword: false
}
render () {
const { label, value, onChange } = this.props
const { showPassword } = this.state
const type = showPassword ? 'text' : 'password'
return (
<Row gutter={24} className="proxy-editor-row">
<Col span={6} className="proxy-editor-label">{label}</Col>
<Col span={18} className="proxy-editor-value">
<Input type={type} value={value} onChange={onChange} align="left"/>
<Icon
className="proxy-editor-passsword-icon"
type={showPassword ? 'hide' : 'show'}
size={20}
onClick={() => this.setState({ showPassword: !showPassword })}
/>
</Col>
</Row>
)
}
}
// cipher selector
export function ProxyCipherSelector ({ ciphers, label, value, onSelect = noop }: {
ciphers: string[],
label: string,
value: string,
onSelect?: (type: string) => void
}) {
return (
<Row gutter={24} className="proxy-editor-row">
<Col span={6} className="proxy-editor-label">{label}</Col>
<Col span={18}>
<Select value={value} onSelect={onSelect}>
{
ciphers.map(cipher => (
<Option value={cipher} key={cipher}>{cipher}</Option>
))
}
</Select>
</Col>
</Row>
)
}

View File

@ -1,11 +1,27 @@
import * as React from 'react'
import { translate } from 'react-i18next'
import classnames from 'classnames'
import { BaseComponentProps, Proxy as IProxy, I18nProps, TagColors } from '@models'
import { Modal, Row, Col } from '@components'
import { Modal } from '@components'
import { getLocalStorageItem, setLocalStorageItem } from '@lib/helper'
import './style.scss'
import {
BaseComponentProps,
Proxy as IProxy,
SsProxyConfigList, VmessProxyConfigList, Socks5ProxyConfigList,
I18nProps,
TagColors,
ProxyType,
SsCipher, VmessCipher, pickCipherWithAlias
} from '@models'
import {
ProxyInputForm,
ProxyColorSelector,
ProxyTypeSelector,
ProxyPasswordForm,
ProxyCipherSelector
} from './FormItems'
interface ModifyProxyDialogProps extends BaseComponentProps, I18nProps {
config: IProxy
onOk?: (config: IProxy) => void
@ -40,9 +56,151 @@ class RawDialog extends React.Component<ModifyProxyDialogProps, ModifyProxyDialo
onOk(config)
}
handleConfigChange = (key: string, value: any) => {
console.log(key, value)
const { config } = this.state
this.setState({ config: { ...config, [key]: value } })
}
getCipherFromType (type) {
switch (type) {
case 'ss':
return SsCipher
case 'vmess':
return VmessCipher
default:
return []
}
}
getConfigListFromType (type) {
switch (type) {
case 'ss':
return SsProxyConfigList
case 'vmess':
return VmessProxyConfigList
case 'socks5':
return Socks5ProxyConfigList
default:
return []
}
}
renderFormItem (key) {
const { t } = this.props
const { config } = this.state
switch (key) {
case 'type':
return (
<ProxyTypeSelector
key={key}
types={ProxyType}
label={t('editDialog.type')}
value={config.type}
onSelect={value => this.handleConfigChange('type', value)}
/>
)
case 'name':
return (
<ProxyInputForm
key={key}
label={t('editDialog.name')}
value={config.name}
onChange={value => this.handleConfigChange('name', value)}
/>
)
case 'server':
return (
<ProxyInputForm
key={key}
label={t('editDialog.server')}
value={config.server}
onChange={value => this.handleConfigChange('server', value)}
/>
)
case 'port':
return (
<ProxyInputForm
key={key}
label={t('editDialog.port')}
value={config.port ? config.port.toString() : ''}
onChange={value => this.handleConfigChange('port', +value)}
/>
)
case 'password':
return (
<ProxyPasswordForm
key={key}
label={t('editDialog.password')}
value={config.password}
onChange={value => this.handleConfigChange('password', value)}
/>
)
case 'cipher':
return (
<ProxyCipherSelector
key={key}
ciphers={this.getCipherFromType(config.type)}
label={t('editDialog.cipher')}
value={pickCipherWithAlias(config.cipher)}
onSelect={value => this.handleConfigChange('cipher', value)}
/>
)
case 'obfs':
return (
<ProxyInputForm
label={t('editDialog.obfs')}
value={config.obfs}
onChange={value => this.handleConfigChange('obfs', value)}
/>
)
case 'obfs-host':
return (
<ProxyInputForm
key={key}
label={t('editDialog.obfs-host')}
value={config['obfs-host']}
onChange={value => this.handleConfigChange('obfs-host', value)}
/>
)
case 'uuid':
return (
<ProxyInputForm
key={key}
label={t('editDialog.uuid')}
value={config.uuid}
onChange={value => this.handleConfigChange('uuid', value)}
/>
)
case 'alterid':
return (
<ProxyInputForm
key={key}
label={t('editDialog.alterid')}
value={config.alterid ? config.alterid.toString() : ''}
onChange={value => this.handleConfigChange('alterid', +value)}
/>
)
case 'tls':
return (
<ProxyInputForm
key={key}
label={t('editDialog.tls')}
value={config.tls ? config.tls.toString() : ''}
onChange={value => this.handleConfigChange('tls', !!value)}
/>
)
default:
return null
}
}
render () {
const { onCancel, t } = this.props
const { currentColor } = this.state
const { currentColor, config } = this.state
const { type } = config
const configList = this.getConfigListFromType(type)
return <Modal
className="proxy-editor"
@ -50,25 +208,14 @@ class RawDialog extends React.Component<ModifyProxyDialogProps, ModifyProxyDialo
onOk={this.handleOk}
onClose={onCancel}
>
<Row gutter={24} style={{ padding: '12px 0' }}>
<Col span={6} style={{ paddingLeft: 0 }}>{t('editDialog.color')}</Col>
<Col span={18}>
<div className="proxy-editor-color-selector">
{
TagColors.map(color => (
<span
className={classnames('color-item', {
'color-item-active': currentColor === color
})}
key={color}
style={{ background: color }}
onClick={() => this.setState({ currentColor: color })}
/>
))
}
</div>
</Col>
</Row>
<ProxyColorSelector
colors={TagColors}
value={currentColor}
onSelect={color => this.setState({ currentColor: color })}
/>
{
configList.map(c => this.renderFormItem(c))
}
</Modal>
}
}

View File

@ -1,13 +1,35 @@
@import '~@styles/variables';
.proxy-editor {
.proxy-editor-row {
padding: 5px 0;
.proxy-editor-label {
padding-left: 0;
line-height: 30px;
}
.proxy-editor-value {
position: relative;
.proxy-editor-passsword-icon {
position: absolute;
right: 15px;
top: 4px;
cursor: pointer;
color: $color-primary-darken;
user-select: none;
}
}
}
.proxy-editor-color-selector {
display: flex;
align-items: center;
.color-item {
position: relative;
margin-right: 10px;
margin-right: 20px;
width: 16px;
height: 16px;
border-radius: 50%;

View File

@ -13,6 +13,7 @@ interface ProxiesProps extends BaseRouterProps, I18nProps {}
interface ProxiesState {
showModifyProxyDialog: boolean
activeConfig?: IProxy
activeConfigIndex?: number
}
@inject(...storeKeys)
@ -21,13 +22,19 @@ class Proxies extends React.Component<ProxiesProps, ProxiesState> {
state = {
showModifyProxyDialog: false,
activeConfig: null
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 })
}
render () {
const { t, config } = this.props
const { showModifyProxyDialog, activeConfig } = this.state
@ -46,7 +53,8 @@ class Proxies extends React.Component<ProxiesProps, ProxiesState> {
<li key={index}>
<Proxy config={p} onEdit={() => this.setState({
showModifyProxyDialog: true,
activeConfig: p
activeConfig: p,
activeConfigIndex: index
})} />
</li>
))
@ -61,11 +69,8 @@ class Proxies extends React.Component<ProxiesProps, ProxiesState> {
{
showModifyProxyDialog && <ModifyProxyDialog
config={activeConfig}
onOk={config => {
console.log(config)
this.setState({ showModifyProxyDialog: false, activeConfig: null })
}}
onCancel={() => this.setState({ showModifyProxyDialog: false, activeConfig: null })}
onOk={this.handleConfigApply}
onCancel={() => this.setState({ showModifyProxyDialog: false, activeConfig: null, activeConfigIndex: -1 })}
/>
}
</div>

View File

@ -144,7 +144,12 @@ class Rules extends React.Component<RulesProps, RulesState> {
isFinal
? rule.type
: (
<Select key={index} value={rule.type} onSelect={type => this.handleModifyType(index, type)}>
<Select
key={index}
value={rule.type}
onSelect={type => this.handleModifyType(index, type)}
style={{ flex: 1 }}
>
{
Object.keys(RuleType)
.filter(type => type !== 'FINAL')

View File

@ -66,7 +66,7 @@ export async function getProxyDelay (name: string) {
const req = await getInstance()
return req.get<{ delay: number }>(`proxies/${name}/delay`, {
params: {
timeout: 2000,
timeout: 20000,
url: 'http://www.gstatic.com/generate_204'
}
})

57
src/models/Cipher.ts Normal file
View File

@ -0,0 +1,57 @@
/**
* ss ciphers which clash supported
* @see https://github.com/Dreamacro/go-shadowsocks2/blob/master/core/cipher.go
*/
export const SsCipher = [
// AEAD ciphers
'AEAD_AES_128_GCM',
'AEAD_AES_192_GCM',
'AEAD_AES_256_GCM',
'AEAD_CHACHA20_POLY1305',
'AEAD_XCHACHA20_POLY1305',
// stream ciphers
'RC4-MD5',
'AES-128-CTR',
'AES-192-CTR',
'AES-256-CTR',
'AES-128-CFB',
'AES-192-CFB',
'AES-256-CFB',
'CHACHA20',
'CHACHA20-IETF',
'XCHACHA20'
]
/**
* vmess ciphers which clash supported
* @see https://github.com/Dreamacro/clash/blob/master/component/vmess/vmess.go#L34
*/
export const VmessCipher = [
'auto',
'none',
'aes-128-gcm',
'chacha20-poly1305'
]
/**
* pickCipherWithAlias returns a cipher of the given name.
*/
export function pickCipherWithAlias (c: string) {
const cipher = c.toUpperCase()
switch (cipher) {
case 'CHACHA20-IETF-POLY1305':
return 'AEAD_CHACHA20_POLY1305'
case 'XCHACHA20-IETF-POLY1305':
return 'AEAD_XCHACHA20_POLY1305'
case 'AES-128-GCM':
return 'AEAD_AES_128_GCM'
case 'AES-196-GCM':
return 'AEAD_AES_196_GCM'
case 'AES-256-GCM':
return 'AEAD_AES_256_GCM'
}
return SsCipher.find(c => c === cipher) || ''
}

View File

@ -2,14 +2,17 @@
* proxy config interface
*/
export enum ProxyType {
Shadowsocks = 'Shadowsocks',
Vmess = 'Vmess',
Socks5 = 'Socks5'
export const ProxyType = {
Shadowsocks: 'ss',
Vmess: 'vmess',
Socks5: 'socks5'
}
export type Proxy = ShadowsocksProxy | VmessProxy | Socks5Proxy
export type Proxy = ShadowsocksProxy & VmessProxy & Socks5Proxy
export const SsProxyConfigList = [
'name', 'type', 'server', 'port', 'cipher', 'password', 'obfs', 'obfs-host'
]
export interface ShadowsocksProxy {
name?: string
@ -29,6 +32,9 @@ export interface ShadowsocksProxy {
}
export const VmessProxyConfigList = [
'name', 'type', 'server', 'port', 'uuid', 'alterid', 'cipher', 'tls'
]
export interface VmessProxy {
name?: string
@ -48,6 +54,7 @@ export interface VmessProxy {
}
export const Socks5ProxyConfigList = ['name', 'type', 'server', 'port']
export interface Socks5Proxy {
name?: string
@ -59,7 +66,7 @@ export interface Socks5Proxy {
}
export type ProxyGroup = SelectProxyGroup | UrlTestProxyGroup | FallbackProxyGroup
export type ProxyGroup = SelectProxyGroup & UrlTestProxyGroup & FallbackProxyGroup
export interface SelectProxyGroup {
name?: string

View File

@ -4,3 +4,4 @@ export * from './Proxy'
export * from './Rule'
export * from './I18n'
export * from './TagColors'
export * from './Cipher'

View File

@ -81,6 +81,7 @@ export class ConfigStore {
}
}
}
@action
async updateConfig () {
const { general, proxy, proxyGroup, rules } = this.config
@ -103,4 +104,32 @@ export class ConfigStore {
// console.log(data)
jsBridge.writeConfigWithString(data)
}
@action
async modifyProxyByIndexAndSave (index: number, config: Models.Proxy) {
const { proxy } = this.config
const fomatedConfig: Models.Proxy = {}
const { type } = config
let configList: string[] = []
switch (type) {
case 'ss':
configList = Models.SsProxyConfigList
break
case 'vmess':
configList = Models.VmessProxyConfigList
break
case 'socks5':
configList = Models.Socks5ProxyConfigList
break
}
for (const configKey of configList) {
fomatedConfig[configKey] = config[configKey]
}
proxy[index] = fomatedConfig
await this.updateConfig()
await this.fetchAndParseConfig()
}
}

View File

@ -6,7 +6,7 @@
@font-face {
font-family: "clash-iconfont";
src: url('//at.alicdn.com/t/font_841708_7ge2gqse7qv.ttf') format('truetype');
src: url('//at.alicdn.com/t/font_841708_e9dax11p22i.ttf') format('truetype');
}
.clash-iconfont {
@ -39,3 +39,7 @@
.icon-info-o::before { content: "\e60c"; }
.icon-setting::before { content: "\e60d"; }
.icon-show::before { content: "\e60e"; }
.icon-hide::before { content: "\e60f"; }