feat: initial theming support
This commit is contained in:
parent
a265c62020
commit
3584ff6179
19 changed files with 223 additions and 150 deletions
|
@ -1,29 +1,19 @@
|
||||||
import {
|
import {
|
||||||
getAPIConfig,
|
getURLAndInit,
|
||||||
genCommonHeaders,
|
genCommonHeaders,
|
||||||
getAPIBaseURL
|
getAPIBaseURL
|
||||||
} from 'm/request-helper';
|
} from 'm/request-helper';
|
||||||
|
|
||||||
const endpoint = '/configs';
|
const endpoint = '/configs';
|
||||||
|
|
||||||
function getURLAndInit() {
|
export async function fetchConfigs(apiConfig) {
|
||||||
const c = getAPIConfig();
|
const { url, init } = getURLAndInit(apiConfig);
|
||||||
const baseURL = getAPIBaseURL(c);
|
return await fetch(url + endpoint, init);
|
||||||
const headers = genCommonHeaders(c);
|
|
||||||
return {
|
|
||||||
url: baseURL + endpoint,
|
|
||||||
init: { headers }
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchConfigs() {
|
export async function updateConfigs(apiConfig, o) {
|
||||||
const { url, init } = getURLAndInit();
|
const { url, init } = getURLAndInit(apiConfig);
|
||||||
return await fetch(url, init);
|
return await fetch(url + endpoint, {
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateConfigs(o) {
|
|
||||||
const { url, init } = getURLAndInit();
|
|
||||||
return await fetch(url, {
|
|
||||||
...init,
|
...init,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
// mode: 'cors',
|
// mode: 'cors',
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {
|
import {
|
||||||
getAPIConfig,
|
getURLAndInit,
|
||||||
genCommonHeaders,
|
genCommonHeaders,
|
||||||
getAPIBaseURL
|
getAPIBaseURL
|
||||||
} from 'm/request-helper';
|
} from 'm/request-helper';
|
||||||
|
@ -10,16 +10,6 @@ const getRandomStr = () => {
|
||||||
return Math.floor((1 + Math.random()) * 0x10000).toString(16);
|
return Math.floor((1 + Math.random()) * 0x10000).toString(16);
|
||||||
};
|
};
|
||||||
|
|
||||||
function getURLAndInit() {
|
|
||||||
const c = getAPIConfig();
|
|
||||||
const baseURL = getAPIBaseURL(c);
|
|
||||||
const headers = genCommonHeaders(c);
|
|
||||||
return {
|
|
||||||
url: baseURL + endpoint,
|
|
||||||
init: { headers }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const Size = 300;
|
const Size = 300;
|
||||||
|
|
||||||
let even = false;
|
let even = false;
|
||||||
|
@ -71,11 +61,11 @@ function pump(reader) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchLogs() {
|
function fetchLogs(apiConfig) {
|
||||||
if (store.fetched) return store;
|
if (store.fetched) return store;
|
||||||
store.fetched = true;
|
store.fetched = true;
|
||||||
const { url, init } = getURLAndInit();
|
const { url, init } = getURLAndInit(apiConfig);
|
||||||
fetch(url, init)
|
fetch(url + endpoint, init)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
const reader = response.body.getReader();
|
const reader = response.body.getReader();
|
||||||
pump(reader);
|
pump(reader);
|
||||||
|
|
|
@ -1,20 +1,10 @@
|
||||||
import {
|
import {
|
||||||
getAPIConfig,
|
getURLAndInit,
|
||||||
genCommonHeaders,
|
genCommonHeaders,
|
||||||
getAPIBaseURL
|
getAPIBaseURL
|
||||||
} from 'm/request-helper';
|
} from 'm/request-helper';
|
||||||
const endpoint = '/proxies';
|
const endpoint = '/proxies';
|
||||||
|
|
||||||
function getURLAndInit() {
|
|
||||||
const c = getAPIConfig();
|
|
||||||
const baseURL = getAPIBaseURL(c);
|
|
||||||
const headers = genCommonHeaders(c);
|
|
||||||
return {
|
|
||||||
url: baseURL + endpoint,
|
|
||||||
init: { headers }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
$ curl "http://127.0.0.1:8080/proxies/Proxy" -XPUT -d '{ "name": "ss3" }' -i
|
$ curl "http://127.0.0.1:8080/proxies/Proxy" -XPUT -d '{ "name": "ss3" }' -i
|
||||||
HTTP/1.1 400 Bad Request
|
HTTP/1.1 400 Bad Request
|
||||||
|
@ -32,16 +22,16 @@ Vary: Origin
|
||||||
Date: Tue, 16 Oct 2018 16:38:33 GMT
|
Date: Tue, 16 Oct 2018 16:38:33 GMT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async function fetchProxies() {
|
async function fetchProxies(config) {
|
||||||
const { url, init } = getURLAndInit();
|
const { url, init } = getURLAndInit(config);
|
||||||
const res = await fetch(url, init);
|
const res = await fetch(url + endpoint, init);
|
||||||
return await res.json();
|
return await res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestToSwitchProxy(name1, name2) {
|
async function requestToSwitchProxy(apiConfig, name1, name2) {
|
||||||
const body = { name: name2 };
|
const body = { name: name2 };
|
||||||
const { url, init } = getURLAndInit();
|
const { url, init } = getURLAndInit(apiConfig);
|
||||||
const fullURL = `${url}/${name1}`;
|
const fullURL = `${url}${endpoint}/${name1}`;
|
||||||
return await fetch(fullURL, {
|
return await fetch(fullURL, {
|
||||||
...init,
|
...init,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
@ -49,10 +39,10 @@ async function requestToSwitchProxy(name1, name2) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestDelayForProxy(name) {
|
async function requestDelayForProxy(apiConfig, name) {
|
||||||
const { url, init } = getURLAndInit();
|
const { url, init } = getURLAndInit(apiConfig);
|
||||||
const qs = 'timeout=5000&url=http://www.google.com/generate_204';
|
const qs = 'timeout=5000&url=http://www.google.com/generate_204';
|
||||||
const fullURL = `${url}/${name}/delay?${qs}`;
|
const fullURL = `${url}${endpoint}/${name}/delay?${qs}`;
|
||||||
return await fetch(fullURL, init);
|
return await fetch(fullURL, init);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,11 @@
|
||||||
import {
|
import {
|
||||||
getAPIConfig,
|
getURLAndInit,
|
||||||
genCommonHeaders,
|
genCommonHeaders,
|
||||||
getAPIBaseURL
|
getAPIBaseURL
|
||||||
} from 'm/request-helper';
|
} from 'm/request-helper';
|
||||||
const endpoint = '/traffic';
|
const endpoint = '/traffic';
|
||||||
const textDecoder = new TextDecoder('utf-8', { stream: true });
|
const textDecoder = new TextDecoder('utf-8', { stream: true });
|
||||||
|
|
||||||
function getURLAndInit() {
|
|
||||||
const c = getAPIConfig();
|
|
||||||
const baseURL = getAPIBaseURL(c);
|
|
||||||
const headers = genCommonHeaders(c);
|
|
||||||
return {
|
|
||||||
url: baseURL + endpoint,
|
|
||||||
init: { headers }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const Size = 150;
|
const Size = 150;
|
||||||
|
|
||||||
const traffic = {
|
const traffic = {
|
||||||
|
@ -69,10 +59,10 @@ function pump(reader) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchData() {
|
function fetchData(apiConfig) {
|
||||||
if (fetched) return traffic;
|
if (fetched) return traffic;
|
||||||
const { url, init } = getURLAndInit();
|
const { url, init } = getURLAndInit(apiConfig);
|
||||||
fetch(url, init).then(response => {
|
fetch(url + endpoint, init).then(response => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
fetched = true;
|
fetched = true;
|
||||||
const reader = response.body.getReader();
|
const reader = response.body.getReader();
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
|
|
||||||
.h1 {
|
.h1 {
|
||||||
padding: 0 40px;
|
padding: 0 40px;
|
||||||
color: #ddd;
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
|
import { useComponentState } from 'm/store';
|
||||||
|
import { getClashAPIConfig } from 'd/app';
|
||||||
|
|
||||||
import Icon from 'c/Icon';
|
import Icon from 'c/Icon';
|
||||||
import ContentHeader from 'c/ContentHeader';
|
import ContentHeader from 'c/ContentHeader';
|
||||||
|
// TODO move this into a redux action
|
||||||
import { fetchLogs } from '../api/logs';
|
import { fetchLogs } from '../api/logs';
|
||||||
|
|
||||||
import yacd from 's/yacd.svg';
|
import yacd from 's/yacd.svg';
|
||||||
|
@ -42,12 +45,16 @@ LogLine.propTypes = {
|
||||||
|
|
||||||
export default function Logs() {
|
export default function Logs() {
|
||||||
const [logs, setLogs] = useState([]);
|
const [logs, setLogs] = useState([]);
|
||||||
|
const { apiConfig } = useComponentState(getClashAPIConfig);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(
|
||||||
const x = fetchLogs();
|
() => {
|
||||||
setLogs(x.logs);
|
const x = fetchLogs(apiConfig);
|
||||||
return x.subscribe(() => setLogs(x.logs));
|
setLogs(x.logs);
|
||||||
}, []);
|
return x.subscribe(() => setLogs(x.logs));
|
||||||
|
},
|
||||||
|
[apiConfig.hostname, apiConfig.port, apiConfig.secret]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { HashRouter as Router, Route } from 'react-router-dom';
|
||||||
// import { hot } from 'react-hot-loader';
|
// import { hot } from 'react-hot-loader';
|
||||||
// import createHistory from 'history/createHashHistory';
|
// import createHistory from 'history/createHashHistory';
|
||||||
// import createHistory from 'history/createBrowserHistory';
|
// import createHistory from 'history/createBrowserHistory';
|
||||||
|
import Theme from 'c/Theme';
|
||||||
import SideBar from 'c/SideBar';
|
import SideBar from 'c/SideBar';
|
||||||
import Home from 'c/Home';
|
import Home from 'c/Home';
|
||||||
import Logs from 'c/Logs';
|
import Logs from 'c/Logs';
|
||||||
|
@ -23,23 +23,27 @@ import s0 from './Root.module.scss';
|
||||||
|
|
||||||
window.store = store;
|
window.store = store;
|
||||||
|
|
||||||
const Root = () => (
|
const Root = () => {
|
||||||
<Provider store={store}>
|
return (
|
||||||
<Router>
|
<Provider store={store}>
|
||||||
<div className={s0.app}>
|
<Theme>
|
||||||
<APIDiscovery />
|
<Router>
|
||||||
<Route path="/" render={() => <SideBar />} />
|
<div className={s0.app}>
|
||||||
<div className={s0.content}>
|
<APIDiscovery />
|
||||||
<Route exact path="/" render={() => <Home />} />
|
<Route path="/" render={() => <SideBar />} />
|
||||||
<Route exact path="/overview" render={() => <Home />} />
|
<div className={s0.content}>
|
||||||
<Route exact path="/configs" render={() => <Config />} />
|
<Route exact path="/" render={() => <Home />} />
|
||||||
<Route exact path="/logs" render={() => <Logs />} />
|
<Route exact path="/overview" render={() => <Home />} />
|
||||||
<Route exact path="/proxies" render={() => <Proxies />} />
|
<Route exact path="/configs" render={() => <Config />} />
|
||||||
</div>
|
<Route exact path="/logs" render={() => <Logs />} />
|
||||||
</div>
|
<Route exact path="/proxies" render={() => <Proxies />} />
|
||||||
</Router>
|
</div>
|
||||||
</Provider>
|
</div>
|
||||||
);
|
</Router>
|
||||||
|
</Theme>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
// <Route exact path="/__0" component={StyleGuide} />
|
// <Route exact path="/__0" component={StyleGuide} />
|
||||||
// <Route exact path="/__1" component={Loading} />
|
// <Route exact path="/__1" component={Loading} />
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
.app {
|
.app {
|
||||||
display: flex;
|
display: flex;
|
||||||
color: #ddd;
|
|
||||||
background: #202020;
|
|
||||||
|
|
||||||
|
background: var(--color-background);
|
||||||
|
color: var(--color-text);
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@
|
||||||
.content {
|
.content {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
|
// background: #202020;
|
||||||
|
|
||||||
// $w: 7px;
|
// $w: 7px;
|
||||||
// &::-webkit-scrollbar {
|
// &::-webkit-scrollbar {
|
||||||
|
|
|
@ -2,6 +2,9 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useActions } from 'm/store';
|
||||||
|
import { switchTheme } from 'd/app';
|
||||||
|
|
||||||
import Icon from 'c/Icon';
|
import Icon from 'c/Icon';
|
||||||
|
|
||||||
import activity from 's/activity.svg';
|
import activity from 's/activity.svg';
|
||||||
|
@ -9,6 +12,7 @@ import settings from 's/settings.svg';
|
||||||
import globe from 's/globe.svg';
|
import globe from 's/globe.svg';
|
||||||
import file from 's/file.svg';
|
import file from 's/file.svg';
|
||||||
import yacd from 's/yacd.svg';
|
import yacd from 's/yacd.svg';
|
||||||
|
import moon from 's/moon.svg';
|
||||||
|
|
||||||
import s from 'c/SideBar.module.scss';
|
import s from 'c/SideBar.module.scss';
|
||||||
|
|
||||||
|
@ -27,7 +31,10 @@ SideBarRow.propTypes = {
|
||||||
labelText: PropTypes.string
|
labelText: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const actions = { switchTheme };
|
||||||
|
|
||||||
function SideBar() {
|
function SideBar() {
|
||||||
|
const { switchTheme } = useActions(actions);
|
||||||
return (
|
return (
|
||||||
<div className={s.root}>
|
<div className={s.root}>
|
||||||
<div className={s.logo}>
|
<div className={s.logo}>
|
||||||
|
@ -40,6 +47,10 @@ function SideBar() {
|
||||||
<SideBarRow to="/configs" iconId={settings.id} labelText="Config" />
|
<SideBarRow to="/configs" iconId={settings.id} labelText="Config" />
|
||||||
<SideBarRow to="/logs" iconId={file.id} labelText="Logs" />
|
<SideBarRow to="/logs" iconId={file.id} labelText="Logs" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={s.themeSwitchContainer} onClick={switchTheme}>
|
||||||
|
<Icon id={moon.id} width={20} height={20} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
.root {
|
.root {
|
||||||
background: #2d2d30;
|
background: var(--color-bg-sidebar);
|
||||||
// width: 220px;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
|
@ -48,3 +48,21 @@
|
||||||
.label {
|
.label {
|
||||||
padding-left: 14px;
|
padding-left: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.themeSwitchContainer {
|
||||||
|
$sz: 50px;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: $sz;
|
||||||
|
height: $sz;
|
||||||
|
padding: 20px 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
17
src/components/Theme.js
Normal file
17
src/components/Theme.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import React, { memo, useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useComponentState } from 'm/store';
|
||||||
|
|
||||||
|
import { getTheme } from 'd/app';
|
||||||
|
|
||||||
|
import s0 from './Theme.module.scss';
|
||||||
|
|
||||||
|
const mapStateToProps = s => ({ theme: getTheme(s) });
|
||||||
|
|
||||||
|
function Theme({ children }) {
|
||||||
|
const { theme } = useComponentState(mapStateToProps);
|
||||||
|
const className = theme === 'dark' ? s0.dark : s0.light;
|
||||||
|
return <div className={className}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(Theme);
|
11
src/components/Theme.module.scss
Normal file
11
src/components/Theme.module.scss
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
.dark {
|
||||||
|
--color-background: #202020;
|
||||||
|
--color-text: #ddd;
|
||||||
|
--color-bg-sidebar: #2d2d30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light {
|
||||||
|
--color-background: #eee;
|
||||||
|
--color-text: #222;
|
||||||
|
--color-bg-sidebar: #2d2d30;
|
||||||
|
}
|
|
@ -2,6 +2,8 @@ import React, { useEffect } from 'react';
|
||||||
import prettyBytes from 'm/pretty-bytes';
|
import prettyBytes from 'm/pretty-bytes';
|
||||||
import { fetchData } from '../api/traffic';
|
import { fetchData } from '../api/traffic';
|
||||||
import { unstable_createResource as createResource } from 'react-cache';
|
import { unstable_createResource as createResource } from 'react-cache';
|
||||||
|
import { useComponentState } from 'm/store';
|
||||||
|
import { getClashAPIConfig } from 'd/app';
|
||||||
|
|
||||||
// const delay = ms => new Promise(r => setTimeout(r, ms));
|
// const delay = ms => new Promise(r => setTimeout(r, ms));
|
||||||
const chartJSResource = createResource(() => {
|
const chartJSResource = createResource(() => {
|
||||||
|
@ -119,29 +121,33 @@ const chartWrapperStyle = {
|
||||||
|
|
||||||
export default function TrafficChart() {
|
export default function TrafficChart() {
|
||||||
const Chart = chartJSResource.read();
|
const Chart = chartJSResource.read();
|
||||||
useEffect(() => {
|
const { hostname, port, secret } = useComponentState(getClashAPIConfig);
|
||||||
const ctx = document.getElementById('trafficChart').getContext('2d');
|
useEffect(
|
||||||
const traffic = fetchData();
|
() => {
|
||||||
const data = {
|
const ctx = document.getElementById('trafficChart').getContext('2d');
|
||||||
labels: traffic.labels,
|
const traffic = fetchData({ hostname, port, secret });
|
||||||
datasets: [
|
const data = {
|
||||||
{
|
labels: traffic.labels,
|
||||||
...upProps,
|
datasets: [
|
||||||
data: traffic.up
|
{
|
||||||
},
|
...upProps,
|
||||||
{
|
data: traffic.up
|
||||||
...downProps,
|
},
|
||||||
data: traffic.down
|
{
|
||||||
}
|
...downProps,
|
||||||
]
|
data: traffic.down
|
||||||
};
|
}
|
||||||
const c = new Chart(ctx, {
|
]
|
||||||
type: 'line',
|
};
|
||||||
data,
|
const c = new Chart(ctx, {
|
||||||
options
|
type: 'line',
|
||||||
});
|
data,
|
||||||
return traffic.subscribe(() => c.update());
|
options
|
||||||
}, []);
|
});
|
||||||
|
return traffic.subscribe(() => c.update());
|
||||||
|
},
|
||||||
|
[hostname, port, secret]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={chartWrapperStyle}>
|
<div style={chartWrapperStyle}>
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import prettyBytes from 'm/pretty-bytes';
|
import prettyBytes from 'm/pretty-bytes';
|
||||||
|
|
||||||
|
import { useComponentState } from 'm/store';
|
||||||
|
import { getClashAPIConfig } from 'd/app';
|
||||||
import { fetchData } from '../api/traffic';
|
import { fetchData } from '../api/traffic';
|
||||||
|
|
||||||
import s0 from 'c/TrafficNow.module.scss';
|
import s0 from 'c/TrafficNow.module.scss';
|
||||||
|
@ -23,13 +25,21 @@ export default function TrafficNow() {
|
||||||
|
|
||||||
function useSpeed() {
|
function useSpeed() {
|
||||||
const [speed, setSpeed] = useState({ upStr: '0 B/s', downStr: '0 B/s' });
|
const [speed, setSpeed] = useState({ upStr: '0 B/s', downStr: '0 B/s' });
|
||||||
useEffect(() => {
|
const { hostname, port, secret } = useComponentState(getClashAPIConfig);
|
||||||
return fetchData().subscribe(o =>
|
useEffect(
|
||||||
setSpeed({
|
() => {
|
||||||
upStr: prettyBytes(o.up) + '/s',
|
return fetchData({
|
||||||
downStr: prettyBytes(o.down) + '/s'
|
hostname,
|
||||||
})
|
port,
|
||||||
);
|
secret
|
||||||
});
|
}).subscribe(o =>
|
||||||
|
setSpeed({
|
||||||
|
upStr: prettyBytes(o.up) + '/s',
|
||||||
|
downStr: prettyBytes(o.down) + '/s'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[hostname, port, secret]
|
||||||
|
);
|
||||||
return speed;
|
return speed;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,12 @@ import { fetchConfigs } from 'd/configs';
|
||||||
import { closeModal } from 'd/modals';
|
import { closeModal } from 'd/modals';
|
||||||
|
|
||||||
const UpdateClashAPIConfig = 'app/UpdateClashAPIConfig';
|
const UpdateClashAPIConfig = 'app/UpdateClashAPIConfig';
|
||||||
|
const SwitchTheme = 'app/SwitchTheme';
|
||||||
|
|
||||||
const StorageKey = 'yacd.haishan.me';
|
const StorageKey = 'yacd.haishan.me';
|
||||||
|
|
||||||
export const getClashAPIConfig = s => s.app.clashAPIConfig;
|
export const getClashAPIConfig = s => s.app.clashAPIConfig;
|
||||||
|
export const getTheme = s => s.app.theme;
|
||||||
|
|
||||||
// TODO to support secret
|
// TODO to support secret
|
||||||
export function updateClashAPIConfig({ hostname: iHostname, port, secret }) {
|
export function updateClashAPIConfig({ hostname: iHostname, port, secret }) {
|
||||||
|
@ -25,6 +27,15 @@ export function updateClashAPIConfig({ hostname: iHostname, port, secret }) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function switchTheme() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const currentTheme = getTheme(getState());
|
||||||
|
const theme = currentTheme === 'light' ? 'dark' : 'light';
|
||||||
|
dispatch({ type: SwitchTheme, payload: { theme } });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// type Theme = 'light' | 'dark';
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
clashAPIConfig: {
|
clashAPIConfig: {
|
||||||
hostname: '127.0.0.1',
|
hostname: '127.0.0.1',
|
||||||
|
@ -36,16 +47,20 @@ const defaultState = {
|
||||||
function getInitialState() {
|
function getInitialState() {
|
||||||
let s = loadState(StorageKey);
|
let s = loadState(StorageKey);
|
||||||
if (!s) s = defaultState;
|
if (!s) s = defaultState;
|
||||||
return s;
|
// TODO flat clashAPIConfig?
|
||||||
|
return { theme: 'dark', ...s };
|
||||||
}
|
}
|
||||||
const initialState = getInitialState();
|
|
||||||
|
|
||||||
export default function reducer(state = initialState, { type, payload }) {
|
export default function reducer(state = getInitialState(), { type, payload }) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case UpdateClashAPIConfig: {
|
case UpdateClashAPIConfig: {
|
||||||
return { ...state, clashAPIConfig: { ...payload } };
|
return { ...state, clashAPIConfig: { ...payload } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case SwitchTheme: {
|
||||||
|
return { ...state, ...payload };
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import * as configsAPI from 'a/configs';
|
import * as configsAPI from 'a/configs';
|
||||||
import { openModal } from 'd/modals';
|
|
||||||
import * as trafficAPI from 'a/traffic';
|
import * as trafficAPI from 'a/traffic';
|
||||||
|
import { openModal } from 'd/modals';
|
||||||
|
import { getClashAPIConfig } from 'd/app';
|
||||||
|
|
||||||
const CompletedFetchConfigs = 'configs/CompletedFetchConfigs';
|
const CompletedFetchConfigs = 'configs/CompletedFetchConfigs';
|
||||||
const OptimisticUpdateConfigs = 'configs/OptimisticUpdateConfigs';
|
const OptimisticUpdateConfigs = 'configs/OptimisticUpdateConfigs';
|
||||||
|
@ -12,7 +13,8 @@ export function fetchConfigs() {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
let res;
|
let res;
|
||||||
try {
|
try {
|
||||||
res = await configsAPI.fetchConfigs();
|
const apiSetup = getClashAPIConfig(getState());
|
||||||
|
res = await configsAPI.fetchConfigs(apiSetup);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// FIXME
|
// FIXME
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
@ -44,7 +46,8 @@ export function fetchConfigs() {
|
||||||
// normally user will land on the "traffic chart" page first
|
// normally user will land on the "traffic chart" page first
|
||||||
// calling this here will let the data start streaming
|
// calling this here will let the data start streaming
|
||||||
// the traffic chart should already subscribed to the streaming
|
// the traffic chart should already subscribed to the streaming
|
||||||
trafficAPI.fetchData();
|
const apiSetup = getClashAPIConfig(getState());
|
||||||
|
trafficAPI.fetchData(apiSetup);
|
||||||
} else {
|
} else {
|
||||||
dispatch(markHaveFetchedConfig());
|
dispatch(markHaveFetchedConfig());
|
||||||
}
|
}
|
||||||
|
@ -62,8 +65,9 @@ function markHaveFetchedConfig() {
|
||||||
|
|
||||||
export function updateConfigs(partialConfg) {
|
export function updateConfigs(partialConfg) {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
|
const apiSetup = getClashAPIConfig(getState());
|
||||||
configsAPI
|
configsAPI
|
||||||
.updateConfigs(partialConfg)
|
.updateConfigs(apiSetup, partialConfg)
|
||||||
.then(
|
.then(
|
||||||
res => {
|
res => {
|
||||||
if (res.ok === false) {
|
if (res.ok === false) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import * as proxiesAPI from 'a/proxies';
|
import * as proxiesAPI from 'a/proxies';
|
||||||
|
import { getClashAPIConfig } from 'd/app';
|
||||||
|
|
||||||
// see all types:
|
// see all types:
|
||||||
// https://github.com/Dreamacro/clash/blob/master/constant/adapters.go
|
// https://github.com/Dreamacro/clash/blob/master/constant/adapters.go
|
||||||
|
@ -10,15 +11,18 @@ const ProxyGroupTypes = ['Fallback', 'URLTest', 'Selector'];
|
||||||
export const getProxies = s => s.proxies.proxies;
|
export const getProxies = s => s.proxies.proxies;
|
||||||
export const getDelay = s => s.proxies.delay;
|
export const getDelay = s => s.proxies.delay;
|
||||||
export const getProxyGroupNames = s => s.proxies.groupNames;
|
export const getProxyGroupNames = s => s.proxies.groupNames;
|
||||||
export const getUserProxies = createSelector(getProxies, proxies => {
|
export const getUserProxies = createSelector(
|
||||||
let o = {};
|
getProxies,
|
||||||
for (const prop in proxies) {
|
proxies => {
|
||||||
if (ProxyTypeBuiltin.indexOf(prop) < 0) {
|
let o = {};
|
||||||
o[prop] = proxies[prop];
|
for (const prop in proxies) {
|
||||||
|
if (ProxyTypeBuiltin.indexOf(prop) < 0) {
|
||||||
|
o[prop] = proxies[prop];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return o;
|
||||||
}
|
}
|
||||||
return o;
|
);
|
||||||
});
|
|
||||||
|
|
||||||
const CompletedFetchProxies = 'proxies/CompletedFetchProxies';
|
const CompletedFetchProxies = 'proxies/CompletedFetchProxies';
|
||||||
const OptimisticSwitchProxy = 'proxies/OptimisticSwitchProxy';
|
const OptimisticSwitchProxy = 'proxies/OptimisticSwitchProxy';
|
||||||
|
@ -43,12 +47,14 @@ export function fetchProxies() {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
// TODO handle errors
|
// TODO handle errors
|
||||||
|
|
||||||
const proxiesCurr = getProxies(getState());
|
const state = getState();
|
||||||
|
const proxiesCurr = getProxies(state);
|
||||||
// TODO this is too aggressive...
|
// TODO this is too aggressive...
|
||||||
if (Object.keys(proxiesCurr).length > 0) return;
|
if (Object.keys(proxiesCurr).length > 0) return;
|
||||||
|
|
||||||
|
const apiConfig = getClashAPIConfig(state);
|
||||||
// TODO show loading animation?
|
// TODO show loading animation?
|
||||||
const json = await proxiesAPI.fetchProxies();
|
const json = await proxiesAPI.fetchProxies(apiConfig);
|
||||||
let { proxies = {} } = json;
|
let { proxies = {} } = json;
|
||||||
|
|
||||||
const groupNames = retrieveGroupNamesFrom(proxies);
|
const groupNames = retrieveGroupNamesFrom(proxies);
|
||||||
|
@ -63,9 +69,10 @@ export function fetchProxies() {
|
||||||
|
|
||||||
export function switchProxy(name1, name2) {
|
export function switchProxy(name1, name2) {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
|
const apiConfig = getClashAPIConfig(getState());
|
||||||
// TODO display error message
|
// TODO display error message
|
||||||
proxiesAPI
|
proxiesAPI
|
||||||
.requestToSwitchProxy(name1, name2)
|
.requestToSwitchProxy(apiConfig, name1, name2)
|
||||||
.then(
|
.then(
|
||||||
res => {
|
res => {
|
||||||
if (res.ok === false) {
|
if (res.ok === false) {
|
||||||
|
@ -97,7 +104,8 @@ export function switchProxy(name1, name2) {
|
||||||
|
|
||||||
function requestDelayForProxyOnce(name) {
|
function requestDelayForProxyOnce(name) {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const res = await proxiesAPI.requestDelayForProxy(name);
|
const apiConfig = getClashAPIConfig(getState());
|
||||||
|
const res = await proxiesAPI.requestDelayForProxy(apiConfig, name);
|
||||||
let error = '';
|
let error = '';
|
||||||
if (res.ok === false) {
|
if (res.ok === false) {
|
||||||
error = res.statusText;
|
error = res.statusText;
|
||||||
|
|
|
@ -1,15 +1,7 @@
|
||||||
import { store } from '../store/configureStore';
|
|
||||||
import { getClashAPIConfig } from 'd/app';
|
|
||||||
|
|
||||||
const headersCommon = {
|
const headersCommon = {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getAPIConfig() {
|
|
||||||
// this is cheating...
|
|
||||||
return getClashAPIConfig(store.getState());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function genCommonHeaders({ secret }) {
|
export function genCommonHeaders({ secret }) {
|
||||||
const h = { ...headersCommon };
|
const h = { ...headersCommon };
|
||||||
if (secret) {
|
if (secret) {
|
||||||
|
@ -21,3 +13,12 @@ export function genCommonHeaders({ secret }) {
|
||||||
export function getAPIBaseURL({ hostname, port }) {
|
export function getAPIBaseURL({ hostname, port }) {
|
||||||
return `http://${hostname}:${port}`;
|
return `http://${hostname}:${port}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getURLAndInit({ hostname, port, secret }) {
|
||||||
|
const baseURL = getAPIBaseURL({ hostname, port });
|
||||||
|
const headers = genCommonHeaders({ secret });
|
||||||
|
return {
|
||||||
|
url: baseURL,
|
||||||
|
init: { headers }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
1
src/svg/moon.svg
Normal file
1
src/svg/moon.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
|
After Width: | Height: | Size: 253 B |
Loading…
Reference in a new issue