From 72aa56007ca987432648f19fc415764a9fa11bc4 Mon Sep 17 00:00:00 2001 From: Sline Date: Wed, 8 Oct 2025 12:02:55 +0800 Subject: [PATCH] feat(ui): implement profiles batch select and i18n (#4972) * feat(ui): implement profiles batch select and i18n * refactor: adjust button position and icon * style: lint fmt --- src/components/profile/profile-item.tsx | 62 +++++- src/locales/en.json | 10 +- src/locales/jp.json | 10 +- src/locales/ru.json | 10 +- src/locales/tr.json | 10 +- src/locales/zh.json | 10 +- src/pages/profiles.tsx | 238 +++++++++++++++++++----- 7 files changed, 295 insertions(+), 55 deletions(-) diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx index a7b3a30c..876466fb 100644 --- a/src/components/profile/profile-item.tsx +++ b/src/components/profile/profile-item.tsx @@ -1,6 +1,11 @@ import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; -import { RefreshRounded, DragIndicatorRounded } from "@mui/icons-material"; +import { + RefreshRounded, + DragIndicatorRounded, + CheckBoxRounded, + CheckBoxOutlineBlankRounded, +} from "@mui/icons-material"; import { Box, Typography, @@ -49,11 +54,24 @@ interface Props { onEdit: () => void; onSave?: (prev?: string, curr?: string) => void; onDelete: () => void; + batchMode?: boolean; + isSelected?: boolean; + onSelectionChange?: () => void; } export const ProfileItem = (props: Props) => { - const { selected, activating, itemData, onSelect, onEdit, onSave, onDelete } = - props; + const { + selected, + activating, + itemData, + onSelect, + onEdit, + onSave, + onDelete, + batchMode, + isSelected, + onSelectionChange, + } = props; const { attributes, listeners, @@ -363,7 +381,12 @@ export const ProfileItem = (props: Props) => { label: "Delete", handler: () => { setAnchorEl(null); - setConfirmOpen(true); + if (batchMode) { + // If in batch mode, just toggle selection instead of showing delete confirmation + onSelectionChange && onSelectionChange(); + } else { + setConfirmOpen(true); + } }, disabled: false, }, @@ -402,7 +425,12 @@ export const ProfileItem = (props: Props) => { label: "Delete", handler: () => { setAnchorEl(null); - setConfirmOpen(true); + if (batchMode) { + // If in batch mode, just toggle selection instead of showing delete confirmation + onSelectionChange && onSelectionChange(); + } else { + setConfirmOpen(true); + } }, disabled: false, }, @@ -510,9 +538,29 @@ export const ProfileItem = (props: Props) => { )} + {batchMode && ( + { + e.stopPropagation(); + onSelectionChange && onSelectionChange(); + }} + > + {isSelected ? ( + + ) : ( + + )} + + )} @@ -527,7 +575,7 @@ export const ProfileItem = (props: Props) => { { const [activatings, setActivatings] = useState([]); const [loading, setLoading] = useState(false); + // Batch selection states + const [batchMode, setBatchMode] = useState(false); + const [selectedProfiles, setSelectedProfiles] = useState>( + new Set(), + ); + // 防止重复切换 const switchingProfileRef = useRef(null); @@ -648,6 +658,88 @@ const ProfilePage = () => { if (text) setUrl(text); }; + // Batch selection functions + const toggleBatchMode = () => { + setBatchMode(!batchMode); + if (!batchMode) { + // Entering batch mode - clear previous selections + setSelectedProfiles(new Set()); + } + }; + + const toggleProfileSelection = (uid: string) => { + setSelectedProfiles((prev) => { + const newSet = new Set(prev); + if (newSet.has(uid)) { + newSet.delete(uid); + } else { + newSet.add(uid); + } + return newSet; + }); + }; + + const selectAllProfiles = () => { + setSelectedProfiles(new Set(profileItems.map((item) => item.uid))); + }; + + const clearAllSelections = () => { + setSelectedProfiles(new Set()); + }; + + const isAllSelected = () => { + return ( + profileItems.length > 0 && profileItems.length === selectedProfiles.size + ); + }; + + const getSelectionState = () => { + if (selectedProfiles.size === 0) { + return "none"; // 无选择 + } else if (selectedProfiles.size === profileItems.length) { + return "all"; // 全选 + } else { + return "partial"; // 部分选择 + } + }; + + const deleteSelectedProfiles = useLockFn(async () => { + if (selectedProfiles.size === 0) return; + + try { + // Get all currently activating profiles + const currentActivating = + profiles.current && selectedProfiles.has(profiles.current) + ? [profiles.current] + : []; + + setActivatings((prev) => [...new Set([...prev, ...currentActivating])]); + + // Delete all selected profiles + for (const uid of selectedProfiles) { + await deleteProfile(uid); + } + + await mutateProfiles(); + await mutateLogs(); + + // If any deleted profile was current, enhance profiles + if (currentActivating.length > 0) { + await onEnhance(false); + } + + // Clear selections and exit batch mode + setSelectedProfiles(new Set()); + setBatchMode(false); + + showNotice("success", t("Selected profiles deleted successfully")); + } catch (err: any) { + showNotice("error", err?.message || err.toString()); + } finally { + setActivatings([]); + } + }); + const mode = useThemeMode(); const islight = mode === "light" ? true : false; const dividercolor = islight @@ -714,51 +806,102 @@ const ProfilePage = () => { contentStyle={{ height: "100%" }} header={ - - - + {!batchMode ? ( + <> + {/* Batch mode toggle button */} + + + - configRef.current?.open()} - > - - + + + - onEnhance(true)} - > - - + configRef.current?.open()} + > + + - {/* 故障检测和紧急恢复按钮 */} - {(error || isStale) && ( - - - + onEnhance(true)} + > + + + + {/* 故障检测和紧急恢复按钮 */} + {(error || isStale) && ( + + + + )} + + ) : ( + // Batch mode header + + + {getSelectionState() === "all" ? ( + + ) : getSelectionState() === "partial" ? ( + + ) : ( + + )} + + + + + + + {t("Selected")} {selectedProfiles.size} {t("items")} + + )} } @@ -861,7 +1004,16 @@ const ProfilePage = () => { // Notice.success(t("Clash Core Restarted"), 1000); } }} - onDelete={() => onDelete(item.uid)} + onDelete={() => { + if (batchMode) { + toggleProfileSelection(item.uid); + } else { + onDelete(item.uid); + } + }} + batchMode={batchMode} + isSelected={selectedProfiles.has(item.uid)} + onSelectionChange={() => toggleProfileSelection(item.uid)} /> ))}