feat: save proxy page state
This commit is contained in:
parent
3b5993652f
commit
7fa3c1e12a
@ -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<ProxySortType>(0);
|
||||
const [filterText, setFilterText] = useState("");
|
||||
const [headState, setHeadState] = useHeadState(groupName);
|
||||
|
||||
const virtuosoRef = useRef<any>();
|
||||
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) => {
|
||||
<>
|
||||
<ProxyHead
|
||||
sx={{ px: 3, my: 0.5, button: { mr: 0.5 } }}
|
||||
showType={showType}
|
||||
sortType={sortType}
|
||||
groupName={groupName}
|
||||
filterText={filterText}
|
||||
headState={headState}
|
||||
onLocation={onLocation}
|
||||
onCheckDelay={onCheckAll}
|
||||
onShowType={setShowType}
|
||||
onSortType={setSortType}
|
||||
onFilterText={setFilterText}
|
||||
onHeadState={setHeadState}
|
||||
/>
|
||||
|
||||
<Virtuoso
|
||||
@ -122,7 +125,7 @@ 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 }}
|
||||
/>
|
||||
|
@ -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<ProxySortType>(0);
|
||||
const [filterText, setFilterText] = useState("");
|
||||
const [headState, setHeadState] = useHeadState(group.name);
|
||||
|
||||
const virtuosoRef = useRef<any>();
|
||||
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 (
|
||||
<>
|
||||
<ListItem button onClick={() => setOpen(!open)} dense>
|
||||
<ListItem
|
||||
button
|
||||
dense
|
||||
onClick={() => setHeadState({ open: !headState.open })}
|
||||
>
|
||||
<ListItemText
|
||||
primary={group.name}
|
||||
secondary={
|
||||
@ -120,21 +130,17 @@ const ProxyGroup = ({ group }: Props) => {
|
||||
}}
|
||||
/>
|
||||
|
||||
{open ? <ExpandLessRounded /> : <ExpandMoreRounded />}
|
||||
{headState.open ? <ExpandLessRounded /> : <ExpandMoreRounded />}
|
||||
</ListItem>
|
||||
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<Collapse in={headState.open} timeout="auto" unmountOnExit>
|
||||
<ProxyHead
|
||||
sx={{ pl: 4, pr: 3, my: 0.5, button: { mr: 0.5 } }}
|
||||
showType={showType}
|
||||
sortType={sortType}
|
||||
groupName={group.name}
|
||||
filterText={filterText}
|
||||
headState={headState}
|
||||
onLocation={onLocation}
|
||||
onCheckDelay={onCheckAll}
|
||||
onShowType={setShowType}
|
||||
onSortType={setSortType}
|
||||
onFilterText={setFilterText}
|
||||
onHeadState={setHeadState}
|
||||
/>
|
||||
|
||||
{!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}
|
||||
/>
|
||||
|
@ -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<HeadState>) => 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 (
|
||||
<Box sx={{ display: "flex", alignItems: "center", ...sx }}>
|
||||
@ -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 && <SortRounded />}
|
||||
{sortType === 1 && <AccessTimeRounded />}
|
||||
@ -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" ? (
|
||||
<WifiTetheringRounded />
|
||||
@ -90,7 +100,7 @@ const ProxyHead = (props: Props) => {
|
||||
size="small"
|
||||
color="inherit"
|
||||
title="proxy detail"
|
||||
onClick={() => props.onShowType(!showType)}
|
||||
onClick={() => onHeadState({ showType: !showType })}
|
||||
>
|
||||
{showType ? <VisibilityRounded /> : <VisibilityOffRounded />}
|
||||
</IconButton>
|
||||
@ -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" && (
|
||||
<TextField
|
||||
autoFocus
|
||||
autoFocus={autoFocus}
|
||||
hiddenLabel
|
||||
value={filterText}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
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 } }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{textState === "url" && (
|
||||
<TextField
|
||||
autoFocus
|
||||
autoFocus={autoFocus}
|
||||
hiddenLabel
|
||||
autoSave="off"
|
||||
autoComplete="off"
|
||||
@ -133,10 +143,7 @@ const ProxyHead = (props: Props) => {
|
||||
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 } }}
|
||||
/>
|
||||
)}
|
||||
|
80
src/components/proxy/use-head-state.ts
Normal file
80
src/components/proxy/use-head-state.ts
Normal 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;
|
||||
}
|
@ -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(() => {
|
||||
|
@ -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) {
|
||||
|
@ -22,3 +22,9 @@ export const atomUpdateState = atom<boolean>({
|
||||
key: "atomUpdateState",
|
||||
default: false,
|
||||
});
|
||||
|
||||
// current profile uid
|
||||
export const atomCurrentProfile = atom<string>({
|
||||
key: "atomCurrentProfile",
|
||||
default: "",
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user