From 7fa3c1e12a23f5e2e12eec6efeb107a671c11f6c Mon Sep 17 00:00:00 2001 From: GyDi Date: Sat, 4 Jun 2022 18:55:39 +0800 Subject: [PATCH] feat: save proxy page state --- src/components/proxy/proxy-global.tsx | 29 +++++----- src/components/proxy/proxy-group.tsx | 46 ++++++++------- src/components/proxy/proxy-head.tsx | 51 +++++++++------- src/components/proxy/use-head-state.ts | 80 ++++++++++++++++++++++++++ src/pages/_layout.tsx | 9 ++- src/pages/profiles.tsx | 8 +++ src/services/states.ts | 6 ++ 7 files changed, 173 insertions(+), 56 deletions(-) create mode 100644 src/components/proxy/use-head-state.ts diff --git a/src/components/proxy/proxy-global.tsx b/src/components/proxy/proxy-global.tsx index d3d36f9..0a22f55 100644 --- a/src/components/proxy/proxy-global.tsx +++ b/src/components/proxy/proxy-global.tsx @@ -5,7 +5,8 @@ import { Virtuoso } from "react-virtuoso"; import { ApiType } from "../../services/types"; import { updateProxy } from "../../services/api"; import { getProfiles, patchProfile } from "../../services/cmds"; -import useSortProxy, { ProxySortType } from "./use-sort-proxy"; +import useSortProxy from "./use-sort-proxy"; +import useHeadState from "./use-head-state"; import useFilterProxy from "./use-filter-proxy"; import delayManager from "../../services/delay"; import ProxyHead from "./proxy-head"; @@ -24,13 +25,19 @@ const ProxyGlobal = (props: Props) => { const { mutate } = useSWRConfig(); const [now, setNow] = useState(curProxy || "DIRECT"); - const [showType, setShowType] = useState(true); - const [sortType, setSortType] = useState(0); - const [filterText, setFilterText] = useState(""); + const [headState, setHeadState] = useHeadState(groupName); const virtuosoRef = useRef(); - const filterProxies = useFilterProxy(proxies, groupName, filterText); - const sortedProxies = useSortProxy(filterProxies, groupName, sortType); + const filterProxies = useFilterProxy( + proxies, + groupName, + headState.filterText + ); + const sortedProxies = useSortProxy( + filterProxies, + groupName, + headState.sortType + ); const { data: profiles } = useSWR("getProfiles", getProfiles); @@ -102,15 +109,11 @@ const ProxyGlobal = (props: Props) => { <> { groupName={groupName} proxy={sortedProxies[index]} selected={sortedProxies[index].name === now} - showType={showType} + showType={headState.showType} onClick={onChangeProxy} sx={{ py: 0, px: 2 }} /> diff --git a/src/components/proxy/proxy-group.tsx b/src/components/proxy/proxy-group.tsx index 9c9deda..384cffe 100644 --- a/src/components/proxy/proxy-group.tsx +++ b/src/components/proxy/proxy-group.tsx @@ -18,7 +18,8 @@ import { import { ApiType } from "../../services/types"; import { updateProxy } from "../../services/api"; import { getProfiles, patchProfile } from "../../services/cmds"; -import useSortProxy, { ProxySortType } from "./use-sort-proxy"; +import useSortProxy from "./use-sort-proxy"; +import useHeadState from "./use-head-state"; import useFilterProxy from "./use-filter-proxy"; import delayManager from "../../services/delay"; import ProxyHead from "./proxy-head"; @@ -30,16 +31,21 @@ interface Props { const ProxyGroup = ({ group }: Props) => { const { mutate } = useSWRConfig(); - const [open, setOpen] = useState(false); const [now, setNow] = useState(group.now); - const [showType, setShowType] = useState(false); - const [sortType, setSortType] = useState(0); - const [filterText, setFilterText] = useState(""); + const [headState, setHeadState] = useHeadState(group.name); const virtuosoRef = useRef(); - const filterProxies = useFilterProxy(group.all, group.name, filterText); - const sortedProxies = useSortProxy(filterProxies, group.name, sortType); + const filterProxies = useFilterProxy( + group.all, + group.name, + headState.filterText + ); + const sortedProxies = useSortProxy( + filterProxies, + group.name, + headState.sortType + ); const { data: profiles } = useSWR("getProfiles", getProfiles); @@ -99,14 +105,18 @@ const ProxyGroup = ({ group }: Props) => { // auto scroll to current index useEffect(() => { - if (open) { + if (headState.open) { setTimeout(() => onLocation(false), 5); } - }, [open]); + }, [headState.open]); return ( <> - setOpen(!open)} dense> + setHeadState({ open: !headState.open })} + > { }} /> - {open ? : } + {headState.open ? : } - + {!sortedProxies.length && ( @@ -160,7 +166,7 @@ const ProxyGroup = ({ group }: Props) => { groupName={group.name} proxy={sortedProxies[index]} selected={sortedProxies[index].name === now} - showType={showType} + showType={headState.showType} sx={{ py: 0, pl: 4 }} onClick={onChangeProxy} /> @@ -178,7 +184,7 @@ const ProxyGroup = ({ group }: Props) => { groupName={group.name} proxy={proxy} selected={proxy.name === now} - showType={showType} + showType={headState.showType} sx={{ py: 0, pl: 4 }} onClick={onChangeProxy} /> diff --git a/src/components/proxy/proxy-head.tsx b/src/components/proxy/proxy-head.tsx index 3663255..6178eb5 100644 --- a/src/components/proxy/proxy-head.tsx +++ b/src/components/proxy/proxy-head.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { Box, IconButton, TextField, SxProps } from "@mui/material"; import { AccessTimeRounded, @@ -14,27 +14,33 @@ import { SortRounded, } from "@mui/icons-material"; import delayManager from "../../services/delay"; +import type { HeadState } from "./use-head-state"; import type { ProxySortType } from "./use-sort-proxy"; interface Props { sx?: SxProps; groupName: string; - showType: boolean; - sortType: ProxySortType; - filterText: string; + headState: HeadState; onLocation: () => void; onCheckDelay: () => void; - onShowType: (val: boolean) => void; - onSortType: (val: ProxySortType) => void; - onFilterText: (val: string) => void; + onHeadState: (val: Partial) => void; } const ProxyHead = (props: Props) => { - const { sx = {}, groupName, showType, sortType, filterText } = props; + const { sx = {}, groupName, headState, onHeadState } = props; - const [textState, setTextState] = useState<"url" | "filter" | null>(null); + const { showType, sortType, filterText, textState, testUrl } = headState; - const [testUrl, setTestUrl] = useState(delayManager.getUrl(groupName) || ""); + const [autoFocus, setAutoFocus] = useState(false); + + useEffect(() => { + // fix the focus conflict + setTimeout(() => setAutoFocus(true), 100); + }, []); + + useEffect(() => { + delayManager.setUrl(groupName, testUrl); + }, [groupName, headState.testUrl]); return ( @@ -54,7 +60,7 @@ const ProxyHead = (props: Props) => { onClick={() => { // Remind the user that it is custom test url if (testUrl?.trim() && textState !== "filter") { - setTextState("url"); + onHeadState({ textState: "url" }); } props.onCheckDelay(); }} @@ -66,7 +72,9 @@ const ProxyHead = (props: Props) => { size="small" color="inherit" title={["sort by default", "sort by delay", "sort by name"][sortType]} - onClick={() => props.onSortType(((sortType + 1) % 3) as ProxySortType)} + onClick={() => + onHeadState({ sortType: ((sortType + 1) % 3) as ProxySortType }) + } > {sortType === 0 && } {sortType === 1 && } @@ -77,7 +85,9 @@ const ProxyHead = (props: Props) => { size="small" color="inherit" title="edit test url" - onClick={() => setTextState((ts) => (ts === "url" ? null : "url"))} + onClick={() => + onHeadState({ textState: textState === "url" ? null : "url" }) + } > {textState === "url" ? ( @@ -90,7 +100,7 @@ const ProxyHead = (props: Props) => { size="small" color="inherit" title="proxy detail" - onClick={() => props.onShowType(!showType)} + onClick={() => onHeadState({ showType: !showType })} > {showType ? : } @@ -100,7 +110,7 @@ const ProxyHead = (props: Props) => { color="inherit" title="filter" onClick={() => - setTextState((ts) => (ts === "filter" ? null : "filter")) + onHeadState({ textState: textState === "filter" ? null : "filter" }) } > {textState === "filter" ? ( @@ -112,20 +122,20 @@ const ProxyHead = (props: Props) => { {textState === "filter" && ( props.onFilterText(e.target.value)} + onChange={(e) => onHeadState({ filterText: e.target.value })} sx={{ ml: 0.5, flex: "1 1 auto", input: { py: 0.65, px: 1 } }} /> )} {textState === "url" && ( { size="small" variant="outlined" placeholder="Test url" - onChange={(e) => { - setTestUrl(e.target.value); - delayManager.setUrl(groupName, e.target.value); - }} + onChange={(e) => onHeadState({ testUrl: e.target.value })} sx={{ ml: 0.5, flex: "1 1 auto", input: { py: 0.65, px: 1 } }} /> )} diff --git a/src/components/proxy/use-head-state.ts b/src/components/proxy/use-head-state.ts new file mode 100644 index 0000000..7fcd44f --- /dev/null +++ b/src/components/proxy/use-head-state.ts @@ -0,0 +1,80 @@ +import { useCallback, useEffect, useState } from "react"; +import { useRecoilValue } from "recoil"; +import { atomCurrentProfile } from "../../services/states"; +import { ProxySortType } from "./use-sort-proxy"; + +export interface HeadState { + open?: boolean; + showType: boolean; + sortType: ProxySortType; + filterText: string; + textState: "url" | "filter" | null; + testUrl: string; +} + +type HeadStateStorage = Record>; + +const HEAD_STATE_KEY = "proxy-head-state"; +const DEFAULT_STATE: HeadState = { + open: false, + showType: false, + sortType: 0, + filterText: "", + textState: null, + testUrl: "", +}; + +export default function useHeadState(groupName: string) { + const current = useRecoilValue(atomCurrentProfile); + + const [state, setState] = useState(DEFAULT_STATE); + + useEffect(() => { + if (!current) { + setState(DEFAULT_STATE); + return; + } + + try { + const data = JSON.parse( + localStorage.getItem(HEAD_STATE_KEY)! + ) as HeadStateStorage; + + const value = data[current][groupName] || DEFAULT_STATE; + + if (value && typeof value === "object") { + setState({ ...DEFAULT_STATE, ...value }); + } else { + setState(DEFAULT_STATE); + } + } catch {} + }, [current, groupName]); + + const setHeadState = useCallback( + (obj: Partial) => { + setState((old) => { + const ret = { ...old, ...obj }; + + setTimeout(() => { + try { + const item = localStorage.getItem(HEAD_STATE_KEY); + + let data = (item ? JSON.parse(item) : {}) as HeadStateStorage; + + if (!data || typeof data !== "object") data = {}; + if (!data[current]) data[current] = {}; + + data[current][groupName] = ret; + + localStorage.setItem(HEAD_STATE_KEY, JSON.stringify(data)); + } catch {} + }); + + return ret; + }); + }, + [current, groupName] + ); + + return [state, setHeadState] as const; +} diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index 58e1c1f..212b2e3 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -3,6 +3,7 @@ import i18next from "i18next"; import relativeTime from "dayjs/plugin/relativeTime"; import useSWR, { SWRConfig, useSWRConfig } from "swr"; import { useEffect } from "react"; +import { useSetRecoilState } from "recoil"; import { useTranslation } from "react-i18next"; import { Route, Routes } from "react-router-dom"; import { alpha, List, Paper, ThemeProvider } from "@mui/material"; @@ -10,7 +11,8 @@ import { listen } from "@tauri-apps/api/event"; import { appWindow } from "@tauri-apps/api/window"; import { routers } from "./_routers"; import { getAxios } from "../services/api"; -import { getVergeConfig } from "../services/cmds"; +import { atomCurrentProfile } from "../services/states"; +import { getVergeConfig, getProfiles } from "../services/cmds"; import { ReactComponent as LogoSvg } from "../assets/image/logo.svg"; import LayoutItem from "../components/layout/layout-item"; import LayoutControl from "../components/layout/layout-control"; @@ -33,6 +35,8 @@ const Layout = () => { const { data: vergeConfig } = useSWR("getVergeConfig", getVergeConfig); const { theme_blur, language } = vergeConfig || {}; + const setCurrentProfile = useSetRecoilState(atomCurrentProfile); + useEffect(() => { window.addEventListener("keydown", (e) => { if (e.key === "Escape") appWindow.close(); @@ -47,6 +51,9 @@ const Layout = () => { // update the verge config listen("verge://refresh-verge-config", () => mutate("getVergeConfig")); + + // set current profile uid + getProfiles().then((data) => setCurrentProfile(data.current ?? "")); }, []); useEffect(() => { diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index 8263898..9279401 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -1,6 +1,7 @@ import useSWR, { useSWRConfig } from "swr"; import { useLockFn } from "ahooks"; import { useEffect, useMemo, useState } from "react"; +import { useSetRecoilState } from "recoil"; import { Box, Button, Grid, TextField } from "@mui/material"; import { useTranslation } from "react-i18next"; import { @@ -10,6 +11,7 @@ import { importProfile, } from "../services/cmds"; import { getProxies, updateProxy } from "../services/api"; +import { atomCurrentProfile } from "../services/states"; import Notice from "../components/base/base-notice"; import BasePage from "../components/base/base-page"; import ProfileNew from "../components/profile/profile-new"; @@ -24,6 +26,8 @@ const ProfilePage = () => { const [disabled, setDisabled] = useState(false); const [dialogOpen, setDialogOpen] = useState(false); + const setCurrentProfile = useSetRecoilState(atomCurrentProfile); + const { data: profiles = {} } = useSWR("getProfiles", getProfiles); // distinguish type @@ -52,6 +56,9 @@ const ProfilePage = () => { const current = profiles.current; const profile = regularItems.find((p) => p.uid === current); + + setCurrentProfile(current); + if (!profile) return; setTimeout(async () => { @@ -121,6 +128,7 @@ const ProfilePage = () => { try { await selectProfile(uid); + setCurrentProfile(uid); mutate("getProfiles", { ...profiles, current: uid }, true); if (force) Notice.success("Refresh clash config", 1000); } catch (err: any) { diff --git a/src/services/states.ts b/src/services/states.ts index 250e31e..015f484 100644 --- a/src/services/states.ts +++ b/src/services/states.ts @@ -22,3 +22,9 @@ export const atomUpdateState = atom({ key: "atomUpdateState", default: false, }); + +// current profile uid +export const atomCurrentProfile = atom({ + key: "atomCurrentProfile", + default: "", +});