mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-10-20 06:28:19 +02:00
refactor: cleanup
This commit is contained in:
1 parent
7962e97474
commit
f05aaa2cf2
31 files changed
+432
-2305
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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={() => {
|
||||
|
||||
Reference in new issue
Block a user