diff options
| author | Haishan <[email protected]> | 2020-03-20 22:19:56 +0800 |
|---|---|---|
| committer | Haishan <[email protected]> | 2020-03-21 13:33:43 +0800 |
| commit | 8e48c01e7aada6978e92a6da1d040f3ef0d37945 (patch) | |
| tree | 63cdf772b88d2cff340449ba98225bdbad526a19 /src | |
| parent | c5d70b5236be5ce0fb067bab3c8eeb6e946a73dd (diff) | |
feat: remembers group collapse state
for https://github.com/haishanh/yacd/issues/480
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/Button.js | 21 | ||||
| -rw-r--r-- | src/components/CollapsibleSectionHeader.js | 2 | ||||
| -rw-r--r-- | src/components/CollapsibleSectionHeader.module.css | 4 | ||||
| -rw-r--r-- | src/components/ProxyGroup.js | 20 | ||||
| -rw-r--r-- | src/components/ProxyProvider.js | 33 | ||||
| -rw-r--r-- | src/components/StateProvider.js | 2 | ||||
| -rw-r--r-- | src/misc/utils.js | 23 | ||||
| -rw-r--r-- | src/store/app.js | 19 | ||||
| -rw-r--r-- | src/store/index.js | 9 |
9 files changed, 109 insertions, 24 deletions
diff --git a/src/components/Button.js b/src/components/Button.js index d5b88cf..2ec0c22 100644 --- a/src/components/Button.js +++ b/src/components/Button.js @@ -16,10 +16,17 @@ type ButtonProps = { isLoading?: boolean, start?: Element | (() => Element), onClick?: (SyntheticEvent<HTMLButtonElement>) => mixed, - kind?: 'primary' | 'minimal' + kind?: 'primary' | 'minimal', + className?: string }; function Button(props: ButtonProps, ref) { - const { onClick, isLoading, kind = 'primary', ...restProps } = props; + const { + onClick, + isLoading, + kind = 'primary', + className, + ...restProps + } = props; const internalOnClick = useCallback( e => { if (isLoading) return; @@ -27,9 +34,13 @@ function Button(props: ButtonProps, ref) { }, [isLoading, onClick] ); - const btnClassName = cx(s0.btn, { - [s0.minimal]: kind === 'minimal' - }); + const btnClassName = cx( + s0.btn, + { + [s0.minimal]: kind === 'minimal' + }, + className + ); return ( <button className={btnClassName} ref={ref} onClick={internalOnClick}> {isLoading ? ( diff --git a/src/components/CollapsibleSectionHeader.js b/src/components/CollapsibleSectionHeader.js index aebb643..3cd0a96 100644 --- a/src/components/CollapsibleSectionHeader.js +++ b/src/components/CollapsibleSectionHeader.js @@ -24,7 +24,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}> + <Button kind="minimal" onClick={toggle} className={s.btn}> <span className={cx(s.arrow, { [s.isOpen]: isOpen })}> <ChevronDown size={20} /> </span> diff --git a/src/components/CollapsibleSectionHeader.module.css b/src/components/CollapsibleSectionHeader.module.css index de24eec..854a0c6 100644 --- a/src/components/CollapsibleSectionHeader.module.css +++ b/src/components/CollapsibleSectionHeader.module.css @@ -16,6 +16,10 @@ } } +.btn { + margin-left: 5px; +} + /* TODO duplicate with connQty in Connections.module.css */ .qty { font-family: var(--font-normal); diff --git a/src/components/ProxyGroup.js b/src/components/ProxyGroup.js index fffb020..d7cf203 100644 --- a/src/components/ProxyGroup.js +++ b/src/components/ProxyGroup.js @@ -2,11 +2,11 @@ import React from 'react'; import cx from 'classnames'; import memoizeOne from 'memoize-one'; -import { connect } from './StateProvider'; +import { connect, useStoreActions } from './StateProvider'; import { getProxies, getRtFilterSwitch } from '../store/proxies'; +import { getCollapsibleIsOpen } from '../store/app'; import CollapsibleSectionHeader from './CollapsibleSectionHeader'; import Proxy, { ProxySmall } from './Proxy'; -import { useToggle } from '../hooks/basic'; import s0 from './ProxyGroup.module.css'; @@ -14,9 +14,17 @@ import { switchProxy } from '../store/proxies'; const { useCallback, useMemo } = React; -function ProxyGroup({ name, all, type, now, apiConfig, dispatch }) { +function ProxyGroup({ name, all, type, now, isOpen, apiConfig, dispatch }) { const isSelectable = useMemo(() => type === 'Selector', [type]); - const [isOpen, toggle] = useToggle(true); + + const { + app: { updateCollapsibleIsOpen } + } = useStoreActions(); + + const toggle = useCallback(() => { + updateCollapsibleIsOpen('proxyGroup', name, !isOpen); + }, [isOpen, updateCollapsibleIsOpen, name]); + const itemOnTapCallback = useCallback( proxyName => { if (!isSelectable) return; @@ -161,11 +169,13 @@ export function ProxyListSummaryView({ export default connect((s, { name, delay }) => { const proxies = getProxies(s); const filterByRt = getRtFilterSwitch(s); + const collapsibleIsOpen = getCollapsibleIsOpen(s); const group = proxies[name]; const { all, type, now } = group; return { all: filterAvailableProxiesAndSort(all, delay, filterByRt), type, - now + now, + isOpen: collapsibleIsOpen[`proxyGroup:${name}`] }; })(ProxyGroup); diff --git a/src/components/ProxyProvider.js b/src/components/ProxyProvider.js index 32071ab..9486128 100644 --- a/src/components/ProxyProvider.js +++ b/src/components/ProxyProvider.js @@ -3,7 +3,7 @@ import { RotateCw, Zap } from 'react-feather'; import { formatDistance } from 'date-fns'; import { motion } from 'framer-motion'; -import { connect } from './StateProvider'; +import { connect, useStoreActions } from './StateProvider'; import Collapsible from './Collapsible'; import CollapsibleSectionHeader from './CollapsibleSectionHeader'; import { @@ -13,7 +13,7 @@ import { } from './ProxyGroup'; import Button from './Button'; -import { getClashAPIConfig } from '../store/app'; +import { getClashAPIConfig, getCollapsibleIsOpen } from '../store/app'; import { getDelay, getRtFilterSwitch, @@ -31,7 +31,8 @@ type Props = { type: 'Proxy' | 'Rule', vehicleType: 'HTTP' | 'File' | 'Compatible', updatedAt?: string, - dispatch: any => void + dispatch: any => void, + isOpen: boolean }; function ProxyProvider({ @@ -39,6 +40,7 @@ function ProxyProvider({ proxies, vehicleType, updatedAt, + isOpen, dispatch, apiConfig }: Props) { @@ -53,8 +55,17 @@ function ProxyProvider({ setIsHealthcheckLoading(false); }, [apiConfig, dispatch, name, setIsHealthcheckLoading]); - const [isCollapsibleOpen, setCollapsibleOpen] = useState(false); - const toggle = useCallback(() => setCollapsibleOpen(x => !x), []); + const { + app: { updateCollapsibleIsOpen } + } = useStoreActions(); + + // const [isCollapsibleOpen, setCollapsibleOpen] = useState(false); + // const toggle = useCallback(() => setCollapsibleOpen(x => !x), []); + + const toggle = useCallback(() => { + updateCollapsibleIsOpen('proxyProvider', name, !isOpen); + }, [isOpen, updateCollapsibleIsOpen, name]); + const timeAgo = formatDistance(new Date(updatedAt), new Date()); return ( <div className={s.body}> @@ -62,13 +73,13 @@ function ProxyProvider({ name={name} toggle={toggle} type={vehicleType} - isOpen={isCollapsibleOpen} + isOpen={isOpen} qty={proxies.length} /> <div className={s.updatedAt}> <small>Updated {timeAgo} ago</small> </div> - <Collapsible isOpen={isCollapsibleOpen}> + <Collapsible isOpen={isOpen}> <ProxyList all={proxies} /> <div className={s.actionFooter}> <Button text="Update" start={<Refresh />} onClick={updateProvider} /> @@ -80,7 +91,7 @@ function ProxyProvider({ /> </div> </Collapsible> - <Collapsible isOpen={!isCollapsibleOpen}> + <Collapsible isOpen={!isOpen}> <ProxyListSummaryView all={proxies} /> </Collapsible> </div> @@ -112,13 +123,15 @@ function Refresh() { ); } -const mapState = (s, { proxies }) => { +const mapState = (s, { proxies, name }) => { const filterByRt = getRtFilterSwitch(s); const delay = getDelay(s); + const collapsibleIsOpen = getCollapsibleIsOpen(s); const apiConfig = getClashAPIConfig(s); return { apiConfig, - proxies: filterAvailableProxiesAndSort(proxies, delay, filterByRt) + proxies: filterAvailableProxiesAndSort(proxies, delay, filterByRt), + isOpen: collapsibleIsOpen[`proxyProvider:${name}`] }; }; diff --git a/src/components/StateProvider.js b/src/components/StateProvider.js index 30b1cda..e675f89 100644 --- a/src/components/StateProvider.js +++ b/src/components/StateProvider.js @@ -99,6 +99,8 @@ function bindActions(actions, dispatch) { const action = actions[key]; if (typeof action === 'function') { boundActions[key] = bindAction(action, dispatch); + } else if (typeof action === 'object') { + boundActions[key] = bindActions(action, dispatch); } } return boundActions; diff --git a/src/misc/utils.js b/src/misc/utils.js new file mode 100644 index 0000000..66146c0 --- /dev/null +++ b/src/misc/utils.js @@ -0,0 +1,23 @@ +export function throttle(fn, timeout) { + let pending = false; + + return (...args) => { + if (!pending) { + pending = true; + fn(...args); + setTimeout(() => { + pending = false; + }, timeout); + } + }; +} + +export function debounce(fn, timeout) { + let timeoutId; + return (...args) => { + if (timeoutId) clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + fn(...args); + }, timeout); + }; +} diff --git a/src/store/app.js b/src/store/app.js index 465a564..ae8e8a2 100644 --- a/src/store/app.js +++ b/src/store/app.js @@ -1,4 +1,5 @@ import { loadState, saveState, clearState } from '../misc/storage'; +import { debounce } from '../misc/utils'; import { fetchConfigs } from './configs'; import { closeModal } from './modals'; @@ -7,6 +8,9 @@ export const getClashAPIConfig = s => s.app.clashAPIConfig; export const getTheme = s => s.app.theme; export const getSelectedChartStyleIndex = s => s.app.selectedChartStyleIndex; export const getLatencyTestUrl = s => s.app.latencyTestUrl; +export const getCollapsibleIsOpen = s => s.app.collapsibleIsOpen; + +const saveStateDebounced = debounce(saveState, 600); export function updateClashAPIConfig({ hostname: iHostname, port, secret }) { return async (dispatch, getState) => { @@ -76,6 +80,16 @@ export function updateAppConfig(name, value) { }; } +export function updateCollapsibleIsOpen(prefix, name, v) { + return (dispatch, getState) => { + dispatch('updateCollapsibleIsOpen', s => { + s.app.collapsibleIsOpen[`${prefix}:${name}`] = v; + }); + // side effect + saveStateDebounced(getState().app); + }; +} + // type Theme = 'light' | 'dark'; const defaultState = { clashAPIConfig: { @@ -85,7 +99,10 @@ const defaultState = { }, latencyTestUrl: 'http://www.gstatic.com/generate_204', selectedChartStyleIndex: 0, - theme: 'dark' + theme: 'dark', + + // type { [string]: boolean } + collapsibleIsOpen: {} }; function parseConfigQueryString() { diff --git a/src/store/index.js b/src/store/index.js index 1fe8459..78ddca3 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,7 +1,8 @@ import { initialState as app, selectChartStyleIndex, - updateAppConfig + updateAppConfig, + updateCollapsibleIsOpen } from './app'; import { initialState as proxies, @@ -25,5 +26,9 @@ export const actions = { selectChartStyleIndex, updateAppConfig, // proxies - toggleUnavailableProxiesFilter + toggleUnavailableProxiesFilter, + + app: { + updateCollapsibleIsOpen + } }; |
