diff --git a/src/api/configs.js b/src/api/configs.js
index 0690221..8889ad0 100644
--- a/src/api/configs.js
+++ b/src/api/configs.js
@@ -1,29 +1,19 @@
import {
- getAPIConfig,
+ getURLAndInit,
genCommonHeaders,
getAPIBaseURL
} from 'm/request-helper';
const endpoint = '/configs';
-function getURLAndInit() {
- const c = getAPIConfig();
- const baseURL = getAPIBaseURL(c);
- const headers = genCommonHeaders(c);
- return {
- url: baseURL + endpoint,
- init: { headers }
- };
+export async function fetchConfigs(apiConfig) {
+ const { url, init } = getURLAndInit(apiConfig);
+ return await fetch(url + endpoint, init);
}
-export async function fetchConfigs() {
- const { url, init } = getURLAndInit();
- return await fetch(url, init);
-}
-
-export async function updateConfigs(o) {
- const { url, init } = getURLAndInit();
- return await fetch(url, {
+export async function updateConfigs(apiConfig, o) {
+ const { url, init } = getURLAndInit(apiConfig);
+ return await fetch(url + endpoint, {
...init,
method: 'PUT',
// mode: 'cors',
diff --git a/src/api/logs.js b/src/api/logs.js
index 59d064a..948a23f 100644
--- a/src/api/logs.js
+++ b/src/api/logs.js
@@ -1,5 +1,5 @@
import {
- getAPIConfig,
+ getURLAndInit,
genCommonHeaders,
getAPIBaseURL
} from 'm/request-helper';
@@ -10,16 +10,6 @@ const getRandomStr = () => {
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;
let even = false;
@@ -71,11 +61,11 @@ function pump(reader) {
});
}
-function fetchLogs() {
+function fetchLogs(apiConfig) {
if (store.fetched) return store;
store.fetched = true;
- const { url, init } = getURLAndInit();
- fetch(url, init)
+ const { url, init } = getURLAndInit(apiConfig);
+ fetch(url + endpoint, init)
.then(response => {
const reader = response.body.getReader();
pump(reader);
diff --git a/src/api/proxies.js b/src/api/proxies.js
index 592a38b..a38d92d 100644
--- a/src/api/proxies.js
+++ b/src/api/proxies.js
@@ -1,20 +1,10 @@
import {
- getAPIConfig,
+ getURLAndInit,
genCommonHeaders,
getAPIBaseURL
} from 'm/request-helper';
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
HTTP/1.1 400 Bad Request
@@ -32,16 +22,16 @@ Vary: Origin
Date: Tue, 16 Oct 2018 16:38:33 GMT
*/
-async function fetchProxies() {
- const { url, init } = getURLAndInit();
- const res = await fetch(url, init);
+async function fetchProxies(config) {
+ const { url, init } = getURLAndInit(config);
+ const res = await fetch(url + endpoint, init);
return await res.json();
}
-async function requestToSwitchProxy(name1, name2) {
+async function requestToSwitchProxy(apiConfig, name1, name2) {
const body = { name: name2 };
- const { url, init } = getURLAndInit();
- const fullURL = `${url}/${name1}`;
+ const { url, init } = getURLAndInit(apiConfig);
+ const fullURL = `${url}${endpoint}/${name1}`;
return await fetch(fullURL, {
...init,
method: 'PUT',
@@ -49,10 +39,10 @@ async function requestToSwitchProxy(name1, name2) {
});
}
-async function requestDelayForProxy(name) {
- const { url, init } = getURLAndInit();
+async function requestDelayForProxy(apiConfig, name) {
+ const { url, init } = getURLAndInit(apiConfig);
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);
}
diff --git a/src/api/traffic.js b/src/api/traffic.js
index 12c88dc..8d1e3b7 100644
--- a/src/api/traffic.js
+++ b/src/api/traffic.js
@@ -1,21 +1,11 @@
import {
- getAPIConfig,
+ getURLAndInit,
genCommonHeaders,
getAPIBaseURL
} from 'm/request-helper';
const endpoint = '/traffic';
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 traffic = {
@@ -69,10 +59,10 @@ function pump(reader) {
});
}
-function fetchData() {
+function fetchData(apiConfig) {
if (fetched) return traffic;
- const { url, init } = getURLAndInit();
- fetch(url, init).then(response => {
+ const { url, init } = getURLAndInit(apiConfig);
+ fetch(url + endpoint, init).then(response => {
if (response.ok) {
fetched = true;
const reader = response.body.getReader();
diff --git a/src/components/ContentHeader.module.scss b/src/components/ContentHeader.module.scss
index caf572e..54e4c99 100644
--- a/src/components/ContentHeader.module.scss
+++ b/src/components/ContentHeader.module.scss
@@ -6,7 +6,6 @@
.h1 {
padding: 0 40px;
- color: #ddd;
text-align: left;
margin: 0;
}
diff --git a/src/components/Logs.js b/src/components/Logs.js
index 235e7ba..08d634f 100644
--- a/src/components/Logs.js
+++ b/src/components/Logs.js
@@ -1,9 +1,12 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
+import { useComponentState } from 'm/store';
+import { getClashAPIConfig } from 'd/app';
import Icon from 'c/Icon';
import ContentHeader from 'c/ContentHeader';
+// TODO move this into a redux action
import { fetchLogs } from '../api/logs';
import yacd from 's/yacd.svg';
@@ -42,12 +45,16 @@ LogLine.propTypes = {
export default function Logs() {
const [logs, setLogs] = useState([]);
+ const { apiConfig } = useComponentState(getClashAPIConfig);
- useEffect(() => {
- const x = fetchLogs();
- setLogs(x.logs);
- return x.subscribe(() => setLogs(x.logs));
- }, []);
+ useEffect(
+ () => {
+ const x = fetchLogs(apiConfig);
+ setLogs(x.logs);
+ return x.subscribe(() => setLogs(x.logs));
+ },
+ [apiConfig.hostname, apiConfig.port, apiConfig.secret]
+ );
return (
diff --git a/src/components/Root.js b/src/components/Root.js
index e9d9141..60b89f4 100644
--- a/src/components/Root.js
+++ b/src/components/Root.js
@@ -4,7 +4,7 @@ import { HashRouter as Router, Route } from 'react-router-dom';
// import { hot } from 'react-hot-loader';
// import createHistory from 'history/createHashHistory';
// import createHistory from 'history/createBrowserHistory';
-
+import Theme from 'c/Theme';
import SideBar from 'c/SideBar';
import Home from 'c/Home';
import Logs from 'c/Logs';
@@ -23,23 +23,27 @@ import s0 from './Root.module.scss';
window.store = store;
-const Root = () => (
-
-
-
-
-
} />
-
-
} />
- } />
- } />
- } />
- } />
-
-
-
-
-);
+const Root = () => {
+ return (
+
+
+
+
+
+
} />
+
+
} />
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+
+ );
+};
//
//
diff --git a/src/components/Root.module.scss b/src/components/Root.module.scss
index e4a0e87..5ecd02e 100644
--- a/src/components/Root.module.scss
+++ b/src/components/Root.module.scss
@@ -1,8 +1,8 @@
.app {
display: flex;
- color: #ddd;
- background: #202020;
+ background: var(--color-background);
+ color: var(--color-text);
min-height: 300px;
height: 100vh;
}
@@ -10,6 +10,7 @@
.content {
flex-grow: 1;
overflow: scroll;
+ // background: #202020;
// $w: 7px;
// &::-webkit-scrollbar {
diff --git a/src/components/SideBar.js b/src/components/SideBar.js
index cee0960..8a38a46 100644
--- a/src/components/SideBar.js
+++ b/src/components/SideBar.js
@@ -2,6 +2,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import { NavLink } from 'react-router-dom';
+import { useActions } from 'm/store';
+import { switchTheme } from 'd/app';
+
import Icon from 'c/Icon';
import activity from 's/activity.svg';
@@ -9,6 +12,7 @@ import settings from 's/settings.svg';
import globe from 's/globe.svg';
import file from 's/file.svg';
import yacd from 's/yacd.svg';
+import moon from 's/moon.svg';
import s from 'c/SideBar.module.scss';
@@ -27,7 +31,10 @@ SideBarRow.propTypes = {
labelText: PropTypes.string
};
+const actions = { switchTheme };
+
function SideBar() {
+ const { switchTheme } = useActions(actions);
return (
@@ -40,6 +47,10 @@ function SideBar() {
+
+
+
+
);
}
diff --git a/src/components/SideBar.module.scss b/src/components/SideBar.module.scss
index 86260f4..a0ca07d 100644
--- a/src/components/SideBar.module.scss
+++ b/src/components/SideBar.module.scss
@@ -1,6 +1,6 @@
.root {
- background: #2d2d30;
- // width: 220px;
+ background: var(--color-bg-sidebar);
+ position: relative;
}
.logo {
@@ -48,3 +48,21 @@
.label {
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;
+ }
+}
diff --git a/src/components/Theme.js b/src/components/Theme.js
new file mode 100644
index 0000000..ed6952e
--- /dev/null
+++ b/src/components/Theme.js
@@ -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
{children}
;
+}
+
+export default memo(Theme);
diff --git a/src/components/Theme.module.scss b/src/components/Theme.module.scss
new file mode 100644
index 0000000..754da63
--- /dev/null
+++ b/src/components/Theme.module.scss
@@ -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;
+}
diff --git a/src/components/TrafficChart.js b/src/components/TrafficChart.js
index 4bfb4a7..5d54c59 100644
--- a/src/components/TrafficChart.js
+++ b/src/components/TrafficChart.js
@@ -2,6 +2,8 @@ import React, { useEffect } from 'react';
import prettyBytes from 'm/pretty-bytes';
import { fetchData } from '../api/traffic';
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 chartJSResource = createResource(() => {
@@ -119,29 +121,33 @@ const chartWrapperStyle = {
export default function TrafficChart() {
const Chart = chartJSResource.read();
- useEffect(() => {
- const ctx = document.getElementById('trafficChart').getContext('2d');
- const traffic = fetchData();
- const data = {
- labels: traffic.labels,
- datasets: [
- {
- ...upProps,
- data: traffic.up
- },
- {
- ...downProps,
- data: traffic.down
- }
- ]
- };
- const c = new Chart(ctx, {
- type: 'line',
- data,
- options
- });
- return traffic.subscribe(() => c.update());
- }, []);
+ const { hostname, port, secret } = useComponentState(getClashAPIConfig);
+ useEffect(
+ () => {
+ const ctx = document.getElementById('trafficChart').getContext('2d');
+ const traffic = fetchData({ hostname, port, secret });
+ const data = {
+ labels: traffic.labels,
+ datasets: [
+ {
+ ...upProps,
+ data: traffic.up
+ },
+ {
+ ...downProps,
+ data: traffic.down
+ }
+ ]
+ };
+ const c = new Chart(ctx, {
+ type: 'line',
+ data,
+ options
+ });
+ return traffic.subscribe(() => c.update());
+ },
+ [hostname, port, secret]
+ );
return (
diff --git a/src/components/TrafficNow.js b/src/components/TrafficNow.js
index 43236a7..09fa43f 100644
--- a/src/components/TrafficNow.js
+++ b/src/components/TrafficNow.js
@@ -1,6 +1,8 @@
import React, { useState, useEffect } from 'react';
import prettyBytes from 'm/pretty-bytes';
+import { useComponentState } from 'm/store';
+import { getClashAPIConfig } from 'd/app';
import { fetchData } from '../api/traffic';
import s0 from 'c/TrafficNow.module.scss';
@@ -23,13 +25,21 @@ export default function TrafficNow() {
function useSpeed() {
const [speed, setSpeed] = useState({ upStr: '0 B/s', downStr: '0 B/s' });
- useEffect(() => {
- return fetchData().subscribe(o =>
- setSpeed({
- upStr: prettyBytes(o.up) + '/s',
- downStr: prettyBytes(o.down) + '/s'
- })
- );
- });
+ const { hostname, port, secret } = useComponentState(getClashAPIConfig);
+ useEffect(
+ () => {
+ return fetchData({
+ hostname,
+ port,
+ secret
+ }).subscribe(o =>
+ setSpeed({
+ upStr: prettyBytes(o.up) + '/s',
+ downStr: prettyBytes(o.down) + '/s'
+ })
+ );
+ },
+ [hostname, port, secret]
+ );
return speed;
}
diff --git a/src/ducks/app.js b/src/ducks/app.js
index 1086413..366f4b0 100644
--- a/src/ducks/app.js
+++ b/src/ducks/app.js
@@ -3,10 +3,12 @@ import { fetchConfigs } from 'd/configs';
import { closeModal } from 'd/modals';
const UpdateClashAPIConfig = 'app/UpdateClashAPIConfig';
+const SwitchTheme = 'app/SwitchTheme';
const StorageKey = 'yacd.haishan.me';
export const getClashAPIConfig = s => s.app.clashAPIConfig;
+export const getTheme = s => s.app.theme;
// TODO to support 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 = {
clashAPIConfig: {
hostname: '127.0.0.1',
@@ -36,16 +47,20 @@ const defaultState = {
function getInitialState() {
let s = loadState(StorageKey);
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) {
case UpdateClashAPIConfig: {
return { ...state, clashAPIConfig: { ...payload } };
}
+ case SwitchTheme: {
+ return { ...state, ...payload };
+ }
+
default:
return state;
}
diff --git a/src/ducks/configs.js b/src/ducks/configs.js
index 0edc8e0..a7d4b83 100644
--- a/src/ducks/configs.js
+++ b/src/ducks/configs.js
@@ -1,6 +1,7 @@
import * as configsAPI from 'a/configs';
-import { openModal } from 'd/modals';
import * as trafficAPI from 'a/traffic';
+import { openModal } from 'd/modals';
+import { getClashAPIConfig } from 'd/app';
const CompletedFetchConfigs = 'configs/CompletedFetchConfigs';
const OptimisticUpdateConfigs = 'configs/OptimisticUpdateConfigs';
@@ -12,7 +13,8 @@ export function fetchConfigs() {
return async (dispatch, getState) => {
let res;
try {
- res = await configsAPI.fetchConfigs();
+ const apiSetup = getClashAPIConfig(getState());
+ res = await configsAPI.fetchConfigs(apiSetup);
} catch (err) {
// FIXME
// eslint-disable-next-line no-console
@@ -44,7 +46,8 @@ export function fetchConfigs() {
// normally user will land on the "traffic chart" page first
// calling this here will let the data start streaming
// the traffic chart should already subscribed to the streaming
- trafficAPI.fetchData();
+ const apiSetup = getClashAPIConfig(getState());
+ trafficAPI.fetchData(apiSetup);
} else {
dispatch(markHaveFetchedConfig());
}
@@ -62,8 +65,9 @@ function markHaveFetchedConfig() {
export function updateConfigs(partialConfg) {
return async (dispatch, getState) => {
+ const apiSetup = getClashAPIConfig(getState());
configsAPI
- .updateConfigs(partialConfg)
+ .updateConfigs(apiSetup, partialConfg)
.then(
res => {
if (res.ok === false) {
diff --git a/src/ducks/proxies.js b/src/ducks/proxies.js
index 7fec378..43b57b8 100644
--- a/src/ducks/proxies.js
+++ b/src/ducks/proxies.js
@@ -1,5 +1,6 @@
import { createSelector } from 'reselect';
import * as proxiesAPI from 'a/proxies';
+import { getClashAPIConfig } from 'd/app';
// see all types:
// 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 getDelay = s => s.proxies.delay;
export const getProxyGroupNames = s => s.proxies.groupNames;
-export const getUserProxies = createSelector(getProxies, proxies => {
- let o = {};
- for (const prop in proxies) {
- if (ProxyTypeBuiltin.indexOf(prop) < 0) {
- o[prop] = proxies[prop];
+export const getUserProxies = createSelector(
+ getProxies,
+ proxies => {
+ let o = {};
+ for (const prop in proxies) {
+ if (ProxyTypeBuiltin.indexOf(prop) < 0) {
+ o[prop] = proxies[prop];
+ }
}
+ return o;
}
- return o;
-});
+);
const CompletedFetchProxies = 'proxies/CompletedFetchProxies';
const OptimisticSwitchProxy = 'proxies/OptimisticSwitchProxy';
@@ -43,12 +47,14 @@ export function fetchProxies() {
return async (dispatch, getState) => {
// TODO handle errors
- const proxiesCurr = getProxies(getState());
+ const state = getState();
+ const proxiesCurr = getProxies(state);
// TODO this is too aggressive...
if (Object.keys(proxiesCurr).length > 0) return;
+ const apiConfig = getClashAPIConfig(state);
// TODO show loading animation?
- const json = await proxiesAPI.fetchProxies();
+ const json = await proxiesAPI.fetchProxies(apiConfig);
let { proxies = {} } = json;
const groupNames = retrieveGroupNamesFrom(proxies);
@@ -63,9 +69,10 @@ export function fetchProxies() {
export function switchProxy(name1, name2) {
return async (dispatch, getState) => {
+ const apiConfig = getClashAPIConfig(getState());
// TODO display error message
proxiesAPI
- .requestToSwitchProxy(name1, name2)
+ .requestToSwitchProxy(apiConfig, name1, name2)
.then(
res => {
if (res.ok === false) {
@@ -97,7 +104,8 @@ export function switchProxy(name1, name2) {
function requestDelayForProxyOnce(name) {
return async (dispatch, getState) => {
- const res = await proxiesAPI.requestDelayForProxy(name);
+ const apiConfig = getClashAPIConfig(getState());
+ const res = await proxiesAPI.requestDelayForProxy(apiConfig, name);
let error = '';
if (res.ok === false) {
error = res.statusText;
diff --git a/src/misc/request-helper.js b/src/misc/request-helper.js
index 7cc17bf..317dada 100644
--- a/src/misc/request-helper.js
+++ b/src/misc/request-helper.js
@@ -1,15 +1,7 @@
-import { store } from '../store/configureStore';
-import { getClashAPIConfig } from 'd/app';
-
const headersCommon = {
'Content-Type': 'application/json'
};
-export function getAPIConfig() {
- // this is cheating...
- return getClashAPIConfig(store.getState());
-}
-
export function genCommonHeaders({ secret }) {
const h = { ...headersCommon };
if (secret) {
@@ -21,3 +13,12 @@ export function genCommonHeaders({ secret }) {
export function getAPIBaseURL({ 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 }
+ };
+}
diff --git a/src/svg/moon.svg b/src/svg/moon.svg
new file mode 100644
index 0000000..bf0877a
--- /dev/null
+++ b/src/svg/moon.svg
@@ -0,0 +1 @@
+