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:
|
||||
- react-app
|
||||
- eslint:recommended
|
||||
- plugin:import/errors
|
||||
|
||||
settings:
|
||||
import/resolver: webpack
|
||||
|
||||
globals:
|
||||
__DEV__: true
|
||||
|
|
31
package.json
31
package.json
|
@ -2,7 +2,6 @@
|
|||
"name": "yacd",
|
||||
"version": "0.1.11",
|
||||
"description": "Yet another Clash dashboard",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"lint": "eslint --cache src",
|
||||
"start": "NODE_ENV=development node server.js",
|
||||
|
@ -26,7 +25,8 @@
|
|||
"not op_mini all"
|
||||
],
|
||||
"keywords": [
|
||||
"react"
|
||||
"react",
|
||||
"clash"
|
||||
],
|
||||
"author": "Haishan <haishanhan@gmail.com> (https://haishan.me)",
|
||||
"private": true,
|
||||
|
@ -71,23 +71,30 @@
|
|||
"@babel/preset-flow": "^7.7.4",
|
||||
"@babel/preset-react": "^7.7.4",
|
||||
"@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",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-eslint": "10.x",
|
||||
"babel-loader": "^8.0.5",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"copy-webpack-plugin": "^5.1.1",
|
||||
"css-loader": "^3.4.2",
|
||||
"cssnano": "^4.1.7",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-react-app": "^5.0.2",
|
||||
"eslint": "6.x",
|
||||
"eslint-config-react-app": "^5.2.1",
|
||||
"eslint-import-resolver-webpack": "^0.12.0",
|
||||
"eslint-plugin-flowtype": "^4.6.0",
|
||||
"eslint-plugin-import": "^2.18.0",
|
||||
"eslint-plugin-flowtype": "4.x",
|
||||
"eslint-plugin-import": "2.x",
|
||||
"eslint-plugin-jest": "^23.6.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||
"eslint-plugin-react": "^7.17.0",
|
||||
"eslint-plugin-react-hooks": "^3.0.0",
|
||||
"eslint-plugin-jsx-a11y": "6.x",
|
||||
"eslint-plugin-react": "7.x",
|
||||
"eslint-plugin-react-hooks": "2.x",
|
||||
"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",
|
||||
"husky": "^4.0.0",
|
||||
"lint-staged": "^10.0.7",
|
||||
|
@ -99,10 +106,12 @@
|
|||
"postcss-nested": "^4.2.0",
|
||||
"postcss-simple-vars": "^5.0.2",
|
||||
"prettier": "^2.0.4",
|
||||
"react-refresh": "0.0.0-experimental-241c4467e",
|
||||
"react-refresh": "^0.8.1",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"style-loader": "^1.1.2",
|
||||
"terser-webpack-plugin": "^2.3.1",
|
||||
"ts-loader": "^7.0.1",
|
||||
"typescript": "^3.8.3",
|
||||
"webpack": "^4.41.6",
|
||||
"webpack-bundle-analyzer": "^3.6.0",
|
||||
"webpack-cli": "^3.3.10",
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
.content {
|
||||
outline: none;
|
||||
position: absolute;
|
||||
position: relative;
|
||||
color: #ddd;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
color: var(--color-text);
|
||||
max-width: 300px;
|
||||
line-height: 1.4;
|
||||
transform: translate(-50%, -50%) scale(1.5);
|
||||
transform: translate(-50%, -50%) scale(1.2);
|
||||
opacity: 0.6;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
|
|
@ -1,39 +1,34 @@
|
|||
import React from 'react';
|
||||
|
||||
import { connect, useStoreActions } from './StateProvider';
|
||||
import { connect } from './StateProvider';
|
||||
|
||||
import Button from './Button';
|
||||
import ContentHeader from './ContentHeader';
|
||||
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 { Fab, Action } from 'react-tiny-fab';
|
||||
import { Fab } from 'react-tiny-fab';
|
||||
|
||||
import './rtf.css';
|
||||
import s0 from './Proxies.module.css';
|
||||
|
||||
import {
|
||||
getDelay,
|
||||
getRtFilterSwitch,
|
||||
getProxyGroupNames,
|
||||
getProxyProviders,
|
||||
fetchProxies,
|
||||
requestDelayAll
|
||||
requestDelayAll,
|
||||
} from '../store/proxies';
|
||||
import { getClashAPIConfig } from '../store/app';
|
||||
|
||||
const { useEffect, useCallback, useRef } = React;
|
||||
const { useState, useEffect, useCallback, useRef } = React;
|
||||
|
||||
function Proxies({
|
||||
dispatch,
|
||||
groupNames,
|
||||
delay,
|
||||
proxyProviders,
|
||||
apiConfig,
|
||||
filterZeroRT
|
||||
}) {
|
||||
function Proxies({ dispatch, groupNames, delay, proxyProviders, apiConfig }) {
|
||||
const refFetchedTimestamp = useRef({});
|
||||
const { toggleUnavailableProxiesFilter } = useStoreActions();
|
||||
const requestDelayAllFn = useCallback(
|
||||
() => dispatch(requestDelayAll(apiConfig)),
|
||||
[apiConfig, dispatch]
|
||||
|
@ -62,11 +57,27 @@ function Proxies({
|
|||
return () => window.removeEventListener('focus', fn, false);
|
||||
}, [fetchProxiesHooked]);
|
||||
|
||||
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
|
||||
const closeSettingsModal = useCallback(() => {
|
||||
setIsSettingsModalOpen(false);
|
||||
}, []);
|
||||
|
||||
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" />
|
||||
<div>
|
||||
{groupNames.map(groupName => {
|
||||
{groupNames.map((groupName) => {
|
||||
return (
|
||||
<div className={s0.group} key={groupName}>
|
||||
<ProxyGroup
|
||||
|
@ -81,27 +92,20 @@ function Proxies({
|
|||
</div>
|
||||
<ProxyProviderList items={proxyProviders} />
|
||||
<div style={{ height: 60 }} />
|
||||
<Fab icon={<Circle />}>
|
||||
<Action text="Test Latency" onClick={requestDelayAllFn}>
|
||||
<Zap width={16} />
|
||||
</Action>
|
||||
<Action
|
||||
text={(filterZeroRT ? 'Show' : 'Hide') + ' Unavailable Proxies'}
|
||||
onClick={toggleUnavailableProxiesFilter}
|
||||
>
|
||||
<Filter width={16} />
|
||||
</Action>
|
||||
</Fab>
|
||||
<Fab
|
||||
icon={<Zap width={16} />}
|
||||
onClick={requestDelayAllFn}
|
||||
text="Test Latency"
|
||||
></Fab>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const mapState = s => ({
|
||||
const mapState = (s) => ({
|
||||
apiConfig: getClashAPIConfig(s),
|
||||
groupNames: getProxyGroupNames(s),
|
||||
proxyProviders: getProxyProviders(s),
|
||||
delay: getDelay(s),
|
||||
filterZeroRT: getRtFilterSwitch(s)
|
||||
});
|
||||
|
||||
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 {
|
||||
padding: 10px 15px;
|
||||
@media (--breakpoint-not-small) {
|
||||
|
|
|
@ -3,8 +3,12 @@ import cx from 'classnames';
|
|||
import memoizeOne from 'memoize-one';
|
||||
|
||||
import { connect, useStoreActions } from './StateProvider';
|
||||
import { getProxies, getRtFilterSwitch } from '../store/proxies';
|
||||
import { getCollapsibleIsOpen } from '../store/app';
|
||||
import { getProxies } from '../store/proxies';
|
||||
import {
|
||||
getCollapsibleIsOpen,
|
||||
getProxySortBy,
|
||||
getHideUnavailableProxies,
|
||||
} from '../store/app';
|
||||
import CollapsibleSectionHeader from './CollapsibleSectionHeader';
|
||||
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 {
|
||||
app: { updateCollapsibleIsOpen }
|
||||
app: { updateCollapsibleIsOpen },
|
||||
} = useStoreActions();
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
|
@ -26,7 +30,7 @@ function ProxyGroup({ name, all, type, now, isOpen, apiConfig, dispatch }) {
|
|||
}, [isOpen, updateCollapsibleIsOpen, name]);
|
||||
|
||||
const itemOnTapCallback = useCallback(
|
||||
proxyName => {
|
||||
(proxyName) => {
|
||||
if (!isSelectable) return;
|
||||
dispatch(switchProxy(apiConfig, name, proxyName));
|
||||
},
|
||||
|
@ -60,23 +64,23 @@ type ProxyListProps = {
|
|||
all: string[],
|
||||
now?: string,
|
||||
isSelectable?: boolean,
|
||||
itemOnTapCallback?: string => void,
|
||||
show?: boolean
|
||||
itemOnTapCallback?: (string) => void,
|
||||
show?: boolean,
|
||||
};
|
||||
export function ProxyList({
|
||||
all,
|
||||
now,
|
||||
isSelectable,
|
||||
itemOnTapCallback,
|
||||
sortedAll
|
||||
sortedAll,
|
||||
}: ProxyListProps) {
|
||||
const proxies = sortedAll || all;
|
||||
|
||||
return (
|
||||
<div className={s0.list}>
|
||||
{proxies.map(proxyName => {
|
||||
{proxies.map((proxyName) => {
|
||||
const proxyClassName = cx(s0.proxy, {
|
||||
[s0.proxySelectable]: isSelectable
|
||||
[s0.proxySelectable]: isSelectable,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
|
@ -107,7 +111,7 @@ const getSortDelay = (d, w) => {
|
|||
};
|
||||
|
||||
function filterAvailableProxies(list, delay) {
|
||||
return list.filter(name => {
|
||||
return list.filter((name) => {
|
||||
const d = delay[name];
|
||||
if (d === undefined) {
|
||||
return true;
|
||||
|
@ -120,19 +124,50 @@ function filterAvailableProxies(list, delay) {
|
|||
});
|
||||
}
|
||||
|
||||
function filterAvailableProxiesAndSortImpl(all, delay, filterByRt) {
|
||||
// all is freezed
|
||||
let filtered = [...all];
|
||||
if (filterByRt) {
|
||||
filtered = filterAvailableProxies(all, delay);
|
||||
}
|
||||
|
||||
return filtered.sort((first, second) => {
|
||||
const d1 = getSortDelay(delay[first], 999999);
|
||||
const d2 = getSortDelay(delay[second], 999999);
|
||||
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
|
||||
let filtered = [...all];
|
||||
if (hideUnavailableProxies) {
|
||||
filtered = filterAvailableProxies(all, delay);
|
||||
}
|
||||
return ProxySortingFns[proxySortBy](filtered, delay);
|
||||
}
|
||||
|
||||
export const filterAvailableProxiesAndSort = memoizeOne(
|
||||
filterAvailableProxiesAndSortImpl
|
||||
);
|
||||
|
@ -141,13 +176,13 @@ export function ProxyListSummaryView({
|
|||
all,
|
||||
now,
|
||||
isSelectable,
|
||||
itemOnTapCallback
|
||||
itemOnTapCallback,
|
||||
}: ProxyListProps) {
|
||||
return (
|
||||
<div className={s0.list}>
|
||||
{all.map(proxyName => {
|
||||
{all.map((proxyName) => {
|
||||
const proxyClassName = cx(s0.proxy, {
|
||||
[s0.proxySelectable]: isSelectable
|
||||
[s0.proxySelectable]: isSelectable,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
|
@ -168,14 +203,21 @@ export function ProxyListSummaryView({
|
|||
|
||||
export default connect((s, { name, delay }) => {
|
||||
const proxies = getProxies(s);
|
||||
const filterByRt = getRtFilterSwitch(s);
|
||||
const collapsibleIsOpen = getCollapsibleIsOpen(s);
|
||||
const proxySortBy = getProxySortBy(s);
|
||||
const hideUnavailableProxies = getHideUnavailableProxies(s);
|
||||
|
||||
const group = proxies[name];
|
||||
const { all, type, now } = group;
|
||||
return {
|
||||
all: filterAvailableProxiesAndSort(all, delay, filterByRt),
|
||||
all: filterAvailableProxiesAndSort(
|
||||
all,
|
||||
delay,
|
||||
hideUnavailableProxies,
|
||||
proxySortBy
|
||||
),
|
||||
type,
|
||||
now,
|
||||
isOpen: collapsibleIsOpen[`proxyGroup:${name}`]
|
||||
isOpen: collapsibleIsOpen[`proxyGroup:${name}`],
|
||||
};
|
||||
})(ProxyGroup);
|
||||
|
|
|
@ -9,16 +9,20 @@ import CollapsibleSectionHeader from './CollapsibleSectionHeader';
|
|||
import {
|
||||
ProxyList,
|
||||
ProxyListSummaryView,
|
||||
filterAvailableProxiesAndSort
|
||||
filterAvailableProxiesAndSort,
|
||||
} from './ProxyGroup';
|
||||
import Button from './Button';
|
||||
|
||||
import { getClashAPIConfig, getCollapsibleIsOpen } from '../store/app';
|
||||
import {
|
||||
getClashAPIConfig,
|
||||
getCollapsibleIsOpen,
|
||||
getProxySortBy,
|
||||
getHideUnavailableProxies,
|
||||
} from '../store/app';
|
||||
import {
|
||||
getDelay,
|
||||
getRtFilterSwitch,
|
||||
updateProviderByName,
|
||||
healthcheckProviderByName
|
||||
healthcheckProviderByName,
|
||||
} from '../store/proxies';
|
||||
|
||||
import s from './ProxyProvider.module.css';
|
||||
|
@ -31,8 +35,8 @@ type Props = {
|
|||
type: 'Proxy' | 'Rule',
|
||||
vehicleType: 'HTTP' | 'File' | 'Compatible',
|
||||
updatedAt?: string,
|
||||
dispatch: any => void,
|
||||
isOpen: boolean
|
||||
dispatch: (any) => void,
|
||||
isOpen: boolean,
|
||||
};
|
||||
|
||||
function ProxyProvider({
|
||||
|
@ -42,7 +46,7 @@ function ProxyProvider({
|
|||
updatedAt,
|
||||
isOpen,
|
||||
dispatch,
|
||||
apiConfig
|
||||
apiConfig,
|
||||
}: Props) {
|
||||
const [isHealthcheckLoading, setIsHealthcheckLoading] = useState(false);
|
||||
const updateProvider = useCallback(
|
||||
|
@ -56,7 +60,7 @@ function ProxyProvider({
|
|||
}, [apiConfig, dispatch, name, setIsHealthcheckLoading]);
|
||||
|
||||
const {
|
||||
app: { updateCollapsibleIsOpen }
|
||||
app: { updateCollapsibleIsOpen },
|
||||
} = useStoreActions();
|
||||
|
||||
// const [isCollapsibleOpen, setCollapsibleOpen] = useState(false);
|
||||
|
@ -101,11 +105,11 @@ function ProxyProvider({
|
|||
const button = {
|
||||
rest: { scale: 1 },
|
||||
// hover: { scale: 1.1 },
|
||||
pressed: { scale: 0.95 }
|
||||
pressed: { scale: 0.95 },
|
||||
};
|
||||
const arrow = {
|
||||
rest: { rotate: 0 },
|
||||
hover: { rotate: 360, transition: { duration: 0.3 } }
|
||||
hover: { rotate: 360, transition: { duration: 0.3 } },
|
||||
};
|
||||
function Refresh() {
|
||||
return (
|
||||
|
@ -124,18 +128,23 @@ function Refresh() {
|
|||
}
|
||||
|
||||
const mapState = (s, { proxies, name }) => {
|
||||
const filterByRt = getRtFilterSwitch(s);
|
||||
const hideUnavailableProxies = getHideUnavailableProxies(s);
|
||||
const delay = getDelay(s);
|
||||
const collapsibleIsOpen = getCollapsibleIsOpen(s);
|
||||
const apiConfig = getClashAPIConfig(s);
|
||||
|
||||
const proxySortBy = getProxySortBy(s);
|
||||
|
||||
return {
|
||||
apiConfig,
|
||||
proxies: filterAvailableProxiesAndSort(proxies, delay, filterByRt),
|
||||
isOpen: collapsibleIsOpen[`proxyProvider:${name}`]
|
||||
proxies: filterAvailableProxiesAndSort(
|
||||
proxies,
|
||||
delay,
|
||||
hideUnavailableProxies,
|
||||
proxySortBy
|
||||
),
|
||||
isOpen: collapsibleIsOpen[`proxyProvider:${name}`],
|
||||
};
|
||||
};
|
||||
|
||||
// const mapState = s => ({
|
||||
// apiConfig: getClashAPIConfig(s)
|
||||
// });
|
||||
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 {
|
||||
position: relative;
|
||||
}
|
||||
|
@ -88,6 +76,7 @@ body.dark {
|
|||
--color-background: #202020;
|
||||
--color-text: #ddd;
|
||||
--color-text-secondary: #ccc;
|
||||
--color-text-highlight: #fff;
|
||||
--color-bg-sidebar: #2d2d30;
|
||||
--color-sb-active-row-bg: #494b4e;
|
||||
--color-input-bg: #2d2d30;
|
||||
|
@ -95,18 +84,22 @@ body.dark {
|
|||
--color-toggle-bg: #353535;
|
||||
--color-toggle-selected: #181818;
|
||||
--color-icon: #c7c7c7;
|
||||
--color-separator: #333;
|
||||
--color-btn-bg: #232323;
|
||||
--color-btn-fg: #bebebe;
|
||||
--color-bg-proxy: #303030;
|
||||
--color-row-odd: #282828;
|
||||
--bg-modal: #1f1f20;
|
||||
--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 {
|
||||
--color-background: #fbfbfb;
|
||||
--color-text: #222;
|
||||
--color-text-secondary: #646464;
|
||||
--color-text-highlight: #040404;
|
||||
--color-bg-sidebar: #e7e7e7;
|
||||
--color-sb-active-row-bg: #d0d0d0;
|
||||
--color-input-bg: #ffffff;
|
||||
|
@ -114,12 +107,15 @@ body.light {
|
|||
--color-toggle-bg: #ffffff;
|
||||
--color-toggle-selected: #d7d7d7;
|
||||
--color-icon: #5b5b5b;
|
||||
--color-separator: #ccc;
|
||||
--color-btn-bg: #f4f4f4;
|
||||
--color-btn-fg: #101010;
|
||||
--color-bg-proxy: #e7e7e7;
|
||||
--color-row-odd: #f5f5f5;
|
||||
--bg-modal: #fbfbfb;
|
||||
--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 {
|
||||
|
|
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 > * {
|
||||
transform-origin: center center;
|
||||
transform: rotate(315deg);
|
||||
transform: rotate(360deg);
|
||||
transition: ease-in-out transform 0.2s;
|
||||
}
|
||||
.rtf.open .rtf--mb > ul {
|
||||
|
@ -107,18 +107,15 @@
|
|||
}
|
||||
|
||||
.rtf--mb {
|
||||
height: 56px;
|
||||
width: 56px;
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
z-index: 9999;
|
||||
/* background-color: #666666; */
|
||||
background: #387cec;
|
||||
/* background: var(--color-btn-bg); */
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border: none;
|
||||
/* border: 1px solid #555; */
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.14), 0 4px 8px rgba(0, 0, 0, 0.28);
|
||||
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 { closeModal } from './modals';
|
||||
|
||||
export const getClashAPIConfig = s => s.app.clashAPIConfig;
|
||||
export const getTheme = s => s.app.theme;
|
||||
export const getSelectedChartStyleIndex = s => s.app.selectedChartStyleIndex;
|
||||
export const getLatencyTestUrl = s => s.app.latencyTestUrl;
|
||||
export const getCollapsibleIsOpen = s => s.app.collapsibleIsOpen;
|
||||
export const getClashAPIConfig = (s) => s.app.clashAPIConfig;
|
||||
export const getTheme = (s) => s.app.theme;
|
||||
export const getSelectedChartStyleIndex = (s) => s.app.selectedChartStyleIndex;
|
||||
export const getLatencyTestUrl = (s) => s.app.latencyTestUrl;
|
||||
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);
|
||||
|
||||
|
@ -16,7 +18,7 @@ export function updateClashAPIConfig({ hostname: iHostname, port, secret }) {
|
|||
return async (dispatch, getState) => {
|
||||
const hostname = iHostname.trim().replace(/^http(s):\/\//, '');
|
||||
const clashAPIConfig = { hostname, port, secret };
|
||||
dispatch('appUpdateClashAPIConfig', s => {
|
||||
dispatch('appUpdateClashAPIConfig', (s) => {
|
||||
s.app.clashAPIConfig = clashAPIConfig;
|
||||
});
|
||||
// side effect
|
||||
|
@ -43,7 +45,7 @@ export function switchTheme() {
|
|||
const theme = currentTheme === 'light' ? 'dark' : 'light';
|
||||
// side effect
|
||||
setTheme(theme);
|
||||
dispatch('storeSwitchTheme', s => {
|
||||
dispatch('storeSwitchTheme', (s) => {
|
||||
s.app.theme = theme;
|
||||
});
|
||||
// side effect
|
||||
|
@ -62,7 +64,7 @@ export function clearStorage() {
|
|||
|
||||
export function selectChartStyleIndex(selectedChartStyleIndex) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch('appSelectChartStyleIndex', s => {
|
||||
dispatch('appSelectChartStyleIndex', (s) => {
|
||||
s.app.selectedChartStyleIndex = selectedChartStyleIndex;
|
||||
});
|
||||
// side effect
|
||||
|
@ -72,7 +74,7 @@ export function selectChartStyleIndex(selectedChartStyleIndex) {
|
|||
|
||||
export function updateAppConfig(name, value) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch('appUpdateAppConfig', s => {
|
||||
dispatch('appUpdateAppConfig', (s) => {
|
||||
s.app[name] = value;
|
||||
});
|
||||
// side effect
|
||||
|
@ -82,7 +84,7 @@ export function updateAppConfig(name, value) {
|
|||
|
||||
export function updateCollapsibleIsOpen(prefix, name, v) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch('updateCollapsibleIsOpen', s => {
|
||||
dispatch('updateCollapsibleIsOpen', (s) => {
|
||||
s.app.collapsibleIsOpen[`${prefix}:${name}`] = v;
|
||||
});
|
||||
// side effect
|
||||
|
@ -95,14 +97,17 @@ const defaultState = {
|
|||
clashAPIConfig: {
|
||||
hostname: '127.0.0.1',
|
||||
port: '7892',
|
||||
secret: ''
|
||||
secret: '',
|
||||
},
|
||||
latencyTestUrl: 'http://www.gstatic.com/generate_204',
|
||||
selectedChartStyleIndex: 0,
|
||||
theme: 'dark',
|
||||
|
||||
// type { [string]: boolean }
|
||||
collapsibleIsOpen: {}
|
||||
collapsibleIsOpen: {},
|
||||
// how proxies are sorted in a group or provider
|
||||
proxySortBy: 'Natural',
|
||||
hideUnavailableProxies: false,
|
||||
};
|
||||
|
||||
function parseConfigQueryString() {
|
||||
|
|
|
@ -2,12 +2,9 @@ import {
|
|||
initialState as app,
|
||||
selectChartStyleIndex,
|
||||
updateAppConfig,
|
||||
updateCollapsibleIsOpen
|
||||
updateCollapsibleIsOpen,
|
||||
} from './app';
|
||||
import {
|
||||
initialState as proxies,
|
||||
toggleUnavailableProxiesFilter
|
||||
} from './proxies';
|
||||
import { initialState as proxies } from './proxies';
|
||||
import { initialState as modals } from './modals';
|
||||
import { initialState as configs } from './configs';
|
||||
import { initialState as rules } from './rules';
|
||||
|
@ -19,16 +16,15 @@ export const initialState = {
|
|||
configs,
|
||||
proxies,
|
||||
rules,
|
||||
logs
|
||||
logs,
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
selectChartStyleIndex,
|
||||
updateAppConfig,
|
||||
// proxies
|
||||
toggleUnavailableProxiesFilter,
|
||||
|
||||
app: {
|
||||
updateCollapsibleIsOpen
|
||||
}
|
||||
updateCollapsibleIsOpen,
|
||||
updateAppConfig,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -11,8 +11,8 @@ type ProxyProvider = {
|
|||
history: Array<{ time: string, delay: number }>,
|
||||
name: string,
|
||||
// Shadowsocks, Http ...
|
||||
type: string
|
||||
}>
|
||||
type: string,
|
||||
}>,
|
||||
};
|
||||
|
||||
// see all types:
|
||||
|
@ -30,21 +30,20 @@ const NonProxyTypes = [
|
|||
'Selector',
|
||||
'URLTest',
|
||||
'LoadBalance',
|
||||
'Unknown'
|
||||
'Unknown',
|
||||
];
|
||||
|
||||
export const getProxies = s => s.proxies.proxies;
|
||||
export const getDelay = s => s.proxies.delay;
|
||||
export const getRtFilterSwitch = s => s.proxies.filterZeroRT;
|
||||
export const getProxyGroupNames = s => s.proxies.groupNames;
|
||||
export const getProxyProviders = s => s.proxies.proxyProviders || [];
|
||||
export const getDangleProxyNames = s => s.proxies.dangleProxyNames;
|
||||
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 || [];
|
||||
export const getDangleProxyNames = (s) => s.proxies.dangleProxyNames;
|
||||
|
||||
export function fetchProxies(apiConfig) {
|
||||
return async (dispatch, getState) => {
|
||||
const [proxiesData, providersData] = await Promise.all([
|
||||
proxiesAPI.fetchProxies(apiConfig),
|
||||
proxiesAPI.fetchProviderProxies(apiConfig)
|
||||
proxiesAPI.fetchProviderProxies(apiConfig),
|
||||
]);
|
||||
|
||||
const [proxyProviders, providerProxies] = formatProxyProviders(
|
||||
|
@ -77,7 +76,7 @@ export function fetchProxies(apiConfig) {
|
|||
if (!providerProxies[v]) dangleProxyNames.push(v);
|
||||
}
|
||||
|
||||
dispatch('store/proxies#fetchProxies', s => {
|
||||
dispatch('store/proxies#fetchProxies', (s) => {
|
||||
s.proxies.proxies = proxies;
|
||||
s.proxies.groupNames = groupNames;
|
||||
s.proxies.delay = delayNext;
|
||||
|
@ -88,7 +87,7 @@ export function fetchProxies(apiConfig) {
|
|||
}
|
||||
|
||||
export function updateProviderByName(apiConfig, name) {
|
||||
return async dispatch => {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
await proxiesAPI.updateProviderByName(apiConfig, name);
|
||||
} catch (x) {
|
||||
|
@ -109,7 +108,7 @@ async function healthcheckProviderByNameInternal(apiConfig, name) {
|
|||
}
|
||||
|
||||
export function healthcheckProviderByName(apiConfig, name) {
|
||||
return async dispatch => {
|
||||
return async (dispatch) => {
|
||||
await healthcheckProviderByNameInternal(apiConfig, name);
|
||||
// should be optimized
|
||||
// but ¯\_(ツ)_/¯
|
||||
|
@ -118,17 +117,17 @@ export function healthcheckProviderByName(apiConfig, name) {
|
|||
}
|
||||
|
||||
export function switchProxy(apiConfig, name1, name2) {
|
||||
return async dispatch => {
|
||||
return async (dispatch) => {
|
||||
proxiesAPI
|
||||
.requestToSwitchProxy(apiConfig, name1, name2)
|
||||
.then(
|
||||
res => {
|
||||
(res) => {
|
||||
if (res.ok === false) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('failed to swith proxy', res.statusText);
|
||||
}
|
||||
},
|
||||
err => {
|
||||
(err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(err, 'failed to swith proxy');
|
||||
}
|
||||
|
@ -137,7 +136,7 @@ export function switchProxy(apiConfig, name1, name2) {
|
|||
dispatch(fetchProxies(apiConfig));
|
||||
});
|
||||
// optimistic UI update
|
||||
dispatch('store/proxies#switchProxy', s => {
|
||||
dispatch('store/proxies#switchProxy', (s) => {
|
||||
const proxies = s.proxies.proxies;
|
||||
if (proxies[name1] && proxies[name1].now) {
|
||||
proxies[name1].now = name2;
|
||||
|
@ -165,18 +164,18 @@ function requestDelayForProxyOnce(apiConfig, name) {
|
|||
...delayPrev,
|
||||
[name]: {
|
||||
error,
|
||||
number: delay
|
||||
}
|
||||
number: delay,
|
||||
},
|
||||
};
|
||||
|
||||
dispatch('requestDelayForProxyOnce', s => {
|
||||
dispatch('requestDelayForProxyOnce', (s) => {
|
||||
s.proxies.delay = delayNext;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function requestDelayForProxy(apiConfig, name) {
|
||||
return async dispatch => {
|
||||
return async (dispatch) => {
|
||||
await dispatch(requestDelayForProxyOnce(apiConfig, name));
|
||||
};
|
||||
}
|
||||
|
@ -185,7 +184,7 @@ export function requestDelayAll(apiConfig) {
|
|||
return async (dispatch, getState) => {
|
||||
const proxyNames = getDangleProxyNames(getState());
|
||||
await Promise.all(
|
||||
proxyNames.map(p => dispatch(requestDelayForProxy(apiConfig, p)))
|
||||
proxyNames.map((p) => dispatch(requestDelayForProxy(apiConfig, p)))
|
||||
);
|
||||
const proxyProviders = getProxyProviders(getState());
|
||||
// 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) {
|
||||
let groupNames = [];
|
||||
let globalAll;
|
||||
|
@ -225,9 +215,9 @@ function retrieveGroupNamesFrom(proxies) {
|
|||
globalAll.push('GLOBAL');
|
||||
// Sort groups according to its index in GLOBAL group
|
||||
groupNames = groupNames
|
||||
.map(name => [globalAll.indexOf(name), name])
|
||||
.map((name) => [globalAll.indexOf(name), name])
|
||||
.sort((a, b) => a[0] - b[0])
|
||||
.map(group => group[1]);
|
||||
.map((group) => group[1]);
|
||||
}
|
||||
return [groupNames, proxyNames];
|
||||
}
|
||||
|
@ -260,5 +250,4 @@ export const initialState = {
|
|||
proxies: {},
|
||||
delay: {},
|
||||
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 webpack = require('webpack');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
|
@ -7,9 +5,10 @@ const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
|||
const HTMLPlugin = require('html-webpack-plugin');
|
||||
const CopyPlugin = require('copy-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 ReactRefreshWebpackPlugin = require('@hsjs/react-refresh-webpack-plugin');
|
||||
// const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
|
||||
|
||||
const pkg = require('./package.json');
|
||||
|
||||
|
@ -64,6 +63,11 @@ const bundleAnalyzerPlugin = new BundleAnalyzerPlugin({
|
|||
const plugins = [
|
||||
html,
|
||||
definePlugin,
|
||||
new ForkTsCheckerWebpackPlugin({ eslint: false }),
|
||||
new ForkTsCheckerNotifierWebpackPlugin({
|
||||
title: 'TypeScript',
|
||||
excludeWarnings: false,
|
||||
}),
|
||||
new CopyPlugin([{ from: 'assets/*', flatten: true }]),
|
||||
new CleanWebpackPlugin(),
|
||||
// chart.js requires moment
|
||||
|
@ -83,14 +87,26 @@ module.exports = {
|
|||
// app: ['react-hot-loader/patch', './src/app.js']
|
||||
app: ['./src/app.js'],
|
||||
},
|
||||
context: __dirname,
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'public'),
|
||||
filename: isDev ? '[name].js' : '[name].[contenthash].js',
|
||||
publicPath: '',
|
||||
},
|
||||
mode: isDev ? 'development' : 'production',
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js'],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
// disable type checker - we will use it in fork plugin
|
||||
transpileOnly: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
|
|
Loading…
Reference in a new issue