feat: allow change proxies sorting in group
This commit is contained in:
parent
7cdbba5bf4
commit
94e2b1e398
25 changed files with 1072 additions and 336 deletions
|
@ -1,24 +1,7 @@
|
||||||
---
|
---
|
||||||
env:
|
|
||||||
browser: true
|
|
||||||
node: true
|
|
||||||
jest/globals: true
|
|
||||||
es6: true
|
|
||||||
|
|
||||||
parser: babel-eslint
|
|
||||||
|
|
||||||
plugins:
|
|
||||||
- import
|
|
||||||
- react-hooks
|
|
||||||
- jest
|
|
||||||
|
|
||||||
extends:
|
extends:
|
||||||
- react-app
|
- react-app
|
||||||
- eslint:recommended
|
- eslint:recommended
|
||||||
- plugin:import/errors
|
|
||||||
|
|
||||||
settings:
|
|
||||||
import/resolver: webpack
|
|
||||||
|
|
||||||
globals:
|
globals:
|
||||||
__DEV__: true
|
__DEV__: true
|
||||||
|
|
31
package.json
31
package.json
|
@ -2,7 +2,6 @@
|
||||||
"name": "yacd",
|
"name": "yacd",
|
||||||
"version": "0.1.11",
|
"version": "0.1.11",
|
||||||
"description": "Yet another Clash dashboard",
|
"description": "Yet another Clash dashboard",
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint --cache src",
|
"lint": "eslint --cache src",
|
||||||
"start": "NODE_ENV=development node server.js",
|
"start": "NODE_ENV=development node server.js",
|
||||||
|
@ -26,7 +25,8 @@
|
||||||
"not op_mini all"
|
"not op_mini all"
|
||||||
],
|
],
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react"
|
"react",
|
||||||
|
"clash"
|
||||||
],
|
],
|
||||||
"author": "Haishan <haishanhan@gmail.com> (https://haishan.me)",
|
"author": "Haishan <haishanhan@gmail.com> (https://haishan.me)",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@ -71,23 +71,30 @@
|
||||||
"@babel/preset-flow": "^7.7.4",
|
"@babel/preset-flow": "^7.7.4",
|
||||||
"@babel/preset-react": "^7.7.4",
|
"@babel/preset-react": "^7.7.4",
|
||||||
"@hsjs/react-refresh-webpack-plugin": "^0.1.3",
|
"@hsjs/react-refresh-webpack-plugin": "^0.1.3",
|
||||||
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.2.0",
|
||||||
|
"@types/jest": "^25.2.1",
|
||||||
|
"@types/react": "^16.9.34",
|
||||||
|
"@typescript-eslint/eslint-plugin": "2.x",
|
||||||
|
"@typescript-eslint/parser": "2.x",
|
||||||
"autoprefixer": "^9.7.3",
|
"autoprefixer": "^9.7.3",
|
||||||
"babel-eslint": "^10.0.3",
|
"babel-eslint": "10.x",
|
||||||
"babel-loader": "^8.0.5",
|
"babel-loader": "^8.0.5",
|
||||||
"clean-webpack-plugin": "^3.0.0",
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
"copy-webpack-plugin": "^5.1.1",
|
"copy-webpack-plugin": "^5.1.1",
|
||||||
"css-loader": "^3.4.2",
|
"css-loader": "^3.4.2",
|
||||||
"cssnano": "^4.1.7",
|
"cssnano": "^4.1.7",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "6.x",
|
||||||
"eslint-config-react-app": "^5.0.2",
|
"eslint-config-react-app": "^5.2.1",
|
||||||
"eslint-import-resolver-webpack": "^0.12.0",
|
"eslint-import-resolver-webpack": "^0.12.0",
|
||||||
"eslint-plugin-flowtype": "^4.6.0",
|
"eslint-plugin-flowtype": "4.x",
|
||||||
"eslint-plugin-import": "^2.18.0",
|
"eslint-plugin-import": "2.x",
|
||||||
"eslint-plugin-jest": "^23.6.0",
|
"eslint-plugin-jest": "^23.6.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
"eslint-plugin-jsx-a11y": "6.x",
|
||||||
"eslint-plugin-react": "^7.17.0",
|
"eslint-plugin-react": "7.x",
|
||||||
"eslint-plugin-react-hooks": "^3.0.0",
|
"eslint-plugin-react-hooks": "2.x",
|
||||||
"file-loader": "^6.0.0",
|
"file-loader": "^6.0.0",
|
||||||
|
"fork-ts-checker-notifier-webpack-plugin": "^2.0.0",
|
||||||
|
"fork-ts-checker-webpack-plugin": "^4.1.3",
|
||||||
"html-webpack-plugin": "^4.2.0",
|
"html-webpack-plugin": "^4.2.0",
|
||||||
"husky": "^4.0.0",
|
"husky": "^4.0.0",
|
||||||
"lint-staged": "^10.0.7",
|
"lint-staged": "^10.0.7",
|
||||||
|
@ -99,10 +106,12 @@
|
||||||
"postcss-nested": "^4.2.0",
|
"postcss-nested": "^4.2.0",
|
||||||
"postcss-simple-vars": "^5.0.2",
|
"postcss-simple-vars": "^5.0.2",
|
||||||
"prettier": "^2.0.4",
|
"prettier": "^2.0.4",
|
||||||
"react-refresh": "0.0.0-experimental-241c4467e",
|
"react-refresh": "^0.8.1",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"style-loader": "^1.1.2",
|
"style-loader": "^1.1.2",
|
||||||
"terser-webpack-plugin": "^2.3.1",
|
"terser-webpack-plugin": "^2.3.1",
|
||||||
|
"ts-loader": "^7.0.1",
|
||||||
|
"typescript": "^3.8.3",
|
||||||
"webpack": "^4.41.6",
|
"webpack": "^4.41.6",
|
||||||
"webpack-bundle-analyzer": "^3.6.0",
|
"webpack-bundle-analyzer": "^3.6.0",
|
||||||
"webpack-cli": "^3.3.10",
|
"webpack-cli": "^3.3.10",
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
outline: none;
|
outline: none;
|
||||||
position: absolute;
|
position: relative;
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
transform: translate(-50%, -50%) scale(1.5);
|
transform: translate(-50%, -50%) scale(1.2);
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +1,34 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { connect, useStoreActions } from './StateProvider';
|
import { connect } from './StateProvider';
|
||||||
|
|
||||||
|
import Button from './Button';
|
||||||
import ContentHeader from './ContentHeader';
|
import ContentHeader from './ContentHeader';
|
||||||
import ProxyGroup from './ProxyGroup';
|
import ProxyGroup from './ProxyGroup';
|
||||||
import { Zap, Filter, Circle } from 'react-feather';
|
import BaseModal from './shared/BaseModal';
|
||||||
|
import Settings from './proxies/Settings';
|
||||||
|
import Equalizer from './svg/Equalizer';
|
||||||
|
import { Zap } from 'react-feather';
|
||||||
|
|
||||||
import ProxyProviderList from './ProxyProviderList';
|
import ProxyProviderList from './ProxyProviderList';
|
||||||
import { Fab, Action } from 'react-tiny-fab';
|
import { Fab } from 'react-tiny-fab';
|
||||||
|
|
||||||
import './rtf.css';
|
import './rtf.css';
|
||||||
import s0 from './Proxies.module.css';
|
import s0 from './Proxies.module.css';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getDelay,
|
getDelay,
|
||||||
getRtFilterSwitch,
|
|
||||||
getProxyGroupNames,
|
getProxyGroupNames,
|
||||||
getProxyProviders,
|
getProxyProviders,
|
||||||
fetchProxies,
|
fetchProxies,
|
||||||
requestDelayAll
|
requestDelayAll,
|
||||||
} from '../store/proxies';
|
} from '../store/proxies';
|
||||||
import { getClashAPIConfig } from '../store/app';
|
import { getClashAPIConfig } from '../store/app';
|
||||||
|
|
||||||
const { useEffect, useCallback, useRef } = React;
|
const { useState, useEffect, useCallback, useRef } = React;
|
||||||
|
|
||||||
function Proxies({
|
function Proxies({ dispatch, groupNames, delay, proxyProviders, apiConfig }) {
|
||||||
dispatch,
|
|
||||||
groupNames,
|
|
||||||
delay,
|
|
||||||
proxyProviders,
|
|
||||||
apiConfig,
|
|
||||||
filterZeroRT
|
|
||||||
}) {
|
|
||||||
const refFetchedTimestamp = useRef({});
|
const refFetchedTimestamp = useRef({});
|
||||||
const { toggleUnavailableProxiesFilter } = useStoreActions();
|
|
||||||
const requestDelayAllFn = useCallback(
|
const requestDelayAllFn = useCallback(
|
||||||
() => dispatch(requestDelayAll(apiConfig)),
|
() => dispatch(requestDelayAll(apiConfig)),
|
||||||
[apiConfig, dispatch]
|
[apiConfig, dispatch]
|
||||||
|
@ -62,11 +57,27 @@ function Proxies({
|
||||||
return () => window.removeEventListener('focus', fn, false);
|
return () => window.removeEventListener('focus', fn, false);
|
||||||
}, [fetchProxiesHooked]);
|
}, [fetchProxiesHooked]);
|
||||||
|
|
||||||
|
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
|
||||||
|
const closeSettingsModal = useCallback(() => {
|
||||||
|
setIsSettingsModalOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<div className={s0.topBar}>
|
||||||
|
<Button kind="minimal" onClick={() => setIsSettingsModalOpen(true)}>
|
||||||
|
<Equalizer size={16} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<BaseModal
|
||||||
|
isOpen={isSettingsModalOpen}
|
||||||
|
onRequestClose={closeSettingsModal}
|
||||||
|
>
|
||||||
|
<Settings />
|
||||||
|
</BaseModal>
|
||||||
<ContentHeader title="Proxies" />
|
<ContentHeader title="Proxies" />
|
||||||
<div>
|
<div>
|
||||||
{groupNames.map(groupName => {
|
{groupNames.map((groupName) => {
|
||||||
return (
|
return (
|
||||||
<div className={s0.group} key={groupName}>
|
<div className={s0.group} key={groupName}>
|
||||||
<ProxyGroup
|
<ProxyGroup
|
||||||
|
@ -81,27 +92,20 @@ function Proxies({
|
||||||
</div>
|
</div>
|
||||||
<ProxyProviderList items={proxyProviders} />
|
<ProxyProviderList items={proxyProviders} />
|
||||||
<div style={{ height: 60 }} />
|
<div style={{ height: 60 }} />
|
||||||
<Fab icon={<Circle />}>
|
<Fab
|
||||||
<Action text="Test Latency" onClick={requestDelayAllFn}>
|
icon={<Zap width={16} />}
|
||||||
<Zap width={16} />
|
onClick={requestDelayAllFn}
|
||||||
</Action>
|
text="Test Latency"
|
||||||
<Action
|
></Fab>
|
||||||
text={(filterZeroRT ? 'Show' : 'Hide') + ' Unavailable Proxies'}
|
|
||||||
onClick={toggleUnavailableProxiesFilter}
|
|
||||||
>
|
|
||||||
<Filter width={16} />
|
|
||||||
</Action>
|
|
||||||
</Fab>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapState = s => ({
|
const mapState = (s) => ({
|
||||||
apiConfig: getClashAPIConfig(s),
|
apiConfig: getClashAPIConfig(s),
|
||||||
groupNames: getProxyGroupNames(s),
|
groupNames: getProxyGroupNames(s),
|
||||||
proxyProviders: getProxyProviders(s),
|
proxyProviders: getProxyProviders(s),
|
||||||
delay: getDelay(s),
|
delay: getDelay(s),
|
||||||
filterZeroRT: getRtFilterSwitch(s)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapState)(Proxies);
|
export default connect(mapState)(Proxies);
|
||||||
|
|
|
@ -1,3 +1,13 @@
|
||||||
|
.topBar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
background: var(--color-background);
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 5px 5px 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.group {
|
.group {
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
@media (--breakpoint-not-small) {
|
@media (--breakpoint-not-small) {
|
||||||
|
|
|
@ -3,8 +3,12 @@ import cx from 'classnames';
|
||||||
import memoizeOne from 'memoize-one';
|
import memoizeOne from 'memoize-one';
|
||||||
|
|
||||||
import { connect, useStoreActions } from './StateProvider';
|
import { connect, useStoreActions } from './StateProvider';
|
||||||
import { getProxies, getRtFilterSwitch } from '../store/proxies';
|
import { getProxies } from '../store/proxies';
|
||||||
import { getCollapsibleIsOpen } from '../store/app';
|
import {
|
||||||
|
getCollapsibleIsOpen,
|
||||||
|
getProxySortBy,
|
||||||
|
getHideUnavailableProxies,
|
||||||
|
} from '../store/app';
|
||||||
import CollapsibleSectionHeader from './CollapsibleSectionHeader';
|
import CollapsibleSectionHeader from './CollapsibleSectionHeader';
|
||||||
import Proxy, { ProxySmall } from './Proxy';
|
import Proxy, { ProxySmall } from './Proxy';
|
||||||
|
|
||||||
|
@ -18,7 +22,7 @@ function ProxyGroup({ name, all, type, now, isOpen, apiConfig, dispatch }) {
|
||||||
const isSelectable = useMemo(() => type === 'Selector', [type]);
|
const isSelectable = useMemo(() => type === 'Selector', [type]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
app: { updateCollapsibleIsOpen }
|
app: { updateCollapsibleIsOpen },
|
||||||
} = useStoreActions();
|
} = useStoreActions();
|
||||||
|
|
||||||
const toggle = useCallback(() => {
|
const toggle = useCallback(() => {
|
||||||
|
@ -26,7 +30,7 @@ function ProxyGroup({ name, all, type, now, isOpen, apiConfig, dispatch }) {
|
||||||
}, [isOpen, updateCollapsibleIsOpen, name]);
|
}, [isOpen, updateCollapsibleIsOpen, name]);
|
||||||
|
|
||||||
const itemOnTapCallback = useCallback(
|
const itemOnTapCallback = useCallback(
|
||||||
proxyName => {
|
(proxyName) => {
|
||||||
if (!isSelectable) return;
|
if (!isSelectable) return;
|
||||||
dispatch(switchProxy(apiConfig, name, proxyName));
|
dispatch(switchProxy(apiConfig, name, proxyName));
|
||||||
},
|
},
|
||||||
|
@ -60,23 +64,23 @@ type ProxyListProps = {
|
||||||
all: string[],
|
all: string[],
|
||||||
now?: string,
|
now?: string,
|
||||||
isSelectable?: boolean,
|
isSelectable?: boolean,
|
||||||
itemOnTapCallback?: string => void,
|
itemOnTapCallback?: (string) => void,
|
||||||
show?: boolean
|
show?: boolean,
|
||||||
};
|
};
|
||||||
export function ProxyList({
|
export function ProxyList({
|
||||||
all,
|
all,
|
||||||
now,
|
now,
|
||||||
isSelectable,
|
isSelectable,
|
||||||
itemOnTapCallback,
|
itemOnTapCallback,
|
||||||
sortedAll
|
sortedAll,
|
||||||
}: ProxyListProps) {
|
}: ProxyListProps) {
|
||||||
const proxies = sortedAll || all;
|
const proxies = sortedAll || all;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={s0.list}>
|
<div className={s0.list}>
|
||||||
{proxies.map(proxyName => {
|
{proxies.map((proxyName) => {
|
||||||
const proxyClassName = cx(s0.proxy, {
|
const proxyClassName = cx(s0.proxy, {
|
||||||
[s0.proxySelectable]: isSelectable
|
[s0.proxySelectable]: isSelectable,
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -107,7 +111,7 @@ const getSortDelay = (d, w) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function filterAvailableProxies(list, delay) {
|
function filterAvailableProxies(list, delay) {
|
||||||
return list.filter(name => {
|
return list.filter((name) => {
|
||||||
const d = delay[name];
|
const d = delay[name];
|
||||||
if (d === undefined) {
|
if (d === undefined) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -120,19 +124,50 @@ function filterAvailableProxies(list, delay) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterAvailableProxiesAndSortImpl(all, delay, filterByRt) {
|
const ProxySortingFns = {
|
||||||
|
Natural: (proxies, _delay) => {
|
||||||
|
return proxies;
|
||||||
|
},
|
||||||
|
LatencyAsc: (proxies, delay) => {
|
||||||
|
return proxies.sort((a, b) => {
|
||||||
|
const d1 = getSortDelay(delay[a], 999999);
|
||||||
|
const d2 = getSortDelay(delay[b], 999999);
|
||||||
|
return d1 - d2;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
LatencyDesc: (proxies, delay) => {
|
||||||
|
return proxies.sort((a, b) => {
|
||||||
|
const d1 = getSortDelay(delay[a], 999999);
|
||||||
|
const d2 = getSortDelay(delay[b], 999999);
|
||||||
|
return d2 - d1;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
NameAsc: (proxies) => {
|
||||||
|
return proxies.sort();
|
||||||
|
},
|
||||||
|
NameDesc: (proxies) => {
|
||||||
|
return proxies.sort((a, b) => {
|
||||||
|
if (a > b) return -1;
|
||||||
|
if (a < b) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function filterAvailableProxiesAndSortImpl(
|
||||||
|
all,
|
||||||
|
delay,
|
||||||
|
hideUnavailableProxies,
|
||||||
|
proxySortBy
|
||||||
|
) {
|
||||||
// all is freezed
|
// all is freezed
|
||||||
let filtered = [...all];
|
let filtered = [...all];
|
||||||
if (filterByRt) {
|
if (hideUnavailableProxies) {
|
||||||
filtered = filterAvailableProxies(all, delay);
|
filtered = filterAvailableProxies(all, delay);
|
||||||
}
|
}
|
||||||
|
return ProxySortingFns[proxySortBy](filtered, delay);
|
||||||
return filtered.sort((first, second) => {
|
|
||||||
const d1 = getSortDelay(delay[first], 999999);
|
|
||||||
const d2 = getSortDelay(delay[second], 999999);
|
|
||||||
return d1 - d2;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const filterAvailableProxiesAndSort = memoizeOne(
|
export const filterAvailableProxiesAndSort = memoizeOne(
|
||||||
filterAvailableProxiesAndSortImpl
|
filterAvailableProxiesAndSortImpl
|
||||||
);
|
);
|
||||||
|
@ -141,13 +176,13 @@ export function ProxyListSummaryView({
|
||||||
all,
|
all,
|
||||||
now,
|
now,
|
||||||
isSelectable,
|
isSelectable,
|
||||||
itemOnTapCallback
|
itemOnTapCallback,
|
||||||
}: ProxyListProps) {
|
}: ProxyListProps) {
|
||||||
return (
|
return (
|
||||||
<div className={s0.list}>
|
<div className={s0.list}>
|
||||||
{all.map(proxyName => {
|
{all.map((proxyName) => {
|
||||||
const proxyClassName = cx(s0.proxy, {
|
const proxyClassName = cx(s0.proxy, {
|
||||||
[s0.proxySelectable]: isSelectable
|
[s0.proxySelectable]: isSelectable,
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -168,14 +203,21 @@ export function ProxyListSummaryView({
|
||||||
|
|
||||||
export default connect((s, { name, delay }) => {
|
export default connect((s, { name, delay }) => {
|
||||||
const proxies = getProxies(s);
|
const proxies = getProxies(s);
|
||||||
const filterByRt = getRtFilterSwitch(s);
|
|
||||||
const collapsibleIsOpen = getCollapsibleIsOpen(s);
|
const collapsibleIsOpen = getCollapsibleIsOpen(s);
|
||||||
|
const proxySortBy = getProxySortBy(s);
|
||||||
|
const hideUnavailableProxies = getHideUnavailableProxies(s);
|
||||||
|
|
||||||
const group = proxies[name];
|
const group = proxies[name];
|
||||||
const { all, type, now } = group;
|
const { all, type, now } = group;
|
||||||
return {
|
return {
|
||||||
all: filterAvailableProxiesAndSort(all, delay, filterByRt),
|
all: filterAvailableProxiesAndSort(
|
||||||
|
all,
|
||||||
|
delay,
|
||||||
|
hideUnavailableProxies,
|
||||||
|
proxySortBy
|
||||||
|
),
|
||||||
type,
|
type,
|
||||||
now,
|
now,
|
||||||
isOpen: collapsibleIsOpen[`proxyGroup:${name}`]
|
isOpen: collapsibleIsOpen[`proxyGroup:${name}`],
|
||||||
};
|
};
|
||||||
})(ProxyGroup);
|
})(ProxyGroup);
|
||||||
|
|
|
@ -9,16 +9,20 @@ import CollapsibleSectionHeader from './CollapsibleSectionHeader';
|
||||||
import {
|
import {
|
||||||
ProxyList,
|
ProxyList,
|
||||||
ProxyListSummaryView,
|
ProxyListSummaryView,
|
||||||
filterAvailableProxiesAndSort
|
filterAvailableProxiesAndSort,
|
||||||
} from './ProxyGroup';
|
} from './ProxyGroup';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
|
|
||||||
import { getClashAPIConfig, getCollapsibleIsOpen } from '../store/app';
|
import {
|
||||||
|
getClashAPIConfig,
|
||||||
|
getCollapsibleIsOpen,
|
||||||
|
getProxySortBy,
|
||||||
|
getHideUnavailableProxies,
|
||||||
|
} from '../store/app';
|
||||||
import {
|
import {
|
||||||
getDelay,
|
getDelay,
|
||||||
getRtFilterSwitch,
|
|
||||||
updateProviderByName,
|
updateProviderByName,
|
||||||
healthcheckProviderByName
|
healthcheckProviderByName,
|
||||||
} from '../store/proxies';
|
} from '../store/proxies';
|
||||||
|
|
||||||
import s from './ProxyProvider.module.css';
|
import s from './ProxyProvider.module.css';
|
||||||
|
@ -31,8 +35,8 @@ type Props = {
|
||||||
type: 'Proxy' | 'Rule',
|
type: 'Proxy' | 'Rule',
|
||||||
vehicleType: 'HTTP' | 'File' | 'Compatible',
|
vehicleType: 'HTTP' | 'File' | 'Compatible',
|
||||||
updatedAt?: string,
|
updatedAt?: string,
|
||||||
dispatch: any => void,
|
dispatch: (any) => void,
|
||||||
isOpen: boolean
|
isOpen: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ProxyProvider({
|
function ProxyProvider({
|
||||||
|
@ -42,7 +46,7 @@ function ProxyProvider({
|
||||||
updatedAt,
|
updatedAt,
|
||||||
isOpen,
|
isOpen,
|
||||||
dispatch,
|
dispatch,
|
||||||
apiConfig
|
apiConfig,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [isHealthcheckLoading, setIsHealthcheckLoading] = useState(false);
|
const [isHealthcheckLoading, setIsHealthcheckLoading] = useState(false);
|
||||||
const updateProvider = useCallback(
|
const updateProvider = useCallback(
|
||||||
|
@ -56,7 +60,7 @@ function ProxyProvider({
|
||||||
}, [apiConfig, dispatch, name, setIsHealthcheckLoading]);
|
}, [apiConfig, dispatch, name, setIsHealthcheckLoading]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
app: { updateCollapsibleIsOpen }
|
app: { updateCollapsibleIsOpen },
|
||||||
} = useStoreActions();
|
} = useStoreActions();
|
||||||
|
|
||||||
// const [isCollapsibleOpen, setCollapsibleOpen] = useState(false);
|
// const [isCollapsibleOpen, setCollapsibleOpen] = useState(false);
|
||||||
|
@ -101,11 +105,11 @@ function ProxyProvider({
|
||||||
const button = {
|
const button = {
|
||||||
rest: { scale: 1 },
|
rest: { scale: 1 },
|
||||||
// hover: { scale: 1.1 },
|
// hover: { scale: 1.1 },
|
||||||
pressed: { scale: 0.95 }
|
pressed: { scale: 0.95 },
|
||||||
};
|
};
|
||||||
const arrow = {
|
const arrow = {
|
||||||
rest: { rotate: 0 },
|
rest: { rotate: 0 },
|
||||||
hover: { rotate: 360, transition: { duration: 0.3 } }
|
hover: { rotate: 360, transition: { duration: 0.3 } },
|
||||||
};
|
};
|
||||||
function Refresh() {
|
function Refresh() {
|
||||||
return (
|
return (
|
||||||
|
@ -124,18 +128,23 @@ function Refresh() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapState = (s, { proxies, name }) => {
|
const mapState = (s, { proxies, name }) => {
|
||||||
const filterByRt = getRtFilterSwitch(s);
|
const hideUnavailableProxies = getHideUnavailableProxies(s);
|
||||||
const delay = getDelay(s);
|
const delay = getDelay(s);
|
||||||
const collapsibleIsOpen = getCollapsibleIsOpen(s);
|
const collapsibleIsOpen = getCollapsibleIsOpen(s);
|
||||||
const apiConfig = getClashAPIConfig(s);
|
const apiConfig = getClashAPIConfig(s);
|
||||||
|
|
||||||
|
const proxySortBy = getProxySortBy(s);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
apiConfig,
|
apiConfig,
|
||||||
proxies: filterAvailableProxiesAndSort(proxies, delay, filterByRt),
|
proxies: filterAvailableProxiesAndSort(
|
||||||
isOpen: collapsibleIsOpen[`proxyProvider:${name}`]
|
proxies,
|
||||||
|
delay,
|
||||||
|
hideUnavailableProxies,
|
||||||
|
proxySortBy
|
||||||
|
),
|
||||||
|
isOpen: collapsibleIsOpen[`proxyProvider:${name}`],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// const mapState = s => ({
|
|
||||||
// apiConfig: getClashAPIConfig(s)
|
|
||||||
// });
|
|
||||||
export default connect(mapState)(ProxyProvider);
|
export default connect(mapState)(ProxyProvider);
|
||||||
|
|
|
@ -1,15 +1,3 @@
|
||||||
@font-face {
|
|
||||||
font-family: 'Roboto Mono';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
src: local('Roboto Mono'), local('RobotoMono-Regular'),
|
|
||||||
url('https://cdn.jsdelivr.net/npm/@hsjs/fonts@0.0.1/robotomono/v5/L0x5DF4xlVMF-BfR8bXMIjhLq3-cXbKD.woff2')
|
|
||||||
format('woff2');
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
|
||||||
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
|
|
||||||
U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
|
|
||||||
.relative {
|
.relative {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
@ -88,6 +76,7 @@ body.dark {
|
||||||
--color-background: #202020;
|
--color-background: #202020;
|
||||||
--color-text: #ddd;
|
--color-text: #ddd;
|
||||||
--color-text-secondary: #ccc;
|
--color-text-secondary: #ccc;
|
||||||
|
--color-text-highlight: #fff;
|
||||||
--color-bg-sidebar: #2d2d30;
|
--color-bg-sidebar: #2d2d30;
|
||||||
--color-sb-active-row-bg: #494b4e;
|
--color-sb-active-row-bg: #494b4e;
|
||||||
--color-input-bg: #2d2d30;
|
--color-input-bg: #2d2d30;
|
||||||
|
@ -95,18 +84,22 @@ body.dark {
|
||||||
--color-toggle-bg: #353535;
|
--color-toggle-bg: #353535;
|
||||||
--color-toggle-selected: #181818;
|
--color-toggle-selected: #181818;
|
||||||
--color-icon: #c7c7c7;
|
--color-icon: #c7c7c7;
|
||||||
|
--color-separator: #333;
|
||||||
--color-btn-bg: #232323;
|
--color-btn-bg: #232323;
|
||||||
--color-btn-fg: #bebebe;
|
--color-btn-fg: #bebebe;
|
||||||
--color-bg-proxy: #303030;
|
--color-bg-proxy: #303030;
|
||||||
--color-row-odd: #282828;
|
--color-row-odd: #282828;
|
||||||
--bg-modal: #1f1f20;
|
--bg-modal: #1f1f20;
|
||||||
--bg-near-transparent: rgba(255, 255, 255, 0.1);
|
--bg-near-transparent: rgba(255, 255, 255, 0.1);
|
||||||
|
--select-border-color: #040404;
|
||||||
|
--select-bg-hover: url(data:image/svg+xml,%0A%20%20%20%20%3Csvg%20width%3D%228%22%20height%3D%2224%22%20viewBox%3D%220%200%208%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%207L7%2011H1L4%207Z%22%20fill%3D%22%23ffffff%22%20%2F%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%2017L1%2013L7%2013L4%2017Z%22%20fill%3D%22%23ffffff%22%20%2F%3E%0A%20%20%20%20%3C%2Fsvg%3E%0A%20%20);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.light {
|
body.light {
|
||||||
--color-background: #fbfbfb;
|
--color-background: #fbfbfb;
|
||||||
--color-text: #222;
|
--color-text: #222;
|
||||||
--color-text-secondary: #646464;
|
--color-text-secondary: #646464;
|
||||||
|
--color-text-highlight: #040404;
|
||||||
--color-bg-sidebar: #e7e7e7;
|
--color-bg-sidebar: #e7e7e7;
|
||||||
--color-sb-active-row-bg: #d0d0d0;
|
--color-sb-active-row-bg: #d0d0d0;
|
||||||
--color-input-bg: #ffffff;
|
--color-input-bg: #ffffff;
|
||||||
|
@ -114,12 +107,15 @@ body.light {
|
||||||
--color-toggle-bg: #ffffff;
|
--color-toggle-bg: #ffffff;
|
||||||
--color-toggle-selected: #d7d7d7;
|
--color-toggle-selected: #d7d7d7;
|
||||||
--color-icon: #5b5b5b;
|
--color-icon: #5b5b5b;
|
||||||
|
--color-separator: #ccc;
|
||||||
--color-btn-bg: #f4f4f4;
|
--color-btn-bg: #f4f4f4;
|
||||||
--color-btn-fg: #101010;
|
--color-btn-fg: #101010;
|
||||||
--color-bg-proxy: #e7e7e7;
|
--color-bg-proxy: #e7e7e7;
|
||||||
--color-row-odd: #f5f5f5;
|
--color-row-odd: #f5f5f5;
|
||||||
--bg-modal: #fbfbfb;
|
--bg-modal: #fbfbfb;
|
||||||
--bg-near-transparent: rgba(0, 0, 0, 0.1);
|
--bg-near-transparent: rgba(0, 0, 0, 0.1);
|
||||||
|
--select-border-color: #999999;
|
||||||
|
--select-bg-hover: url(data:image/svg+xml,%0A%20%20%20%20%3Csvg%20width%3D%228%22%20height%3D%2224%22%20viewBox%3D%220%200%208%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%207L7%2011H1L4%207Z%22%20fill%3D%22%23222222%22%20%2F%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%2017L1%2013L7%2013L4%2017Z%22%20fill%3D%22%23222222%22%20%2F%3E%0A%20%20%20%20%3C%2Fsvg%3E%0A%20%20);
|
||||||
}
|
}
|
||||||
|
|
||||||
.flexCenter {
|
.flexCenter {
|
||||||
|
|
75
src/components/proxies/Settings.js
Normal file
75
src/components/proxies/Settings.js
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { getProxySortBy, getHideUnavailableProxies } from '../../store/app';
|
||||||
|
|
||||||
|
import Switch from '../SwitchThemed';
|
||||||
|
import { connect, useStoreActions } from '../StateProvider';
|
||||||
|
import Select from '../shared/Select';
|
||||||
|
import s from './Settings.module.css';
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
['Natural', 'Original order in config file'],
|
||||||
|
['LatencyAsc', 'By latency from small to big'],
|
||||||
|
['LatencyDesc', 'By latency from big to small'],
|
||||||
|
['NameAsc', 'By name alphabetically (A-Z)'],
|
||||||
|
['NameDesc', 'By name alphabetically (Z-A)'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const { useCallback } = React;
|
||||||
|
|
||||||
|
function Settings({ appConfig }) {
|
||||||
|
const {
|
||||||
|
app: { updateAppConfig },
|
||||||
|
} = useStoreActions();
|
||||||
|
|
||||||
|
const handleProxySortByOnChange = useCallback(
|
||||||
|
(e) => {
|
||||||
|
updateAppConfig('proxySortBy', e.target.value);
|
||||||
|
},
|
||||||
|
[updateAppConfig]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleHideUnavailablesSwitchOnChange = useCallback(
|
||||||
|
(v) => {
|
||||||
|
updateAppConfig('hideUnavailableProxies', v);
|
||||||
|
},
|
||||||
|
[updateAppConfig]
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={s.labeledInput}>
|
||||||
|
<span>Sorting in group</span>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
options={options}
|
||||||
|
selected={appConfig.proxySortBy}
|
||||||
|
onChange={handleProxySortByOnChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<div className={s.labeledInput}>
|
||||||
|
<span>Hide unavailable proxies</span>
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
name="hideUnavailableProxies"
|
||||||
|
checked={appConfig.hideUnavailableProxies}
|
||||||
|
onChange={handleHideUnavailablesSwitchOnChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapState = (s) => {
|
||||||
|
const proxySortBy = getProxySortBy(s);
|
||||||
|
const hideUnavailableProxies = getHideUnavailableProxies(s);
|
||||||
|
return {
|
||||||
|
appConfig: {
|
||||||
|
proxySortBy,
|
||||||
|
hideUnavailableProxies,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export default connect(mapState)(Settings);
|
17
src/components/proxies/Settings.module.css
Normal file
17
src/components/proxies/Settings.module.css
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
.labeledInput {
|
||||||
|
max-width: 85vw;
|
||||||
|
width: 400px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 13px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--color-separator);
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
margin: 1rem 0px;
|
||||||
|
}
|
|
@ -13,7 +13,7 @@
|
||||||
}
|
}
|
||||||
.rtf.open .rtf--mb > * {
|
.rtf.open .rtf--mb > * {
|
||||||
transform-origin: center center;
|
transform-origin: center center;
|
||||||
transform: rotate(315deg);
|
transform: rotate(360deg);
|
||||||
transition: ease-in-out transform 0.2s;
|
transition: ease-in-out transform 0.2s;
|
||||||
}
|
}
|
||||||
.rtf.open .rtf--mb > ul {
|
.rtf.open .rtf--mb > ul {
|
||||||
|
@ -107,18 +107,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.rtf--mb {
|
.rtf--mb {
|
||||||
height: 56px;
|
height: 48px;
|
||||||
width: 56px;
|
width: 48px;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
/* background-color: #666666; */
|
|
||||||
background: #387cec;
|
background: #387cec;
|
||||||
/* background: var(--color-btn-bg); */
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
border: none;
|
border: none;
|
||||||
/* border: 1px solid #555; */
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.14), 0 4px 8px rgba(0, 0, 0, 0.28);
|
box-shadow: 0 0 4px rgba(0, 0, 0, 0.14), 0 4px 8px rgba(0, 0, 0, 0.28);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
30
src/components/shared/BaseModal.js
Normal file
30
src/components/shared/BaseModal.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import Modal from 'react-modal';
|
||||||
|
import cx from 'classnames';
|
||||||
|
|
||||||
|
import modalStyle from '../Modal.module.css';
|
||||||
|
import s from './BaseModal.module.css';
|
||||||
|
|
||||||
|
const { useMemo } = React;
|
||||||
|
|
||||||
|
export default function BaseModal({ isOpen, onRequestClose, children }) {
|
||||||
|
const className = useMemo(
|
||||||
|
() => ({
|
||||||
|
base: cx(modalStyle.content, s.cnt),
|
||||||
|
afterOpen: s.afterOpen,
|
||||||
|
beforeClose: '',
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onRequestClose={onRequestClose}
|
||||||
|
className={className}
|
||||||
|
overlayClassName={cx(modalStyle.overlay, s.overlay)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
17
src/components/shared/BaseModal.module.css
Normal file
17
src/components/shared/BaseModal.module.css
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
.overlay {
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
.cnt {
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--bg-modal);
|
||||||
|
color: var(--color-text);
|
||||||
|
line-height: 1.4;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
transform: translate(-50%, -50%) scale(1.2);
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.12) 0px 4px 4px, rgba(0, 0, 0, 0.24) 0px 16px 32px;
|
||||||
|
}
|
||||||
|
.afterOpen {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
29
src/components/shared/Select.module.css
Normal file
29
src/components/shared/Select.module.css
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
.select {
|
||||||
|
height: 30px;
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 8px;
|
||||||
|
background-color: transparent;
|
||||||
|
appearance: none;
|
||||||
|
/* background-color: rgb(36, 36, 36); */
|
||||||
|
/* -webkit-appearance: none; */
|
||||||
|
color: var(--color-text);
|
||||||
|
/* color: rgb(153, 153, 153); */
|
||||||
|
padding-right: 20px;
|
||||||
|
background-image: url(data:image/svg+xml,%0A%20%20%20%20%3Csvg%20width%3D%228%22%20height%3D%2224%22%20viewBox%3D%220%200%208%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%207L7%2011H1L4%207Z%22%20fill%3D%22%23999999%22%20%2F%3E%0A%20%20%20%20%20%20%3Cpath%20d%3D%22M4%2017L1%2013L7%2013L4%2017Z%22%20fill%3D%22%23999999%22%20%2F%3E%0A%20%20%20%20%3C%2Fsvg%3E%0A%20%20);
|
||||||
|
border-radius: 4px;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
border-image: initial;
|
||||||
|
border-color: var(--select-border-color);
|
||||||
|
transition: all 100ms ease 0s;
|
||||||
|
background-position: calc(100% - 8px) center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select:hover,
|
||||||
|
.select:focus {
|
||||||
|
border-color: rgb(52, 52, 52);
|
||||||
|
outline: none !important;
|
||||||
|
color: var(--color-text-highlight);
|
||||||
|
background-image: var(--select-bg-hover);
|
||||||
|
}
|
21
src/components/shared/Select.tsx
Normal file
21
src/components/shared/Select.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import s from './Select.module.css';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
options: Array<string[]>;
|
||||||
|
selected: string;
|
||||||
|
onChange: (event: React.ChangeEvent<HTMLSelectElement>) => any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Select({ options, selected, onChange }: Props) {
|
||||||
|
return (
|
||||||
|
<select className={s.select} value={selected} onChange={onChange}>
|
||||||
|
{options.map(([value, name]) => (
|
||||||
|
<option key={value} value={value}>
|
||||||
|
{name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
30
src/components/svg/Equalizer.tsx
Normal file
30
src/components/svg/Equalizer.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
size?: number;
|
||||||
|
color?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Equalizer({
|
||||||
|
color = 'currentColor',
|
||||||
|
size = 24,
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M2 6h9M18.5 6H22" />
|
||||||
|
<circle cx="16" cy="6" r="2" />
|
||||||
|
<path d="M22 18h-9M6 18H2" />
|
||||||
|
<circle r="2" transform="matrix(-1 0 0 1 8 18)" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
5
src/custom.d.ts
vendored
Normal file
5
src/custom.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// for css modules
|
||||||
|
declare module '*.module.css' {
|
||||||
|
const classes: { [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
2
src/misc/constants.ts
Normal file
2
src/misc/constants.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
// const ProxySortingOptions =
|
|
@ -4,11 +4,13 @@ import { debounce } from '../misc/utils';
|
||||||
import { fetchConfigs } from './configs';
|
import { fetchConfigs } from './configs';
|
||||||
import { closeModal } from './modals';
|
import { closeModal } from './modals';
|
||||||
|
|
||||||
export const getClashAPIConfig = s => s.app.clashAPIConfig;
|
export const getClashAPIConfig = (s) => s.app.clashAPIConfig;
|
||||||
export const getTheme = s => s.app.theme;
|
export const getTheme = (s) => s.app.theme;
|
||||||
export const getSelectedChartStyleIndex = s => s.app.selectedChartStyleIndex;
|
export const getSelectedChartStyleIndex = (s) => s.app.selectedChartStyleIndex;
|
||||||
export const getLatencyTestUrl = s => s.app.latencyTestUrl;
|
export const getLatencyTestUrl = (s) => s.app.latencyTestUrl;
|
||||||
export const getCollapsibleIsOpen = s => s.app.collapsibleIsOpen;
|
export const getCollapsibleIsOpen = (s) => s.app.collapsibleIsOpen;
|
||||||
|
export const getProxySortBy = (s) => s.app.proxySortBy;
|
||||||
|
export const getHideUnavailableProxies = (s) => s.app.hideUnavailableProxies;
|
||||||
|
|
||||||
const saveStateDebounced = debounce(saveState, 600);
|
const saveStateDebounced = debounce(saveState, 600);
|
||||||
|
|
||||||
|
@ -16,7 +18,7 @@ export function updateClashAPIConfig({ hostname: iHostname, port, secret }) {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const hostname = iHostname.trim().replace(/^http(s):\/\//, '');
|
const hostname = iHostname.trim().replace(/^http(s):\/\//, '');
|
||||||
const clashAPIConfig = { hostname, port, secret };
|
const clashAPIConfig = { hostname, port, secret };
|
||||||
dispatch('appUpdateClashAPIConfig', s => {
|
dispatch('appUpdateClashAPIConfig', (s) => {
|
||||||
s.app.clashAPIConfig = clashAPIConfig;
|
s.app.clashAPIConfig = clashAPIConfig;
|
||||||
});
|
});
|
||||||
// side effect
|
// side effect
|
||||||
|
@ -43,7 +45,7 @@ export function switchTheme() {
|
||||||
const theme = currentTheme === 'light' ? 'dark' : 'light';
|
const theme = currentTheme === 'light' ? 'dark' : 'light';
|
||||||
// side effect
|
// side effect
|
||||||
setTheme(theme);
|
setTheme(theme);
|
||||||
dispatch('storeSwitchTheme', s => {
|
dispatch('storeSwitchTheme', (s) => {
|
||||||
s.app.theme = theme;
|
s.app.theme = theme;
|
||||||
});
|
});
|
||||||
// side effect
|
// side effect
|
||||||
|
@ -62,7 +64,7 @@ export function clearStorage() {
|
||||||
|
|
||||||
export function selectChartStyleIndex(selectedChartStyleIndex) {
|
export function selectChartStyleIndex(selectedChartStyleIndex) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch('appSelectChartStyleIndex', s => {
|
dispatch('appSelectChartStyleIndex', (s) => {
|
||||||
s.app.selectedChartStyleIndex = selectedChartStyleIndex;
|
s.app.selectedChartStyleIndex = selectedChartStyleIndex;
|
||||||
});
|
});
|
||||||
// side effect
|
// side effect
|
||||||
|
@ -72,7 +74,7 @@ export function selectChartStyleIndex(selectedChartStyleIndex) {
|
||||||
|
|
||||||
export function updateAppConfig(name, value) {
|
export function updateAppConfig(name, value) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch('appUpdateAppConfig', s => {
|
dispatch('appUpdateAppConfig', (s) => {
|
||||||
s.app[name] = value;
|
s.app[name] = value;
|
||||||
});
|
});
|
||||||
// side effect
|
// side effect
|
||||||
|
@ -82,7 +84,7 @@ export function updateAppConfig(name, value) {
|
||||||
|
|
||||||
export function updateCollapsibleIsOpen(prefix, name, v) {
|
export function updateCollapsibleIsOpen(prefix, name, v) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch('updateCollapsibleIsOpen', s => {
|
dispatch('updateCollapsibleIsOpen', (s) => {
|
||||||
s.app.collapsibleIsOpen[`${prefix}:${name}`] = v;
|
s.app.collapsibleIsOpen[`${prefix}:${name}`] = v;
|
||||||
});
|
});
|
||||||
// side effect
|
// side effect
|
||||||
|
@ -95,14 +97,17 @@ const defaultState = {
|
||||||
clashAPIConfig: {
|
clashAPIConfig: {
|
||||||
hostname: '127.0.0.1',
|
hostname: '127.0.0.1',
|
||||||
port: '7892',
|
port: '7892',
|
||||||
secret: ''
|
secret: '',
|
||||||
},
|
},
|
||||||
latencyTestUrl: 'http://www.gstatic.com/generate_204',
|
latencyTestUrl: 'http://www.gstatic.com/generate_204',
|
||||||
selectedChartStyleIndex: 0,
|
selectedChartStyleIndex: 0,
|
||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
|
|
||||||
// type { [string]: boolean }
|
// type { [string]: boolean }
|
||||||
collapsibleIsOpen: {}
|
collapsibleIsOpen: {},
|
||||||
|
// how proxies are sorted in a group or provider
|
||||||
|
proxySortBy: 'Natural',
|
||||||
|
hideUnavailableProxies: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
function parseConfigQueryString() {
|
function parseConfigQueryString() {
|
||||||
|
|
|
@ -2,12 +2,9 @@ import {
|
||||||
initialState as app,
|
initialState as app,
|
||||||
selectChartStyleIndex,
|
selectChartStyleIndex,
|
||||||
updateAppConfig,
|
updateAppConfig,
|
||||||
updateCollapsibleIsOpen
|
updateCollapsibleIsOpen,
|
||||||
} from './app';
|
} from './app';
|
||||||
import {
|
import { initialState as proxies } from './proxies';
|
||||||
initialState as proxies,
|
|
||||||
toggleUnavailableProxiesFilter
|
|
||||||
} from './proxies';
|
|
||||||
import { initialState as modals } from './modals';
|
import { initialState as modals } from './modals';
|
||||||
import { initialState as configs } from './configs';
|
import { initialState as configs } from './configs';
|
||||||
import { initialState as rules } from './rules';
|
import { initialState as rules } from './rules';
|
||||||
|
@ -19,16 +16,15 @@ export const initialState = {
|
||||||
configs,
|
configs,
|
||||||
proxies,
|
proxies,
|
||||||
rules,
|
rules,
|
||||||
logs
|
logs,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
selectChartStyleIndex,
|
selectChartStyleIndex,
|
||||||
updateAppConfig,
|
updateAppConfig,
|
||||||
// proxies
|
|
||||||
toggleUnavailableProxiesFilter,
|
|
||||||
|
|
||||||
app: {
|
app: {
|
||||||
updateCollapsibleIsOpen
|
updateCollapsibleIsOpen,
|
||||||
}
|
updateAppConfig,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,8 +11,8 @@ type ProxyProvider = {
|
||||||
history: Array<{ time: string, delay: number }>,
|
history: Array<{ time: string, delay: number }>,
|
||||||
name: string,
|
name: string,
|
||||||
// Shadowsocks, Http ...
|
// Shadowsocks, Http ...
|
||||||
type: string
|
type: string,
|
||||||
}>
|
}>,
|
||||||
};
|
};
|
||||||
|
|
||||||
// see all types:
|
// see all types:
|
||||||
|
@ -30,21 +30,20 @@ const NonProxyTypes = [
|
||||||
'Selector',
|
'Selector',
|
||||||
'URLTest',
|
'URLTest',
|
||||||
'LoadBalance',
|
'LoadBalance',
|
||||||
'Unknown'
|
'Unknown',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const getProxies = s => s.proxies.proxies;
|
export const getProxies = (s) => s.proxies.proxies;
|
||||||
export const getDelay = s => s.proxies.delay;
|
export const getDelay = (s) => s.proxies.delay;
|
||||||
export const getRtFilterSwitch = s => s.proxies.filterZeroRT;
|
export const getProxyGroupNames = (s) => s.proxies.groupNames;
|
||||||
export const getProxyGroupNames = s => s.proxies.groupNames;
|
export const getProxyProviders = (s) => s.proxies.proxyProviders || [];
|
||||||
export const getProxyProviders = s => s.proxies.proxyProviders || [];
|
export const getDangleProxyNames = (s) => s.proxies.dangleProxyNames;
|
||||||
export const getDangleProxyNames = s => s.proxies.dangleProxyNames;
|
|
||||||
|
|
||||||
export function fetchProxies(apiConfig) {
|
export function fetchProxies(apiConfig) {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const [proxiesData, providersData] = await Promise.all([
|
const [proxiesData, providersData] = await Promise.all([
|
||||||
proxiesAPI.fetchProxies(apiConfig),
|
proxiesAPI.fetchProxies(apiConfig),
|
||||||
proxiesAPI.fetchProviderProxies(apiConfig)
|
proxiesAPI.fetchProviderProxies(apiConfig),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [proxyProviders, providerProxies] = formatProxyProviders(
|
const [proxyProviders, providerProxies] = formatProxyProviders(
|
||||||
|
@ -77,7 +76,7 @@ export function fetchProxies(apiConfig) {
|
||||||
if (!providerProxies[v]) dangleProxyNames.push(v);
|
if (!providerProxies[v]) dangleProxyNames.push(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch('store/proxies#fetchProxies', s => {
|
dispatch('store/proxies#fetchProxies', (s) => {
|
||||||
s.proxies.proxies = proxies;
|
s.proxies.proxies = proxies;
|
||||||
s.proxies.groupNames = groupNames;
|
s.proxies.groupNames = groupNames;
|
||||||
s.proxies.delay = delayNext;
|
s.proxies.delay = delayNext;
|
||||||
|
@ -88,7 +87,7 @@ export function fetchProxies(apiConfig) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateProviderByName(apiConfig, name) {
|
export function updateProviderByName(apiConfig, name) {
|
||||||
return async dispatch => {
|
return async (dispatch) => {
|
||||||
try {
|
try {
|
||||||
await proxiesAPI.updateProviderByName(apiConfig, name);
|
await proxiesAPI.updateProviderByName(apiConfig, name);
|
||||||
} catch (x) {
|
} catch (x) {
|
||||||
|
@ -109,7 +108,7 @@ async function healthcheckProviderByNameInternal(apiConfig, name) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function healthcheckProviderByName(apiConfig, name) {
|
export function healthcheckProviderByName(apiConfig, name) {
|
||||||
return async dispatch => {
|
return async (dispatch) => {
|
||||||
await healthcheckProviderByNameInternal(apiConfig, name);
|
await healthcheckProviderByNameInternal(apiConfig, name);
|
||||||
// should be optimized
|
// should be optimized
|
||||||
// but ¯\_(ツ)_/¯
|
// but ¯\_(ツ)_/¯
|
||||||
|
@ -118,17 +117,17 @@ export function healthcheckProviderByName(apiConfig, name) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function switchProxy(apiConfig, name1, name2) {
|
export function switchProxy(apiConfig, name1, name2) {
|
||||||
return async dispatch => {
|
return async (dispatch) => {
|
||||||
proxiesAPI
|
proxiesAPI
|
||||||
.requestToSwitchProxy(apiConfig, name1, name2)
|
.requestToSwitchProxy(apiConfig, name1, name2)
|
||||||
.then(
|
.then(
|
||||||
res => {
|
(res) => {
|
||||||
if (res.ok === false) {
|
if (res.ok === false) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('failed to swith proxy', res.statusText);
|
console.log('failed to swith proxy', res.statusText);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
err => {
|
(err) => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(err, 'failed to swith proxy');
|
console.log(err, 'failed to swith proxy');
|
||||||
}
|
}
|
||||||
|
@ -137,7 +136,7 @@ export function switchProxy(apiConfig, name1, name2) {
|
||||||
dispatch(fetchProxies(apiConfig));
|
dispatch(fetchProxies(apiConfig));
|
||||||
});
|
});
|
||||||
// optimistic UI update
|
// optimistic UI update
|
||||||
dispatch('store/proxies#switchProxy', s => {
|
dispatch('store/proxies#switchProxy', (s) => {
|
||||||
const proxies = s.proxies.proxies;
|
const proxies = s.proxies.proxies;
|
||||||
if (proxies[name1] && proxies[name1].now) {
|
if (proxies[name1] && proxies[name1].now) {
|
||||||
proxies[name1].now = name2;
|
proxies[name1].now = name2;
|
||||||
|
@ -165,18 +164,18 @@ function requestDelayForProxyOnce(apiConfig, name) {
|
||||||
...delayPrev,
|
...delayPrev,
|
||||||
[name]: {
|
[name]: {
|
||||||
error,
|
error,
|
||||||
number: delay
|
number: delay,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
dispatch('requestDelayForProxyOnce', s => {
|
dispatch('requestDelayForProxyOnce', (s) => {
|
||||||
s.proxies.delay = delayNext;
|
s.proxies.delay = delayNext;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requestDelayForProxy(apiConfig, name) {
|
export function requestDelayForProxy(apiConfig, name) {
|
||||||
return async dispatch => {
|
return async (dispatch) => {
|
||||||
await dispatch(requestDelayForProxyOnce(apiConfig, name));
|
await dispatch(requestDelayForProxyOnce(apiConfig, name));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -185,7 +184,7 @@ export function requestDelayAll(apiConfig) {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const proxyNames = getDangleProxyNames(getState());
|
const proxyNames = getDangleProxyNames(getState());
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
proxyNames.map(p => dispatch(requestDelayForProxy(apiConfig, p)))
|
proxyNames.map((p) => dispatch(requestDelayForProxy(apiConfig, p)))
|
||||||
);
|
);
|
||||||
const proxyProviders = getProxyProviders(getState());
|
const proxyProviders = getProxyProviders(getState());
|
||||||
// one by one
|
// one by one
|
||||||
|
@ -196,15 +195,6 @@ export function requestDelayAll(apiConfig) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toggleUnavailableProxiesFilter() {
|
|
||||||
return (dispatch, getState) => {
|
|
||||||
const preState = getRtFilterSwitch(getState());
|
|
||||||
dispatch('store/proxies#toggleUnavailableProxiesFilter', s => {
|
|
||||||
s.proxies.filterZeroRT = !preState;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function retrieveGroupNamesFrom(proxies) {
|
function retrieveGroupNamesFrom(proxies) {
|
||||||
let groupNames = [];
|
let groupNames = [];
|
||||||
let globalAll;
|
let globalAll;
|
||||||
|
@ -225,9 +215,9 @@ function retrieveGroupNamesFrom(proxies) {
|
||||||
globalAll.push('GLOBAL');
|
globalAll.push('GLOBAL');
|
||||||
// Sort groups according to its index in GLOBAL group
|
// Sort groups according to its index in GLOBAL group
|
||||||
groupNames = groupNames
|
groupNames = groupNames
|
||||||
.map(name => [globalAll.indexOf(name), name])
|
.map((name) => [globalAll.indexOf(name), name])
|
||||||
.sort((a, b) => a[0] - b[0])
|
.sort((a, b) => a[0] - b[0])
|
||||||
.map(group => group[1]);
|
.map((group) => group[1]);
|
||||||
}
|
}
|
||||||
return [groupNames, proxyNames];
|
return [groupNames, proxyNames];
|
||||||
}
|
}
|
||||||
|
@ -260,5 +250,4 @@ export const initialState = {
|
||||||
proxies: {},
|
proxies: {},
|
||||||
delay: {},
|
delay: {},
|
||||||
groupNames: [],
|
groupNames: [],
|
||||||
filterZeroRT: false
|
|
||||||
};
|
};
|
||||||
|
|
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowSyntheticDefaultImports": false,
|
||||||
|
"target": "es5",
|
||||||
|
"downlevelIteration": true,
|
||||||
|
"importHelpers": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"jsx": "react",
|
||||||
|
"allowJs": false,
|
||||||
|
"checkJs": false,
|
||||||
|
"lib": ["dom", "es2015", "es2016", "es2017", "esnext"],
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": false,
|
||||||
|
"suppressImplicitAnyIndexErrors": true,
|
||||||
|
"types": ["jest"]
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,3 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
const TerserPlugin = require('terser-webpack-plugin');
|
const TerserPlugin = require('terser-webpack-plugin');
|
||||||
|
@ -7,9 +5,10 @@ const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
||||||
const HTMLPlugin = require('html-webpack-plugin');
|
const HTMLPlugin = require('html-webpack-plugin');
|
||||||
const CopyPlugin = require('copy-webpack-plugin');
|
const CopyPlugin = require('copy-webpack-plugin');
|
||||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||||
|
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
||||||
|
const ForkTsCheckerNotifierWebpackPlugin = require('fork-ts-checker-notifier-webpack-plugin');
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
const ReactRefreshWebpackPlugin = require('@hsjs/react-refresh-webpack-plugin');
|
const ReactRefreshWebpackPlugin = require('@hsjs/react-refresh-webpack-plugin');
|
||||||
// const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
|
|
||||||
|
|
||||||
const pkg = require('./package.json');
|
const pkg = require('./package.json');
|
||||||
|
|
||||||
|
@ -64,6 +63,11 @@ const bundleAnalyzerPlugin = new BundleAnalyzerPlugin({
|
||||||
const plugins = [
|
const plugins = [
|
||||||
html,
|
html,
|
||||||
definePlugin,
|
definePlugin,
|
||||||
|
new ForkTsCheckerWebpackPlugin({ eslint: false }),
|
||||||
|
new ForkTsCheckerNotifierWebpackPlugin({
|
||||||
|
title: 'TypeScript',
|
||||||
|
excludeWarnings: false,
|
||||||
|
}),
|
||||||
new CopyPlugin([{ from: 'assets/*', flatten: true }]),
|
new CopyPlugin([{ from: 'assets/*', flatten: true }]),
|
||||||
new CleanWebpackPlugin(),
|
new CleanWebpackPlugin(),
|
||||||
// chart.js requires moment
|
// chart.js requires moment
|
||||||
|
@ -83,14 +87,26 @@ module.exports = {
|
||||||
// app: ['react-hot-loader/patch', './src/app.js']
|
// app: ['react-hot-loader/patch', './src/app.js']
|
||||||
app: ['./src/app.js'],
|
app: ['./src/app.js'],
|
||||||
},
|
},
|
||||||
|
context: __dirname,
|
||||||
output: {
|
output: {
|
||||||
path: path.resolve(__dirname, 'public'),
|
path: path.resolve(__dirname, 'public'),
|
||||||
filename: isDev ? '[name].js' : '[name].[contenthash].js',
|
filename: isDev ? '[name].js' : '[name].[contenthash].js',
|
||||||
publicPath: '',
|
publicPath: '',
|
||||||
},
|
},
|
||||||
mode: isDev ? 'development' : 'production',
|
mode: isDev ? 'development' : 'production',
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ts', '.tsx', '.js'],
|
||||||
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
loader: 'ts-loader',
|
||||||
|
options: {
|
||||||
|
// disable type checker - we will use it in fork plugin
|
||||||
|
transpileOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
|
|
Loading…
Reference in a new issue