diff options
| author | Haishan <[email protected]> | 2020-04-26 17:35:03 +0800 |
|---|---|---|
| committer | Haishan <[email protected]> | 2020-04-26 17:59:02 +0800 |
| commit | 94e2b1e3985f8f4cfeb26a43c59cada184c7d4aa (patch) | |
| tree | 2d88e5e3ace986e1c08f3eca5e787d1339249dd2 /src | |
| parent | 7cdbba5bf47062f80a0dc7d80a62ff977d4f568e (diff) | |
feat: allow change proxies sorting in group
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/Modal.module.css | 2 | ||||
| -rw-r--r-- | src/components/ModalCloseAllConnections.module.css | 2 | ||||
| -rw-r--r-- | src/components/Proxies.js | 62 | ||||
| -rw-r--r-- | src/components/Proxies.module.css | 10 | ||||
| -rw-r--r-- | src/components/ProxyGroup.js | 90 | ||||
| -rw-r--r-- | src/components/ProxyProvider.js | 41 | ||||
| -rw-r--r-- | src/components/Root.css | 20 | ||||
| -rw-r--r-- | src/components/proxies/Settings.js | 75 | ||||
| -rw-r--r-- | src/components/proxies/Settings.module.css | 17 | ||||
| -rw-r--r-- | src/components/rtf.css | 9 | ||||
| -rw-r--r-- | src/components/shared/BaseModal.js | 30 | ||||
| -rw-r--r-- | src/components/shared/BaseModal.module.css | 17 | ||||
| -rw-r--r-- | src/components/shared/Select.module.css | 29 | ||||
| -rw-r--r-- | src/components/shared/Select.tsx | 21 | ||||
| -rw-r--r-- | src/components/svg/Equalizer.tsx | 30 | ||||
| -rw-r--r-- | src/custom.d.ts | 5 | ||||
| -rw-r--r-- | src/misc/constants.ts | 2 | ||||
| -rw-r--r-- | src/store/app.js | 29 | ||||
| -rw-r--r-- | src/store/index.js | 16 | ||||
| -rw-r--r-- | src/store/proxies.js | 57 |
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 }; |
