Add: external controller setting dialog

This commit is contained in:
Jason 2018-10-08 20:46:29 +08:00
parent b60ebfe75f
commit 44a1fdf449
16 changed files with 596 additions and 18 deletions

16
package-lock.json generated
View File

@ -1762,7 +1762,7 @@
},
"browserify-aes": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
"resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
"integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
"dev": true,
"requires": {
@ -1799,7 +1799,7 @@
},
"browserify-rsa": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
"resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
"integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
"dev": true,
"requires": {
@ -2531,7 +2531,7 @@
},
"create-hash": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
"dev": true,
"requires": {
@ -2544,7 +2544,7 @@
},
"create-hmac": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
"resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
"integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
"dev": true,
"requires": {
@ -3184,7 +3184,7 @@
},
"diffie-hellman": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
"resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
"integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
"dev": true,
"requires": {
@ -7761,7 +7761,7 @@
},
"buffer": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
"resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
"integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
"dev": true,
"requires": {
@ -8341,7 +8341,7 @@
},
"parse-asn1": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
"resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
"integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==",
"dev": true,
"requires": {
@ -10275,7 +10275,7 @@
},
"sha.js": {
"version": "2.4.11",
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
"resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
"dev": true,
"requires": {

View File

@ -0,0 +1,45 @@
import * as React from 'react'
import classnames from 'classnames'
import { Icon } from '@components'
import { BaseComponentProps } from '@models'
import './style.scss'
interface AlertProps extends BaseComponentProps {
message?: string
type?: 'success' | 'info' | 'warning' | 'error'
inside?: boolean
}
export class Alert extends React.Component<AlertProps, {}> {
static defaultProps: AlertProps = {
message: '',
type: 'info',
inside: false
}
iconMap = {
success: 'check',
info: 'info',
warning: 'info',
error: 'close'
}
render () {
const { message, type, inside, children, className, style } = this.props
return (
<div className={classnames('alert', `alert-${inside ? 'note' : 'box'}-${type}`, className)} style={style}>
<span className="alert-icon">
<Icon type={this.iconMap[type]} size={26} />
</span>
{
message
? <p className="alert-message">{message}</p>
: <div className="alert-message">{children}</div>
}
</div>
)
}
}

View File

@ -0,0 +1,97 @@
@import '~@styles/variables';
$iconSize: 20px;
$borderWidth: 4px;
@mixin box ($color) {
background: linear-gradient(135deg, darken($color, 5%), $color);
box-shadow: 0 2px 8px rgba($color: darken($color, 5%), $alpha: 0.3);
.alert-icon > i {
color: $color;
}
}
@mixin note ($color) {
background: rgba($color: $color, $alpha: 0.05);
border-radius: 1px 4px 4px 1px;
border-left: 2px solid $color;
box-shadow: 0 2px 8px rgba($color: darken($color, 5%), $alpha: 0.3);
.alert-icon {
background: $color;
> i {
color: $color-white;
}
}
.alert-message {
color: darken($color: $color, $amount: 20%);
}
}
.alert {
padding: 15px;
background: $color-white;
border-radius: 4px;
box-shadow: 0 2px 8px rgba($color: $color-primary-dark, $alpha: 0.3);
font-size: 13px;
line-height: 1.6;
text-align: justify;
display: flex;
.alert-icon {
margin-right: 10px;
width: $iconSize;
height: $iconSize;
border-radius: 50%;
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
background: $color-white;
> i {
transform: scale(0.5);
font-weight: bold;
}
}
.alert-message {
width: 100%;
color: $color-white;
}
}
.alert-box-success {
@include box($color-green);
}
.alert-box-info {
@include box($color-primary);
}
.alert-box-warning {
@include box($color-orange);
}
.alert-box-error {
@include box($color-red);
}
.alert-note-success {
@include note($color-green);
}
.alert-note-info {
@include note($color-primary);
}
.alert-note-warning {
@include note($color-orange);
}
.alert-note-error {
@include note($color-red);
}

View File

@ -0,0 +1,30 @@
import * as React from 'react'
import classnames from 'classnames'
import { BaseComponentProps } from '@models'
import './style.scss'
interface ButtonProps extends BaseComponentProps {
type?: 'primary' | 'normal' | 'danger' | 'success' | 'warning'
onClick?: React.MouseEventHandler<HTMLButtonElement>
}
export class Button extends React.Component<ButtonProps, {}> {
static defaultProps: ButtonProps = {
type: 'normal',
onClick: () => {}
}
render () {
const { type, onClick, children, className, style } = this.props
return (
<button
className={classnames('button', `button-${type}`, className)}
style={style}
onClick={onClick}
>{children}</button>
)
}
}

View File

@ -0,0 +1,88 @@
@import '~@styles/variables';
.button {
outline: 0;
padding: 0 15px;
height: 32px;
line-height: 32px;
border-radius: 16px;
font-size: 14px;
cursor: pointer;
transition: all 150ms ease;
}
.button-primary {
color: $color-white;
border: none;
background: linear-gradient(135deg, $color-primary, $color-primary-dark);
box-shadow: 0 2px 8px rgba($color: $color-primary-dark, $alpha: 0.5);
&:hover {
border: none;
}
&:active {
box-shadow: 0 0 2px rgba($color: $color-primary-dark, $alpha: 0.5);
}
}
.button-normal {
color: $color-gray-darken;
background: $color-white;
border: 1px solid rgba($color: $color-black, $alpha: 0.1);
&:hover {
border-color: $color-gray-dark;
color: $color-primary-darken;
}
&:active {
background: darken($color-white, 2%);
color: $color-primary-darken;
}
}
.button-danger {
color: $color-white;
border: none;
background: linear-gradient(135deg, $color-red, darken($color-red, 10%));
box-shadow: 0 2px 8px rgba($color: darken($color-red, 10%), $alpha: 0.5);
&:hover {
border: none;
}
&:active {
box-shadow: 0 0 2px rgba($color: darken($color-red, 10%), $alpha: 0.5);
}
}
.button-success {
color: $color-white;
border: none;
background: linear-gradient(135deg, $color-green, darken($color-green, 5%));
box-shadow: 0 2px 8px rgba($color: darken($color-green, 5%), $alpha: 0.5);
&:hover {
border: none;
}
&:active {
box-shadow: 0 0 2px rgba($color: darken($color-green, 5%), $alpha: 0.5);
}
}
.button-warning {
color: $color-white;
border: none;
background: linear-gradient(135deg, $color-orange, darken($color-orange, 5%));
box-shadow: 0 2px 8px rgba($color: darken($color-orange, 5%), $alpha: 0.5);
&:hover {
border: none;
}
&:active {
box-shadow: 0 0 2px rgba($color: darken($color-orange, 5%), $alpha: 0.5);
}
}

View File

@ -0,0 +1,105 @@
import * as React from 'react'
import classnames from 'classnames'
import { createPortal } from 'react-dom'
import { BaseComponentProps } from '@models'
import { Button } from '@components'
import './style.scss'
const noop = () => {}
interface ModalProps extends BaseComponentProps {
// show modal
show?: boolean
// modal title
title: string
// size
size?: 'small' | 'big'
// body className
bodyClassName?: string
// body style
bodyStyle?: React.CSSProperties
// show footer
footer?: boolean
// on click ok
onOk?: typeof noop
// on click close
onClose?: typeof noop
}
export class Modal extends React.Component<ModalProps, {}> {
static defaultProps: ModalProps = {
show: true,
title: 'Modal',
size: 'small',
footer: true,
onOk: noop,
onClose: noop
}
// portal container
$container: Element
$modal = React.createRef<HTMLDivElement>()
constructor (props) {
super(props)
// create container element
const container = document.createElement('div')
document.body.appendChild(container)
this.$container = container
}
componentWillUnmount () {
document.body.removeChild(this.$container)
}
private handleMaskClick = (e) => {
const { onClose } = this.props
const el = this.$modal.current
if (el && !el.contains(e.target)) {
onClose()
}
}
render () {
const { show, size, title, footer, children, className, bodyClassName, style, bodyStyle, onOk, onClose } = this.props
const modal = (
<div
className={classnames('modal-mask', { 'modal-show': show })}
onClick={this.handleMaskClick}
>
<div
className={classnames('modal', `modal-${size}`, className)}
style={style}
ref={this.$modal}
>
<div className="modal-title">{title}</div>
<div
className={classnames('modal-body', bodyClassName)}
style={bodyStyle}
>{children}</div>
{
footer && (
<div className="footer">
<Button onClick={() => onClose()}> </Button>
<Button type="primary" onClick={() => onOk()}> </Button>
</div>
)
}
</div>
</div>
)
return createPortal(modal, this.$container)
}
}

View File

@ -0,0 +1,78 @@
@import '~@styles/variables';
$width: 400px;
$bigWidth: 600px;
.modal-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba($color: $color-black, $alpha: 0.15);
opacity: 0;
pointer-events: none;
transition: all 500ms ease;
display: flex;
justify-content: center;
align-items: center;
.modal {
margin-top: -50px;
padding: 20px 30px;
background: $color-white;
box-shadow: 0 2px 16px rgba($color: $color-primary-darken, $alpha: 0.2);
border-radius: 4px;
display: flex;
flex-direction: column;
transform: scale(0);
transition: all 300ms cubic-bezier(0.32, 0.26, 0.71, 1.29);
.modal-title {
margin: 5px 0;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
font-weight: bold;
font-size: 18px;
color: $color-primary-dark;
text-shadow: 0 2px 6px rgba($color: $color-primary-dark, $alpha: 0.4);
}
.modal-body {
margin: 10px 0;
font-size: 14px;
color: $color-primary-darken;
}
.footer {
width: 100%;
margin: 5px 0;
display: flex;
align-items: center;
justify-content: flex-end;
.button {
margin-left: 10px;
}
}
}
.modal-small {
width: $width;
}
.modal-big {
width: $bigWidth;
}
}
.modal-show {
opacity: 1;
pointer-events: visible;
.modal {
transform: scale(1);
}
}

View File

@ -8,3 +8,6 @@ export * from './ButtonSelect'
export * from './Tags'
export * from './Input'
export * from './Select'
export * from './Modal'
export * from './Alert'
export * from './Button'

View File

@ -0,0 +1,71 @@
import * as React from 'react'
import { Modal, Input, Row, Col, Alert } from '@components'
import './style.scss'
interface ExternalControllerDrawerProps {
show: boolean
host: string
port: string
onConfirm: (host: string, port: string) => void
onCancel: () => void
}
interface ExternalControllerDrawerState {
host: string,
port: string
}
export class ExternalControllerDrawer extends React.Component<ExternalControllerDrawerProps, ExternalControllerDrawerState> {
state = {
host: this.props.host,
port: this.props.port
}
private handleOk = () => {
const { onConfirm } = this.props
const { host, port } = this.state
onConfirm(host, port)
}
render () {
const { show, onCancel } = this.props
const { host, port } = this.state
return (
<Modal
show={show}
title="编辑外部控制设置"
bodyClassName="external-controller"
onClose={onCancel}
onOk={this.handleOk}
>
<Alert type="info" inside={true}>
<p> Clash Clash Dashboard </p>
</Alert>
<Row gutter={24} align="middle">
<Col span={4} className="title">Host</Col>
<Col span={20} className="form">
<Input
align="left"
inside={true}
value={host}
onChange={host => this.setState({ host })}
/>
</Col>
</Row>
<Row gutter={24} align="middle">
<Col span={4} className="title"></Col>
<Col span={20} className="form">
<Input
align="left"
inside={true}
value={port}
onChange={port => this.setState({ port })}
/>
</Col>
</Row>
</Modal>
)
}
}

View File

@ -0,0 +1,22 @@
@import '~@styles/variables';
.external-controller {
.row {
padding: 0;
}
.alert {
margin: 10px 0;
}
.title,
.form {
margin: 15px 0;
}
.title {
margin-top: 15px;
font-size: 14px;
font-weight: bold;
}
}

View File

@ -0,0 +1 @@
export * from './ExternalControllerDrawer'

View File

@ -1,7 +1,8 @@
import * as React from 'react'
import { Header, Card, Row, Col, Switch, ButtonSelect, ButtonSelectOptions, Input, Icon } from '@components'
import { translate } from 'react-i18next'
import { changeLanguage } from 'i18next'
import { Header, Card, Row, Col, Switch, ButtonSelect, ButtonSelectOptions, Input, Icon } from '@components'
import { ExternalControllerDrawer } from './components'
import { I18nProps } from '@models'
import './style.scss'
@ -14,7 +15,9 @@ class Settings extends React.Component<I18nProps, {}> {
proxyMode: 'Rule',
socks5ProxyPort: 7891,
httpProxyPort: 7890,
externalController: '127.0.0.1:7892'
externalControllerHost: '127.0.0.1',
externalControllerPort: '7892',
showEditDrawer: false
}
languageOptions: ButtonSelectOptions[] = [
@ -35,7 +38,9 @@ class Settings extends React.Component<I18nProps, {}> {
proxyMode,
socks5ProxyPort,
httpProxyPort,
externalController
externalControllerHost,
externalControllerPort,
showEditDrawer
} = this.state
const proxyModeOptions: ButtonSelectOptions[] = [
{ label: t('values.global'), value: 'Global' },
@ -116,11 +121,15 @@ class Settings extends React.Component<I18nProps, {}> {
<Col span={3} offset={2}>
<Input value={httpProxyPort} onChange={httpProxyPort => this.setState({ httpProxyPort })}></Input>
</Col>
<Col span={5} offset={1}>
<Col span={4} offset={1}>
<span className="label">{t('labels.externalController')}</span>
</Col>
<Col span={5} offset={1}>
<Input value={externalController} ></Input>
<Col className="external-controller" span={6} offset={1}>
<span>{`${externalControllerHost}:${externalControllerPort}`}</span>
<span
className="modify-btn"
onClick={() => this.setState({ showEditDrawer: true })}
></span>
</Col>
</Row>
</Card>
@ -132,6 +141,16 @@ class Settings extends React.Component<I18nProps, {}> {
<p className="version-info">{t('versionString', { version: 'unknown' })}</p>
<span className="check-update-btn">{t('checkUpdate')}</span>
</Card>
<ExternalControllerDrawer
show={showEditDrawer}
host={externalControllerHost}
port={externalControllerPort}
onConfirm={(host, port) => console.log(host, port)}
onCancel={() => this.setState({ showEditDrawer: false })}
>
<div>666666</div>
</ExternalControllerDrawer>
</div>
)
}

View File

@ -17,6 +17,22 @@
font-size: 14px;
color: $color-primary-darken;
}
.external-controller {
font-size: 14px;
color: $color-primary-darken;
display: flex;
justify-content: flex-end;
font-weight: normal;
line-height: 17px;
.modify-btn {
margin-left: 5px;
font-size: 12px;
color: $color-primary-dark;
cursor: pointer;
}
}
}
.clash-version {

View File

@ -4,7 +4,7 @@
// styles initial
html {
box-sizing: border-box;
background: rgba($color: $color-white, $alpha: 0.9);
background: rgba($color: $color-white, $alpha: 0.8);
}
*,

View File

@ -6,7 +6,7 @@
@font-face {
font-family: "clash-iconfont";
src: url('//at.alicdn.com/t/font_841708_2viqaiy9h37.ttf') format('truetype');
src: url('//at.alicdn.com/t/font_841708_h6oakuryxxb.ttf') format('truetype');
}
.clash-iconfont {
@ -20,8 +20,6 @@
.icon-close::before { content: "\e602"; }
.icon-info::before { content: "\e603"; }
.icon-drag::before { content: "\e604"; }
.icon-down-arrow-o::before { content: "\e605"; }
@ -35,3 +33,7 @@
.icon-triangle-down::before { content: "\e609"; }
.icon-up-arrow-o::before { content: "\e60a"; }
.icon-info::before { content: "\e60b"; }
.icon-info-o::before { content: "\e60c"; }

View File

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