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
This commit is contained in:
@@ -18,6 +18,10 @@ import {
|
||||
LocalFireDepartmentRounded,
|
||||
RefreshRounded,
|
||||
TextSnippetOutlined,
|
||||
CheckBoxOutlineBlankRounded,
|
||||
CheckBoxRounded,
|
||||
IndeterminateCheckBoxRounded,
|
||||
DeleteRounded,
|
||||
} from "@mui/icons-material";
|
||||
import { LoadingButton } from "@mui/lab";
|
||||
import { Box, Button, Divider, Grid, IconButton, Stack } from "@mui/material";
|
||||
@@ -104,6 +108,12 @@ const ProfilePage = () => {
|
||||
const [activatings, setActivatings] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Batch selection states
|
||||
const [batchMode, setBatchMode] = useState(false);
|
||||
const [selectedProfiles, setSelectedProfiles] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
|
||||
// 防止重复切换
|
||||
const switchingProfileRef = useRef<string | null>(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={
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title={t("Update All Profiles")}
|
||||
onClick={onUpdateAll}
|
||||
>
|
||||
<RefreshRounded />
|
||||
</IconButton>
|
||||
{!batchMode ? (
|
||||
<>
|
||||
{/* Batch mode toggle button */}
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title={t("Batch Operations")}
|
||||
onClick={toggleBatchMode}
|
||||
>
|
||||
<CheckBoxOutlineBlankRounded />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title={t("View Runtime Config")}
|
||||
onClick={() => configRef.current?.open()}
|
||||
>
|
||||
<TextSnippetOutlined />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title={t("Update All Profiles")}
|
||||
onClick={onUpdateAll}
|
||||
>
|
||||
<RefreshRounded />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
title={t("Reactivate Profiles")}
|
||||
onClick={() => onEnhance(true)}
|
||||
>
|
||||
<LocalFireDepartmentRounded />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title={t("View Runtime Config")}
|
||||
onClick={() => configRef.current?.open()}
|
||||
>
|
||||
<TextSnippetOutlined />
|
||||
</IconButton>
|
||||
|
||||
{/* 故障检测和紧急恢复按钮 */}
|
||||
{(error || isStale) && (
|
||||
<IconButton
|
||||
size="small"
|
||||
color="warning"
|
||||
title="数据异常,点击强制刷新"
|
||||
onClick={onEmergencyRefresh}
|
||||
sx={{
|
||||
animation: "pulse 2s infinite",
|
||||
"@keyframes pulse": {
|
||||
"0%": { opacity: 1 },
|
||||
"50%": { opacity: 0.5 },
|
||||
"100%": { opacity: 1 },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ClearRounded />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
title={t("Reactivate Profiles")}
|
||||
onClick={() => onEnhance(true)}
|
||||
>
|
||||
<LocalFireDepartmentRounded />
|
||||
</IconButton>
|
||||
|
||||
{/* 故障检测和紧急恢复按钮 */}
|
||||
{(error || isStale) && (
|
||||
<IconButton
|
||||
size="small"
|
||||
color="warning"
|
||||
title="数据异常,点击强制刷新"
|
||||
onClick={onEmergencyRefresh}
|
||||
sx={{
|
||||
animation: "pulse 2s infinite",
|
||||
"@keyframes pulse": {
|
||||
"0%": { opacity: 1 },
|
||||
"50%": { opacity: 0.5 },
|
||||
"100%": { opacity: 1 },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ClearRounded />
|
||||
</IconButton>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// Batch mode header
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title={isAllSelected() ? t("Deselect All") : t("Select All")}
|
||||
onClick={
|
||||
isAllSelected() ? clearAllSelections : selectAllProfiles
|
||||
}
|
||||
>
|
||||
{getSelectionState() === "all" ? (
|
||||
<CheckBoxRounded />
|
||||
) : getSelectionState() === "partial" ? (
|
||||
<IndeterminateCheckBoxRounded />
|
||||
) : (
|
||||
<CheckBoxOutlineBlankRounded />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
title={t("Delete Selected Profiles")}
|
||||
onClick={deleteSelectedProfiles}
|
||||
disabled={selectedProfiles.size === 0}
|
||||
>
|
||||
<DeleteRounded />
|
||||
</IconButton>
|
||||
<Button size="small" variant="outlined" onClick={toggleBatchMode}>
|
||||
{t("Done")}
|
||||
</Button>
|
||||
<Box
|
||||
sx={{ flex: 1, textAlign: "right", color: "text.secondary" }}
|
||||
>
|
||||
{t("Selected")} {selectedProfiles.size} {t("items")}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
@@ -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)}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user