summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorHaishan <[email protected]>2020-04-26 17:35:03 +0800
committerHaishan <[email protected]>2020-04-26 17:59:02 +0800
commit94e2b1e3985f8f4cfeb26a43c59cada184c7d4aa (patch)
tree2d88e5e3ace986e1c08f3eca5e787d1339249dd2 /src
parent7cdbba5bf47062f80a0dc7d80a62ff977d4f568e (diff)
feat: allow change proxies sorting in group
Diffstat (limited to 'src')
-rw-r--r--src/components/Modal.module.css2
-rw-r--r--src/components/ModalCloseAllConnections.module.css2
-rw-r--r--src/components/Proxies.js62
-rw-r--r--src/components/Proxies.module.css10
-rw-r--r--src/components/ProxyGroup.js90
-rw-r--r--src/components/ProxyProvider.js41
-rw-r--r--src/components/Root.css20
-rw-r--r--src/components/proxies/Settings.js75
-rw-r--r--src/components/proxies/Settings.module.css17
-rw-r--r--src/components/rtf.css9
-rw-r--r--src/components/shared/BaseModal.js30
-rw-r--r--src/components/shared/BaseModal.module.css17
-rw-r--r--src/components/shared/Select.module.css29
-rw-r--r--src/components/shared/Select.tsx21
-rw-r--r--src/components/svg/Equalizer.tsx30
-rw-r--r--src/custom.d.ts5
-rw-r--r--src/misc/constants.ts2
-rw-r--r--src/store/app.js29
-rw-r--r--src/store/index.js16
-rw-r--r--src/store/proxies.js57
20 files changed, 419 insertions, 145 deletions
diff --git a/src/components/Modal.module.css b/src/components/Modal.module.css
index 1b183bc..6192a1f 100644
--- a/src/components/Modal.module.css
+++ b/src/components/Modal.module.css
@@ -10,7 +10,7 @@
.content {
outline: none;
- position: absolute;
+ position: relative;
color: #ddd;
top: 50%;
left: 50%;
diff --git a/src/components/ModalCloseAllConnections.module.css b/src/components/ModalCloseAllConnections.module.css
index f3b54c1..9bb7c6a 100644
--- a/src/components/ModalCloseAllConnections.module.css
+++ b/src/components/ModalCloseAllConnections.module.css
@@ -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;
}
diff --git a/src/components/Proxies.js b/src/components/Proxies.js
index acb26dd..729b57a 100644
--- a/src/components/Proxies.js
+++ b/src/components/Proxies.js
@@ -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);
diff --git a/src/components/Proxies.module.css b/src/components/Proxies.module.css
index 5520a2e..2a72f51 100644
--- a/src/components/Proxies.module.css
+++ b/src/components/Proxies.module.css
@@ -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) {
diff --git a/src/components/ProxyGroup.js b/src/components/ProxyGroup.js
index d7cf203..b1a53c0 100644
--- a/src/components/ProxyGroup.js
+++ b/src/components/ProxyGroup.js
@@ -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) {
+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 (filterByRt) {
+ if (hideUnavailableProxies) {
filtered = filterAvailableProxies(all, delay);
}
-
- return filtered.sort((first, second) => {
- const d1 = getSortDelay(delay[first], 999999);
- const d2 = getSortDelay(delay[second], 999999);
- return d1 - d2;
- });
+ 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);
diff --git a/src/components/ProxyProvider.js b/src/components/ProxyProvider.js
index 9486128..64dc93b 100644
--- a/src/components/ProxyProvider.js
+++ b/src/components/ProxyProvider.js
@@ -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);
diff --git a/src/components/Root.css b/src/components/Root.css
index fd7d6d9..22cae24 100644
--- a/src/components/Root.css
+++ b/src/components/Root.css
@@ -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/[email protected]/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 {
diff --git a/src/components/proxies/Settings.js b/src/components/proxies/Settings.js
new file mode 100644
index 0000000..e21ae72
--- /dev/null
+++ b/src/components/proxies/Settings.js
@@ -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);
diff --git a/src/components/proxies/Settings.module.css b/src/components/proxies/Settings.module.css
new file mode 100644
index 0000000..364d07d
--- /dev/null
+++ b/src/components/proxies/Settings.module.css
@@ -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;
+}
diff --git a/src/components/rtf.css b/src/components/rtf.css
index 1a68f6b..a61b35d 100644
--- a/src/components/rtf.css
+++ b/src/components/rtf.css
@@ -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;
diff --git a/src/components/shared/BaseModal.js b/src/components/shared/BaseModal.js
new file mode 100644
index 0000000..8bbdbf4
--- /dev/null
+++ b/src/components/shared/BaseModal.js
@@ -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>
+ );
+}
diff --git a/src/components/shared/BaseModal.module.css b/src/components/shared/BaseModal.module.css
new file mode 100644
index 0000000..1d206e1
--- /dev/null
+++ b/src/components/shared/BaseModal.module.css
@@ -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);
+}
diff --git a/src/components/shared/Select.module.css b/src/components/shared/Select.module.css
new file mode 100644
index 0000000..32343ea
--- /dev/null
+++ b/src/components/shared/Select.module.css
@@ -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);
+}
diff --git a/src/components/shared/Select.tsx b/src/components/shared/Select.tsx
new file mode 100644
index 0000000..03ac084
--- /dev/null
+++ b/src/components/shared/Select.tsx
@@ -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>
+ );
+}
diff --git a/src/components/svg/Equalizer.tsx b/src/components/svg/Equalizer.tsx
new file mode 100644
index 0000000..274720f
--- /dev/null
+++ b/src/components/svg/Equalizer.tsx
@@ -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>
+ );
+}
diff --git a/src/custom.d.ts b/src/custom.d.ts
new file mode 100644
index 0000000..9041f77
--- /dev/null
+++ b/src/custom.d.ts
@@ -0,0 +1,5 @@
+// for css modules
+declare module '*.module.css' {
+ const classes: { [key: string]: string };
+ export default classes;
+}
diff --git a/src/misc/constants.ts b/src/misc/constants.ts
new file mode 100644
index 0000000..6ac5393
--- /dev/null
+++ b/src/misc/constants.ts
@@ -0,0 +1,2 @@
+
+// const ProxySortingOptions =
diff --git a/src/store/app.js b/src/store/app.js
index ae8e8a2..ceb3c94 100644
--- a/src/store/app.js
+++ b/src/store/app.js
@@ -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() {
diff --git a/src/store/index.js b/src/store/index.js
index 78ddca3..b7ad5e4 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -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,
+ },
};
diff --git a/src/store/proxies.js b/src/store/proxies.js
index 3896fee..f96bec5 100644
--- a/src/store/proxies.js
+++ b/src/store/proxies.js
@@ -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
};