Feature: mobile support

This commit is contained in:
Dreamacro 2019-06-12 17:55:57 +08:00
parent 64d612dd53
commit 293e484f1d
13 changed files with 286 additions and 79 deletions

View File

@ -23,3 +23,9 @@
display: flex; display: flex;
align-items: center; align-items: center;
} }
@media (max-width: 768px) {
.header > h1 {
font-size: 20px;
}
}

View File

@ -3,6 +3,9 @@
$width: 400px; $width: 400px;
$bigWidth: 600px; $bigWidth: 600px;
$mobileWidth: 280px;
$mobileBigWidth: 480px;
.modal-mask { .modal-mask {
position: fixed; position: fixed;
top: 0; top: 0;
@ -76,3 +79,15 @@ $bigWidth: 600px;
transform: scale(1); transform: scale(1);
} }
} }
@media (max-width: 768px) {
.modal-mask {
.modal-small {
width: $mobileWidth;
}
.modal-big {
width: $mobileBigWidth;
}
}
}

View File

@ -28,12 +28,12 @@ export default class App extends React.Component<AppProps, {}> {
// { path: '/', name: 'Overview', component: Overview, exact: true }, // { path: '/', name: 'Overview', component: Overview, exact: true },
{ path: '/proxies', name: 'Proxies', component: Proxies }, { path: '/proxies', name: 'Proxies', component: Proxies },
{ path: '/logs', name: 'Logs', component: Logs }, { path: '/logs', name: 'Logs', component: Logs },
{ path: '/rules', name: 'Rules', component: Rules }, { path: '/rules', name: 'Rules', component: Rules, noMobile: true },
{ path: '/settings', name: 'Settings', component: Settings } { path: '/settings', name: 'Settings', component: Settings }
] ]
return ( return (
<div className={classnames('app', { 'clash-x': !isClashX() })}> <div className={classnames('app', { 'not-clashx': !isClashX() })}>
<SlideBar routes={routes} /> <SlideBar routes={routes} />
<div className="page-container"> <div className="page-container">
<Route exact path="/" component={() => <Redirect to="/proxies"/>}/> <Route exact path="/" component={() => <Redirect to="/proxies"/>}/>

View File

@ -20,3 +20,31 @@
font-weight: bold; font-weight: bold;
} }
} }
@media (max-width: 768px) {
.external-controller {
// hack <Row />
.row {
flex-direction: column;
align-items: flex-start;
}
.title {
margin: 5px 0;
}
.form {
margin: 5px 0;
}
// hack <Col />
.column {
width: 100%;
}
// hack <Alert />
.alert {
display: none;
}
}
}

View File

@ -16,6 +16,7 @@
.proxy-group-name { .proxy-group-name {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap;
padding: 0 20px; padding: 0 20px;
width: 120px; width: 120px;
} }
@ -51,3 +52,32 @@
flex: 1; flex: 1;
margin-left: 30px; margin-left: 30px;
} }
@media (max-width: 768px) {
.proxy-group {
flex-direction: column;
}
.proxy-group-name {
width: 140px;
}
.proxy-group-part {
width: 100%;
height: 42px;
margin-top: 6px;
justify-content: space-between;
}
.proxy-group-type {
margin-right: 20px;
}
.proxy-group-tags-container {
padding: 5px 0 10px;
}
.proxy-group-tags {
margin-left: 20px;
}
}

View File

@ -64,3 +64,20 @@
background-color: $color-gray-darken; background-color: $color-gray-darken;
} }
} }
@media (max-width: 768px) {
.proxy-item {
$height: 70px;
height: $height;
.proxy-delay {
left: unset;
bottom: unset;
top: 0;
right: 20px;
height: $height;
line-height: $height;
}
}
}

View File

@ -37,10 +37,6 @@
} }
@include response(xxs) { @include response(xxs) {
--columns: 4;
}
@include response(tiny) {
--columns: 3; --columns: 3;
} }
@ -68,3 +64,22 @@
text-shadow: 0 2px 6px rgba($color: $color-primary-dark, $alpha: 0.4); text-shadow: 0 2px 6px rgba($color: $color-primary-dark, $alpha: 0.4);
cursor: pointer; cursor: pointer;
} }
@media (max-width: 768px) {
.proxies-group-card {
margin: 12px 0;
}
.proxies-list {
margin-right: 0;
padding-bottom: 20px;
flex-wrap: unset;
flex-direction: column;
> li {
width: 100%;
margin-right: 0;
margin-bottom: 10px;
}
}
}

View File

@ -112,70 +112,83 @@ class Settings extends React.Component<SettingProps, {}> {
<Header title={t('title')} /> <Header title={t('title')} />
<Card className="settings-card"> <Card className="settings-card">
<Row gutter={24} align="middle"> <Row gutter={24} align="middle">
<Col span={6} offset={1}> <Col span={12}>
<Col span={14} offset={1}>
<span className="label">{t('labels.startAtLogin')}</span> <span className="label">{t('labels.startAtLogin')}</span>
</Col> </Col>
<Col span={4} className="value-column"> <Col span={8} className="value-column">
<Switch disabled={!isClashX} checked={startAtLogin} onChange={this.handleStartAtLoginChange} /> <Switch disabled={!isClashX} checked={startAtLogin} onChange={this.handleStartAtLoginChange} />
</Col> </Col>
<Col span={4} offset={1}> </Col>
<Col span={12}>
<Col span={8} offset={1}>
<span className="label">{t('labels.language')}</span> <span className="label">{t('labels.language')}</span>
</Col> </Col>
<Col span={7} className="value-column"> <Col span={14} className="value-column">
<ButtonSelect options={this.languageOptions} value={lng.replace(/-.+$/, '')} onSelect={this.changeLanguage} /> <ButtonSelect options={this.languageOptions} value={lng.replace(/-.+$/, '')} onSelect={this.changeLanguage} />
</Col> </Col>
</Col>
</Row> </Row>
<Row gutter={24} align="middle"> <Row gutter={24} align="middle">
<Col span={6} offset={1}> <Col span={12}>
<Col span={14} offset={1}>
<span className="label">{t('labels.setAsSystemProxy')}</span> <span className="label">{t('labels.setAsSystemProxy')}</span>
</Col> </Col>
<Col span={4} className="value-column"> <Col span={8} className="value-column">
<Switch <Switch
disabled={!isClashX} disabled={!isClashX}
checked={systemProxy} checked={systemProxy}
onChange={this.handleSetSystemProxy} onChange={this.handleSetSystemProxy}
/> />
</Col> </Col>
<Col span={7} offset={1}> </Col>
<Col span={12}>
<Col span={8} offset={1}>
<span className="label">{t('labels.allowConnectFromLan')}</span> <span className="label">{t('labels.allowConnectFromLan')}</span>
</Col> </Col>
<Col span={4} className="value-column"> <Col span={14} className="value-column">
<Switch <Switch
checked={allowLan} checked={allowLan}
onChange={this.handleAllowLanChange} onChange={this.handleAllowLanChange}
/> />
</Col> </Col>
</Col>
</Row> </Row>
</Card> </Card>
<Card className="settings-card"> <Card className="settings-card">
<Row gutter={24} align="middle"> <Row gutter={24} align="middle">
<Col span={3} offset={1}> <Col span={12}>
<Col span={8} offset={1}>
<span className="label">{t('labels.proxyMode')}</span> <span className="label">{t('labels.proxyMode')}</span>
</Col> </Col>
<Col span={7} className="value-column"> <Col span={14} className="value-column">
<ButtonSelect <ButtonSelect
options={proxyModeOptions} options={proxyModeOptions}
value={mode} value={mode}
onSelect={this.handleProxyModeChange} onSelect={this.handleProxyModeChange}
/> />
</Col> </Col>
<Col span={5} offset={1}> </Col>
<Col span={12}>
<Col span={14} offset={1}>
<span className="label">{t('labels.socks5ProxyPort')}</span> <span className="label">{t('labels.socks5ProxyPort')}</span>
</Col> </Col>
<Col span={3} offset={3}> <Col span={8}>
<Input <Input
value={socks5ProxyPort} value={socks5ProxyPort}
onChange={socks5ProxyPort => this.setState({ socks5ProxyPort: parseInt(socks5ProxyPort, 10) })} onChange={socks5ProxyPort => this.setState({ socks5ProxyPort: parseInt(socks5ProxyPort, 10) })}
onBlur={this.handleSocksPortSave} onBlur={this.handleSocksPortSave}
/> />
</Col> </Col>
</Col>
</Row> </Row>
<Row gutter={24} align="middle"> <Row gutter={24} align="middle">
<Col span={5} offset={1}> <Col span={12}>
<Col span={14} offset={1}>
<span className="label">{t('labels.httpProxyPort')}</span> <span className="label">{t('labels.httpProxyPort')}</span>
</Col> </Col>
<Col span={3} offset={2}> <Col span={8}>
<Input <Input
type="number" type="number"
value={httpProxyPort} value={httpProxyPort}
@ -183,15 +196,18 @@ class Settings extends React.Component<SettingProps, {}> {
onBlur={this.handleHttpPortSave} onBlur={this.handleHttpPortSave}
/> />
</Col> </Col>
<Col span={4} offset={1}> </Col>
<Col span={12}>
<Col span={8} offset={1}>
<span className="label">{t('labels.externalController')}</span> <span className="label">{t('labels.externalController')}</span>
</Col> </Col>
<Col className="external-controller" span={6} offset={1}> <Col className="external-controller" span={14}>
<span>{`${externalControllerHost}:${externalControllerPort}`}</span> <span>{`${externalControllerHost}:${externalControllerPort}`}</span>
<span className="modify-btn" onClick={() => this.props.store.setShowAPIModal(true)}> <span className="modify-btn" onClick={() => this.props.store.setShowAPIModal(true)}>
</span> </span>
</Col> </Col>
</Col>
</Row> </Row>
</Card> </Card>

View File

@ -78,3 +78,19 @@
} }
} }
} }
@media (max-width: 768px) {
.settings-card {
// hack <Row />
.row {
flex-direction: column;
padding-top: 0;
padding-bottom: 0;
> .column {
width: 100%;
margin: 10px 0;
}
}
}
}

View File

@ -1,6 +1,7 @@
import * as React from 'react' import * as React from 'react'
import { NavLink } from 'react-router-dom' import { NavLink } from 'react-router-dom'
import { translate } from 'react-i18next' import { translate } from 'react-i18next'
import classnames from 'classnames'
import { I18nProps } from '@models' import { I18nProps } from '@models'
import './style.scss' import './style.scss'
@ -10,6 +11,7 @@ interface SidebarProps extends I18nProps {
routes: { routes: {
path: string path: string
name: string name: string
noMobile?: boolean
exact?: boolean exact?: boolean
}[] }[]
} }
@ -23,8 +25,8 @@ class Sidebar extends React.Component<SidebarProps, {}> {
<ul className="sidebar-menu"> <ul className="sidebar-menu">
{ {
routes.map( routes.map(
({ path, name, exact }) => ( ({ path, name, exact, noMobile }) => (
<li className="item" key={name}> <li className={classnames('item', { 'no-mobile': noMobile })} key={name}>
<NavLink to={path} activeClassName="active" exact={!!exact}>{ t(name) }</NavLink> <NavLink to={path} activeClassName="active" exact={!!exact}>{ t(name) }</NavLink>
</li> </li>
) )

View File

@ -51,3 +51,46 @@
} }
} }
} }
@media (max-width: 768px) {
.sidebar {
width: 100%;
height: 60px;
flex-direction: row;
background: $background;
z-index: 10;
}
.sidebar-logo {
margin: 0 15px;
width: 36px;
height: 36px;
}
.sidebar-menu {
flex: 1;
flex-direction: row;
justify-content: center;
margin-top: 0;
overflow-x: scroll;
padding: 10px;
&::-webkit-scrollbar {
display: none;
}
.item {
margin-top: 0;
> a {
width: 80px;
height: 32px;
line-height: 32px;
}
}
.item.no-mobile {
display: none;
}
}
}

View File

@ -37,8 +37,8 @@ body {
padding-left: 150px; padding-left: 150px;
} }
.app.clash-x { .app.not-clashx {
background: #f4f5f6; background: $background;
} }
.page-container { .page-container {
@ -49,9 +49,7 @@ body {
} }
.page { .page {
padding: 20px 35px; padding: 20px 35px 30px 0;
padding-left: 0;
padding-bottom: 30px;
width: 100%; width: 100%;
height: 100vh; height: 100vh;
margin: 0 auto; margin: 0 auto;
@ -62,3 +60,25 @@ body {
.container { .container {
margin: 20px 0; margin: 20px 0;
} }
@media (max-width: 768px) {
.app {
padding-left: 0;
padding-top: 60px;
}
.page-container {
width: 100%;
padding: 0 10px;
height: calc(100vh - 60px);
&::-webkit-scrollbar {
display: none;
}
}
.page {
padding: 0 0 20px;
height: 100%;
}
}

View File

@ -3,6 +3,8 @@
* Style Common Variables * Style Common Variables
*/ */
$background: #f4f5f6;
// primary colors // primary colors
$color-primary: #57befc; $color-primary: #57befc;
$color-primary-dark: #2c8af8; $color-primary-dark: #2c8af8;
@ -31,9 +33,6 @@ $color-black: #000;
} }
$breakpoints: ( $breakpoints: (
'tiny': (
max-width: 680px
),
'xxs': ( 'xxs': (
max-width: 760px max-width: 760px
), ),