summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorHaishan <[email protected]>2020-03-20 22:19:56 +0800
committerHaishan <[email protected]>2020-03-21 13:33:43 +0800
commit8e48c01e7aada6978e92a6da1d040f3ef0d37945 (patch)
tree63cdf772b88d2cff340449ba98225bdbad526a19 /src
parentc5d70b5236be5ce0fb067bab3c8eeb6e946a73dd (diff)
feat: remembers group collapse state
for https://github.com/haishanh/yacd/issues/480
Diffstat (limited to 'src')
-rw-r--r--src/components/Button.js21
-rw-r--r--src/components/CollapsibleSectionHeader.js2
-rw-r--r--src/components/CollapsibleSectionHeader.module.css4
-rw-r--r--src/components/ProxyGroup.js20
-rw-r--r--src/components/ProxyProvider.js33
-rw-r--r--src/components/StateProvider.js2
-rw-r--r--src/misc/utils.js23
-rw-r--r--src/store/app.js19
-rw-r--r--src/store/index.js9
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
+ }
};