diff options
| author | Matain <[email protected]> | 2022-06-12 23:38:31 +0800 |
|---|---|---|
| committer | Matain <[email protected]> | 2022-06-12 23:38:31 +0800 |
| commit | e4e921e0b93f74bf126ca80cbb83f5e912f73a88 (patch) | |
| tree | ca586f4753f5266ab67051235c7a79370fca1333 /src | |
| parent | a825925cc97d95762634d234ef06be1627a21fb1 (diff) | |
| parent | ea5d7cf003eeef30cb7bbe789c6ba7f314bf1ce4 (diff) | |
Merge branch 'haishanh-master'
Diffstat (limited to 'src')
69 files changed, 518 insertions, 761 deletions
diff --git a/src/api/configs.ts b/src/api/configs.ts index 3734958..8a9b339 100644 --- a/src/api/configs.ts +++ b/src/api/configs.ts @@ -24,10 +24,7 @@ function configsPatchWorkaround(o: ClashConfigPartial) { return o; } -export async function updateConfigs( - apiConfig: ClashAPIConfig, - o: ClashConfigPartial -) { +export async function updateConfigs(apiConfig: ClashAPIConfig, o: ClashConfigPartial) { const { url, init } = getURLAndInit(apiConfig); const body = JSON.stringify(configsPatchWorkaround(o)); return await fetch(url + endpoint, { ...init, body, method: 'PATCH' }); diff --git a/src/api/connections.ts b/src/api/connections.ts index cedb227..6690f30 100644 --- a/src/api/connections.ts +++ b/src/api/connections.ts @@ -53,10 +53,7 @@ function appendData(s: string) { type UnsubscribeFn = () => void; let wsState: number; -export function fetchData( - apiConfig: ClashAPIConfig, - listener: unknown -): UnsubscribeFn | void { +export function fetchData(apiConfig: ClashAPIConfig, listener: unknown): UnsubscribeFn | void { if (fetched || wsState === 1) { if (listener) return subscribe(listener); } diff --git a/src/api/proxies.ts b/src/api/proxies.ts index 079106b..be09d7c 100644 --- a/src/api/proxies.ts +++ b/src/api/proxies.ts @@ -1,3 +1,5 @@ +import { ClashAPIConfig } from '$src/types'; + import { getURLAndInit } from '../misc/request-helper'; const endpoint = '/proxies'; @@ -14,16 +16,21 @@ $ curl "http://127.0.0.1:8080/proxies/GLOBAL" -XPUT -d '{ "name": "Proxy" }' -i HTTP/1.1 204 No Content */ -export async function fetchProxies(config) { +export async function fetchProxies(config: ClashAPIConfig) { const { url, init } = getURLAndInit(config); const res = await fetch(url + endpoint, init); return await res.json(); } -export async function requestToSwitchProxy(apiConfig, name1, name2) { - const body = { name: name2 }; +export async function requestToSwitchProxy( + apiConfig: ClashAPIConfig, + groupName: string, + name: string +) { + const body = { name }; const { url, init } = getURLAndInit(apiConfig); - const fullURL = `${url}${endpoint}/${name1}`; + const group = encodeURIComponent(groupName); + const fullURL = `${url}${endpoint}/${group}`; return await fetch(fullURL, { ...init, method: 'PUT', @@ -32,12 +39,12 @@ export async function requestToSwitchProxy(apiConfig, name1, name2) { } export async function requestDelayForProxy( - apiConfig, - name, + apiConfig: ClashAPIConfig, + name: string, latencyTestUrl = 'http://www.gstatic.com/generate_204' ) { const { url, init } = getURLAndInit(apiConfig); - const qs = `timeout=5000&url=${latencyTestUrl}`; + const qs = `timeout=5000&url=${encodeURIComponent(latencyTestUrl)}`; const fullURL = `${url}${endpoint}/${encodeURIComponent(name)}/delay?${qs}`; return await fetch(fullURL, init); } @@ -62,17 +69,14 @@ export async function fetchProviderProxies(config) { return await res.json(); } -export async function updateProviderByName(config, name) { +export async function updateProviderByName(config: ClashAPIConfig, name: string) { const { url, init } = getURLAndInit(config); const options = { ...init, method: 'PUT' }; return await fetch(url + '/providers/proxies/' + name, options); } -export async function healthcheckProviderByName(config, name) { +export async function healthcheckProviderByName(config: ClashAPIConfig, name: string) { const { url, init } = getURLAndInit(config); const options = { ...init, method: 'GET' }; - return await fetch( - url + '/providers/proxies/' + name + '/healthcheck', - options - ); + return await fetch(url + '/providers/proxies/' + name + '/healthcheck', options); } diff --git a/src/api/rule-provider.ts b/src/api/rule-provider.ts index ec9fa7b..5ecd61e 100644 --- a/src/api/rule-provider.ts +++ b/src/api/rule-provider.ts @@ -31,10 +31,7 @@ function normalizeAPIResponse(data: RuleProviderAPIData) { return { byName, names }; } -export async function fetchRuleProviders( - endpoint: string, - apiConfig: ClashAPIConfig -) { +export async function fetchRuleProviders(endpoint: string, apiConfig: ClashAPIConfig) { const { url, init } = getURLAndInit(apiConfig); let data = { providers: {} }; diff --git a/src/api/rules.ts b/src/api/rules.ts index b57b0e3..4d18c23 100644 --- a/src/api/rules.ts +++ b/src/api/rules.ts @@ -12,9 +12,7 @@ type RuleAPIItem = { proxy: string; }; -function normalizeAPIResponse(json: { - rules: Array<RuleAPIItem>; -}): Array<RuleItem> { +function normalizeAPIResponse(json: { rules: Array<RuleAPIItem> }): Array<RuleItem> { invariant( json.rules && json.rules.length >= 0, 'there is no valid rules list in the rules API response' diff --git a/src/api/traffic.ts b/src/api/traffic.ts index cd18aac..aa9143c 100644 --- a/src/api/traffic.ts +++ b/src/api/traffic.ts @@ -27,7 +27,7 @@ const traffic = { this.subscribers.forEach((f) => f(o)); }, - subscribe(listener: (x:any) => void) { + subscribe(listener: (x: any) => void) { this.subscribers.push(listener); return () => { const idx = this.subscribers.indexOf(listener); diff --git a/src/components/APIConfig.tsx b/src/components/APIConfig.tsx index 6e11bc4..4a11c92 100644 --- a/src/components/APIConfig.tsx +++ b/src/components/APIConfig.tsx @@ -72,9 +72,9 @@ function APIConfig({ dispatch }) { const detectApiServer = async () => { // if there is already a clash API server at `/`, just use it as default value const res = await fetch('/'); - res.json().then(data => { + res.json().then((data) => { if (data['hello'] === 'clash') { - setBaseURL(window.location.origin) + setBaseURL(window.location.origin); } }); }; @@ -82,7 +82,6 @@ function APIConfig({ dispatch }) { detectApiServer(); }, []); - return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions <div className={s0.root} ref={contentEl} onKeyDown={handleContentOnKeyDown}> diff --git a/src/components/APIDiscovery.tsx b/src/components/APIDiscovery.tsx index f34c886..d211e04 100644 --- a/src/components/APIDiscovery.tsx +++ b/src/components/APIDiscovery.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { ThemeSwitcher } from 'src/components/shared/ThemeSwitcher'; -import { DOES_NOT_SUPPORT_FETCH, errors } from 'src/misc/errors'; +import { DOES_NOT_SUPPORT_FETCH, errors, YacdError } from 'src/misc/errors'; import { getClashAPIConfig } from 'src/store/app'; import { fetchConfigs } from 'src/store/configs'; import { closeModal } from 'src/store/modals'; @@ -16,9 +16,7 @@ const { useCallback, useEffect } = React; function APIDiscovery({ dispatch, apiConfig, modals }) { if (!window.fetch) { const { detail } = errors[DOES_NOT_SUPPORT_FETCH]; - const err = new Error(detail); - // @ts-expect-error ts-migrate(2339) FIXME: Property 'code' does not exist on type 'Error'. - err.code = DOES_NOT_SUPPORT_FETCH; + const err = new YacdError(detail, DOES_NOT_SUPPORT_FETCH); throw err; } diff --git a/src/components/BackendList.tsx b/src/components/BackendList.tsx index 8e0d906..9ad833c 100644 --- a/src/components/BackendList.tsx +++ b/src/components/BackendList.tsx @@ -2,10 +2,7 @@ import cx from 'clsx'; import * as React from 'react'; import { Eye, EyeOff, X as Close } from 'react-feather'; import { useToggle } from 'src/hooks/basic'; -import { - getClashAPIConfigs, - getSelectedClashAPIConfigIndex, -} from 'src/store/app'; +import { getClashAPIConfigs, getSelectedClashAPIConfigIndex } from 'src/store/app'; import { ClashAPIConfig } from 'src/types'; import s from './BackendList.module.scss'; @@ -113,8 +110,6 @@ function Item({ {secret ? ( <> <span className={s.secret}>{show ? secret : '***'}</span> - - {/* @ts-expect-error ts-migrate(2322) FIXME: Type 'boolean | (() => void)' is not assignable to... Remove this comment to see the full error message */} <Button onClick={toggle} className={s.eye}> <Icon size={20} /> </Button> @@ -137,11 +132,7 @@ function Button({ disabled?: boolean; }) { return ( - <button - disabled={disabled} - className={cx(className, s.btn)} - onClick={onClick} - > + <button disabled={disabled} className={cx(className, s.btn)} onClick={onClick}> {children} </button> ); diff --git a/src/components/Button.module.scss b/src/components/Button.module.scss index b46d79c..710d3ad 100644 --- a/src/components/Button.module.scss +++ b/src/components/Button.module.scss @@ -23,16 +23,16 @@ transform: scale(0.97); } - font-size: 0.75em; - padding: 4px 7px; - @media (--breakpoint-not-small) { - font-size: small; - padding: 6px 12px; + padding: 10px 13px; + + &.circular { + padding: 8px; } &.minimal { border-color: transparent; background: none; + padding: 6px 12px; &:focus { border-color: var(--color-focus-blue); } diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 1725d1b..8125edc 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -17,7 +17,7 @@ type ButtonProps = { isLoading?: boolean; onClick?: (e: React.MouseEvent<HTMLButtonElement>) => unknown; disabled?: boolean; - kind?: 'primary' | 'minimal'; + kind?: 'primary' | 'minimal' | 'circular'; className?: string; title?: string; } & ButtonInternalProps; @@ -36,7 +36,7 @@ function Button(props: ButtonProps, ref: React.Ref<HTMLButtonElement>) { ...restProps } = props; const internalProps = { children, label, text, start }; - const internalOnClick = useCallback( + const internalOnClick = useCallback<React.MouseEventHandler<HTMLButtonElement>>( (e) => { if (isLoading) return; onClick && onClick(e); @@ -45,7 +45,7 @@ function Button(props: ButtonProps, ref: React.Ref<HTMLButtonElement>) { ); const btnClassName = cx( s0.btn, - { [s0.minimal]: kind === 'minimal' }, + { [s0.minimal]: kind === 'minimal', [s0.circular]: kind === 'circular' }, className ); return ( @@ -76,9 +76,7 @@ function ButtonInternal({ children, label, text, start }: ButtonInternalProps) { return ( <> {start ? ( - <span className={s0.btnStart}> - {typeof start === 'function' ? start() : start} - </span> + <span className={s0.btnStart}>{typeof start === 'function' ? start() : start}</span> ) : null} {children || label || text} </> diff --git a/src/components/Collapsible.tsx b/src/components/Collapsible.tsx index e9a1ee8..65284cd 100644 --- a/src/components/Collapsible.tsx +++ b/src/components/Collapsible.tsx @@ -1,17 +1,18 @@ -import React from 'react'; +import type { MutableRefObject } from 'react'; +import * as React from 'react'; import ResizeObserver from 'resize-observer-polyfill'; import { framerMotionResouce } from '../misc/motion'; const { memo, useState, useRef, useEffect } = React; -function usePrevious(value) { +function usePrevious(value: any) { const ref = useRef(); useEffect(() => void (ref.current = value), [value]); return ref.current; } -function useMeasure() { +function useMeasure(): [MutableRefObject<HTMLElement>, { height: number }] { const ref = useRef(); const [bounds, set] = useState({ height: 0 }); useEffect(() => { @@ -27,7 +28,7 @@ const variantsCollpapsibleWrap = { height: 'auto', transition: { duration: 0 }, }, - open: (height) => ({ + open: (height: number) => ({ height, opacity: 1, visibility: 'visible', @@ -50,30 +51,21 @@ const variantsCollpapsibleChildContainer = { }, }; -// @ts-expect-error ts-migrate(2339) FIXME: Property 'isOpen' does not exist on type '{ childr... Remove this comment to see the full error message -const Collapsible = memo(({ children, isOpen }) => { +type CollapsibleProps = { children: React.ReactNode; isOpen?: boolean }; + +const Collapsible = memo(({ children, isOpen }: CollapsibleProps) => { const module = framerMotionResouce.read(); const motion = module.motion; const previous = usePrevious(isOpen); - // @ts-expect-error ts-migrate(2339) FIXME: Property 'height' does not exist on type 'MutableR... Remove this comment to see the full error message const [refToMeature, { height }] = useMeasure(); return ( <div> <motion.div - animate={ - isOpen && previous === isOpen - ? 'initialOpen' - : isOpen - ? 'open' - : 'closed' - } + animate={isOpen && previous === isOpen ? 'initialOpen' : isOpen ? 'open' : 'closed'} custom={height} variants={variantsCollpapsibleWrap} > - <motion.div - variants={variantsCollpapsibleChildContainer} - ref={refToMeature} - > + <motion.div variants={variantsCollpapsibleChildContainer} ref={refToMeature}> {children} </motion.div> </motion.div> diff --git a/src/components/CollapsibleSectionHeader.tsx b/src/components/CollapsibleSectionHeader.tsx index 2d5ecd1..8b701e1 100644 --- a/src/components/CollapsibleSectionHeader.tsx +++ b/src/components/CollapsibleSectionHeader.tsx @@ -39,12 +39,7 @@ export default function Header({ name, type, toggle, isOpen, qty }: Props) { {typeof qty === 'number' ? <span className={s.qty}>{qty}</span> : null} - <Button - kind="minimal" - onClick={toggle} - className={s.btn} - title="Toggle collapsible section" - > + <Button kind="minimal" onClick={toggle} className={s.btn} title="Toggle collapsible section"> <span className={cx(s.arrow, { [s.isOpen]: isOpen })}> <ChevronDown size={20} /> </span> diff --git a/src/components/Config.tsx b/src/components/Config.tsx index ded79ad..804e87f 100644 --- a/src/components/Config.tsx +++ b/src/components/Config.tsx @@ -7,21 +7,8 @@ import Select from 'src/components/shared/Select'; import { ClashGeneralConfig, DispatchFn, State } from 'src/store/types'; import { ClashAPIConfig } from 'src/types'; -import { fetchVersion } from '$src/api/version'; - -import { - getClashAPIConfig, - getLatencyTestUrl, - getSelectedChartStyleIndex, -} from '../store/app'; -import { - fetchConfigs, - flushFakeIPPool, - getConfigs, - reloadConfigFile, - updateConfigs, - updateGeoDatabasesFile, -} from '../store/configs'; +import { getClashAPIConfig, getLatencyTestUrl, getSelectedChartStyleIndex } from '../store/app'; +import { fetchConfigs, getConfigs, updateConfigs } from '../store/configs'; import { openModal } from '../store/modals'; import Button from './Button'; import s0 from './Config.module.scss'; @@ -119,7 +106,7 @@ function ConfigImpl({ }, [dispatch]); const setConfigState = useCallback( - (name, val) => { + (name: keyof ClashGeneralConfig, val: ClashGeneralConfig[keyof ClashGeneralConfig]) => { setConfigStateInternal({ ...configState, [name]: val }); }, [configState] @@ -169,14 +156,14 @@ function ConfigImpl({ [apiConfig, dispatch, setConfigState, setTunConfigState] ); - const handleInputOnChange = useCallback( + const handleInputOnChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>( (e) => handleChangeValue(e.target), [handleChangeValue] ); const { selectChartStyleIndex, updateAppConfig } = useStoreActions(); - const handleInputOnBlur = useCallback( + const handleInputOnBlur = useCallback<React.FocusEventHandler<HTMLInputElement>>( (e) => { const target = e.target; const { name, value } = target; @@ -232,7 +219,6 @@ function ConfigImpl({ name={f.key} value={configState[f.key]} onChange={handleInputOnChange} - // @ts-expect-error ts-migrate(2322) FIXME: Type '{ name: string; value: any; onChange: (e: an... Remove this comment to see the full error message onBlur={handleInputOnBlur} /> </div> @@ -243,10 +229,8 @@ function ConfigImpl({ <div className={s0.label}>Mode</div> <Select options={modeOptions} - selected={configState['mode']} - onChange={(e) => - handleChangeValue({ name: 'mode', value: e.target.value }) - } + selected={mode} + onChange={(e) => handleChangeValue({ name: 'mode', value: e.target.value })} /> </div> @@ -255,137 +239,4 @@ function ConfigImpl({ <Select options={logLeveOptions} selected={configState['log-level']} - onChange={(e) => - handleChangeValue({ name: 'log-level', value: e.target.value }) - } - /> - </div> - - <div> - <div className={s0.label}>{t('allow_lan')}</div> - <div className={s0.wrapSwitch}> - <Switch - name="allow-lan" - checked={configState['allow-lan']} - onChange={(value: boolean) => - handleChangeValue({ name: 'allow-lan', value: value }) - } - /> - </div> - </div> - { version.meta && - <div> - <div className={s0.label}>{t('tls_sniffing')}</div> - <div className={s0.wrapSwitch}> - <Switch - name="sniffing" - checked={configState['sniffing']} - onChange={(value: boolean) => - handleChangeValue({ name: 'sniffing', value: value }) - } - /> - </div> - </div>} - </div> - <div className={s0.sep} > - <div /> - </div> - { version.meta && - <> - <div className={s0.section}> - <div> - <div className={s0.label}>{t('enable_tun_device')}</div> - <div className={s0.wrapSwitch}> - <Switch - checked={configState['tun']?.enable} - onChange={(value: boolean) => - handleChangeValue({ name: 'enable', value: value }) - } - /> - </div> - </div> - <div> - <div className={s0.label}>TUN IP Stack</div> - <Select - options={tunStackOptions} - selected={configState['tun']?.stack} - onChange={(e) => - handleChangeValue({ name: 'stack', value: e.target.value }) - } - /> - </div> - </div> - <div className={s0.sep}> - <div /> - </div> - <div className={s0.section}> - <div> - <div className={s0.label}>Reload</div> - <Button - start={<RotateCw size={16} />} - label={t('reload_config_file')} - onClick={handleReloadConfigFile} /> - </div> - <div> - <div className={s0.label}>GEO Databases</div> - <Button - start={<DownloadCloud size={16} />} - label={t('update_geo_databases_file')} - onClick={handleUpdateGeoDatabasesFile} /> - </div> - <div> - <div className={s0.label}>FakeIP</div> - <Button - start={<Trash2 size={16} />} - label={t('flush_fake_ip_pool')} - onClick={handleFlushFakeIPPool} /> - </div> - </div> - <div className={s0.sep}> - <div /> - </div> - </>} - - <div className={s0.section}> - <div> - <div className={s0.label}>{t('latency_test_url')}</div> - <SelfControlledInput - name="latencyTestUrl" - type="text" - value={latencyTestUrl} - onBlur={handleInputOnBlur} - /> - </div> - <div> - <div className={s0.label}>{t('lang')}</div> - <div> - <Select - options={langOptions} - selected={i18n.language} - onChange={(e) => i18n.changeLanguage(e.target.value)} - /> - </div> - </div> - - <div> - <div className={s0.label}>{t('chart_style')}</div> - <Selection2 - OptionComponent={TrafficChartSample} - optionPropsList={propsList} - selectedIndex={selectedChartStyleIndex} - onChange={selectChartStyleIndex} - /> - </div> - - <div> - <div className={s0.label}>Action</div> - <Button - start={<LogOut size={16} />} - label="Switch backend" - onClick={openAPIConfigModal} - /> - </div> - </div> - </div> - ); -} + onChange={(e) => handleChangeValue({ name: 'log-level', value: e.target.value })} diff --git a/src/components/ConnectionTable.tsx b/src/components/ConnectionTable.tsx index a8ad827..879442a 100644 --- a/src/components/ConnectionTable.tsx +++ b/src/components/ConnectionTable.tsx @@ -71,11 +71,8 @@ function Table({ data }) { return ( <div {...headerGroup.getHeaderGroupProps()} className={s.tr}> {headerGroup.headers.map((column) => ( - <div - {...column.getHeaderProps(column.getSortByToggleProps())} - className={s.th} - > - <span>{t(column.render('Header'))}</span> + <div {...column.getHeaderProps(column.getSortByToggleProps())} className={s.th}> + <span>{column.render('Header')}</span> <span className={s.sortIconContainer}> {column.isSorted ? ( <span className={column.isSortedDesc ? '' : s.rotate180}> diff --git a/src/components/Connections.tsx b/src/components/Connections.tsx index 3769b32..a36031e 100644 --- a/src/components/Connections.tsx +++ b/src/components/Connections.tsx @@ -124,7 +124,7 @@ function renderTableOrPlaceholder(conns: FormattedConn[]) { ); } -function ConnQty({ qty }) { +function connQty({ qty }) { return qty < 100 ? '' + qty : '99+'; } @@ -194,17 +194,11 @@ function Conn({ apiConfig }) { <TabList> <Tab> <span>{t('Active')}</span> - <span className={s.connQty}> - {/* @ts-expect-error ts-migrate(2786) FIXME: 'ConnQty' cannot be used as a JSX component. */} - <ConnQty qty={filteredConns.length} /> - </span> + <span className={s.connQty}>{connQty({ qty: filteredConns.length })}</span> </Tab> <Tab> <span>{t('Closed')}</span> - <span className={s.connQty}> - {/* @ts-expect-error ts-migrate(2786) FIXME: 'ConnQty' cannot be used as a JSX component. */} - <ConnQty qty={filteredClosedConns.length} /> - </span> + <span className={s.connQty}>{connQty({ qty: filteredClosedConns.length })}</span> </Tab> </TabList> <div className={s.inputWrapper}> diff --git a/src/components/Input.tsx b/src/components/Input.tsx index c132a3b..efb5665 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -7,7 +7,8 @@ const { useState, useRef, useEffect, useCallback } = React; type InputProps = { value?: string | number; type?: string; - onChange?: (...args: any[]) => any; + onChange?: React.ChangeEventHandler<HTMLInputElement>; + onBlur?: React.FocusEventHandler<HTMLInputElement>; name?: string; placeholder?: string; }; @@ -26,17 +27,7 @@ export function SelfControlledInput({ value, ...restProps }) { } refValue.current = value; }, [value]); - const onChange = useCallback( - (e) => setInternalValue(e.target.value), - [setInternalValue] - ); + const onChange = useCallback((e) => setInternalValue(e.target.value), [setInternalValue]); - return ( - <input - className={s0.input} - value={internalValue} - onChange={onChange} - {...restProps} - /> - ); + return <input className={s0.input} value={internalValue} onChange={onChange} {...restProps} />; } diff --git a/src/components/Logs.tsx b/src/components/Logs.tsx index 1dd00a8..003ff0d 100644 --- a/src/components/Logs.tsx +++ b/src/components/Logs.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; import { Pause, Play } from 'react-feather'; import { useTranslation } from 'react-i18next'; -import { fetchLogs, reconnect as reconnectLogs,stop as stopLogs } from 'src/api/logs'; +import { areEqual, FixedSizeList as List, ListChildComponentProps } from 'react-window'; +import { fetchLogs, reconnect as reconnectLogs, stop as stopLogs } from 'src/api/logs'; import ContentHeader from 'src/components/ContentHeader'; import LogSearch from 'src/components/LogSearch'; import { connect, useStoreActions } from 'src/components/StateProvider'; diff --git a/src/components/Root.scss b/src/components/Root.scss index 8a7a57c..0a7fc4f 100644 --- a/src/components/Root.scss +++ b/src/components/Root.scss @@ -90,6 +90,7 @@ body { --color-background: #202020; --color-background2: rgba(32, 32, 32, 0.3); --color-bg-card: #2d2d2d; + --card-hover-border-lightness: 30%; --color-text: #ddd; --color-text-secondary: #ccc; --color-text-highlight: #fff; @@ -118,6 +119,7 @@ body { --color-background: #eee; --color-background2: rgba(240, 240, 240, 0.3); --color-bg-card: #fafafa; + --card-hover-border-lightness: 80%; --color-text: #222; --color-text-secondary: #646464; --color-text-highlight: #040404; diff --git a/src/components/Rules.tsx b/src/components/Rules.tsx index e105edf..4b17a4b 100644 --- a/src/components/Rules.tsx +++ b/src/components/Rules.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { areEqual, VariableSizeList } from 'react-window'; import { RuleProviderItem } from 'src/components/rules/RuleProviderItem'; @@ -7,7 +7,7 @@ import { RulesPageFab } from 'src/components/rules/RulesPageFab'; import { TextFilter } from 'src/components/shared/TextFitler'; import { ruleFilterText } from 'src/store/rules'; import { State } from 'src/store/types'; -import { ClashAPIConfig } from 'src/types'; +import { ClashAPIConfig, RuleType } from 'src/types'; import useRemainingViewPortHeight from '../hooks/useRemainingViewPortHeight'; import { getClashAPIConfig } from '../store/app'; @@ -41,15 +41,24 @@ function getItemSizeFactory({ provider }) { const providerQty = provider.names.length; if (idx < providerQty) { // provider - return 90; + return 110; } // rule return 60; }; } -// @ts-expect-error ts-migrate(2339) FIXME: Property 'index' does not exist on type '{ childre... Remove this comment to see the full error message -const Row = memo(({ index, style, data }) => { +type RowProps = { + index: number; + style: React.CSSProperties; + data: { + apiConfig: ClashAPIConfig; + rules: RuleType[]; + provider: { names: string[]; byName: any }; + }; +}; + +const Row = memo(({ index, style, data }: RowProps) => { const { rules, provider, apiConfig } = data; const providerQty = provider.names.length; diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 6edc4a5..9762d8f 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -25,12 +25,7 @@ function RuleSearch({ dispatch, searchText, updateSearchText }) { <div className={s0.RuleSearch}> <div className={s0.RuleSearchContainer}> <div className={s0.inputWrapper}> - <input - type="text" - value={text} - onChange={onChange} - className={s0.input} - /> + <input type="text" value={text} onChange={onChange} className={s0.input} /> </div> <div className={s0.iconWrapper}> <SearchIcon size={20} /> diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx index dbe7f0d..2c5ba0c 100644 --- a/src/components/SideBar.tsx +++ b/src/components/SideBar.tsx @@ -3,14 +3,7 @@ import cx from 'clsx'; import * as React from 'react'; import { Info } from 'react-feather'; import { useTranslation } from 'react-i18next'; -import { - FcAreaChart, - FcDocument, - FcGlobe, - FcLink, - FcRuler, - FcSettings, -} from 'react-icons/fc'; +import { FcAreaChart, FcDocument, FcGlobe, FcLink, FcRuler, FcSettings } from 'react-icons/fc'; import { Link, useLocation } from 'react-router-dom'; import { ThemeSwitcher } from 'src/components/shared/ThemeSwitcher'; diff --git a/src/components/StyleGuide.tsx b/src/components/StyleGuide.tsx index ee38697..910c538 100644 --- a/src/components/StyleGuide.tsx +++ b/src/components/StyleGuide.tsx @@ -4,6 +4,7 @@ import Loading from 'src/components/Loading'; import Button from './Button'; import Input from './Input'; +import { ZapAnimated } from './shared/ZapAnimated'; import SwitchThemed from './SwitchThemed'; import ToggleSwitch from './ToggleSwitch'; @@ -21,7 +22,9 @@ const optionsRule = [ { label: 'Direct', value: 'Direct' }, ]; -const Pane = ({ children, style }) => <div style={{ ...paneStyle, ...style }}>{children}</div>; +const Pane = ({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) => ( + <div style={{ ...paneStyle, ...style }}>{children}</div> +); function useToggle(initialState = false) { const [onoff, setonoff] = React.useState(initialState); @@ -40,19 +43,18 @@ class StyleGuide extends PureComponent { render() { return ( <div> - {/* @ts-expect-error ts-migrate(2741) FIXME: Property 'style' is missing in type '{ children: E... Remove this comment to see the full error message */} + <Pane> + <ZapAnimated /> + </Pane> <Pane> <SwitchExample /> </Pane> - {/* @ts-expect-error ts-migrate(2741) FIXME: Property 'style' is missing in type '{ children: E... Remove this comment to see the full error message */} <Pane> <Input /> </Pane> - {/* @ts-expect-error ts-migrate(2741) FIXME: Property 'style' is missing in type '{ children: E... Remove this comment to see the full error message */} <Pane> <ToggleSwitch name="test" options={optionsRule} value="Rule" onChange={noop} /> </Pane> - {/* @ts-expect-error ts-migrate(2741) FIXME: Property 'style' is missing in type '{ children: E... Remove this comment to see the full error message */} <Pane> <Button text="Test Lxatency" start={<Zap size={16} />} /> <Button text="Test Lxatency" start={<Zap size={16} />} isLoading /> diff --git a/src/components/SvgYacd.tsx b/src/components/SvgYacd.tsx index 49455e3..803936c 100644 --- a/src/components/SvgYacd.tsx +++ b/src/components/SvgYacd.tsx @@ -23,19 +23,24 @@ function SvgYacd({ }: Props) { const faceClasName = cx({ [s.path]: animate }); return ( - <svg xmlns="http://www.w3.org/2000/svg" version="1.2" viewBox="0 0 512 512" width={width} height={height}> - <path id="Layer" className={faceClasName} fill={c0} stroke={line} strokeLinecap="round" strokeWidth="4" - d="m280.8 182.4l119-108.3c1.9-1.7 4.3-2.7 6.8-2.4l39.5 4.1c2.1 0.3 3.9 2.2 3.9 4.4v251.1c0 2-1.5 3.9-3.5 4.4l-41.9 9c-0.5 0.3-1.2 0.3-1.9 0.3h-18.8c-2.4 0-4.4-2-4.4-4.4v-132.9c0-7.5-9-11.7-14.8-6.3l-59 53.4c-2.2 2.2-5.4 2.9-8.5 1.9-27.1-8-56.3-8-83.4 0-2.9 1-6.1 0.3-8.5-1.9l-59-53.4c-5.6-5.4-14.6-1.2-14.6 6.3v132.9c0 2.4-2.2 4.4-4.7 4.4h-18.7c-0.7 0-1.2 0-2-0.3l-41.6-9c-2-0.5-3.5-2.4-3.5-4.4v-251.1c0-2.2 1.8-4.1 3.9-4.4l39.5-4.1c2.5-0.3 4.9 0.7 6.9 2.4l115.7 105.3c2 1.7 4.6 2.5 7.1 2.2 15.3-2.2 31.4-1.9 46.5 0.8z"/> - <path id="Layer" className={faceClasName} fill={c0} stroke={line} strokeLinecap="round" strokeWidth="4" - d="m269.4 361.8l-7.1 13.4c-2.4 4.2-8.5 4.2-11 0l-7-13.4c-2.5-4.1 0.7-9.3 5.3-9h14.4c4.9 0 7.8 4.9 5.4 9z"/> - <path id="Layer" className={faceClasName} fill={c1} stroke={line} strokeLinecap="round" strokeWidth="4" - d="m160.7 362.5c3.6 0 6.8 3.2 6.8 6.9 0 3.6-3.2 6.5-6.8 6.5h-94.6c-3.6 0-6.8-2.9-6.8-6.5 0-3.7 3.2-6.9 6.8-6.9z" /> - <path id="Layer" className={faceClasName} fill={c1} stroke={line} strokeLinecap="round" strokeWidth="4" - d="m158.7 394.7c3.4-1 7.1 1 8.3 4.4 1 3.4-1 7.3-4.4 8.3l-92.8 31.7c-3.4 1.2-7.3-0.7-8.3-4.2-1.2-3.6 0.7-7.3 4.4-8.5z" /> - <path id="Layer" className={faceClasName} fill={c1} stroke={line} strokeLinecap="round" strokeWidth="4" - d="m446.1 426.4c3.4 1.2 5.3 4.9 4.3 8.5-1.2 3.5-4.8 5.4-8.2 4.2l-93.1-31.7c-3.5-1-5.4-4.9-4.2-8.3 1-3.4 4.9-5.4 8.3-4.4z" /> - <path id="Layer" className={faceClasName} fill={c1} stroke={line} strokeLinecap="round" strokeWidth="4" - d="m445.8 362.5c3.7 0 6.6 3.2 6.6 6.9 0 3.6-2.9 6.5-6.6 6.5h-94.8c-3.6 0-6.6-2.9-6.6-6.5 0-3.7 3-6.9 6.6-6.9z" /> + <svg width={width} height={height} viewBox="0 0 320 320" xmlns="http://www.w3.org/2000/svg"> + <g fill="none" fillRule="evenodd"> + {/* face */} + <path + d="M71.689 53.055c9.23-1.487 25.684 27.263 41.411 56.663 18.572-8.017 71.708-7.717 93.775 0 4.714-15.612 31.96-57.405 41.626-56.663 3.992.088 13.07 31.705 23.309 94.96 2.743 16.949 7.537 47.492 14.38 91.63-42.339 17.834-84.37 26.751-126.095 26.751-41.724 0-83.756-8.917-126.095-26.751C52.973 116.244 65.536 54.047 71.689 53.055z" + stroke={stroke} + strokeWidth="4" + strokeLinecap="round" + fill={c0} + className={faceClasName} + /> + <circle fill={eye} cx="216.5" cy="181.5" r="14.5" /> + <circle fill={eye} cx="104.5" cy="181.5" r="14.5" /> + {/* mouth */} + <g stroke={mouth} strokeLinecap="round" strokeWidth="4"> + <path d="M175.568 218.694c-2.494 1.582-5.534 2.207-8.563 1.508-3.029-.7-5.487-2.594-7.035-5.11M143.981 218.694c2.494 1.582 5.534 2.207 8.563 1.508 3.03-.7 5.488-2.594 7.036-5.11" /> + </g> + </g> </svg> ); } diff --git a/src/components/ToggleSwitch.tsx b/src/components/ToggleSwitch.tsx index 9eb1019..58400c9 100644 --- a/src/components/ToggleSwitch.tsx +++ b/src/components/ToggleSwitch.tsx @@ -10,10 +10,7 @@ type Props = { }; function ToggleSwitch({ options, value, name, onChange }: Props) { - const idxSelected = useMemo( - () => options.map((o) => o.value).indexOf(value), - [options, value] - ); + const idxSelected = useMemo(() => options.map((o) => o.value).indexOf(value), [options, value]); const getPortionPercentage = useCallback( (idx: number) => { diff --git a/src/components/TrafficChart.tsx b/src/components/TrafficChart.tsx index 0d67ae0..588049d 100644 --- a/src/components/TrafficChart.tsx +++ b/src/components/TrafficChart.tsx @@ -1,21 +1,17 @@ import * as React from 'react'; import { useTranslation } from 'react-i18next'; -import { State } from '$src/store/types'; +import { State } from '$src/store/types'; import { fetchData } from '../api/traffic'; import useLineChart from '../hooks/useLineChart'; -import { - chartJSResource, - chartStyles, - commonDataSetProps, -} from '../misc/chart'; +import { chartJSResource, chartStyles, commonDataSetProps } from '../misc/chart'; import { getClashAPIConfig, getSelectedChartStyleIndex } from '../store/app'; import { connect } from './StateProvider'; const { useMemo } = React; -const chartWrapperStyle = { +const chartWrapperStyle: React.CSSProperties = { // make chartjs chart responsive position: 'relative', maxWidth: 1000, @@ -51,13 +47,12 @@ function TrafficChart({ apiConfig, selectedChartStyleIndex }) { }, ], }), - [ traffic, selectedChartStyleIndex, t] + [traffic, selectedChartStyleIndex, t] ); useLineChart(ChartMod.Chart, 'trafficChart', data, traffic); return ( - // @ts-expect-error ts-migrate(2322) FIXME: Type '{ position: string; maxWidth: number; }' is ... Remove this comment to see the full error message <div style={chartWrapperStyle}> <canvas id="trafficChart" /> </div> diff --git a/src/components/TrafficNow.module.scss b/src/components/TrafficNow.module.scss index 4d47ad2..d86b993 100644 --- a/src/components/TrafficNow.module.scss +++ b/src/components/TrafficNow.module.scss @@ -6,10 +6,12 @@ justify-content: space-between; max-width: 1000px; + display: grid; + grid-template-columns: repeat(auto-fit, 180px); + grid-gap: 10px; + .sec { padding: 10px; - width: 19%; - margin: 3px; background-color: var(--color-bg-card); border-radius: 10px; box-shadow: 0px 1px 5px rgba(0, 0, 0, 0.1); diff --git a/src/components/about/About.tsx b/src/components/about/About.tsx index 864e42c..f59ce20 100644 --- a/src/components/about/About.tsx +++ b/src/components/about/About.tsx @@ -11,15 +11,7 @@ import s from './About.module.scss'; type Props = { apiConfig: ClashAPIConfig }; -function Version({ - name, - link, - version, -}: { - name: string; - link: string; - version: string; -}) { +function Version({ name, link, version }: { name: string; link: string; version: string }) { return ( <div className={s.root}> <h2>{name}</h2> @@ -28,12 +20,7 @@ function Version({ <span className={s.mono}>{version}</span> </p> <p> - <a - className={s.link} - href={link} - target="_blank" - rel="noopener noreferrer" - > + <a className={s.link} href={link} target="_blank" rel="noopener noreferrer"> <GitHub size={20} /> <span>Source</span> </a> @@ -50,17 +37,9 @@ function AboutImpl(props: Props) { <> <ContentHeader title="About" /> {version && version.version ? ( - <Version - name={version.meta?'Clash.Meta':'Clash'} - version={version.version} - link="https://github.com/metacubex/clash.meta" - /> + <Version name="Clash" version={version.version} link="https://github.com/Dreamacro/clash" /> ) : null} - <Version - name="Yacd" - version={__VERSION__} - link="https://github.com/metacubex/yacd" - /> + <Version name="Yacd" version={__VERSION__} link="https://github.com/haishanh/yacd" /> </> ); } diff --git a/src/components/proxies/ClosePrevConns.tsx b/src/components/proxies/ClosePrevConns.tsx index 5617efe..f26a5e9 100644 --- a/src/components/proxies/ClosePrevConns.tsx +++ b/src/components/proxies/ClosePrevConns.tsx @@ -10,10 +10,7 @@ type Props = { onClickSecondaryButton?: () => void; }; -export function ClosePrevConns({ - onClickPrimaryButton, - onClickSecondaryButton, -}: Props) { +export function ClosePrevConns({ onClickPrimaryButton, onClickSecondaryButton }: Props) { const primaryButtonRef = useRef<HTMLButtonElement>(null); const secondaryButtonRef = useRef<HTMLButtonElement>(null); useEffect(() => { @@ -33,8 +30,8 @@ export function ClosePrevConns({ <div onKeyDown={handleKeyDown}> <h2>Close Connections?</h2> <p> - Click "Yes" to close those connections that are still using the old - selected proxy in this group + Click "Yes" to close those connections that are still using the old selected proxy in this + group </p> <div style={{ height: 30 }} /> <FlexCenter> diff --git a/src/components/proxies/Proxies.tsx b/src/components/proxies/Proxies.tsx index c1606dd..12d3cb2 100644 --- a/src/components/proxies/Proxies.tsx +++ b/src/components/proxies/Proxies.tsx @@ -35,9 +35,7 @@ function Proxies({ apiConfig, showModalClosePrevConns, }) { - const refFetchedTimestamp = useRef<{ startAt?: number; completeAt?: number }>( - {} - ); + const refFetchedTimestamp = useRef<{ startAt?: number; completeAt?: number }>({}); const fetchProxiesHooked = useCallback(() => { refFetchedTimestamp.current.startAt = Date.now(); @@ -75,10 +73,7 @@ function Proxies({ return ( <> - <BaseModal - isOpen={isSettingsModalOpen} - onRequestClose={closeSettingsModal} - > + <BaseModal isOpen={isSettingsModalOpen} onRequestClose={closeSettingsModal}> <Settings /> </BaseModal> <div className={s0.topBar}> @@ -110,15 +105,8 @@ function Proxies({ </div> <ProxyProviderList items={proxyProviders} /> <div style={{ height: 60 }} /> - <ProxyPageFab - dispatch={dispatch} - apiConfig={apiConfig} - proxyProviders={proxyProviders} - /> - <BaseModal - isOpen={showModalClosePrevConns} - onRequestClose={closeModalClosePrevConns} - > + <ProxyPageFab dispatch={dispatch} apiConfig={apiConfig} proxyProviders={proxyProviders} /> + <BaseModal isOpen={showModalClosePrevConns} onRequestClose={closeModalClosePrevConns}> <ClosePrevConns onClickPrimaryButton={() => closePrevConnsAndTheModal(apiConfig)} onClickSecondaryButton={closeModalClosePrevConns} diff --git a/src/components/proxies/Proxy.module.scss b/src/components/proxies/Proxy.module.scss index 4507a07..72087cf 100644 --- a/src/components/proxies/Proxy.module.scss +++ b/src/components/proxies/Proxy.module.scss @@ -15,7 +15,7 @@ border: 1px solid var(--color-focus-blue); } - max-width: 280px; + max-width: 200px; @media (--breakpoint-not-small) { min-width: 200px; border-radius: 10px; @@ -34,7 +34,7 @@ transition: transform 0.2s ease-in-out; cursor: pointer; &:hover { - transform: translateY(-2px); + border-color: hsl(0deg, 0%, var(--card-hover-border-lightness)); } } } @@ -58,19 +58,37 @@ width: 100%; margin-bottom: 5px; font-size: 0.85em; - @media (--breakpoint-not-small) { - font-size: 0.85em; - } + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .proxySmall { - width: 11px; - height: 11px; + --size: 13px; + width: var(--size); + height: var(--size); border-radius: 50%; - border: 1px solid var(--color-background); + position: relative; &.now { - border-color: var(--color-text-secondary); + --size: 15px; + &:before { + --size-dot: 7px; + content: ''; + position: absolute; + width: var(--size-dot); + height: var(--size-dot); + background-color: #fff; + // For non-primitive proxy type like "Selector", "LoadBalance", "DIRECT", etc. we are using a transparent + // background, and this selected indicator has a white background. In "light" them mode, the constrast + // between the bg of the indicator and the "background" is too small. In that case we want to add a + // border around this indicator so it's more distinguishable. + border: 1px solid var(--color-proxy-dot-selected-ind-bo); + border-radius: 4px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } } &.selectable { diff --git a/src/components/proxies/Proxy.tsx b/src/components/proxies/Proxy.tsx index 424d320..47a3d54 100644 --- a/src/components/proxies/Proxy.tsx +++ b/src/components/proxies/Proxy.tsx @@ -1,6 +1,8 @@ +import { TooltipPopup, useTooltip } from '@reach/tooltip'; import cx from 'clsx'; import * as React from 'react'; -import { keyCodes } from 'src/misc/keycode'; + +import { State } from '$src/store/types'; import { getDelay, getProxies, NonProxyTypes } from '../../store/proxies'; import { connect } from '../StateProvider'; @@ -20,11 +22,7 @@ const colorMap = { na: '#909399', }; -function getLabelColor({ - number, -}: { - number?: number; -} = {}) { +function getLabelColor({ number }: { number?: number } = {}) { if (number === 0) { return colorMap.na; } else if (number < 200) { @@ -37,39 +35,25 @@ function getLabelColor({ return colorMap.na; } -function getProxyDotBackgroundColor( - latency: { - number?: number; - }, - proxyType: string -) { +function getProxyDotStyle(latency: { number?: number }, proxyType: string) { if (NonProxyTypes.indexOf(proxyType) > -1) { - return 'linear-gradient(135deg, white 15%, #999 15% 30%, white 30% 45%, #999 45% 60%, white 60% 75%, #999 75% 90%, white 90% 100%)'; + return { border: '1px dotted #777' }; } - return getLabelColor(latency); + const bg = getLabelColor(latency); + return { background: bg }; } type ProxyProps = { name: string; now?: boolean; proxy: any; - latency: any; + latency?: { number?: number }; isSelectable?: boolean; onClick?: (proxyName: string) => unknown; }; -function ProxySmallImpl({ - now, - name, - proxy, - latency, - isSelectable, - onClick, -}: ProxyProps) { - const color = useMemo(() => getProxyDotBackgroundColor(latency, proxy.type), [ - latency, - proxy, - ]); +function ProxySmallImpl({ now, name, proxy, latency, isSelectable, onClick }: ProxyProps) { + const style = useMemo(() => getProxyDotStyle(latency, proxy.type), [latency, proxy]); const title = useMemo(() => { let ret = name; if (latency && typeof latency.number === 'number') { @@ -83,17 +67,12 @@ function ProxySmallImpl({ }, [name, onClick, isSelectable]); const className = useMemo(() => { - return cx(s0.proxySmall, { - [s0.now]: now, - [s0.selectable]: isSelectable, - }); + return cx(s0.proxySmall, { [s0.now]: now, [s0.selectable]: isSelectable }); }, [isSelectable, now]); const handleKeyDown = React.useCallback( (e: React.KeyboardEvent) => { - if (e.keyCode === keyCodes.Enter) { - doSelect(); - } + if (e.key === 'Enter') doSelect(); }, [doSelect] ); @@ -102,7 +81,7 @@ function ProxySmallImpl({ <div title={title} className={className} - style={{ background: color }} + style={style} onClick={doSelect} onKeyDown={handleKeyDown} role={isSelectable ? 'menuitem' : ''} @@ -115,33 +94,46 @@ function formatProxyType(t: string) { return t; } -function ProxyImpl({ - now, - name, - proxy, - latency, - isSelectable, - onClick, -}: ProxyProps) { +const positionProxyNameTooltip = (triggerRect: { left: number; top: number }) => { + return { + left: triggerRect.left + window.scrollX - 5, + top: triggerRect.top + window.scrollY - 38, + }; +}; + +function ProxyNameTooltip({ children, label, 'aria-label': ariaLabel }) { + const [trigger, tooltip] = useTooltip(); + return ( + <> + {React.cloneElement(children, trigger)} + <TooltipPopup + {...tooltip} + label={label} + aria-label={ariaLabel} + position={positionProxyNameTooltip} + /> + </> + ); +} + +function ProxyImpl({ now, name, proxy, latency, isSelectable, onClick }: ProxyProps) { const color = useMemo(() => getLabelColor(latency), [latency]); const doSelect = React.useCallback(() => { isSelectable && onClick && onClick(name); }, [name, onClick, isSelectable]); const handleKeyDown = React.useCallback( (e: React.KeyboardEvent) => { - if (e.keyCode === keyCodes.Enter) { - doSelect(); - } + if (e.key === 'Enter') doSelect(); }, [doSelect] ); const className = useMemo(() => { return cx(s0.proxy, { [s0.now]: now, - [s0.error]: latency && latency.error, + // [s0.error]: latency && latency.error, [s0.selectable]: isSelectable, }); - }, [isSelectable, now, latency]); + }, [isSelectable, now]); return ( <div @@ -151,26 +143,26 @@ function ProxyImpl({ onKeyDown={handleKeyDown} role={isSelectable ? 'menuitem' : ''} > - <div className={s0.proxyName}>{name}</div> + <div className={s0.proxyName}> + <ProxyNameTooltip label={name} aria-label={'proxy name: ' + name}> + <span>{name}</span> + </ProxyNameTooltip> + </div> <div className={s0.row}> <span className={s0.proxyType} style={{ opacity: now ? 0.6 : 0.2 }}> {formatProxyType(proxy.type)} </span> - {latency && latency.number ? ( - <ProxyLatency number={latency.number} color={color} /> - ) : null} + <ProxyLatency number={latency?.number} color={color} /> </div> </div> ); } -const mapState = (s: any, { name }) => { +const mapState = (s: State, { name }) => { const proxies = getProxies(s); const delay = getDelay(s); - return { - proxy: proxies[name], - latency: delay[name], - }; + const proxy = proxies[name] || { name, type: 'Unknown', history: [] }; + return { proxy, latency: delay[name] }; }; export const Proxy = connect(mapState)(ProxyImpl); diff --git a/src/components/proxies/ProxyGroup.module.scss b/src/components/proxies/ProxyGroup.module.scss index 5409ea8..85b68b6 100644 --- a/src/components/proxies/ProxyGroup.module.scss +++ b/src/components/proxies/ProxyGroup.module.scss @@ -2,10 +2,12 @@ margin-bottom: 12px; } -.zapWrapper { - width: 20px; - height: 20px; +.groupHead { display: flex; + flex-wrap: wrap; align-items: center; - justify-content: center; +} + +.action { + margin: 0 5px; } diff --git a/src/components/proxies/ProxyGroup.tsx b/src/components/proxies/ProxyGroup.tsx index 633a4b9..e8ebadf 100644 --- a/src/components/proxies/ProxyGroup.tsx +++ b/src/components/proxies/ProxyGroup.tsx @@ -1,30 +1,37 @@ +import Tooltip from '@reach/tooltip'; import * as React from 'react'; -import { Zap } from 'react-feather'; -import { useQuery } from 'react-query'; -import * as proxiesAPI from '$src/api/proxies'; -import { fetchVersion } from '$src/api/version'; -import { getCollapsibleIsOpen, getHideUnavailableProxies, getProxySortBy } from '$src/store/app'; -import { fetchProxies, getProxies, switchProxy } from '$src/store/proxies'; +import { useState2 } from '$src/hooks/basic'; +import { DelayMapping, DispatchFn, ProxiesMapping, State } from '$src/store/types'; +import { ClashAPIConfig } from '$src/types'; +import { getCollapsibleIsOpen, getHideUnavailableProxies, getProxySortBy } from '../../store/app'; +import { fetchProxies, getProxies, switchProxy } from '../../store/proxies'; import Button from '../Button'; import CollapsibleSectionHeader from '../CollapsibleSectionHeader'; +import { ZapAnimated } from '../shared/ZapAnimated'; import { connect, useStoreActions } from '../StateProvider'; import { useFilteredAndSorted } from './hooks'; import s0 from './ProxyGroup.module.scss'; import { ProxyList, ProxyListSummaryView } from './ProxyList'; +import { fetchVersion } from '$src/api/version'; +import { useQuery } from 'react-query'; +const { createElement, useCallback, useMemo } = React; -const { createElement, useCallback, useMemo, useState } = React; - - -function ZapWrapper() { - return ( - <div className={s0.zapWrapper}> - <Zap size={16} /> - </div> - ); -} +type ProxyGroupImplProps = { + name: string; + all: string[]; + delay: DelayMapping; + hideUnavailableProxies: boolean; + proxySortBy: string; + proxies: ProxiesMapping; + type: string; + now: string; + isOpen: boolean; + apiConfig: ClashAPIConfig; + dispatch: DispatchFn; +}; function ProxyGroupImpl({ name, @@ -38,14 +45,8 @@ function ProxyGroupImpl({ isOpen, apiConfig, dispatch, -}) { - const all = useFilteredAndSorted( - allItems, - delay, - hideUnavailableProxies, - proxySortBy, - proxies - ); +}: ProxyGroupImplProps) { + const all = useFilteredAndSorted(allItems, delay, hideUnavailableProxies, proxySortBy, proxies); const { data: version } = useQuery(['/version', apiConfig], () => fetchVersion('/version',apiConfig) @@ -63,15 +64,17 @@ function ProxyGroupImpl({ }, [isOpen, updateCollapsibleIsOpen, name]); const itemOnTapCallback = useCallback( - (proxyName) => { + (proxyName: string) => { if (!isSelectable) return; dispatch(switchProxy(apiConfig, name, proxyName)); }, [apiConfig, dispatch, name, isSelectable] ); - const [isTestingLatency, setIsTestingLatency] = useState(false); + + const testingLatency = useState2(false); const testLatency = useCallback(async () => { - setIsTestingLatency(true); + if (testingLatency.value) return; + testingLatency.set(true); try { if (version.meta==true){ await proxiesAPI.requestDelayForProxyGroup(apiConfig,name); @@ -87,7 +90,7 @@ function ProxyGroupImpl({ return ( <div className={s0.group}> - <div style={{ display: 'flex', alignItems: 'center' }}> + <div className={s0.groupHead}> <CollapsibleSectionHeader name={name} type={type} @@ -95,14 +98,13 @@ function ProxyGroupImpl({ qty={all.length} isOpen={isOpen} /> - <Button - title="Test latency" - kind="minimal" - onClick={testLatency} - isLoading={isTestingLatency} - > - <ZapWrapper /> - </Button> + <div className={s0.action}> + <Tooltip label={'Test latency'}> + <Button kind="circular" onClick={testLatency}> + <ZapAnimated animate={testingLatency.value} size={16} /> + </Button> + </Tooltip> + </div> </div> {createElement(isOpen ? ProxyList : ProxyListSummaryView, { all, @@ -114,7 +116,7 @@ function ProxyGroupImpl({ ); } -export const ProxyGroup = connect((s, { name, delay }) => { +export const ProxyGroup = connect((s: State, { name, delay }) => { const proxies = getProxies(s); const collapsibleIsOpen = getCollapsibleIsOpen(s); const proxySortBy = getProxySortBy(s); @@ -133,3 +135,7 @@ export const ProxyGroup = connect((s, { name, delay }) => { isOpen: collapsibleIsOpen[`proxyGroup:${name}`], }; })(ProxyGroupImpl); +function setIsTestingLatency (arg0: boolean) { + throw new Error('Function not implemented.'); +} + diff --git a/src/components/proxies/ProxyLatency.tsx b/src/components/proxies/ProxyLatency.tsx index 48e55af..29036d5 100644 --- a/src/components/proxies/ProxyLatency.tsx +++ b/src/components/proxies/ProxyLatency.tsx @@ -3,14 +3,14 @@ import * as React from 'react'; import s0 from './ProxyLatency.module.scss'; type ProxyLatencyProps = { - number: number; + number?: number; color: string; }; export function ProxyLatency({ number, color }: ProxyLatencyProps) { return ( <span className={s0.proxyLatency} style={{ color }}> - <span>{number} ms</span> + {typeof number === 'number' && number !== 0 ? number + ' ms' : ' '} </span> ); } diff --git a/src/components/proxies/ProxyList.module.scss b/src/components/proxies/ProxyList.module.scss index 1814929..12fea7e 100644 --- a/src/components/proxies/ProxyList.module.scss +++ b/src/components/proxies/ProxyList.module.scss @@ -6,8 +6,10 @@ } .listSummaryView { - margin: 8px 0; + margin: 14px 0; display: grid; grid-template-columns: repeat(auto-fill, 13px); grid-gap: 10px; + place-items: center; + max-width: 900px; } diff --git a/src/components/proxies/ProxyList.tsx b/src/components/proxies/ProxyList.tsx index a86bb88..3856c68 100644 --- a/src/components/proxies/ProxyList.tsx +++ b/src/components/proxies/ProxyList.tsx @@ -11,12 +11,7 @@ type ProxyListProps = { show?: boolean; }; -export function ProxyList({ - all, - now, - isSelectable, - itemOnTapCallback, -}: ProxyListProps) { +export function ProxyList({ all, now, isSelectable, itemOnTapCallback }: ProxyListProps) { const proxies = all; return ( diff --git a/src/components/proxies/ProxyPageFab.tsx b/src/components/proxies/ProxyPageFab.tsx index 7cc6d03..44e446f 100644 --- a/src/components/proxies/ProxyPageFab.tsx +++ b/src/components/proxies/ProxyPageFab.tsx @@ -2,12 +2,7 @@ import * as React from 'react'; import { Zap } from 'react-feather'; import { useTranslation } from 'react-i18next'; import { useUpdateProviderItems } from 'src/components/proxies/proxies.hooks'; -import { - Action, - Fab, - IsFetching, - position as fabPosition, -} from 'src/components/shared/Fab'; +import { Action, Fab, IsFetching, position as fabPosition } from 'src/components/shared/Fab'; import { RotateIcon } from 'src/components/shared/RotateIcon'; import { requestDelayAll } from 'src/store/proxies'; import { DispatchFn, FormattedProxyProvider } from 'src/store/types'; diff --git a/src/components/proxies/ProxyProvider.module.scss b/src/components/proxies/ProxyProvider.module.scss index 534305b..bc66bca 100644 --- a/src/components/proxies/ProxyProvider.module.scss +++ b/src/components/proxies/ProxyProvider.module.scss @@ -5,21 +5,25 @@ } } -.body { +.main { padding: 10px 15px; @media (--breakpoint-not-small) { padding: 10px 40px; } } -.actionFooter { +.head { display: flex; - button { - margin: 0 5px; - &:first-child { - margin-left: 0; - } - } + align-items: center; + flex-wrap: wrap; +} + +.action { + margin: 0 5px; + display: grid; + grid-template-columns: auto auto; + gap: 10px; + place-items: center; } .refresh { diff --git a/src/components/proxies/ProxyProvider.tsx b/src/components/proxies/ProxyProvider.tsx index 055a572..7939190 100644 --- a/src/components/proxies/ProxyProvider.tsx +++ b/src/components/proxies/ProxyProvider.tsx @@ -1,8 +1,8 @@ +import Tooltip from '@reach/tooltip'; import { formatDistance } from 'date-fns'; import * as React from 'react'; -import { RotateCw, Zap } from 'react-feather'; +import { RotateCw } from 'react-feather'; import Button from 'src/components/Button'; -import Collapsible from 'src/components/Collapsible'; import CollapsibleSectionHeader from 'src/components/CollapsibleSectionHeader'; import { useUpdateProviderItem } from 'src/components/proxies/proxies.hooks'; import { connect, useStoreActions } from 'src/components/StateProvider'; @@ -14,17 +14,20 @@ import { getProxySortBy, } from 'src/store/app'; import { getDelay, healthcheckProviderByName } from 'src/store/proxies'; -import { DelayMapping } from 'src/store/types'; +import { DelayMapping, State } from 'src/store/types'; +import { useState2 } from '$src/hooks/basic'; + +import { ZapAnimated } from '../shared/ZapAnimated'; import { useFilteredAndSorted } from './hooks'; import { ProxyList, ProxyListSummaryView } from './ProxyList'; import s from './ProxyProvider.module.scss'; -const { useState, useCallback } = React; +const { useCallback } = React; type Props = { name: string; - proxies: Array<string>; + proxies: string[]; delay: DelayMapping; hideUnavailableProxies: boolean; proxySortBy: string; @@ -48,21 +51,17 @@ function ProxyProviderImpl({ dispatch, apiConfig, }: Props) { - const proxies = useFilteredAndSorted( - all, - delay, - hideUnavailableProxies, - proxySortBy - ); - const [isHealthcheckLoading, setIsHealthcheckLoading] = useState(false); + const proxies = useFilteredAndSorted(all, delay, hideUnavailableProxies, proxySortBy); + const checkingHealth = useState2(false); const updateProvider = useUpdateProviderItem({ dispatch, apiConfig, name }); - const healthcheckProvider = useCallback(async () => { - setIsHealthcheckLoading(true); - await dispatch(healthcheckProviderByName(apiConfig, name)); - setIsHealthcheckLoading(false); - }, [apiConfig, dispatch, name, setIsHealthcheckLoading]); + const healthcheckProvider = useCallback(() => { + if (checkingHealth.value) return; + checkingHealth.set(true); + const stop = () => checkingHealth.set(false); + dispatch(healthcheckProviderByName(apiConfig, name)).then(stop, stop); + }, [apiConfig, dispatch, name, checkingHealth]); const { app: { updateCollapsibleIsOpen }, @@ -74,34 +73,33 @@ function ProxyProviderImpl({ const timeAgo = formatDistance(new Date(updatedAt), new Date()); return ( - <div className={s.body}> - <CollapsibleSectionHeader - name={name} - toggle={toggle} - type={vehicleType} - isOpen={isOpen} - qty={proxies.length} - /> + <div className={s.main}> + <div className={s.head}> + <CollapsibleSectionHeader + name={name} + toggle={toggle} + type={vehicleType} + isOpen={isOpen} + qty={proxies.length} + /> + + <div className={s.action}> + <Tooltip label={'Update'}> + <Button kind="circular" onClick={updateProvider}> + <Refresh /> + </Button> + </Tooltip> + <Tooltip label={'Health Check'}> + <Button kind="circular" onClick={healthcheckProvider}> + <ZapAnimated animate={checkingHealth.value} size={16} /> + </Button> + </Tooltip> + </div> + </div> <div className={s.updatedAt}> <small>Updated {timeAgo} ago</small> </div> - {/* @ts-expect-error ts-migrate(2322) FIXME: Type '{ children: Element[]; isOpen: boolean; }' i... Remove this comment to see the full error message */} - <Collapsible isOpen={isOpen}> - <ProxyList all={proxies} /> - <div className={s.actionFooter}> - <Button text="Update" start={<Refresh />} onClick={updateProvider} /> - <Button - text="Health Check" - start={<Zap size={16} />} - onClick={healthcheckProvider} - isLoading={isHealthcheckLoading} - /> - </div> - </Collapsible> - {/* @ts-expect-error ts-migrate(2322) FIXME: Type '{ children: Element; isOpen: boolean; }' is ... Remove this comment to see the full error message */} - <Collapsible isOpen={!isOpen}> - <ProxyListSummaryView all={proxies} /> - </Collapsible> + {isOpen ? <ProxyList all={proxies} /> : <ProxyListSummaryView all={proxies} />} </div> ); } @@ -132,7 +130,7 @@ function Refresh() { ); } -const mapState = (s, { proxies, name }) => { +const mapState = (s: State, { proxies, name }) => { const hideUnavailableProxies = getHideUnavailableProxies(s); const delay = getDelay(s); const collapsibleIsOpen = getCollapsibleIsOpen(s); diff --git a/src/components/proxies/ProxyProviderList.tsx b/src/components/proxies/ProxyProviderList.tsx index 1528f37..754eeac 100644 --- a/src/components/proxies/ProxyProviderList.tsx +++ b/src/components/proxies/ProxyProviderList.tsx @@ -3,11 +3,7 @@ import ContentHeader from 'src/components/ContentHeader'; import { ProxyProvider } from 'src/components/proxies/ProxyProvider'; import { FormattedProxyProvider } from 'src/store/types'; -export function ProxyProviderList({ - items, -}: { - items: FormattedProxyProvider[]; -}) { +export function ProxyProviderList({ items }: { items: FormattedProxyProvider[] }) { if (items.length === 0) return null; return ( diff --git a/src/components/proxies/Settings.tsx b/src/components/proxies/Settings.tsx index 5e1ff98..55e18fe 100644 --- a/src/components/proxies/Settings.tsx +++ b/src/components/proxies/Settings.tsx @@ -2,11 +2,7 @@ import * as React from 'react'; import { useTranslation } from 'react-i18next'; import Select from 'src/components/shared/Select'; -import { - getAutoCloseOldConns, - getHideUnavailableProxies, - getProxySortBy, -} from '../../store/app'; +import { getAutoCloseOldConns, getHideUnavailableProxies, getProxySortBy } from '../../store/app'; import { connect, useStoreActions } from '../StateProvider'; import Switch from '../SwitchThemed'; import s from './Settings.module.scss'; diff --git a/src/components/proxies/hooks.tsx b/src/components/proxies/hooks.tsx index 861c0e5..a43dc0e 100644 --- a/src/components/proxies/hooks.tsx +++ b/src/components/proxies/hooks.tsx @@ -45,22 +45,14 @@ const getSortDelay = ( const ProxySortingFns = { Natural: (proxies: string[]) => proxies, - LatencyAsc: ( - proxies: string[], - delay: DelayMapping, - proxyMapping?: ProxiesMapping - ) => { + LatencyAsc: (proxies: string[], delay: DelayMapping, proxyMapping?: ProxiesMapping) => { return proxies.sort((a, b) => { const d1 = getSortDelay(delay[a], proxyMapping && proxyMapping[a]); const d2 = getSortDelay(delay[b], proxyMapping && proxyMapping[b]); return d1 - d2; }); }, - LatencyDesc: ( - proxies: string[], - delay: DelayMapping, - proxyMapping?: ProxiesMapping - ) => { + LatencyDesc: (proxies: string[], delay: DelayMapping, proxyMapping?: ProxiesMapping) => { return proxies.sort((a, b) => { const d1 = getSortDelay(delay[a], proxyMapping && proxyMapping[a]); const d2 = getSortDelay(delay[b], proxyMapping && proxyMapping[b]); diff --git a/src/components/proxies/proxies.hooks.tsx b/src/components/proxies/proxies.hooks.tsx index ec51c9b..20695ac 100644 --- a/src/components/proxies/proxies.hooks.tsx +++ b/src/components/proxies/proxies.hooks.tsx @@ -14,11 +14,10 @@ export function useUpdateProviderItem({ apiConfig: ClashAPIConfig; name: string; }) { - return useCallback(() => dispatch(updateProviderByName(apiConfig, name)), [ - apiConfig, - dispatch, - name, - ]); + return useCallback( + () => dispatch(updateProviderByName(apiConfig, name)), + [apiConfig, dispatch, name] + ); } export function useUpdateProviderItems({ diff --git a/src/components/rules/RuleProviderItem.module.scss b/src/components/rules/RuleProviderItem.module.scss index 532ec8a..c3e1f07 100644 --- a/src/components/rules/RuleProviderItem.module.scss +++ b/src/components/rules/RuleProviderItem.module.scss @@ -13,6 +13,7 @@ .middle { display: grid; + gap: 6px; grid-template-rows: 1fr auto auto; align-items: center; } @@ -21,13 +22,13 @@ color: #777; } -.refreshButtonWrapper { +.action { display: grid; - place-items: center; - opacity: 0; - transition: opacity 0.2s; + gap: 4px; + grid-template-columns: auto 1fr; + align-items: center; } -.RuleProviderItem:hover .refreshButtonWrapper { - opacity: 1; +.refreshBtn { + padding: 5px; } diff --git a/src/components/rules/RuleProviderItem.tsx b/src/components/rules/RuleProviderItem.tsx index fe4610e..c27e464 100644 --- a/src/components/rules/RuleProviderItem.tsx +++ b/src/components/rules/RuleProviderItem.tsx @@ -16,26 +16,22 @@ export function RuleProviderItem({ ruleCount, apiConfig, }) { - const [onClickRefreshButton, isRefreshing] = useUpdateRuleProviderItem( - name, - apiConfig - ); + const [onClickRefreshButton, isRefreshing] = useUpdateRuleProviderItem(name, apiConfig); const timeAgo = formatDistance(new Date(updatedAt), new Date()); return ( <div className={s.RuleProviderItem}> <span className={s.left}>{idx}</span> <div className={s.middle}> <SectionNameType name={name} type={`${vehicleType} / ${behavior}`} /> - <div className={s.gray}> - {ruleCount < 2 ? `${ruleCount} rule` : `${ruleCount} rules`} + <div className={s.gray}>{ruleCount < 2 ? `${ruleCount} rule` : `${ruleCount} rules`}</div> + <div className={s.action}> + <Button onClick={onClickRefreshButton} disabled={isRefreshing} className={s.refreshBtn}> + <RotateIcon isRotating={isRefreshing} size={13} /> + <span className="visually-hidden">Refresh</span> + </Button> + <small className={s.gray}>Updated {timeAgo} ago</small> </div> - <small className={s.gray}>Updated {timeAgo} ago</small> </div> - <span className={s.refreshButtonWrapper}> - <Button onClick={onClickRefreshButton} disabled={isRefreshing}> - <RotateIcon isRotating={isRefreshing} /> - </Button> - </span> </div> ); } diff --git a/src/components/shared/Fab.tsx b/src/components/shared/Fab.tsx index 832306e..8e72432 100644 --- a/src/components/shared/Fab.tsx +++ b/src/components/shared/Fab.tsx @@ -28,8 +28,7 @@ const AB: React.FC<ABProps> = ({ children, ...p }) => ( </button> ); -interface MBProps - extends Omit<React.HTMLAttributes<HTMLButtonElement>, 'tabIndex'> { +interface MBProps extends Omit<React.HTMLAttributes<HTMLButtonElement>, 'tabIndex'> { tabIndex?: number; } @@ -77,10 +76,7 @@ const Fab: React.FC<FabProps> = ({ return event === 'click' ? (isOpen ? close() : open()) : null; }; - const actionOnClick = ( - e: React.FormEvent, - userFunc: (e: React.FormEvent) => void - ) => { + const actionOnClick = (e: React.FormEvent, userFunc: (e: React.FormEvent) => void) => { e.persist(); setIsOpen(false); setTimeout(() => { @@ -141,9 +137,7 @@ const Fab: React.FC<FabProps> = ({ </MB> {text && ( <span - className={`${'right' in style ? 'right' : ''} ${ - alwaysShowTitle ? 'always-show' : '' - }`} + className={`${'right' in style ? 'right' : ''} ${alwaysShowTitle ? 'always-show' : ''}`} aria-hidden={ariaHidden} > {text} diff --git a/src/components/shared/RotateIcon.tsx b/src/components/shared/RotateIcon.tsx index 7e3ceae..d291ece 100644 --- a/src/components/shared/RotateIcon.tsx +++ b/src/components/shared/RotateIcon.tsx @@ -4,13 +4,12 @@ import { RotateCw } from 'react-feather'; import s from './RotateIcon.module.scss'; -export function RotateIcon({ isRotating }: { isRotating: boolean }) { - const cls = cx(s.rotate, { - [s.isRotating]: isRotating, - }); +export function RotateIcon(props: { isRotating: boolean; size?: number }) { + const size = props.size || 16; + const cls = cx(s.rotate, { [s.isRotating]: props.isRotating }); return ( <span className={cls}> - <RotateCw width={16} /> + <RotateCw size={size} /> </span> ); } diff --git a/src/components/shared/TextFitler.tsx b/src/components/shared/TextFitler.tsx index e4a4a88..7af61ac 100644 --- a/src/components/shared/TextFitler.tsx +++ b/src/components/shared/TextFitler.tsx @@ -4,10 +4,7 @@ import { useTextInut } from 'src/hooks/useTextInput'; import s from './TextFitler.module.scss'; -export function TextFilter(props: { - textAtom: RecoilState<string>; - placeholder?: string; -}) { +export function TextFilter(props: { textAtom: RecoilState<string>; placeholder?: string }) { const [onChange, text] = useTextInut(props.textAtom); return ( <input diff --git a/src/components/shared/ThemeSwitcher.module.scss b/src/components/shared/ThemeSwitcher.module.scss index c5de126..951376a 100644 --- a/src/components/shared/ThemeSwitcher.module.scss +++ b/src/components/shared/ThemeSwitcher.module.scss @@ -29,7 +29,8 @@ height: var(--sz); select { cursor: pointer; - padding-left: var(--sz); + padding-left: calc(var(--sz) - 2px); + font-size: 0; width: var(--sz); height: var(--sz); appearance: none; diff --git a/src/components/shared/ZapAnimated.module.scss b/src/components/shared/ZapAnimated.module.scss new file mode 100644 index 0000000..e4cb37b --- /dev/null +++ b/src/components/shared/ZapAnimated.module.scss @@ -0,0 +1,12 @@ +.animate { + --saturation: 70%; + stroke: hsl(46deg var(--saturation) 45%); + animation: zap-pulse 0.7s 0s ease-in-out none normal infinite; +} + +// prettier-ignore +@keyframes zap-pulse { + 0% { stroke: hsl(46deg var(--saturation) 45%); } + 50% { stroke: hsl(46deg var(--saturation) 95%); } + 100% { stroke: hsl(46deg var(--saturation) 45%); } +} diff --git a/src/components/shared/ZapAnimated.tsx b/src/components/shared/ZapAnimated.tsx new file mode 100644 index 0000000..e3b153a --- /dev/null +++ b/src/components/shared/ZapAnimated.tsx @@ -0,0 +1,25 @@ +import cx from 'clsx'; +import * as React from 'react'; + +import s from './ZapAnimated.module.scss'; + +export function ZapAnimated(props: { size?: number; animate?: boolean }) { + const size = props.size || 24; + const cls = cx({ [s.animate]: props.animate }); + return ( + <svg + className={cls} + xmlns="http://www.w3.org/2000/svg" + width={size} + height={size} + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" + > + <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" /> + </svg> + ); +} diff --git a/src/components/svg/Equalizer.tsx b/src/components/svg/Equalizer.tsx index 274720f..ae3c858 100644 --- a/src/components/svg/Equalizer.tsx +++ b/src/components/svg/Equalizer.tsx @@ -5,10 +5,7 @@ type Props = { color?: string; }; -export default function Equalizer({ - color = 'currentColor', - size = 24, -}: Props) { +export default function Equalizer({ color = 'currentColor', size = 24 }: Props) { return ( <svg fill="none" diff --git a/src/hooks/basic.ts b/src/hooks/basic.ts index 587d92d..1e8aeae 100644 --- a/src/hooks/basic.ts +++ b/src/hooks/basic.ts @@ -1,9 +1,14 @@ -import React from 'react'; +import * as React from 'react'; const { useState, useCallback } = React; -export function useToggle(initialValue = false) { +export function useToggle(initialValue = false): [boolean, () => void] { const [isOn, setState] = useState(initialValue); const toggle = useCallback(() => setState((x) => !x), []); return [isOn, toggle]; } + +export function useState2<T>(v: T) { + const [value, set] = useState(v); + return { value, set }; +} diff --git a/src/hooks/useRemainingViewPortHeight.ts b/src/hooks/useRemainingViewPortHeight.ts index 9f3470d..2c920c2 100644 --- a/src/hooks/useRemainingViewPortHeight.ts +++ b/src/hooks/useRemainingViewPortHeight.ts @@ -9,9 +9,10 @@ const { useState, useRef, useCallback, useLayoutEffect } = React; * to the bottom of the view port * */ -export default function useRemainingViewPortHeight< - ElementType extends HTMLDivElement ->(): [React.MutableRefObject<ElementType>, number] { +export default function useRemainingViewPortHeight<ElementType extends HTMLDivElement>(): [ + React.MutableRefObject<ElementType>, + number +] { const ref = useRef<ElementType>(null); const [containerHeight, setContainerHeight] = useState(200); const updateContainerHeight = useCallback(() => { diff --git a/src/hooks/useTextInput.ts b/src/hooks/useTextInput.ts index 1fa19f7..853044c 100644 --- a/src/hooks/useTextInput.ts +++ b/src/hooks/useTextInput.ts @@ -9,9 +9,7 @@ export function useTextInut( ): [(e: React.ChangeEvent<HTMLInputElement>) => void, string] { const [, setTextGlobal] = useRecoilState(x); const [text, setText] = useState(''); - const setTextDebounced = useMemo(() => debounce(setTextGlobal, 300), [ - setTextGlobal, - ]); + const setTextDebounced = useMemo(() => debounce(setTextGlobal, 300), [setTextGlobal]); const onChange = useCallback( (e: React.ChangeEvent<HTMLInputElement>) => { setText(e.target.value); diff --git a/src/misc/chart.ts b/src/misc/chart.ts index c62fa06..56e05a2 100644 --- a/src/misc/chart.ts +++ b/src/misc/chart.ts @@ -1,4 +1,4 @@ -import { createAsset } from "use-asset" +import { createAsset } from 'use-asset'; import prettyBytes from './pretty-bytes'; export const chartJSResource = createAsset(() => { @@ -11,7 +11,7 @@ export const commonChartOptions: import('chart.js').ChartOptions<'line'> = { responsive: true, maintainAspectRatio: true, plugins: { - legend: { labels: { boxWidth: 20 } } + legend: { labels: { boxWidth: 20 } }, }, scales: { x: { display: false, type: 'category' }, diff --git a/src/misc/errors.ts b/src/misc/errors.ts index 1bc369a..ec12b6a 100644 --- a/src/misc/errors.ts +++ b/src/misc/errors.ts @@ -1,5 +1,12 @@ export const DOES_NOT_SUPPORT_FETCH = 0; +export class YacdError extends Error { + constructor(public message: string, public code?: string | number) { + super(message); + Error.captureStackTrace(this, this.constructor); + } +} + export const errors = { [DOES_NOT_SUPPORT_FETCH]: { message: 'Browser not supported!', diff --git a/src/misc/i18n.ts b/src/misc/i18n.ts index cf1a4f2..eecd72a 100644 --- a/src/misc/i18n.ts +++ b/src/misc/i18n.ts @@ -8,10 +8,7 @@ const allLocales = { en: import('src/i18n/en'), }; -type BackendRequestCallback = ( - err: null, - result: { status: number; data: any } -) => void; +type BackendRequestCallback = (err: null, result: { status: number; data: any }) => void; i18next .use(HttpBackend) diff --git a/src/misc/motion.ts b/src/misc/motion.ts index 5f2fe50..7fac864 100644 --- a/src/misc/motion.ts +++ b/src/misc/motion.ts @@ -1,5 +1,3 @@ import { createResource } from './createResource'; -export const framerMotionResouce = createResource( - () => import('framer-motion') -); +export const framerMotionResouce = createResource(() => import('framer-motion')); diff --git a/src/misc/request-helper.ts b/src/misc/request-helper.ts index 3bc8476..c01b994 100644 --- a/src/misc/request-helper.ts +++ b/src/misc/request-helper.ts @@ -12,7 +12,7 @@ function genCommonHeaders({ secret }: { secret?: string }) { return h; } function buildWebSocketURLBase(baseURL: string, params: URLSearchParams, endpoint: string) { - const qs = '?' + params.toString() + const qs = '?' + params.toString(); const url = new URL(baseURL); url.protocol === 'https:' ? (url.protocol = 'wss:') : (url.protocol = 'ws:'); return `${trimTrailingSlash(url.href)}${endpoint}${qs}`; @@ -32,7 +32,7 @@ export function buildWebSocketURL(apiConfig: ClashAPIConfig, endpoint: string) { token: secret, }); - return buildWebSocketURLBase(baseURL, params, endpoint) + return buildWebSocketURLBase(baseURL, params, endpoint); } export function buildLogsWebSocketURL(apiConfig: LogsAPIConfig, endpoint: string) { @@ -42,5 +42,5 @@ export function buildLogsWebSocketURL(apiConfig: LogsAPIConfig, endpoint: string level: logLevel, }); - return buildWebSocketURLBase(baseURL, params, endpoint) + return buildWebSocketURLBase(baseURL, params, endpoint); } diff --git a/src/misc/shallowEqual.ts b/src/misc/shallowEqual.ts index 241b725..937bc27 100644 --- a/src/misc/shallowEqual.ts +++ b/src/misc/shallowEqual.ts @@ -12,12 +12,7 @@ function is(x, y) { export default function shallowEqual(objA, objB) { if (is(objA, objB)) return true; - if ( - typeof objA !== 'object' || - objA === null || - typeof objB !== 'object' || - objB === null - ) { + if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) { return false; } diff --git a/src/misc/utils.ts b/src/misc/utils.ts index d3a7bfc..9497026 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -1,7 +1,4 @@ -export function throttle<T extends any[]>( - fn: (...args: T) => unknown, - timeout: number -) { +export function throttle<T extends any[]>(fn: (...args: T) => unknown, timeout: number) { let pending = false; return (...args: T) => { @@ -15,10 +12,7 @@ export function throttle<T extends any[]>( }; } -export function debounce<T extends any[]>( - fn: (...args: T) => unknown, - timeout: number -) { +export function debounce<T extends any[]>(fn: (...args: T) => unknown, timeout: number) { let timeoutId: ReturnType<typeof setTimeout>; return (...args: T) => { if (timeoutId) clearTimeout(timeoutId); diff --git a/src/store/app.ts b/src/store/app.ts index 7262b32..2789981 100644 --- a/src/store/app.ts +++ b/src/store/app.ts @@ -152,7 +152,7 @@ export function updateCollapsibleIsOpen(prefix: string, name: string, v: boolean const defaultClashAPIConfig = { baseURL: document.getElementById('app')?.getAttribute('data-base-url') ?? 'http://127.0.0.1:9090', secret: '', - addedAt: 0 + addedAt: 0, }; // type Theme = 'light' | 'dark'; const defaultState: StateApp = { @@ -169,7 +169,7 @@ const defaultState: StateApp = { proxySortBy: 'Natural', hideUnavailableProxies: false, autoCloseOldConns: false, - logStreamingPaused: false + logStreamingPaused: false, }; function parseConfigQueryString() { diff --git a/src/store/proxies.tsx b/src/store/proxies.tsx index e06d16c..5f4c0aa 100644 --- a/src/store/proxies.tsx +++ b/src/store/proxies.tsx @@ -1,9 +1,10 @@ import { atom } from 'recoil'; -/* import { ProxyItem, ProxiesMapping, DelayMapping } from 'src/store/types'; */ import { + DelayMapping, DispatchFn, FormattedProxyProvider, GetStateFn, + LatencyHistory, ProxiesMapping, ProxyItem, ProxyProvider, @@ -35,10 +36,10 @@ const noop = () => null; export const NonProxyTypes = [ 'Direct', - 'Fallback', 'Reject', - 'Pass', + 'Relay', 'Selector', + 'Fallback', 'URLTest', 'LoadBalance', 'Unknown', @@ -49,8 +50,20 @@ export const getDelay = (s: State) => s.proxies.delay; export const getProxyGroupNames = (s: State) => s.proxies.groupNames; export const getProxyProviders = (s: State) => s.proxies.proxyProviders || []; export const getDangleProxyNames = (s: State) => s.proxies.dangleProxyNames; -export const getShowModalClosePrevConns = (s: State) => - s.proxies.showModalClosePrevConns; +export const getShowModalClosePrevConns = (s: State) => s.proxies.showModalClosePrevConns; + +function mapLatency(names: string[], getProxy: (name: string) => { history: LatencyHistory }) { + const result: DelayMapping = {}; + for (const name of names) { + const p = getProxy(name) || { history: [] }; + const history = p.history; + const h = history[history.length - 1]; + if (h && typeof h.delay === 'number') { + result[name] = { number: h.delay }; + } + } + return result; +} export function fetchProxies(apiConfig: ClashAPIConfig) { return async (dispatch: any, getState: any) => { @@ -59,37 +72,28 @@ export function fetchProxies(apiConfig: ClashAPIConfig) { proxiesAPI.fetchProviderProxies(apiConfig), ]); - const { - providers: proxyProviders, - proxies: providerProxies, - } = formatProxyProviders(providersData.providers); - const proxies = { ...providerProxies, ...proxiesData.proxies }; - const [groupNames, proxyNames] = retrieveGroupNamesFrom(proxies); + const { proxyProviders, providerProxyRecord } = formatProxyProviders(providersData.providers); - const delayPrev = getDelay(getState()); - const delayNext = { ...delayPrev }; + const proxies = { ...providerProxyRecord, ...proxiesData.proxies }; + const [groupNames, proxyNames] = retrieveGroupNamesFrom(proxies); - for (let i = 0; i < proxyNames.length; i++) { - const name = proxyNames[i]; - const { history } = proxies[name] || { history: [] }; - const h = history[history.length - 1]; - if (h && typeof h.delay === 'number') { - delayNext[name] = { number: h.delay }; - } - } + const delayNext = { + ...getDelay(getState()), + ...mapLatency(Object.keys(proxies), (name) => proxies[name]), + }; // proxies that are not from a provider const dangleProxyNames = []; for (const v of proxyNames) { - if (!providerProxies[v]) dangleProxyNames.push(v); + if (!providerProxyRecord[v]) dangleProxyNames.push(v); } dispatch('store/proxies#fetchProxies', (s: State) => { s.proxies.proxies = proxies; s.proxies.groupNames = groupNames; + s.proxies.dangleProxyNames = dangleProxyNames; s.proxies.delay = delayNext; s.proxies.proxyProviders = proxyProviders; - s.proxies.dangleProxyNames = dangleProxyNames; }); }; } @@ -122,10 +126,7 @@ export function updateProviders(apiConfig: ClashAPIConfig, names: string[]) { }; } -async function healthcheckProviderByNameInternal( - apiConfig: ClashAPIConfig, - name: string -) { +async function healthcheckProviderByNameInternal(apiConfig: ClashAPIConfig, name: string) { try { await proxiesAPI.healthcheckProviderByName(apiConfig, name); } catch (x) { @@ -133,10 +134,7 @@ async function healthcheckProviderByNameInternal( } } -export function healthcheckProviderByName( - apiConfig: ClashAPIConfig, - name: string -) { +export function healthcheckProviderByName(apiConfig: ClashAPIConfig, name: string) { return async (dispatch: DispatchFn) => { await healthcheckProviderByNameInternal(apiConfig, name); // should be optimized @@ -169,16 +167,10 @@ async function closeGroupConns( } } - await Promise.all( - idsToClose.map((id) => connAPI.closeConnById(apiConfig, id).catch(noop)) - ); + await Promise.all(idsToClose.map((id) => connAPI.closeConnById(apiConfig, id).catch(noop))); } -function resolveChain( - proxies: ProxiesMapping, - groupName: string, - itemName: string -) { +function resolveChain(proxies: ProxiesMapping, groupName: string, itemName: string) { const chain = [itemName, groupName]; let child: ProxyItem; @@ -198,11 +190,7 @@ async function switchProxyImpl( itemName: string ) { try { - const res = await proxiesAPI.requestToSwitchProxy( - apiConfig, - groupName, - itemName - ); + const res = await proxiesAPI.requestToSwitchProxy(apiConfig, groupName, itemName); if (res.ok === false) { throw new Error(`failed to switch proxy: res.statusText`); } @@ -220,11 +208,6 @@ async function switchProxyImpl( // no wait closePrevConns(apiConfig, proxies, { groupName, itemName }); } - - /* dispatch('showModalClosePrevConns', (s: GlobalState) => { */ - /* s.proxies.showModalClosePrevConns = true; */ - /* s.proxies.switchProxyCtx = { to: { groupName, itemName } }; */ - /* }); */ } function closeModalClosePrevConns() { @@ -268,16 +251,10 @@ function closePrevConnsAndTheModal(apiConfig: ClashAPIConfig) { }; } -export function switchProxy( - apiConfig: ClashAPIConfig, - groupName: string, - itemName: string -) { +export function switchProxy(apiConfig: ClashAPIConfig, groupName: string, itemName: string) { return async (dispatch: DispatchFn, getState: GetStateFn) => { // switch proxy asynchronously - switchProxyImpl(dispatch, getState, apiConfig, groupName, itemName).catch( - noop - ); + switchProxyImpl(dispatch, getState, apiConfig, groupName, itemName).catch(noop); // optimistic UI update dispatch('store/proxies#switchProxy', (s) => { @@ -292,25 +269,13 @@ export function switchProxy( function requestDelayForProxyOnce(apiConfig: ClashAPIConfig, name: string) { return async (dispatch: DispatchFn, getState: GetStateFn) => { const latencyTestUrl = getLatencyTestUrl(getState()); - const res = await proxiesAPI.requestDelayForProxy( - apiConfig, - name, - latencyTestUrl - ); + const res = await proxiesAPI.requestDelayForProxy(apiConfig, name, latencyTestUrl); let error = ''; if (res.ok === false) { error = res.statusText; } const { delay } = await res.json(); - - const delayPrev = getDelay(getState()); - const delayNext = { - ...delayPrev, - [name]: { - error, - number: delay, - }, - }; + const delayNext = { ...getDelay(getState()), [name]: { error, number: delay } }; dispatch('requestDelayForProxyOnce', (s) => { s.proxies.delay = delayNext; @@ -324,17 +289,35 @@ export function requestDelayForProxy(apiConfig: ClashAPIConfig, name: string) { }; } -export function requestDelayForProxies( - apiConfig: ClashAPIConfig, - names: string[] -) { +export function requestDelayForProxies(apiConfig: ClashAPIConfig, names: string[]) { return async (dispatch: DispatchFn, getState: GetStateFn) => { - const proxyNames = getDangleProxyNames(getState()); + const proxies = getProxies(getState()); + const latencyTestUrl = getLatencyTestUrl(getState()); + + const proxyDedupMap = new Map<string, boolean>(); + const providerDedupMap = new Map<string, boolean>(); - const works = names - // remove names that are provided by proxy providers - .filter((p) => proxyNames.indexOf(p) > -1) - .map((p) => dispatch(requestDelayForProxy(apiConfig, p))); + const works = names.map((name) => { + const p = proxies[name]; + if (!p.__provider) { + if (proxyDedupMap.get(name)) { + return undefined; + } else { + proxyDedupMap.set(name, true); + return proxiesAPI.requestDelayForProxy(apiConfig, name, latencyTestUrl); + } + } else if (p.__provider) { + // this one is from a proxy provider + if (providerDedupMap.get(p.__provider)) { + return undefined; + } else { + providerDedupMap.set(p.__provider, true); + return healthcheckProviderByNameInternal(apiConfig, p.__provider); + } + } else { + return undefined; + } + }); await Promise.all(works); await dispatch(fetchProxies(apiConfig)); }; @@ -343,8 +326,9 @@ export function requestDelayForProxies( export function requestDelayAll(apiConfig: ClashAPIConfig) { return async (dispatch: DispatchFn, getState: GetStateFn) => { const proxyNames = getDangleProxyNames(getState()); + const latencyTestUrl = getLatencyTestUrl(getState()); await Promise.all( - proxyNames.map((p) => dispatch(requestDelayForProxy(apiConfig, p))) + proxyNames.map((p) => proxiesAPI.requestDelayForProxy(apiConfig, p, latencyTestUrl)) ); const proxyProviders = getProxyProviders(getState()); // one by one @@ -364,7 +348,7 @@ function retrieveGroupNamesFrom(proxies: Record<string, ProxyItem>) { if (p.all && Array.isArray(p.all)) { groupNames.push(prop); if (prop === 'GLOBAL') { - globalAll = p.all; + globalAll = Array.from(p.all); } } else if (NonProxyTypes.indexOf(p.type) < 0) { proxyNames.push(prop); @@ -386,15 +370,14 @@ type ProvidersRaw = { [key: string]: ProxyProvider; }; -function formatProxyProviders( - providersInput: ProvidersRaw -): { - providers: Array<FormattedProxyProvider>; - proxies: { [key: string]: ProxyItem }; +function formatProxyProviders(providersInput: ProvidersRaw): { + proxyProviders: Array<FormattedProxyProvider>; + providerProxyRecord: ProxiesMapping; } { const keys = Object.keys(providersInput); - const providers = []; - const proxies = {}; + const proxyProviders = []; + const providerProxyRecord: ProxiesMapping = {}; + for (let i = 0; i < keys.length; i++) { const provider: ProxyProvider = providersInput[keys[i]]; if (provider.name === 'default' || provider.vehicleType === 'Compatible') { @@ -404,19 +387,16 @@ function formatProxyProviders( const names = []; for (let j = 0; j < proxiesArr.length; j++) { const proxy = proxiesArr[j]; - proxies[proxy.name] = proxy; + providerProxyRecord[proxy.name] = { ...proxy, __provider: provider.name }; names.push(proxy.name); } // mutate directly provider.proxies = names; - providers.push(provider); + proxyProviders.push(provider); } - return { - providers, - proxies, - }; + return { proxyProviders, providerProxyRecord }; } export const actions = { diff --git a/src/store/types.ts b/src/store/types.ts index 3a3e412..faf8a92 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -44,39 +44,45 @@ export type TunPartial<T> = { ///// store.proxies -type LatencyHistory = Array<{ time: string; delay: number }>; -type PrimitiveProxyType = 'Shadowsocks' | 'Snell' | 'Socks5' | 'Http' | 'Vmess'; +type LatencyHistoryItem = { time: string; delay: number }; +export type LatencyHistory = LatencyHistoryItem[]; + export type ProxyItem = { name: string; - type: PrimitiveProxyType; + type: string; history: LatencyHistory; all?: string[]; now?: string; + + __provider?: string; }; + +export type ProxyDelayItem = { + number?: number; +}; + export type ProxiesMapping = Record<string, ProxyItem>; -export type DelayMapping = Record<string, { number?: number }>; +export type DelayMapping = Record<string, ProxyDelayItem>; export type ProxyProvider = { name: string; type: 'Proxy'; updatedAt: string; vehicleType: 'HTTP' | 'File' | 'Compatible'; - proxies: Array<ProxyItem>; + proxies: ProxyItem[]; }; -export type FormattedProxyProvider = Omit<ProxyProvider, 'proxies'> & { - proxies: string[]; -}; +export type FormattedProxyProvider = Omit<ProxyProvider, 'proxies'> & { proxies: string[] }; export type SwitchProxyCtxItem = { groupName: string; itemName: string }; -type SwitchProxyCtx = { - to: SwitchProxyCtxItem; -}; +type SwitchProxyCtx = { to: SwitchProxyCtxItem }; + export type StateProxies = { - proxies: ProxiesMapping; - delay: DelayMapping; groupNames: string[]; proxyProviders?: FormattedProxyProvider[]; + + proxies: ProxiesMapping; + delay: DelayMapping; dangleProxyNames?: string[]; showModalClosePrevConns: boolean; @@ -53,8 +53,7 @@ registerRoute( // precache, in this case same-origin .png requests like those from in public/ registerRoute( // Add in any other file extensions or routing criteria as needed. - ({ url }) => - url.origin === self.location.origin && url.pathname.endsWith('.png'), + ({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst. new StaleWhileRevalidate({ cacheName: 'images', diff --git a/src/swRegistration.ts b/src/swRegistration.ts index 55ceb0b..0a684a8 100644 --- a/src/swRegistration.ts +++ b/src/swRegistration.ts @@ -3,9 +3,7 @@ const isLocalhost = Boolean( // [::1] is the IPv6 localhost address. window.location.hostname === '[::1]' || // 127.0.0.0/8 are considered localhost for IPv4. - window.location.hostname.match( - /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ - ) + window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/) ); type Config = { @@ -34,9 +32,7 @@ export function register(config?: Config) { // Add some additional logging to localhost, pointing developers to the // service worker/PWA documentation. navigator.serviceWorker.ready.then(() => { - console.log( - 'This web app is being served cache-first by a service worker' - ); + console.log('This web app is being served cache-first by a service worker'); }); } else { // Is not localhost. Just register service worker @@ -114,9 +110,7 @@ function checkValidServiceWorker(swUrl: string, config?: Config) { } }) .catch(() => { - console.log( - 'No internet connection found. App is running in offline mode.' - ); + console.log('No internet connection found. App is running in offline mode.'); }); } diff --git a/src/types.ts b/src/types.ts index 694289b..8a07865 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,4 +3,6 @@ export type ClashAPIConfig = { secret?: string; }; -export type LogsAPIConfig = ClashAPIConfig & { logLevel: string };
\ No newline at end of file +export type LogsAPIConfig = ClashAPIConfig & { logLevel: string }; + +export type RuleType = { id?: number; type?: string; payload?: string; proxy?: string }; |
