feat: save proxy page state

This commit is contained in:
GyDi 2022-06-04 18:55:39 +08:00
parent 3b5993652f
commit 7fa3c1e12a
No known key found for this signature in database
GPG Key ID: 1C95E0D3467B3084
7 changed files with 173 additions and 56 deletions

View File

@ -5,7 +5,8 @@ import { Virtuoso } from "react-virtuoso";
import { ApiType } from "../../services/types"; import { ApiType } from "../../services/types";
import { updateProxy } from "../../services/api"; import { updateProxy } from "../../services/api";
import { getProfiles, patchProfile } from "../../services/cmds"; 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 useFilterProxy from "./use-filter-proxy";
import delayManager from "../../services/delay"; import delayManager from "../../services/delay";
import ProxyHead from "./proxy-head"; import ProxyHead from "./proxy-head";
@ -24,13 +25,19 @@ const ProxyGlobal = (props: Props) => {
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
const [now, setNow] = useState(curProxy || "DIRECT"); const [now, setNow] = useState(curProxy || "DIRECT");
const [showType, setShowType] = useState(true); const [headState, setHeadState] = useHeadState(groupName);
const [sortType, setSortType] = useState<ProxySortType>(0);
const [filterText, setFilterText] = useState("");
const virtuosoRef = useRef<any>(); const virtuosoRef = useRef<any>();
const filterProxies = useFilterProxy(proxies, groupName, filterText); const filterProxies = useFilterProxy(
const sortedProxies = useSortProxy(filterProxies, groupName, sortType); proxies,
groupName,
headState.filterText
);
const sortedProxies = useSortProxy(
filterProxies,
groupName,
headState.sortType
);
const { data: profiles } = useSWR("getProfiles", getProfiles); const { data: profiles } = useSWR("getProfiles", getProfiles);
@ -102,15 +109,11 @@ const ProxyGlobal = (props: Props) => {
<> <>
<ProxyHead <ProxyHead
sx={{ px: 3, my: 0.5, button: { mr: 0.5 } }} sx={{ px: 3, my: 0.5, button: { mr: 0.5 } }}
showType={showType}
sortType={sortType}
groupName={groupName} groupName={groupName}
filterText={filterText} headState={headState}
onLocation={onLocation} onLocation={onLocation}
onCheckDelay={onCheckAll} onCheckDelay={onCheckAll}
onShowType={setShowType} onHeadState={setHeadState}
onSortType={setSortType}
onFilterText={setFilterText}
/> />
<Virtuoso <Virtuoso
@ -122,7 +125,7 @@ const ProxyGlobal = (props: Props) => {
groupName={groupName} groupName={groupName}
proxy={sortedProxies[index]} proxy={sortedProxies[index]}
selected={sortedProxies[index].name === now} selected={sortedProxies[index].name === now}
showType={showType} showType={headState.showType}
onClick={onChangeProxy} onClick={onChangeProxy}
sx={{ py: 0, px: 2 }} sx={{ py: 0, px: 2 }}
/> />

View File

@ -18,7 +18,8 @@ import {
import { ApiType } from "../../services/types"; import { ApiType } from "../../services/types";
import { updateProxy } from "../../services/api"; import { updateProxy } from "../../services/api";
import { getProfiles, patchProfile } from "../../services/cmds"; 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 useFilterProxy from "./use-filter-proxy";
import delayManager from "../../services/delay"; import delayManager from "../../services/delay";
import ProxyHead from "./proxy-head"; import ProxyHead from "./proxy-head";
@ -30,16 +31,21 @@ interface Props {
const ProxyGroup = ({ group }: Props) => { const ProxyGroup = ({ group }: Props) => {
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
const [open, setOpen] = useState(false);
const [now, setNow] = useState(group.now); const [now, setNow] = useState(group.now);
const [showType, setShowType] = useState(false); const [headState, setHeadState] = useHeadState(group.name);
const [sortType, setSortType] = useState<ProxySortType>(0);
const [filterText, setFilterText] = useState("");
const virtuosoRef = useRef<any>(); const virtuosoRef = useRef<any>();
const filterProxies = useFilterProxy(group.all, group.name, filterText); const filterProxies = useFilterProxy(
const sortedProxies = useSortProxy(filterProxies, group.name, sortType); group.all,
group.name,
headState.filterText
);
const sortedProxies = useSortProxy(
filterProxies,
group.name,
headState.sortType
);
const { data: profiles } = useSWR("getProfiles", getProfiles); const { data: profiles } = useSWR("getProfiles", getProfiles);
@ -99,14 +105,18 @@ const ProxyGroup = ({ group }: Props) => {
// auto scroll to current index // auto scroll to current index
useEffect(() => { useEffect(() => {
if (open) { if (headState.open) {
setTimeout(() => onLocation(false), 5); setTimeout(() => onLocation(false), 5);
} }
}, [open]); }, [headState.open]);
return ( return (
<> <>
<ListItem button onClick={() => setOpen(!open)} dense> <ListItem
button
dense
onClick={() => setHeadState({ open: !headState.open })}
>
<ListItemText <ListItemText
primary={group.name} primary={group.name}
secondary={ secondary={
@ -120,21 +130,17 @@ const ProxyGroup = ({ group }: Props) => {
}} }}
/> />
{open ? <ExpandLessRounded /> : <ExpandMoreRounded />} {headState.open ? <ExpandLessRounded /> : <ExpandMoreRounded />}
</ListItem> </ListItem>
<Collapse in={open} timeout="auto" unmountOnExit> <Collapse in={headState.open} timeout="auto" unmountOnExit>
<ProxyHead <ProxyHead
sx={{ pl: 4, pr: 3, my: 0.5, button: { mr: 0.5 } }} sx={{ pl: 4, pr: 3, my: 0.5, button: { mr: 0.5 } }}
showType={showType}
sortType={sortType}
groupName={group.name} groupName={group.name}
filterText={filterText} headState={headState}
onLocation={onLocation} onLocation={onLocation}
onCheckDelay={onCheckAll} onCheckDelay={onCheckAll}
onShowType={setShowType} onHeadState={setHeadState}
onSortType={setSortType}
onFilterText={setFilterText}
/> />
{!sortedProxies.length && ( {!sortedProxies.length && (
@ -160,7 +166,7 @@ const ProxyGroup = ({ group }: Props) => {
groupName={group.name} groupName={group.name}
proxy={sortedProxies[index]} proxy={sortedProxies[index]}
selected={sortedProxies[index].name === now} selected={sortedProxies[index].name === now}
showType={showType} showType={headState.showType}
sx={{ py: 0, pl: 4 }} sx={{ py: 0, pl: 4 }}
onClick={onChangeProxy} onClick={onChangeProxy}
/> />
@ -178,7 +184,7 @@ const ProxyGroup = ({ group }: Props) => {
groupName={group.name} groupName={group.name}
proxy={proxy} proxy={proxy}
selected={proxy.name === now} selected={proxy.name === now}
showType={showType} showType={headState.showType}
sx={{ py: 0, pl: 4 }} sx={{ py: 0, pl: 4 }}
onClick={onChangeProxy} onClick={onChangeProxy}
/> />

View File

@ -1,4 +1,4 @@
import { useState } from "react"; import { useEffect, useState } from "react";
import { Box, IconButton, TextField, SxProps } from "@mui/material"; import { Box, IconButton, TextField, SxProps } from "@mui/material";
import { import {
AccessTimeRounded, AccessTimeRounded,
@ -14,27 +14,33 @@ import {
SortRounded, SortRounded,
} from "@mui/icons-material"; } from "@mui/icons-material";
import delayManager from "../../services/delay"; import delayManager from "../../services/delay";
import type { HeadState } from "./use-head-state";
import type { ProxySortType } from "./use-sort-proxy"; import type { ProxySortType } from "./use-sort-proxy";
interface Props { interface Props {
sx?: SxProps; sx?: SxProps;
groupName: string; groupName: string;
showType: boolean; headState: HeadState;
sortType: ProxySortType;
filterText: string;
onLocation: () => void; onLocation: () => void;
onCheckDelay: () => void; onCheckDelay: () => void;
onShowType: (val: boolean) => void; onHeadState: (val: Partial<HeadState>) => void;
onSortType: (val: ProxySortType) => void;
onFilterText: (val: string) => void;
} }
const ProxyHead = (props: Props) => { 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 ( return (
<Box sx={{ display: "flex", alignItems: "center", ...sx }}> <Box sx={{ display: "flex", alignItems: "center", ...sx }}>
@ -54,7 +60,7 @@ const ProxyHead = (props: Props) => {
onClick={() => { onClick={() => {
// Remind the user that it is custom test url // Remind the user that it is custom test url
if (testUrl?.trim() && textState !== "filter") { if (testUrl?.trim() && textState !== "filter") {
setTextState("url"); onHeadState({ textState: "url" });
} }
props.onCheckDelay(); props.onCheckDelay();
}} }}
@ -66,7 +72,9 @@ const ProxyHead = (props: Props) => {
size="small" size="small"
color="inherit" color="inherit"
title={["sort by default", "sort by delay", "sort by name"][sortType]} 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 && <SortRounded />} {sortType === 0 && <SortRounded />}
{sortType === 1 && <AccessTimeRounded />} {sortType === 1 && <AccessTimeRounded />}
@ -77,7 +85,9 @@ const ProxyHead = (props: Props) => {
size="small" size="small"
color="inherit" color="inherit"
title="edit test url" title="edit test url"
onClick={() => setTextState((ts) => (ts === "url" ? null : "url"))} onClick={() =>
onHeadState({ textState: textState === "url" ? null : "url" })
}
> >
{textState === "url" ? ( {textState === "url" ? (
<WifiTetheringRounded /> <WifiTetheringRounded />
@ -90,7 +100,7 @@ const ProxyHead = (props: Props) => {
size="small" size="small"
color="inherit" color="inherit"
title="proxy detail" title="proxy detail"
onClick={() => props.onShowType(!showType)} onClick={() => onHeadState({ showType: !showType })}
> >
{showType ? <VisibilityRounded /> : <VisibilityOffRounded />} {showType ? <VisibilityRounded /> : <VisibilityOffRounded />}
</IconButton> </IconButton>
@ -100,7 +110,7 @@ const ProxyHead = (props: Props) => {
color="inherit" color="inherit"
title="filter" title="filter"
onClick={() => onClick={() =>
setTextState((ts) => (ts === "filter" ? null : "filter")) onHeadState({ textState: textState === "filter" ? null : "filter" })
} }
> >
{textState === "filter" ? ( {textState === "filter" ? (
@ -112,20 +122,20 @@ const ProxyHead = (props: Props) => {
{textState === "filter" && ( {textState === "filter" && (
<TextField <TextField
autoFocus autoFocus={autoFocus}
hiddenLabel hiddenLabel
value={filterText} value={filterText}
size="small" size="small"
variant="outlined" variant="outlined"
placeholder="Filter conditions" placeholder="Filter conditions"
onChange={(e) => 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 } }} sx={{ ml: 0.5, flex: "1 1 auto", input: { py: 0.65, px: 1 } }}
/> />
)} )}
{textState === "url" && ( {textState === "url" && (
<TextField <TextField
autoFocus autoFocus={autoFocus}
hiddenLabel hiddenLabel
autoSave="off" autoSave="off"
autoComplete="off" autoComplete="off"
@ -133,10 +143,7 @@ const ProxyHead = (props: Props) => {
size="small" size="small"
variant="outlined" variant="outlined"
placeholder="Test url" placeholder="Test url"
onChange={(e) => { onChange={(e) => onHeadState({ testUrl: e.target.value })}
setTestUrl(e.target.value);
delayManager.setUrl(groupName, e.target.value);
}}
sx={{ ml: 0.5, flex: "1 1 auto", input: { py: 0.65, px: 1 } }} sx={{ ml: 0.5, flex: "1 1 auto", input: { py: 0.65, px: 1 } }}
/> />
)} )}

View File

@ -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<string, Record<string, HeadState>>;
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<HeadState>(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<HeadState>) => {
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;
}

View File

@ -3,6 +3,7 @@ import i18next from "i18next";
import relativeTime from "dayjs/plugin/relativeTime"; import relativeTime from "dayjs/plugin/relativeTime";
import useSWR, { SWRConfig, useSWRConfig } from "swr"; import useSWR, { SWRConfig, useSWRConfig } from "swr";
import { useEffect } from "react"; import { useEffect } from "react";
import { useSetRecoilState } from "recoil";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Route, Routes } from "react-router-dom"; import { Route, Routes } from "react-router-dom";
import { alpha, List, Paper, ThemeProvider } from "@mui/material"; 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 { appWindow } from "@tauri-apps/api/window";
import { routers } from "./_routers"; import { routers } from "./_routers";
import { getAxios } from "../services/api"; 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 { ReactComponent as LogoSvg } from "../assets/image/logo.svg";
import LayoutItem from "../components/layout/layout-item"; import LayoutItem from "../components/layout/layout-item";
import LayoutControl from "../components/layout/layout-control"; import LayoutControl from "../components/layout/layout-control";
@ -33,6 +35,8 @@ const Layout = () => {
const { data: vergeConfig } = useSWR("getVergeConfig", getVergeConfig); const { data: vergeConfig } = useSWR("getVergeConfig", getVergeConfig);
const { theme_blur, language } = vergeConfig || {}; const { theme_blur, language } = vergeConfig || {};
const setCurrentProfile = useSetRecoilState(atomCurrentProfile);
useEffect(() => { useEffect(() => {
window.addEventListener("keydown", (e) => { window.addEventListener("keydown", (e) => {
if (e.key === "Escape") appWindow.close(); if (e.key === "Escape") appWindow.close();
@ -47,6 +51,9 @@ const Layout = () => {
// update the verge config // update the verge config
listen("verge://refresh-verge-config", () => mutate("getVergeConfig")); listen("verge://refresh-verge-config", () => mutate("getVergeConfig"));
// set current profile uid
getProfiles().then((data) => setCurrentProfile(data.current ?? ""));
}, []); }, []);
useEffect(() => { useEffect(() => {

View File

@ -1,6 +1,7 @@
import useSWR, { useSWRConfig } from "swr"; import useSWR, { useSWRConfig } from "swr";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useSetRecoilState } from "recoil";
import { Box, Button, Grid, TextField } from "@mui/material"; import { Box, Button, Grid, TextField } from "@mui/material";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
@ -10,6 +11,7 @@ import {
importProfile, importProfile,
} from "../services/cmds"; } from "../services/cmds";
import { getProxies, updateProxy } from "../services/api"; import { getProxies, updateProxy } from "../services/api";
import { atomCurrentProfile } from "../services/states";
import Notice from "../components/base/base-notice"; import Notice from "../components/base/base-notice";
import BasePage from "../components/base/base-page"; import BasePage from "../components/base/base-page";
import ProfileNew from "../components/profile/profile-new"; import ProfileNew from "../components/profile/profile-new";
@ -24,6 +26,8 @@ const ProfilePage = () => {
const [disabled, setDisabled] = useState(false); const [disabled, setDisabled] = useState(false);
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const setCurrentProfile = useSetRecoilState(atomCurrentProfile);
const { data: profiles = {} } = useSWR("getProfiles", getProfiles); const { data: profiles = {} } = useSWR("getProfiles", getProfiles);
// distinguish type // distinguish type
@ -52,6 +56,9 @@ const ProfilePage = () => {
const current = profiles.current; const current = profiles.current;
const profile = regularItems.find((p) => p.uid === current); const profile = regularItems.find((p) => p.uid === current);
setCurrentProfile(current);
if (!profile) return; if (!profile) return;
setTimeout(async () => { setTimeout(async () => {
@ -121,6 +128,7 @@ const ProfilePage = () => {
try { try {
await selectProfile(uid); await selectProfile(uid);
setCurrentProfile(uid);
mutate("getProfiles", { ...profiles, current: uid }, true); mutate("getProfiles", { ...profiles, current: uid }, true);
if (force) Notice.success("Refresh clash config", 1000); if (force) Notice.success("Refresh clash config", 1000);
} catch (err: any) { } catch (err: any) {

View File

@ -22,3 +22,9 @@ export const atomUpdateState = atom<boolean>({
key: "atomUpdateState", key: "atomUpdateState",
default: false, default: false,
}); });
// current profile uid
export const atomCurrentProfile = atom<string>({
key: "atomCurrentProfile",
default: "",
});