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 @@ +