feat: support proxy provider
This commit is contained in:
parent
040c5de04a
commit
d81592ec97
22 changed files with 935 additions and 304 deletions
|
@ -41,7 +41,9 @@
|
|||
"classnames": "^2.2.6",
|
||||
"core-js": "^3.4.5",
|
||||
"date-fns": "^2.8.1",
|
||||
"framer-motion": "^1.7.0",
|
||||
"history": "^4.7.2",
|
||||
"immer": "^5.0.1",
|
||||
"invariant": "^2.2.4",
|
||||
"lodash-es": "^4.17.14",
|
||||
"memoize-one": "^5.1.1",
|
||||
|
@ -97,6 +99,7 @@
|
|||
"postcss-nested": "^4.2.0",
|
||||
"prettier": "^1.17.1",
|
||||
"react-hot-loader": "^4.12.18",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"style-loader": "^1.0.1",
|
||||
"svg-sprite-loader": "^4.1.2",
|
||||
"terser-webpack-plugin": "^2.2.1",
|
||||
|
|
|
@ -18,13 +18,13 @@ Vary: Origin
|
|||
Date: Tue, 16 Oct 2018 16:38:33 GMT
|
||||
*/
|
||||
|
||||
async function fetchProxies(config) {
|
||||
export async function fetchProxies(config) {
|
||||
const { url, init } = getURLAndInit(config);
|
||||
const res = await fetch(url + endpoint, init);
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
async function requestToSwitchProxy(apiConfig, name1, name2) {
|
||||
export async function requestToSwitchProxy(apiConfig, name1, name2) {
|
||||
const body = { name: name2 };
|
||||
const { url, init } = getURLAndInit(apiConfig);
|
||||
const fullURL = `${url}${endpoint}/${name1}`;
|
||||
|
@ -35,11 +35,27 @@ async function requestToSwitchProxy(apiConfig, name1, name2) {
|
|||
});
|
||||
}
|
||||
|
||||
async function requestDelayForProxy(apiConfig, name) {
|
||||
export async function requestDelayForProxy(apiConfig, name) {
|
||||
const { url, init } = getURLAndInit(apiConfig);
|
||||
const qs = 'timeout=5000&url=http://www.google.com/generate_204';
|
||||
const fullURL = `${url}${endpoint}/${name}/delay?${qs}`;
|
||||
return await fetch(fullURL, init);
|
||||
}
|
||||
|
||||
export { fetchProxies, requestToSwitchProxy, requestDelayForProxy };
|
||||
export async function fetchProviderProxies(config) {
|
||||
const { url, init } = getURLAndInit(config);
|
||||
const res = await fetch(url + '/providers/proxies', init);
|
||||
if (res.status === 404) {
|
||||
return { providers: {} };
|
||||
}
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
export async function updateProviderByName(config, name) {
|
||||
const { url, init } = getURLAndInit(config);
|
||||
const options = {
|
||||
...init,
|
||||
method: 'PUT'
|
||||
};
|
||||
return await fetch(url + '/providers/proxies/' + name, options);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
|
||||
import s0 from 'c/Button.module.css';
|
||||
const noop = () => {};
|
||||
|
@ -13,6 +14,14 @@ function Button({ children, label, onClick = noop }, ref) {
|
|||
);
|
||||
}
|
||||
|
||||
export function ButtonPlain({ children, label, onClick = noop }) {
|
||||
return (
|
||||
<button className={cx(s0.btn, s0.plain)} onClick={onClick}>
|
||||
{children || label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function WithIcon({ text, icon, onClick = noop }, ref) {
|
||||
return (
|
||||
<button className={s0.btn} ref={ref} onClick={onClick}>
|
||||
|
|
|
@ -24,6 +24,22 @@
|
|||
font-size: 1em;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
&.plain {
|
||||
border-radius: 100%;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
border-color: transparent;
|
||||
background: none;
|
||||
&:focus {
|
||||
border-color: var(--color-focus-blue);
|
||||
}
|
||||
&:hover {
|
||||
background: #387cec;
|
||||
border: 1px solid #387cec;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.withIconWrapper {
|
||||
|
|
|
@ -1,63 +1,81 @@
|
|||
import React from 'react';
|
||||
import { useActions, useStoreState } from 'm/store';
|
||||
import { useStoreState } from 'm/store';
|
||||
|
||||
import ContentHeader from 'c/ContentHeader';
|
||||
import ProxyGroup from 'c/ProxyGroup';
|
||||
import { ButtonWithIcon } from 'c/Button';
|
||||
import { connect } from './StateProvider';
|
||||
|
||||
import ContentHeader from './ContentHeader';
|
||||
import ProxyGroup from './ProxyGroup';
|
||||
import { ButtonWithIcon } from './Button';
|
||||
import { Zap } from 'react-feather';
|
||||
|
||||
import s0 from 'c/Proxies.module.css';
|
||||
import ProxyProviderList from './ProxyProviderList';
|
||||
|
||||
import s0 from './Proxies.module.css';
|
||||
|
||||
import {
|
||||
getProxies,
|
||||
getDelay,
|
||||
getProxyGroupNames,
|
||||
getProxyProviders,
|
||||
fetchProxies,
|
||||
requestDelayAll
|
||||
} from 'd/proxies';
|
||||
} from '../store/proxies';
|
||||
|
||||
const { useEffect, useMemo } = React;
|
||||
import { getClashAPIConfig } from '../ducks/app';
|
||||
|
||||
const { useEffect, useMemo, useCallback } = React;
|
||||
|
||||
const mapStateToProps = s => ({
|
||||
proxies: getProxies(s),
|
||||
groupNames: getProxyGroupNames(s)
|
||||
apiConfig: getClashAPIConfig(s)
|
||||
});
|
||||
|
||||
const actions = {
|
||||
fetchProxies,
|
||||
requestDelayAll
|
||||
};
|
||||
|
||||
export default function Proxies() {
|
||||
const { fetchProxies, requestDelayAll } = useActions(actions);
|
||||
function Proxies({ dispatch, groupNames, proxies, delay, proxyProviders }) {
|
||||
const { apiConfig } = useStoreState(mapStateToProps);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await fetchProxies();
|
||||
// await requestDelayAll();
|
||||
})();
|
||||
}, [fetchProxies, requestDelayAll]);
|
||||
const { groupNames } = useStoreState(mapStateToProps);
|
||||
dispatch(fetchProxies(apiConfig));
|
||||
}, [dispatch, apiConfig]);
|
||||
const requestDelayAllFn = useCallback(
|
||||
() => dispatch(requestDelayAll(apiConfig)),
|
||||
[apiConfig, dispatch]
|
||||
);
|
||||
const icon = useMemo(() => <Zap width={16} />, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContentHeader title="Proxies" />
|
||||
<div className={s0.body}>
|
||||
<div>
|
||||
<div className="fabgrp">
|
||||
<ButtonWithIcon
|
||||
text="Test Latency"
|
||||
icon={icon}
|
||||
onClick={requestDelayAll}
|
||||
onClick={requestDelayAllFn}
|
||||
/>
|
||||
{/* <Button onClick={requestDelayAll}>Test Latency</Button> */}
|
||||
</div>
|
||||
{groupNames.map(groupName => {
|
||||
return (
|
||||
<div className={s0.group} key={groupName}>
|
||||
<ProxyGroup name={groupName} />
|
||||
<ProxyGroup
|
||||
name={groupName}
|
||||
proxies={proxies}
|
||||
delay={delay}
|
||||
apiConfig={apiConfig}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<ProxyProviderList items={proxyProviders} />
|
||||
<div style={{ height: 60 }} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const mapState = s => ({
|
||||
groupNames: getProxyGroupNames(s),
|
||||
proxies: getProxies(s),
|
||||
proxyProviders: getProxyProviders(s),
|
||||
delay: getDelay(s)
|
||||
});
|
||||
|
||||
export default connect(mapState)(Proxies);
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
.body {
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
.group {
|
||||
padding: 10px 15px;
|
||||
@media (--breakpoint-not-small) {
|
||||
|
|
|
@ -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 (
|
||||
<div
|
||||
className={cx(s0.proxySmall, { [s0.now]: now })}
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Proxy({ now, name, proxy, latency }: ProxyProps) {
|
||||
const color = useMemo(() => getLabelColor(latency), [latency]);
|
||||
return (
|
||||
<div
|
||||
className={cx(s0.proxy, {
|
||||
|
@ -46,14 +79,22 @@ function Proxy({ now, name }) {
|
|||
{proxy.type}
|
||||
</div>
|
||||
<div className={s0.proxyLatencyWrap}>
|
||||
{latency && latency.number ? <ProxyLatency latency={latency} /> : null}
|
||||
{latency && latency.number ? (
|
||||
<ProxyLatency number={latency.number} color={color} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<div className={s0.group}>
|
||||
<div className={s0.header}>
|
||||
<h2>
|
||||
<span>{name}</span>
|
||||
<span>{group.type}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div className={s0.list}>
|
||||
{all.map(proxyName => {
|
||||
const isSelectable = group.type === 'Selector';
|
||||
const proxyClassName = cx(s0.proxy, {
|
||||
[s0.proxySelectable]: isSelectable
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={proxyClassName}
|
||||
key={proxyName}
|
||||
onClick={() => {
|
||||
if (!isSelectable) return;
|
||||
actions.switchProxy(name, proxyName);
|
||||
}}
|
||||
>
|
||||
<Proxy
|
||||
isSelectable={isSelectable}
|
||||
name={proxyName}
|
||||
now={proxyName === group.now}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<SectionNameType name={name} type={group.type} />
|
||||
</div>
|
||||
<ProxyList
|
||||
all={all}
|
||||
now={now}
|
||||
isSelectable={isSelectable}
|
||||
itemOnTapCallback={itemOnTapCallback}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={s0.list}>
|
||||
{all.map(proxyName => {
|
||||
const proxyClassName = cx(s0.proxy, {
|
||||
[s0.proxySelectable]: isSelectable
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={proxyClassName}
|
||||
key={proxyName}
|
||||
onClick={() => {
|
||||
if (!isSelectable || !itemOnTapCallback) return;
|
||||
itemOnTapCallback(proxyName);
|
||||
}}
|
||||
>
|
||||
<Proxy name={proxyName} now={proxyName === now} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProxyListSummaryView({
|
||||
all,
|
||||
now,
|
||||
isSelectable,
|
||||
itemOnTapCallback
|
||||
}: ProxyListProps) {
|
||||
return (
|
||||
<div className={s0.list}>
|
||||
{all.map(proxyName => {
|
||||
const proxyClassName = cx(s0.proxy, {
|
||||
[s0.proxySelectable]: isSelectable
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={proxyClassName}
|
||||
key={proxyName}
|
||||
onClick={() => {
|
||||
if (!isSelectable || !itemOnTapCallback) return;
|
||||
itemOnTapCallback(proxyName);
|
||||
}}
|
||||
>
|
||||
<ProxySmall name={proxyName} now={proxyName === now} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(ProxyGroup);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<span className={s0.proxyLatency} style={{ color }}>
|
||||
{error !== '' ? <span>{error}</span> : <span>{number} ms</span>}
|
||||
<span>{number} ms</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
ProxyLatency.propTypes = {
|
||||
latency: PropTypes.shape({
|
||||
number: PropTypes.number,
|
||||
error: PropTypes.string
|
||||
})
|
||||
};
|
||||
|
|
211
src/components/ProxyProvider.js
Normal file
211
src/components/ProxyProvider.js
Normal file
|
@ -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<string>,
|
||||
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 (
|
||||
<div className={s.body}>
|
||||
<div className={s.header} onClick={toggle}>
|
||||
<SectionNameType name={item.name} type={item.vehicleType} />
|
||||
<ButtonPlain>
|
||||
<span className={cx(s.arrow, { [s.isOpen]: isCollapsibleOpen })}>
|
||||
<ChevronDown />
|
||||
</span>
|
||||
</ButtonPlain>
|
||||
</div>
|
||||
<div className={s.updatedAt}>
|
||||
<small>Updated {timeAgo} ago</small>
|
||||
</div>
|
||||
<Collapsible2 isOpen={isCollapsibleOpen}>
|
||||
<ProxyList all={item.proxies} />
|
||||
<div className={s.actionFooter}>
|
||||
<ButtonWithIcon
|
||||
text="Update"
|
||||
icon={<Refresh />}
|
||||
onClick={updateProvider}
|
||||
/>
|
||||
</div>
|
||||
</Collapsible2>
|
||||
<Collapsible2 isOpen={!isCollapsibleOpen}>
|
||||
<ProxyListSummaryView all={item.proxies} />
|
||||
</Collapsible2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<motion.div
|
||||
className={s.refresh}
|
||||
variants={button}
|
||||
initial="rest"
|
||||
whileHover="hover"
|
||||
whileTap="pressed"
|
||||
>
|
||||
<motion.div className="flexCenter" variants={arrow}>
|
||||
<RotateCw size={16} />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
// <div>
|
||||
// <a.div
|
||||
// style={{
|
||||
// opacity,
|
||||
// willChange: 'transform, opacity, height, visibility',
|
||||
// visibility,
|
||||
// height: isOpen && previous === isOpen ? 'auto' : height
|
||||
// }}>
|
||||
// <a.div style={{ transform }} ref={refToMeature} children={children} />
|
||||
// </a.div>
|
||||
// </div>
|
||||
// );
|
||||
// });
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<motion.div
|
||||
animate={
|
||||
isOpen && previous === isOpen
|
||||
? 'initialOpen'
|
||||
: isOpen
|
||||
? 'open'
|
||||
: 'closed'
|
||||
}
|
||||
custom={height}
|
||||
variants={variantsCollpapsibleWrap}
|
||||
>
|
||||
<motion.div
|
||||
variants={variantsCollpapsibleChildContainer}
|
||||
ref={refToMeature}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const mapState = s => ({
|
||||
// proxies: getProxies(s)
|
||||
});
|
||||
export default connect(mapState)(ProxyProvider);
|
43
src/components/ProxyProvider.module.css
Normal file
43
src/components/ProxyProvider.module.css
Normal file
|
@ -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;
|
||||
}
|
19
src/components/ProxyProviderList.js
Normal file
19
src/components/ProxyProviderList.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
|
||||
import ContentHeader from './ContentHeader';
|
||||
import ProxyProvider from './ProxyProvider';
|
||||
|
||||
function ProxyProviderList({ items }) {
|
||||
return (
|
||||
<>
|
||||
<ContentHeader title="Proxy Provider" />
|
||||
<div>
|
||||
{items.map(item => (
|
||||
<ProxyProvider key={item.name} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProxyProviderList;
|
|
@ -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;
|
||||
|
|
|
@ -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 = () => (
|
||||
<ErrorBoundary>
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<div className={s0.app}>
|
||||
<APIDiscovery />
|
||||
<Route path="/" render={props => <SideBar {...props} />} />
|
||||
<div className={s0.content}>
|
||||
<Suspense fallback={<Loading2 />}>
|
||||
<Route exact path="/" render={() => <Home />} />
|
||||
<Route exact path="/connections" component={Connections} />
|
||||
<Route exact path="/overview" render={() => <Home />} />
|
||||
<Route exact path="/configs" component={Config} />
|
||||
<Route exact path="/logs" component={Logs} />
|
||||
<Route exact path="/proxies" render={() => <Proxies />} />
|
||||
<Route exact path="/rules" render={() => <Rules />} />
|
||||
</Suspense>
|
||||
<StateProvider initialState={initialState}>
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<div className={s0.app}>
|
||||
<APIDiscovery />
|
||||
<Route path="/" render={props => <SideBar {...props} />} />
|
||||
<div className={s0.content}>
|
||||
<Suspense fallback={<Loading2 />}>
|
||||
<Route exact path="/" render={() => <Home />} />
|
||||
<Route exact path="/connections" component={Connections} />
|
||||
<Route exact path="/overview" render={() => <Home />} />
|
||||
<Route exact path="/configs" component={Config} />
|
||||
<Route exact path="/logs" component={Logs} />
|
||||
<Route exact path="/proxies" render={() => <Proxies />} />
|
||||
<Route exact path="/rules" render={() => <Rules />} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Router>
|
||||
</Provider>
|
||||
</Router>
|
||||
</Provider>
|
||||
</StateProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
// <Route exact path="/__0" render={() => <StyleGuide />} />
|
||||
// <Route exact path="/__1" component={Loading} />
|
||||
|
||||
export default hot(Root);
|
||||
|
|
79
src/components/StateProvider.js
Normal file
79
src/components/StateProvider.js
Normal file
|
@ -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 (
|
||||
<StateContext.Provider value={state}>
|
||||
<DispatchContext.Provider value={dispatch}>
|
||||
{children}
|
||||
</DispatchContext.Provider>
|
||||
</StateContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
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 <MemoComponent {...nextProps} />;
|
||||
}
|
||||
return Connected;
|
||||
};
|
||||
}
|
12
src/components/shared/Basic.js
Normal file
12
src/components/shared/Basic.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
|
||||
import s from './Basic.module.css';
|
||||
|
||||
export function SectionNameType({ name, type }) {
|
||||
return (
|
||||
<h2 className={s.sectionNameType}>
|
||||
<span>{name}</span>
|
||||
<span>{type}</span>
|
||||
</h2>
|
||||
);
|
||||
}
|
14
src/components/shared/Basic.module.css
Normal file
14
src/components/shared/Basic.module.css
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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];
|
||||
}
|
100
yarn.lock
100
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==
|
||||
|
|
Loading…
Reference in a new issue