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/components | |
| parent | a825925cc97d95762634d234ef06be1627a21fb1 (diff) | |
| parent | ea5d7cf003eeef30cb7bbe789c6ba7f314bf1ce4 (diff) | |
Merge branch 'haishanh-master'
Diffstat (limited to 'src/components')
47 files changed, 364 insertions, 576 deletions
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" |
