Files
everyone-can-use-english/enjoy/src/renderer/components/preferences/disk-usage.tsx
2024-10-20 14:50:58 +08:00

295 lines
8.6 KiB
TypeScript

import { t } from "i18next";
import {
AlertDialog,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
Badge,
Button,
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
Separator,
ScrollArea,
RadioGroup,
RadioGroupItem,
Label,
Progress,
toast,
} from "@renderer/components/ui";
import { useContext, useEffect, useState } from "react";
import { AppSettingsProviderContext } from "@/renderer/context";
import { humanFileSize } from "@/utils";
import { LoaderIcon } from "lucide-react";
export const DiskUsage = () => {
return (
<div className="flex items-start justify-between py-4">
<div className="">
<div className="mb-2">{t("diskUsage")}</div>
<div className="text-sm text-muted-foreground mb-2">
{t("diskUsageDescription")}
</div>
</div>
<div className="">
<div className="mb-2 flex items-center space-x-2 justify-end">
<UsageDetail />
<ReleaseDiskSpace />
</div>
</div>
</div>
);
};
const UsageDetail = () => {
const [open, setOpen] = useState(false);
const [usage, setUsage] = useState<DiskUsageType>([]);
const [loading, setLoading] = useState(false);
const { EnjoyApp } = useContext(AppSettingsProviderContext);
const openPath = async (filePath: string) => {
if (filePath?.match(/.+\.json$/)) {
await EnjoyApp.shell.openPath(filePath.split("/").slice(0, -1).join("/"));
} else if (filePath) {
await EnjoyApp.shell.openPath(filePath);
}
};
useEffect(() => {
if (open) {
setLoading(true);
EnjoyApp.app
.diskUsage()
.then((usage) => {
setUsage(usage);
})
.catch((err) => {
toast.error(err.message);
})
.finally(() => {
setLoading(false);
});
}
}, [open]);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="secondary" size="sm">
{t("detail")}
</Button>
</DialogTrigger>
<DialogContent className={loading ? "" : "h-3/5"}>
{loading && (
<div className="flex items-center justify-center">
<LoaderIcon className="w-6 h-6 animate-spin" />
</div>
)}
{!loading && (
<>
<DialogHeader>
<DialogTitle>{t("diskUsage")}</DialogTitle>
<DialogDescription className="sr-only">
{t("diskUsageDescription")}
</DialogDescription>
</DialogHeader>
<div className="h-full overflow-hidden">
<ScrollArea className="h-full px-4">
<div className="grid gap-4">
{usage.map((item) => (
<div key={item.name}>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center space-x-2 mb-2">
<Badge>/{item.path.split("/").pop()}</Badge>
<div className="text-sm text-muted-foreground">
{humanFileSize(item.size)}
</div>
</div>
<div className="text-sm">
{t(`libraryDescriptions.${item.name}`)}
</div>
</div>
<Button
onClick={() => openPath(item.path)}
variant="secondary"
size="sm"
>
{t("open")}
</Button>
</div>
<Separator className="my-2" />
</div>
))}
</div>
</ScrollArea>
</div>
</>
)}
</DialogContent>
</Dialog>
);
};
const ReleaseDiskSpace = () => {
const [open, setOpen] = useState(false);
const [abortController, setAbortController] =
useState<AbortController | null>(null);
const [deleteBulkType, setDeleteBulkType] = useState("noAssessment");
const [stats, setStats] = useState<{
noAssessment: string[];
scoreLessThan90: string[];
scoreLessThan80: string[];
all: string[];
}>({
noAssessment: [],
scoreLessThan90: [],
scoreLessThan80: [],
all: [],
});
const [deleting, setDeleting] = useState(false);
const [deleted, setDeleted] = useState<string[]>([]);
const { EnjoyApp } = useContext(AppSettingsProviderContext);
const refreshStats = () => {
EnjoyApp.recordings.statsForDeleteBulk().then((s) => {
setStats(s);
});
};
const handleDestroyBulk = async () => {
const pendings = stats[deleteBulkType as keyof typeof stats];
if (pendings.length === 0) {
toast.warning(t("noRecordingsToDelete"));
return;
}
setDeleting(true);
const controller = new AbortController();
setAbortController(controller);
// seperate pendings into chunks of 100
const chunks = [];
for (let i = 0; i < pendings.length; i += 100) {
chunks.push(pendings.slice(i, i + 100));
}
try {
for (const chunk of chunks) {
if (controller.signal.aborted) {
break;
}
await EnjoyApp.recordings.destroyBulk({
ids: chunk,
});
setDeleted((prev) => [...prev, ...chunk]);
}
} catch (error) {
if (error.name === "AbortError") {
toast.warning(t("bulkDeleteAborted"));
} else {
toast.error(t(error.message));
}
} finally {
refreshStats();
setDeleting(false);
setAbortController(null);
}
};
useEffect(() => {
if (open) {
refreshStats();
} else {
setDeleted([]);
}
}, [open]);
useEffect(() => {
return () => {
abortController?.abort();
};
}, [abortController]);
return (
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogTrigger asChild>
<Button
variant="secondary"
className="text-destructive hover:bg-destructive hover:text-white"
size="sm"
>
{t("releaseDiskSpace")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("bulkDeleteRecordings")}</AlertDialogTitle>
<AlertDialogDescription className="mb-4">
{t("bulkDeleteRecordingsConfirmation")}
</AlertDialogDescription>
</AlertDialogHeader>
<RadioGroup
className="mb-4"
value={deleteBulkType}
onValueChange={(value) => setDeleteBulkType(value)}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="noAssessment" id="noAssessment" />
<Label htmlFor="noAssessment">
{t("deleteRecordingsWithoutAssessment")}(
{stats.noAssessment.length})
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="scoreLessThan90" id="scoreLessThan90" />
<Label htmlFor="scoreLessThan90">
{t("deleteRecordingsWithScoreLessThan90")}(
{stats.scoreLessThan90.length})
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="scoreLessThan80" id="scoreLessThan80" />
<Label htmlFor="scoreLessThan80">
{t("deleteRecordingsWithScoreLessThan80")}(
{stats.scoreLessThan80.length})
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="all" id="all" />
<Label htmlFor="all" className="text-destructive">
{t("deleteAllRecordings")}({stats.all.length})
</Label>
</div>
</RadioGroup>
{deleting && (
<div className="mb-4">
<Progress
value={deleted.length}
max={stats[deleteBulkType as keyof typeof stats].length}
className="mb-4"
/>
</div>
)}
<AlertDialogFooter>
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
<Button disabled={deleting} onClick={handleDestroyBulk}>
{deleting && <LoaderIcon className="w-4 h-4 mr-2 animate-spin" />}
{deleting ? t("deleting") : t("delete")}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};