refactor: cleanup

This commit is contained in:
zhom committed 2026-09-22 01:17:33 +04:00
1 parent 7962e97474
commit f05aaa2cf2
31 files changed
+432 -2305

No files matched your search

-276
View File
@@ -1,276 +0,0 @@
"use client";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { open } from "@tauri-apps/plugin-dialog";
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { LoadingButton } from "@/components/loading-button";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import { translateBackendError } from "@/lib/backend-errors";
import { formatBytes } from "@/lib/format-bytes";
import { showSuccessToast } from "@/lib/toast-utils";
import type { DataRootInfo, DataRootMoveProgress } from "@/types";
/** Join a chosen folder with the app's own folder name, both separator styles. */
function resolveDestination(folder: string, appDirectoryName: string): string {
const separator = folder.includes("\\") && !folder.includes("/") ? "\\" : "/";
const trimmed = folder.replace(/[\\/]+$/, "");
return `${trimmed}${separator}${appDirectoryName}`;
}
function percentOf(progress: DataRootMoveProgress): number {
if (progress.phase === "scanning") return 0;
if (progress.phase !== "copying") return 100;
if (progress.total_bytes > 0) {
return Math.min(
100,
Math.round((progress.copied_bytes / progress.total_bytes) * 100),
);
}
if (progress.total_files > 0) {
return Math.min(
100,
Math.round((progress.copied_files / progress.total_files) * 100),
);
}
return 0;
}
export function DataRootSetting() {
const { t } = useTranslation();
const [info, setInfo] = useState<DataRootInfo | null>(null);
const [destination, setDestination] = useState<string | null>(null);
const [progress, setProgress] = useState<DataRootMoveProgress | null>(null);
const [error, setError] = useState<string | null>(null);
const [isMoving, setIsMoving] = useState(false);
const movePending = useRef(false);
const load = useCallback(async () => {
try {
setInfo(await invoke<DataRootInfo>("get_data_root_info"));
} catch (err) {
console.error("Failed to read the data directory:", err);
}
}, []);
useEffect(() => {
void load();
}, [load]);
useEffect(() => {
let unlisten: (() => void) | undefined;
void listen<DataRootMoveProgress>("data-root-move-progress", (event) => {
setProgress(event.payload);
}).then((stop) => {
unlisten = stop;
});
return () => {
unlisten?.();
};
}, []);
const handleChoose = useCallback(async () => {
if (!info) return;
try {
const picked = await open({
directory: true,
multiple: false,
title: t("settings.dataRoot.chooseTitle"),
});
if (typeof picked === "string") {
setError(null);
setDestination(resolveDestination(picked, info.app_directory_name));
}
} catch (err) {
console.error("Failed to choose a folder:", err);
setError(translateBackendError(t, err));
}
}, [info, t]);
const handleUseDefault = useCallback(async () => {
try {
setError(null);
setInfo(await invoke<DataRootInfo>("clear_data_root_choice"));
showSuccessToast(t("settings.dataRoot.usingDefault"));
} catch (err) {
console.error("Failed to clear the data directory choice:", err);
setError(translateBackendError(t, err));
}
}, [t]);
const handleMove = useCallback(async () => {
if (!destination || movePending.current) return;
movePending.current = true;
setIsMoving(true);
setError(null);
try {
const moved = await invoke<DataRootInfo>("move_data_root", {
destination,
});
setInfo(moved);
setDestination(null);
showSuccessToast(t("settings.dataRoot.moved"));
} catch (err) {
console.error("Failed to move the data directory:", err);
setError(translateBackendError(t, err));
setProgress(null);
} finally {
movePending.current = false;
setIsMoving(false);
}
}, [destination, t]);
if (!info) {
return (
<p
role="status"
data-slot="data-root-loading"
className="text-xs text-muted-foreground"
>
{t("settings.dataRoot.reading")}
</p>
);
}
const phaseLabel = progress
? t(`settings.dataRoot.phase.${progress.phase}`)
: null;
return (
<div
data-slot="data-root-setting"
className="grid gap-3 rounded-lg border p-3"
>
<div className="grid gap-1">
<span className="text-sm font-medium">
{t("settings.dataRoot.current")}
</span>
<span
data-slot="data-root-active-path"
className="font-mono text-xs break-all text-muted-foreground select-all"
>
{info.active_path}
</span>
<span
data-slot="data-root-size"
className="text-xs text-muted-foreground"
>
{t("settings.dataRoot.usage", {
size: formatBytes(info.size_bytes),
files: info.file_count,
})}
</span>
</div>
<p className="text-xs text-muted-foreground">
{t("settings.dataRoot.description")}
</p>
{info.active_path_missing && (
<div
role="alert"
data-slot="data-root-missing"
className="grid gap-2 text-xs text-warning-text"
>
<p>{t("settings.dataRoot.missing")}</p>
<Button
type="button"
variant="outline"
size="sm"
className="justify-self-start"
data-slot="data-root-use-default"
disabled={isMoving}
onClick={() => void handleUseDefault()}
>
{t("settings.dataRoot.useDefault")}
</Button>
</div>
)}
{info.overridden_by_environment && (
<p
role="status"
data-slot="data-root-overridden"
className="text-xs text-warning-text"
>
{t("settings.dataRoot.environmentOverride")}
</p>
)}
<div className="flex flex-wrap items-center gap-2">
<Button
type="button"
variant="outline"
size="sm"
data-slot="data-root-choose"
disabled={isMoving}
onClick={() => void handleChoose()}
>
{t("settings.dataRoot.choose")}
</Button>
<LoadingButton
size="sm"
data-slot="data-root-move"
isLoading={isMoving}
disabled={destination === null || isMoving}
onClick={() => void handleMove()}
>
{t("settings.dataRoot.move")}
</LoadingButton>
</div>
{destination && (
<p className="text-xs text-muted-foreground">
{t("settings.dataRoot.willMoveTo")}{" "}
<span
data-slot="data-root-destination"
className="font-mono break-all text-foreground"
>
{destination}
</span>
</p>
)}
{progress && isMoving && (
<div
role="status"
aria-live="polite"
data-slot="data-root-progress"
className="grid gap-1.5"
>
<Progress value={percentOf(progress)} />
<span className="text-xs text-muted-foreground">
{phaseLabel}
{progress.phase === "copying" &&
` ${t("settings.dataRoot.copiedCount", {
copied: progress.copied_files,
total: progress.total_files,
})}`}
</span>
</div>
)}
{info.restart_required && (
<p
role="status"
data-slot="data-root-restart-required"
className="text-xs text-warning-text"
>
{t("settings.dataRoot.restartRequired")}
</p>
)}
{error && (
<p
role="alert"
data-slot="data-root-error"
className="text-xs text-destructive-text"
>
{error}
</p>
)}
</div>
);
}
-70
View File
@@ -8,7 +8,6 @@ import { motion, useReducedMotion } from "motion/react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { BsCamera, BsMic } from "react-icons/bs";
import { DataRootSetting } from "@/components/data-root-setting";
import { DnsBlocklistDialog } from "@/components/dns-blocklist-dialog";
import { LoadingButton } from "@/components/loading-button";
import { useTheme } from "@/components/theme-provider";
@@ -83,17 +82,6 @@ interface AppSettings {
keep_decrypted_profiles_in_ram?: boolean;
fingerprint_gate_disabled?: boolean;
vpn_extension_warning_disabled?: boolean;
trash_retention_days?: number;
}
/** Retention choices offered for the trash, in days. The backend clamps to 1..=365. */
const TRASH_RETENTION_OPTIONS = [7, 14, 30, 90, 365];
const DEFAULT_TRASH_RETENTION_DAYS = 30;
/** The offered choices, plus the stored value when it is not one of them. */
function trashRetentionOptions(current: number): number[] {
if (TRASH_RETENTION_OPTIONS.includes(current)) return TRASH_RETENTION_OPTIONS;
return [...TRASH_RETENTION_OPTIONS, current].sort((a, b) => a - b);
}
interface CustomThemeState {
@@ -844,10 +832,6 @@ export function SettingsDialog({
([key]) => Boolean(settings[key]) !== Boolean(originalSettings[key]),
)
.map(([, key]) => t(key)),
...((settings.trash_retention_days ?? DEFAULT_TRASH_RETENTION_DAYS) !==
(originalSettings.trash_retention_days ?? DEFAULT_TRASH_RETENTION_DAYS)
? [t("settings.trashRetention")]
: []),
];
const hasChanges = pendingChanges.length > 0;
const sections = [
@@ -875,7 +859,6 @@ export function SettingsDialog({
["dns", "dnsBlocklist.title", ["dnsBlocklist"]],
["encryption", "settings.encryption.title", ["settings.encryption"]],
["commercial", "settings.commercial.title", ["settings.commercial"]],
["dataRoot", "settings.dataRoot.title", ["settings.dataRoot"]],
[
"advanced",
"settings.advanced.title",
@@ -885,8 +868,6 @@ export function SettingsDialog({
"settings.disableAutoUpdatesDescription",
"settings.keepDecryptedProfilesInRam",
"settings.keepDecryptedProfilesInRamDescription",
"settings.trashRetention",
"settings.trashRetentionDescription",
"settings.privacy",
],
],
@@ -1656,19 +1637,6 @@ export function SettingsDialog({
</div>
</div>
{/* Data directory Section */}
<div
data-settings-section="dataRoot"
tabIndex={-1}
hidden={!sectionVisible("dataRoot")}
className="scroll-mt-2 space-y-4 focus:outline-none"
>
<Label className="text-base font-medium">
{t("settings.dataRoot.title")}
</Label>
<DataRootSetting />
</div>
{/* Advanced Section */}
<div
data-settings-section="advanced"
@@ -1730,44 +1698,6 @@ export function SettingsDialog({
</div>
</div>
<div className="grid gap-2 rounded-lg border p-3">
<Label
htmlFor="trash-retention-select"
className="text-sm font-medium"
>
{t("settings.trashRetention")}
</Label>
<Select
value={String(
settings.trash_retention_days ??
DEFAULT_TRASH_RETENTION_DAYS,
)}
onValueChange={(value) => {
updateSetting("trash_retention_days", Number(value));
}}
>
<SelectTrigger
id="trash-retention-select"
data-slot="trash-retention-select"
>
<SelectValue />
</SelectTrigger>
<SelectContent>
{trashRetentionOptions(
settings.trash_retention_days ??
DEFAULT_TRASH_RETENTION_DAYS,
).map((days) => (
<SelectItem key={days} value={String(days)}>
{t("settings.trashRetentionDays", { count: days })}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
{t("settings.trashRetentionDescription")}
</p>
</div>
<LoadingButton
isLoading={isClearingCache}
onClick={() => {