+
- {/* */}
{groupNames.map(groupName => {
return (
);
})}
+
+
>
);
}
+
+const mapState = s => ({
+ groupNames: getProxyGroupNames(s),
+ proxies: getProxies(s),
+ proxyProviders: getProxyProviders(s),
+ delay: getDelay(s)
+});
+
+export default connect(mapState)(Proxies);
diff --git a/src/components/Proxies.module.css b/src/components/Proxies.module.css
index 72b70fb..5520a2e 100644
--- a/src/components/Proxies.module.css
+++ b/src/components/Proxies.module.css
@@ -1,7 +1,3 @@
-.body {
- padding-bottom: 50px;
-}
-
.group {
padding: 10px 15px;
@media (--breakpoint-not-small) {
diff --git a/src/components/Proxy.js b/src/components/Proxy.js
index b7efc84..117ff76 100644
--- a/src/components/Proxy.js
+++ b/src/components/Proxy.js
@@ -1,13 +1,36 @@
import React from 'react';
-import PropTypes from 'prop-types';
import cx from 'classnames';
-import { useStoreState } from 'm/store';
-import ProxyLatency from 'c/ProxyLatency';
+import { connect } from './StateProvider';
+import ProxyLatency from './ProxyLatency';
+
+import { getProxies, getDelay } from '../store/proxies';
import s0 from './Proxy.module.css';
-import { getDelay, getProxies } from 'd/proxies';
+const { useMemo } = React;
+
+const colorMap = {
+ // green
+ good: '#67c23a',
+ // yellow
+ normal: '#d4b75c',
+ // orange
+ bad: '#e67f3c',
+ // bad: '#F56C6C',
+ na: '#909399'
+};
+
+function getLabelColor({ number, error } = {}) {
+ if (number < 200) {
+ return colorMap.good;
+ } else if (number < 400) {
+ return colorMap.normal;
+ } else if (typeof number === 'number') {
+ return colorMap.bad;
+ }
+ return colorMap.na;
+}
/*
const colors = {
@@ -22,18 +45,28 @@ const colors = {
};
*/
-const mapStateToProps = s => {
- return {
- proxies: getProxies(s),
- delay: getDelay(s)
- };
+type ProxyProps = {
+ name: string,
+ now?: boolean,
+
+ // connect injected
+ // TODO refine type
+ proxy: any,
+ latency: any
};
-function Proxy({ now, name }) {
- const { proxies, delay } = useStoreState(mapStateToProps);
- const latency = delay[name];
- const proxy = proxies[name];
+function ProxySmallImpl({ now, name, proxy, latency }: ProxyProps) {
+ const color = useMemo(() => getLabelColor(latency), [latency]);
+ return (
+
+ );
+}
+function Proxy({ now, name, proxy, latency }: ProxyProps) {
+ const color = useMemo(() => getLabelColor(latency), [latency]);
return (
- {latency && latency.number ?
: null}
+ {latency && latency.number ? (
+
+ ) : null}
);
}
-Proxy.propTypes = {
- now: PropTypes.bool,
- name: PropTypes.string
+
+const mapState = (s, { name }) => {
+ const proxies = getProxies(s);
+ const delay = getDelay(s);
+ return {
+ proxy: proxies[name],
+ latency: delay[name]
+ };
};
-export default Proxy;
+export default connect(mapState)(Proxy);
+export const ProxySmall = connect(mapState)(ProxySmallImpl);
diff --git a/src/components/Proxy.module.css b/src/components/Proxy.module.css
index 6f42ccf..2af1ce8 100644
--- a/src/components/Proxy.module.css
+++ b/src/components/Proxy.module.css
@@ -2,10 +2,15 @@
position: relative;
padding: 5px;
border-radius: 8px;
+ overflow: hidden;
+
+ max-width: 280px;
@media (--breakpoint-not-small) {
+ min-width: 150px;
border-radius: 10px;
padding: 10px;
}
+
background-color: var(--color-bg-proxy-selected);
&.now {
background-color: var(--color-focus-blue);
@@ -40,3 +45,12 @@
display: flex;
align-items: flex-end;
}
+
+.proxySmall {
+ .now {
+ outline: pink solid 1px;
+ }
+ width: 12px;
+ height: 12px;
+ border-radius: 8px;
+}
diff --git a/src/components/ProxyGroup.js b/src/components/ProxyGroup.js
index 337b09b..d824920 100644
--- a/src/components/ProxyGroup.js
+++ b/src/components/ProxyGroup.js
@@ -1,60 +1,108 @@
import React from 'react';
-import PropTypes from 'prop-types';
import cx from 'classnames';
-import { useActions, useStoreState } from 'm/store';
-import Proxy from 'c/Proxy';
+import Proxy, { ProxySmall } from './Proxy';
+import { SectionNameType } from './shared/Basic';
import s0 from './ProxyGroup.module.css';
-import { getProxies, switchProxy } from 'd/proxies';
+import { switchProxy } from '../store/proxies';
-const mapStateToProps = s => ({
- proxies: getProxies(s)
-});
+const { memo, useCallback, useMemo } = React;
-export default function ProxyGroup({ name }) {
- const { proxies } = useStoreState(mapStateToProps);
- const actions = useActions({ switchProxy });
+function ProxyGroup({ name, proxies, apiConfig, dispatch }) {
const group = proxies[name];
- const { all } = group;
+ const { all, type, now } = group;
+
+ const isSelectable = useMemo(() => type === 'Selector', [type]);
+
+ const itemOnTapCallback = useCallback(
+ proxyName => {
+ if (!isSelectable) return;
+
+ dispatch(switchProxy(apiConfig, name, proxyName));
+ // switchProxyFn(name, proxyName);
+ },
+ [apiConfig, dispatch, name, isSelectable]
+ );
return (
-
- {name}
- {group.type}
-
-
-
- {all.map(proxyName => {
- const isSelectable = group.type === 'Selector';
- const proxyClassName = cx(s0.proxy, {
- [s0.proxySelectable]: isSelectable
- });
- return (
-
{
- if (!isSelectable) return;
- actions.switchProxy(name, proxyName);
- }}
- >
-
-
- );
- })}
+
+
);
}
-ProxyGroup.propTypes = {
- name: PropTypes.string
+type ProxyListProps = {
+ all: string[],
+ now?: string,
+ isSelectable?: boolean,
+ itemOnTapCallback?: string => void
};
+export function ProxyList({
+ all,
+ now,
+ isSelectable,
+ itemOnTapCallback
+}: ProxyListProps) {
+ return (
+
+ {all.map(proxyName => {
+ const proxyClassName = cx(s0.proxy, {
+ [s0.proxySelectable]: isSelectable
+ });
+ return (
+
{
+ if (!isSelectable || !itemOnTapCallback) return;
+ itemOnTapCallback(proxyName);
+ }}
+ >
+
+
+ );
+ })}
+
+ );
+}
+
+export function ProxyListSummaryView({
+ all,
+ now,
+ isSelectable,
+ itemOnTapCallback
+}: ProxyListProps) {
+ return (
+
+ {all.map(proxyName => {
+ const proxyClassName = cx(s0.proxy, {
+ [s0.proxySelectable]: isSelectable
+ });
+ return (
+
{
+ if (!isSelectable || !itemOnTapCallback) return;
+ itemOnTapCallback(proxyName);
+ }}
+ >
+
+
+ );
+ })}
+
+ );
+}
+
+export default memo(ProxyGroup);
diff --git a/src/components/ProxyGroup.module.css b/src/components/ProxyGroup.module.css
index 08c4b42..748aa67 100644
--- a/src/components/ProxyGroup.module.css
+++ b/src/components/ProxyGroup.module.css
@@ -1,32 +1,19 @@
.header {
- > h2 {
- margin-top: 0;
-
- font-size: 1.3em;
- @media (--breakpoint-not-small) {
- font-size: 1.5em;
- }
-
- span:nth-child(2) {
- font-size: 12px;
- color: #777;
- font-weight: normal;
- margin: 0 0.3em;
- }
- }
+ margin-bottom: 12px;
}
.list {
display: flex;
flex-wrap: wrap;
+ margin-top: 8px;
}
.proxy {
- max-width: 280px;
- margin: 2px;
+ margin-right: 5px;
+ margin-bottom: 5px;
@media (--breakpoint-not-small) {
- min-width: 150px;
- margin: 10px;
+ margin-right: 10px;
+ margin-bottom: 10px;
}
transition: transform 0.2s ease-in-out;
diff --git a/src/components/ProxyLatency.js b/src/components/ProxyLatency.js
index 33a94f5..5cde880 100644
--- a/src/components/ProxyLatency.js
+++ b/src/components/ProxyLatency.js
@@ -1,39 +1,16 @@
-import React, { useMemo } from 'react';
-import PropTypes from 'prop-types';
+import React from 'react';
import s0 from './ProxyLatency.module.css';
-const colorMap = {
- good: '#67C23A',
- normal: '#E6A23C',
- bad: '#F56C6C',
- na: '#909399'
+type ProxyLatencyProps = {
+ number: number,
+ color: string
};
-function getLabelColor(number, error) {
- if (error !== '') {
- return colorMap.na;
- } else if (number < 200) {
- return colorMap.good;
- } else if (number < 400) {
- return colorMap.normal;
- }
- return colorMap.bad;
-}
-
-export default function ProxyLatency({ latency }) {
- const { number, error } = latency;
- const color = useMemo(() => getLabelColor(number, error), [number, error]);
+export default function ProxyLatency({ number, color }: ProxyLatencyProps) {
return (
- {error !== '' ? {error} : {number} ms}
+ {number} ms
);
}
-
-ProxyLatency.propTypes = {
- latency: PropTypes.shape({
- number: PropTypes.number,
- error: PropTypes.string
- })
-};
diff --git a/src/components/ProxyProvider.js b/src/components/ProxyProvider.js
new file mode 100644
index 0000000..e18fe17
--- /dev/null
+++ b/src/components/ProxyProvider.js
@@ -0,0 +1,211 @@
+import React from 'react';
+import { ChevronDown, RotateCw } from 'react-feather';
+import { formatDistance } from 'date-fns';
+import ResizeObserver from 'resize-observer-polyfill';
+import { motion } from 'framer-motion';
+import cx from 'classnames';
+
+import { useStoreState } from '../misc/store';
+import { getClashAPIConfig } from '../ducks/app';
+import { connect } from './StateProvider';
+import { SectionNameType } from './shared/Basic';
+import { ProxyList, ProxyListSummaryView } from './ProxyGroup';
+import { ButtonWithIcon, ButtonPlain } from './Button';
+
+import { updateProviderByName } from '../store/proxies';
+
+import s from './ProxyProvider.module.css';
+
+const { memo, useState, useRef, useEffect, useCallback } = React;
+
+type Props = {
+ item: Array<{
+ name: string,
+ proxies: Array
,
+ type: 'Proxy' | 'Rule',
+ vehicleType: 'HTTP' | 'File' | 'Compatible',
+ updatedAt?: string
+ }>,
+ proxies: {
+ [string]: any
+ },
+ dispatch: any => void
+};
+
+const mapStateToProps = s => ({
+ apiConfig: getClashAPIConfig(s)
+});
+
+function ProxyProvider({ item, dispatch }: Props) {
+ const { apiConfig } = useStoreState(mapStateToProps);
+ const updateProvider = useCallback(
+ () => dispatch(updateProviderByName(apiConfig, item.name)),
+ [apiConfig, dispatch, item.name]
+ );
+
+ const [isCollapsibleOpen, setCollapsibleOpen] = useState(false);
+ const toggle = useCallback(() => setCollapsibleOpen(x => !x), []);
+ const timeAgo = formatDistance(new Date(item.updatedAt), new Date());
+ return (
+
+
+
+
+
+
+
+
+
+
+ Updated {timeAgo} ago
+
+
+
+
+ }
+ onClick={updateProvider}
+ />
+
+
+
+
+
+
+ );
+}
+
+const button = {
+ rest: { scale: 1 },
+ // hover: { scale: 1.1 },
+ pressed: { scale: 0.95 }
+};
+const arrow = {
+ rest: { rotate: 0 },
+ hover: { rotate: 360, transition: { duration: 0.3 } }
+};
+function Refresh() {
+ return (
+
+
+
+
+
+ );
+}
+
+function usePrevious(value) {
+ const ref = useRef();
+ useEffect(() => void (ref.current = value), [value]);
+ return ref.current;
+}
+
+function useMeasure() {
+ const ref = useRef();
+ const [bounds, set] = useState({ height: 0 });
+ useEffect(() => {
+ const ro = new ResizeObserver(([entry]) => set(entry.contentRect));
+ if (ref.current) ro.observe(ref.current);
+ return () => ro.disconnect();
+ }, []);
+ return [ref, bounds];
+}
+
+// import { useSpring, a } from 'react-spring';
+// const Collapsible = memo(({ children, isOpen }) => {
+// const previous = usePrevious(isOpen);
+// const [refToMeature, { height: viewHeight }] = useMeasure();
+// const { height, opacity, visibility, transform } = useSpring({
+// from: {
+// height: 0,
+// opacity: 0,
+// transform: 'translate3d(20px,0,0)',
+// visibility: 'hidden'
+// },
+// to: {
+// height: isOpen ? viewHeight : 0,
+// opacity: isOpen ? 1 : 0,
+// visibility: isOpen ? 'visible' : 'hidden',
+// transform: `translate3d(${isOpen ? 0 : 20}px,0,0)`
+// }
+// });
+// return (
+//
+// );
+// });
+
+const variantsCollpapsibleWrap = {
+ initialOpen: {
+ height: 'auto',
+ transition: { duration: 0 }
+ },
+ open: height => ({
+ height,
+ opacity: 1,
+ visibility: 'visible',
+ transition: { duration: 0.3 }
+ }),
+ closed: {
+ height: 0,
+ opacity: 0,
+ visibility: 'hidden',
+ transition: { duration: 0.3 }
+ }
+};
+const variantsCollpapsibleChildContainer = {
+ open: {
+ x: 0
+ },
+ closed: {
+ x: 20
+ }
+};
+
+const Collapsible2 = memo(({ children, isOpen }) => {
+ const previous = usePrevious(isOpen);
+ const [refToMeature, { height }] = useMeasure();
+ return (
+
+
+
+ {children}
+
+
+
+ );
+});
+
+const mapState = s => ({
+ // proxies: getProxies(s)
+});
+export default connect(mapState)(ProxyProvider);
diff --git a/src/components/ProxyProvider.module.css b/src/components/ProxyProvider.module.css
new file mode 100644
index 0000000..6668f67
--- /dev/null
+++ b/src/components/ProxyProvider.module.css
@@ -0,0 +1,43 @@
+.header {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+
+ .arrow {
+ display: inline-flex;
+ transform: rotate(0deg);
+ transition: transform 0.3s;
+ &.isOpen {
+ transform: rotate(180deg);
+ }
+
+ &:focus {
+ outline: var(--color-focus-blue) solid 1px;
+ }
+ }
+}
+
+.updatedAt {
+ margin-bottom: 12px;
+ small {
+ color: #777;
+ }
+}
+
+.body {
+ padding: 10px 15px;
+ @media (--breakpoint-not-small) {
+ padding: 10px 40px;
+ }
+}
+
+.actionFooter {
+ display: flex;
+}
+
+.refresh {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ cursor: pointer;
+}
diff --git a/src/components/ProxyProviderList.js b/src/components/ProxyProviderList.js
new file mode 100644
index 0000000..2ae0fce
--- /dev/null
+++ b/src/components/ProxyProviderList.js
@@ -0,0 +1,19 @@
+import React from 'react';
+
+import ContentHeader from './ContentHeader';
+import ProxyProvider from './ProxyProvider';
+
+function ProxyProviderList({ items }) {
+ return (
+ <>
+
+
+ {items.map(item => (
+
+ ))}
+
+ >
+ );
+}
+
+export default ProxyProviderList;
diff --git a/src/components/Root.css b/src/components/Root.css
index d611a02..3622d57 100644
--- a/src/components/Root.css
+++ b/src/components/Root.css
@@ -10,6 +10,13 @@
U+FEFF, U+FFFD;
}
+.relative {
+ position: relative;
+}
+/* .absolute { */
+/* position: absolute; */
+/* } */
+
.border-left,
.border-top,
.border-bottom {
@@ -113,6 +120,12 @@ body.light {
--bg-modal: #fbfbfb;
}
+.flexCenter {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
/* TODO remove fabgrp in component css files */
.fabgrp {
position: fixed;
diff --git a/src/components/Root.js b/src/components/Root.js
index 56c0be0..4147fd2 100644
--- a/src/components/Root.js
+++ b/src/components/Root.js
@@ -1,5 +1,6 @@
import React, { Suspense } from 'react';
-import { Provider } from 'm/store';
+import { Provider } from '../misc/store';
+import StateProvider from './StateProvider';
import { HashRouter as Router, Route } from 'react-router-dom';
import { hot } from 'react-hot-loader/root';
import Loading2 from 'c/Loading2';
@@ -36,30 +37,38 @@ const Rules = React.lazy(() =>
window.store = store;
+const initialState = {
+ proxies: {
+ proxies: {},
+ delay: {},
+ groupNames: []
+ }
+};
+
const Root = () => (
-
-
-
-
-
} />
-
-
}>
-
} />
-
- } />
-
-
- } />
- } />
-
+
+
+
+
+
+
} />
+
+
}>
+
} />
+
+ } />
+
+
+ } />
+ } />
+
+
-
-
-
+
+
+
);
-// } />
-//
export default hot(Root);
diff --git a/src/components/StateProvider.js b/src/components/StateProvider.js
new file mode 100644
index 0000000..adb1b24
--- /dev/null
+++ b/src/components/StateProvider.js
@@ -0,0 +1,79 @@
+import React from 'react';
+import produce, * as immer from 'immer';
+
+const {
+ createContext,
+ memo,
+ useRef,
+ useEffect,
+ useCallback,
+ useContext,
+ useState
+} = React;
+
+const StateContext = createContext(null);
+const DispatchContext = createContext(null);
+
+export { immer };
+
+export function useStoreState() {
+ return useContext(StateContext);
+}
+
+export function useStoreDispatch() {
+ return useContext(DispatchContext);
+}
+
+export default function Provider({ initialState, children }) {
+ const stateRef = useRef(initialState);
+ const [state, setState] = useState(initialState);
+ const getState = useCallback(() => stateRef.current, []);
+ useEffect(() => {
+ if (process.env.NODE_ENV === 'development') {
+ window.getState2 = getState;
+ }
+ }, [getState]);
+ const dispatch = useCallback(
+ (actionId, fn, thunk) => {
+ // if (thunk) return thunk(dispatch, getState);
+ if (typeof actionId === 'function') return actionId(dispatch, getState);
+
+ const stateNext = produce(getState(), fn);
+ if (stateNext !== stateRef.current) {
+ if (process.env.NODE_ENV === 'development') {
+ // eslint-disable-next-line no-console
+ console.log(actionId, stateNext);
+ }
+ stateRef.current = stateNext;
+ setState(stateNext);
+ }
+ },
+ [getState]
+ );
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+export function connect(mapStateToProps) {
+ return Component => {
+ const MemoComponent = memo(Component);
+ function Connected(props) {
+ const state = useContext(StateContext);
+ const dispatch = useContext(DispatchContext);
+ const mapped = mapStateToProps(state, props);
+ const nextProps = {
+ ...props,
+ ...mapped,
+ dispatch
+ };
+ return ;
+ }
+ return Connected;
+ };
+}
diff --git a/src/components/shared/Basic.js b/src/components/shared/Basic.js
new file mode 100644
index 0000000..9d07a39
--- /dev/null
+++ b/src/components/shared/Basic.js
@@ -0,0 +1,12 @@
+import React from 'react';
+
+import s from './Basic.module.css';
+
+export function SectionNameType({ name, type }) {
+ return (
+
+ {name}
+ {type}
+
+ );
+}
diff --git a/src/components/shared/Basic.module.css b/src/components/shared/Basic.module.css
new file mode 100644
index 0000000..8e3e65c
--- /dev/null
+++ b/src/components/shared/Basic.module.css
@@ -0,0 +1,14 @@
+h2.sectionNameType {
+ margin: 0;
+ font-size: 1.3em;
+ @media (--breakpoint-not-small) {
+ font-size: 1.5em;
+ }
+
+ span:nth-child(2) {
+ font-size: 12px;
+ color: #777;
+ font-weight: normal;
+ margin: 0 0.3em;
+ }
+}
diff --git a/src/ducks/index.js b/src/ducks/index.js
index 35ccc10..33e5067 100644
--- a/src/ducks/index.js
+++ b/src/ducks/index.js
@@ -1,7 +1,6 @@
import { combineReducers } from 'redux';
import app from './app';
import modals from './modals';
-import proxies from './proxies';
import rules from './rules';
import logs from './logs';
import configs from './configs';
@@ -9,7 +8,6 @@ import configs from './configs';
export default combineReducers({
app,
modals,
- proxies,
rules,
logs,
configs
diff --git a/src/ducks/proxies.js b/src/store/proxies.js
similarity index 56%
rename from src/ducks/proxies.js
rename to src/store/proxies.js
index 3bc29d3..fe51368 100644
--- a/src/ducks/proxies.js
+++ b/src/store/proxies.js
@@ -1,5 +1,4 @@
-import * as proxiesAPI from 'a/proxies';
-import { getClashAPIConfig } from 'd/app';
+import * as proxiesAPI from '../api/proxies';
// see all types:
// https://github.com/Dreamacro/clash/blob/master/constant/adapters.go
@@ -12,10 +11,136 @@ const ProxyTypes = ['Shadowsocks', 'Snell', 'Socks5', 'Http', 'Vmess'];
export const getProxies = s => s.proxies.proxies;
export const getDelay = s => s.proxies.delay;
export const getProxyGroupNames = s => s.proxies.groupNames;
+export const getProxyProviders = s => s.proxies.proxyProviders || [];
-const CompletedFetchProxies = 'proxies/CompletedFetchProxies';
-const OptimisticSwitchProxy = 'proxies/OptimisticSwitchProxy';
-const CompletedRequestDelayForProxy = 'proxies/CompletedRequestDelayForProxy';
+export function fetchProxies(apiConfig) {
+ return async (dispatch, getState) => {
+ const [proxiesData, providersData] = await Promise.all([
+ proxiesAPI.fetchProxies(apiConfig),
+ proxiesAPI.fetchProviderProxies(apiConfig)
+ ]);
+
+ const [proxyProviders, providerProxies] = formatProxyProviders(
+ providersData.providers
+ );
+ const proxies = { ...providerProxies, ...proxiesData.proxies };
+ const [groupNames, proxyNames] = retrieveGroupNamesFrom(proxies);
+
+ const delayPrev = getDelay(getState());
+ const delayNext = { ...delayPrev };
+
+ for (let i = 0; i < proxyNames.length; i++) {
+ const name = proxyNames[i];
+ const { history } = proxies[name] || { history: [] };
+ const h = history[history.length - 1];
+ if (h) {
+ const ret = { error: '' };
+ if (h.delay === 0) {
+ ret.error = 'LikelyTimeout';
+ } else {
+ ret.number = h.delay;
+ }
+ delayNext[name] = ret;
+ }
+ }
+
+ dispatch('store/proxies#fetchProxies', s => {
+ s.proxies.proxies = proxies;
+ s.proxies.groupNames = groupNames;
+ s.proxies.delay = delayNext;
+ s.proxies.proxyProviders = proxyProviders;
+ });
+ };
+}
+
+export function updateProviderByName(apiConfig, name) {
+ return async dispatch => {
+ try {
+ await proxiesAPI.updateProviderByName(apiConfig, name);
+ } catch (x) {
+ // ignore
+ }
+ // should be optimized
+ // but ¯\_(ツ)_/¯
+ dispatch(fetchProxies(apiConfig));
+ };
+}
+
+export function switchProxy(apiConfig, name1, name2) {
+ return async dispatch => {
+ proxiesAPI
+ .requestToSwitchProxy(apiConfig, name1, name2)
+ .then(
+ res => {
+ if (res.ok === false) {
+ // eslint-disable-next-line no-console
+ console.log('failed to swith proxy', res.statusText);
+ }
+ },
+ err => {
+ // eslint-disable-next-line no-console
+ console.log(err, 'failed to swith proxy');
+ }
+ )
+ .then(() => {
+ dispatch(fetchProxies(apiConfig));
+ });
+ // optimistic UI update
+ dispatch('store/proxies#switchProxy', s => {
+ const proxies = s.proxies.proxies;
+ if (proxies[name1] && proxies[name1].now) {
+ proxies[name1].now = name2;
+ }
+ });
+ };
+}
+
+function requestDelayForProxyOnce(apiConfig, name) {
+ return async (dispatch, getState) => {
+ const res = await proxiesAPI.requestDelayForProxy(apiConfig, name);
+ let error = '';
+ if (res.ok === false) {
+ error = res.statusText;
+ }
+ const { delay } = await res.json();
+
+ const delayPrev = getDelay(getState());
+ const delayNext = {
+ ...delayPrev,
+ [name]: {
+ error,
+ number: delay
+ }
+ };
+
+ dispatch('requestDelayForProxyOnce', s => {
+ s.proxies.delay = delayNext;
+ });
+ };
+}
+
+export function requestDelayForProxy(apiConfig, name) {
+ return async dispatch => {
+ await dispatch(requestDelayForProxyOnce(apiConfig, name));
+ };
+}
+
+export function requestDelayAll(apiConfig) {
+ return async (dispatch, getState) => {
+ const state = getState();
+ const proxies = getProxies(state);
+ const keys = Object.keys(proxies);
+ const proxyNames = [];
+ keys.forEach(k => {
+ if (proxies[k].type === 'Vmess' || proxies[k].type === 'Shadowsocks') {
+ proxyNames.push(k);
+ }
+ });
+ await Promise.all(
+ proxyNames.map(p => dispatch(requestDelayForProxy(apiConfig, p)))
+ );
+ };
+}
function retrieveGroupNamesFrom(proxies) {
let groupNames = [];
@@ -44,141 +169,26 @@ function retrieveGroupNamesFrom(proxies) {
return [groupNames, proxyNames];
}
-export function fetchProxies() {
- return async (dispatch, getState) => {
- // TODO handle errors
-
- const state = getState();
-
- const apiConfig = getClashAPIConfig(state);
- // TODO show loading animation?
- const json = await proxiesAPI.fetchProxies(apiConfig);
- let { proxies = {} } = json;
-
- const [groupNames, proxyNames] = retrieveGroupNamesFrom(proxies);
- const delayPrev = getDelay(getState());
-
- const delayNext = { ...delayPrev };
-
- for (let i = 0; i < proxyNames.length; i++) {
- const name = proxyNames[i];
- const { history } = proxies[name] || { history: [] };
- const h = history[history.length - 1];
- if (h) {
- const ret = { error: '' };
- if (h.delay === 0) {
- ret.error = 'LikelyTimeout';
- } else {
- ret.number = h.delay;
- }
- delayNext[name] = ret;
- }
+function formatProxyProviders(providersInput) {
+ const keys = Object.keys(providersInput);
+ const providers = [];
+ const proxies = {};
+ for (let i = 0; i < keys.length; i++) {
+ const provider = providersInput[keys[i]];
+ if (provider.name === 'default' || provider.vehicleType === 'Compatible')
+ continue;
+ const proxiesArr = provider.proxies;
+ const names = [];
+ for (let j = 0; j < proxiesArr.length; j++) {
+ const proxy = proxiesArr[j];
+ proxies[proxy.name] = proxy;
+ names.push(proxy.name);
}
- dispatch({
- type: CompletedFetchProxies,
- payload: { proxies, groupNames, delay: delayNext }
- });
- };
-}
-
-export function switchProxy(name1, name2) {
- return async (dispatch, getState) => {
- const apiConfig = getClashAPIConfig(getState());
- // TODO display error message
- proxiesAPI
- .requestToSwitchProxy(apiConfig, name1, name2)
- .then(
- res => {
- if (res.ok === false) {
- // eslint-disable-next-line no-console
- console.log('failed to swith proxy', res.statusText);
- }
- },
- err => {
- // eslint-disable-next-line no-console
- console.log(err, 'failed to swith proxy');
- }
- )
- .then(() => {
- // fetchProxies again
- dispatch(fetchProxies());
- });
- // optimistic UI update
- const proxiesCurr = getProxies(getState());
- const proxiesNext = { ...proxiesCurr };
- if (proxiesNext[name1] && proxiesNext[name1].now) {
- proxiesNext[name1].now = name2;
- }
- dispatch({
- type: OptimisticSwitchProxy,
- payload: { proxies: proxiesNext }
- });
- };
-}
-
-function requestDelayForProxyOnce(name) {
- return async (dispatch, getState) => {
- const apiConfig = getClashAPIConfig(getState());
- const res = await proxiesAPI.requestDelayForProxy(apiConfig, name);
- let error = '';
- if (res.ok === false) {
- error = res.statusText;
- }
- const { delay } = await res.json();
-
- const delayPrev = getDelay(getState());
- const delayNext = {
- ...delayPrev,
- [name]: {
- error,
- number: delay
- }
- };
-
- dispatch({
- type: CompletedRequestDelayForProxy,
- payload: { delay: delayNext }
- });
- };
-}
-
-export function requestDelayForProxy(name) {
- return async dispatch => {
- await dispatch(requestDelayForProxyOnce(name));
- };
-}
-
-export function requestDelayAll() {
- return async (dispatch, getState) => {
- const state = getState();
- const proxies = getProxies(state);
- const keys = Object.keys(proxies);
- const proxyNames = [];
- keys.forEach(k => {
- if (proxies[k].type === 'Vmess' || proxies[k].type === 'Shadowsocks') {
- proxyNames.push(k);
- }
- });
- await Promise.all(proxyNames.map(p => dispatch(requestDelayForProxy(p))));
- };
-}
-
-const initialState = {
- proxies: {},
- delay: {},
- groupNames: []
-};
-
-export default function reducer(state = initialState, { type, payload }) {
- switch (type) {
- case CompletedRequestDelayForProxy:
- case OptimisticSwitchProxy:
- case CompletedFetchProxies: {
- return { ...state, ...payload };
- }
-
- default:
- return state;
+ // mutate directly
+ provider.proxies = names;
+ providers.push(provider);
}
+
+ return [providers, proxies];
}
diff --git a/yarn.lock b/yarn.lock
index b3b0da0..3c773fd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -875,6 +875,18 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
+"@emotion/is-prop-valid@^0.8.2":
+ version "0.8.5"
+ resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.5.tgz#2dda0791f0eafa12b7a0a5b39858405cc7bde983"
+ integrity sha512-6ZODuZSFofbxSbcxwsFz+6ioPjb0ISJRRPLZ+WIbjcU2IMU0Io+RGQjjaTgOvNQl007KICBm7zXQaYQEC1r6Bg==
+ dependencies:
+ "@emotion/memoize" "0.7.3"
+
+"@emotion/memoize@0.7.3":
+ version "0.7.3"
+ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.3.tgz#5b6b1c11d6a6dddf1f2fc996f74cf3b219644d78"
+ integrity sha512-2Md9mH6mvo+ygq1trTeVp2uzAKwE2P7In0cRpD/M9Q70aH8L+rxMLbb3JCN2JoSWsV2O+DdFjfbbXoMoLBczow==
+
"@hot-loader/react-dom@16.10.2":
version "16.10.2"
resolved "https://registry.yarnpkg.com/@hot-loader/react-dom/-/react-dom-16.10.2.tgz#91920442252acac6f343eef5df41aca333f7dcea"
@@ -911,6 +923,22 @@
"@nodelib/fs.scandir" "2.1.3"
fastq "^1.6.0"
+"@popmotion/easing@^1.0.1", "@popmotion/easing@^1.0.2":
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/@popmotion/easing/-/easing-1.0.2.tgz#17d925c45b4bf44189e5a38038d149df42d8c0b4"
+ integrity sha512-IkdW0TNmRnWTeWI7aGQIVDbKXPWHVEYdGgd5ZR4SH/Ty/61p63jCjrPxX1XrR7IGkl08bjhJROStD7j+RKgoIw==
+
+"@popmotion/popcorn@^0.4.2", "@popmotion/popcorn@^0.4.4":
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/@popmotion/popcorn/-/popcorn-0.4.4.tgz#a5f906fccdff84526e3fcb892712d7d8a98d6adc"
+ integrity sha512-jYO/8319fKoNLMlY4ZJPiPu8Ea8occYwRZhxpaNn/kZsK4QG2E7XFlXZMJBsTWDw7I1i0uaqyC4zn1nwEezLzg==
+ dependencies:
+ "@popmotion/easing" "^1.0.1"
+ framesync "^4.0.1"
+ hey-listen "^1.0.8"
+ style-value-types "^3.1.7"
+ tslib "^1.10.0"
+
"@samverschueren/stream-to-observable@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f"
@@ -3522,6 +3550,30 @@ fragment-cache@^0.2.1:
dependencies:
map-cache "^0.2.2"
+framer-motion@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-1.7.0.tgz#36d038874c8e2c45ad6e4824df8a61508bacbda0"
+ integrity sha512-bE1aWkX0AKgmxMjFnAcXZ1d47TcuKugLn3D5cHvZcQwBiDPJuKD1WzwdTaDb7EHc+vbiWfBgMbECWxQFb9di5g==
+ dependencies:
+ "@popmotion/easing" "^1.0.2"
+ "@popmotion/popcorn" "^0.4.2"
+ framesync "^4.0.4"
+ hey-listen "^1.0.8"
+ popmotion "9.0.0-beta-8"
+ style-value-types "^3.1.6"
+ stylefire "^7.0.0"
+ tslib "^1.10.0"
+ optionalDependencies:
+ "@emotion/is-prop-valid" "^0.8.2"
+
+framesync@^4.0.0, framesync@^4.0.1, framesync@^4.0.4:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/framesync/-/framesync-4.0.4.tgz#79c42c0118f26821c078570db0ff81fb863516a2"
+ integrity sha512-mdP0WvVHe0/qA62KG2LFUAOiWLng5GLpscRlwzBxu2VXOp6B8hNs5C5XlFigsMgrfDrr2YbqTsgdWZTc4RXRMQ==
+ dependencies:
+ hey-listen "^1.0.8"
+ tslib "^1.10.0"
+
fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
@@ -3871,6 +3923,11 @@ hex-color-regex@^1.1.0:
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
+hey-listen@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68"
+ integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==
+
history@^4.7.2, history@^4.9.0:
version "4.10.1"
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
@@ -4069,6 +4126,11 @@ image-size@^0.5.1:
resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"
integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=
+immer@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/immer/-/immer-5.0.1.tgz#1a1184fa758f68f1b5573db840825fb5164cceca"
+ integrity sha512-KFHV1ivrBmPCVRhjy9oBooypnPfJ876NTrWXMNoUhXFAaWWAViVqZ4l6HxPST52qcN82qqsR38/pCGYRWP5W7w==
+
import-cwd@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
@@ -5873,6 +5935,18 @@ please-upgrade-node@^3.1.1, please-upgrade-node@^3.2.0:
dependencies:
semver-compare "^1.0.0"
+popmotion@9.0.0-beta-8:
+ version "9.0.0-beta-8"
+ resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-9.0.0-beta-8.tgz#f5a709f11737734e84f2a6b73f9bcf25ee30c388"
+ integrity sha512-6eQzqursPvnP7ePvdfPeY4wFHmS3OLzNP8rJRvmfFfEIfpFqrQgLsM50Gd9AOvGKJtYJOFknNG+dsnzCpgIdAA==
+ dependencies:
+ "@popmotion/easing" "^1.0.1"
+ "@popmotion/popcorn" "^0.4.2"
+ framesync "^4.0.4"
+ hey-listen "^1.0.8"
+ style-value-types "^3.1.6"
+ tslib "^1.10.0"
+
posix-character-classes@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@@ -6808,6 +6882,11 @@ reselect@^4.0.0:
resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7"
integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==
+resize-observer-polyfill@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
+ integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
+
resolve-cwd@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
@@ -7488,6 +7567,25 @@ style-loader@^1.0.1:
loader-utils "^1.2.3"
schema-utils "^2.0.1"
+style-value-types@^3.1.6, style-value-types@^3.1.7:
+ version "3.1.7"
+ resolved "https://registry.yarnpkg.com/style-value-types/-/style-value-types-3.1.7.tgz#3d7d3cf9cb9f9ee86c00e19ba65d6a181a0db33a"
+ integrity sha512-jPaG5HcAPs3vetSwOJozrBXxuHo9tjZVnbRyBjxqb00c2saIoeuBJc1/2MtvB8eRZy41u/BBDH0CpfzWixftKg==
+ dependencies:
+ hey-listen "^1.0.8"
+ tslib "^1.10.0"
+
+stylefire@^7.0.0:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/stylefire/-/stylefire-7.0.1.tgz#69777dc9ffc48248284ea5c0a3fe03ba5c468339"
+ integrity sha512-xp7hGiK5xyX7xINQI7qWagcLMyS6aLLbP2kMA3DTS4X31uPI3M7IyOm9TPmXwaMKG9fJ2Cgo4F2okcNB6nzxKQ==
+ dependencies:
+ "@popmotion/popcorn" "^0.4.4"
+ framesync "^4.0.0"
+ hey-listen "^1.0.8"
+ style-value-types "^3.1.7"
+ tslib "^1.10.0"
+
stylehacks@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5"
@@ -7779,7 +7877,7 @@ tryer@^1.0.1:
resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8"
integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==
-tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
+tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
version "1.10.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==