diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx index dad1daa..bc9e566 100644 --- a/src/components/Input/index.tsx +++ b/src/components/Input/index.tsx @@ -9,6 +9,7 @@ interface InputProps extends BaseComponentProps { align?: 'left' | 'center' | 'right' inside?: boolean autoFocus?: boolean + type?: string onChange?: (value: string, event?: React.ChangeEvent) => void onBlur?: (event?: React.FocusEvent) => void } @@ -20,6 +21,7 @@ export class Input extends React.Component { align: 'center', inside: false, autoFocus: false, + type: 'text', onChange: () => {}, onBlur: () => {} } @@ -32,6 +34,7 @@ export class Input extends React.Component { align, inside, autoFocus, + type, onChange, onBlur } = this.props @@ -42,6 +45,7 @@ export class Input extends React.Component { style={style} value={value} autoFocus={autoFocus} + type={type} onChange={event => onChange(event.target.value, event)} onBlur={onBlur} /> diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index d5f6d6b..88bbb8e 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -49,6 +49,8 @@ export class Modal extends React.Component { $modal = React.createRef() + $mask = React.createRef() + constructor (props) { super(props) @@ -64,9 +66,8 @@ export class Modal extends React.Component { 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 { const modal = (
{ 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 { } 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 { 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 { ref={this.$target} onClick={this.handleShowDropList} > - {matchChild.props.children} + {matchChild && matchChild.props && matchChild.props.children}
{hasCreateDropList && createPortal(dropDownList, this.$container)} diff --git a/src/components/Select/style.scss b/src/components/Select/style.scss index 66e32b0..9fc493b 100644 --- a/src/components/Select/style.scss +++ b/src/components/Select/style.scss @@ -1,7 +1,6 @@ @import '~@styles/variables'; .select { - flex: 1; cursor: pointer; font-size: 14px; line-height: 30px; diff --git a/src/containers/Proxies/components/ModifyProxyDialog/FormItems.tsx b/src/containers/Proxies/components/ModifyProxyDialog/FormItems.tsx new file mode 100644 index 0000000..f0edb50 --- /dev/null +++ b/src/containers/Proxies/components/ModifyProxyDialog/FormItems.tsx @@ -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 ( + + {label} + + + + + ) +} + +// color selector +export function ProxyColorSelector ({ colors, value, onSelect = noop }: { + colors: string[], + value: string, + onSelect?: (color: string) => void +}) { + return ( + +
+ { + colors.map(color => ( + onSelect(color)} + /> + )) + } +
+
+ ) +} + +// input form +export function ProxyInputForm ({ label, value, onChange = noop }: { + label: string, + value: string, + onChange?: (value: string) => void +}) { + return ( + + {label} + + + + + ) +} + +// 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 ( + + {label} + + + this.setState({ showPassword: !showPassword })} + /> + + + ) + } +} + +// cipher selector +export function ProxyCipherSelector ({ ciphers, label, value, onSelect = noop }: { + ciphers: string[], + label: string, + value: string, + onSelect?: (type: string) => void +}) { + return ( + + {label} + + + + + ) +} diff --git a/src/containers/Proxies/components/ModifyProxyDialog/index.tsx b/src/containers/Proxies/components/ModifyProxyDialog/index.tsx index 2eee9df..bea134a 100644 --- a/src/containers/Proxies/components/ModifyProxyDialog/index.tsx +++ b/src/containers/Proxies/components/ModifyProxyDialog/index.tsx @@ -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 { + 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 ( + this.handleConfigChange('type', value)} + /> + ) + case 'name': + return ( + this.handleConfigChange('name', value)} + /> + ) + case 'server': + return ( + this.handleConfigChange('server', value)} + /> + ) + case 'port': + return ( + this.handleConfigChange('port', +value)} + /> + ) + case 'password': + return ( + this.handleConfigChange('password', value)} + /> + ) + case 'cipher': + return ( + this.handleConfigChange('cipher', value)} + /> + ) + case 'obfs': + return ( + this.handleConfigChange('obfs', value)} + /> + ) + case 'obfs-host': + return ( + this.handleConfigChange('obfs-host', value)} + /> + ) + case 'uuid': + return ( + this.handleConfigChange('uuid', value)} + /> + ) + case 'alterid': + return ( + this.handleConfigChange('alterid', +value)} + /> + ) + case 'tls': + return ( + 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 - - {t('editDialog.color')} - -
- { - TagColors.map(color => ( - this.setState({ currentColor: color })} - /> - )) - } -
- -
+ this.setState({ currentColor: color })} + /> + { + configList.map(c => this.renderFormItem(c)) + }
} } diff --git a/src/containers/Proxies/components/ModifyProxyDialog/style.scss b/src/containers/Proxies/components/ModifyProxyDialog/style.scss index 11c2541..6de7ed5 100644 --- a/src/containers/Proxies/components/ModifyProxyDialog/style.scss +++ b/src/containers/Proxies/components/ModifyProxyDialog/style.scss @@ -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%; diff --git a/src/containers/Proxies/index.tsx b/src/containers/Proxies/index.tsx index 41c008a..56f0f41 100644 --- a/src/containers/Proxies/index.tsx +++ b/src/containers/Proxies/index.tsx @@ -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 { 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 {
  • this.setState({ showModifyProxyDialog: true, - activeConfig: p + activeConfig: p, + activeConfigIndex: index })} />
  • )) @@ -61,11 +69,8 @@ class Proxies extends React.Component { { showModifyProxyDialog && { - 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 })} /> }
    diff --git a/src/containers/Rules/index.tsx b/src/containers/Rules/index.tsx index b27eab6..9898a6a 100644 --- a/src/containers/Rules/index.tsx +++ b/src/containers/Rules/index.tsx @@ -144,7 +144,12 @@ class Rules extends React.Component { isFinal ? rule.type : ( - this.handleModifyType(index, type)} + style={{ flex: 1 }} + > { Object.keys(RuleType) .filter(type => type !== 'FINAL') diff --git a/src/lib/request.ts b/src/lib/request.ts index 89706d7..f8b2b94 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -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' } }) diff --git a/src/models/Cipher.ts b/src/models/Cipher.ts new file mode 100644 index 0000000..048b901 --- /dev/null +++ b/src/models/Cipher.ts @@ -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) || '' +} diff --git a/src/models/Proxy.ts b/src/models/Proxy.ts index ae48bd9..6d7cb30 100644 --- a/src/models/Proxy.ts +++ b/src/models/Proxy.ts @@ -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 diff --git a/src/models/index.ts b/src/models/index.ts index 5995a13..7f47b8b 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -4,3 +4,4 @@ export * from './Proxy' export * from './Rule' export * from './I18n' export * from './TagColors' +export * from './Cipher' diff --git a/src/stores/ConfigStore.ts b/src/stores/ConfigStore.ts index f8e4624..22fd4ed 100644 --- a/src/stores/ConfigStore.ts +++ b/src/stores/ConfigStore.ts @@ -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() + } } diff --git a/src/styles/iconfont.scss b/src/styles/iconfont.scss index 2133e68..7f0fee4 100644 --- a/src/styles/iconfont.scss +++ b/src/styles/iconfont.scss @@ -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"; }