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
@@ -2048,6 +2048,7 @@ export default function Home() {
|
||||
let unlistenProgress: (() => void) | undefined;
|
||||
let unlistenCompleted: (() => void) | undefined;
|
||||
let unlistenWayfernBlocked: (() => void) | undefined;
|
||||
let unlistenGenerationLimit: (() => void) | undefined;
|
||||
let unlistenMcpLocalDeprecated: (() => void) | undefined;
|
||||
let unlistenMcpLocalMigrated: (() => void) | undefined;
|
||||
|
||||
@@ -2112,6 +2113,19 @@ export default function Home() {
|
||||
});
|
||||
});
|
||||
|
||||
unlistenGenerationLimit = await listen(
|
||||
"profile-generation-limit-reached",
|
||||
() => {
|
||||
showToast({
|
||||
id: "profile-generation-limit-reached",
|
||||
type: "error",
|
||||
title: t("profileGenerationLimit.title"),
|
||||
description: t("profileGenerationLimit.description"),
|
||||
duration: 15000,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
unlistenWayfernBlocked = await listen("wayfern-paid-blocked", () => {
|
||||
showToast({
|
||||
id: "wayfern-paid-blocked",
|
||||
@@ -2160,6 +2174,7 @@ export default function Home() {
|
||||
unlistenProgress?.();
|
||||
unlistenCompleted?.();
|
||||
unlistenWayfernBlocked?.();
|
||||
unlistenGenerationLimit?.();
|
||||
unlistenMcpLocalDeprecated?.();
|
||||
unlistenMcpLocalMigrated?.();
|
||||
}
|
||||
@@ -2172,6 +2187,7 @@ export default function Home() {
|
||||
unlistenProgress?.();
|
||||
unlistenCompleted?.();
|
||||
unlistenWayfernBlocked?.();
|
||||
unlistenGenerationLimit?.();
|
||||
unlistenMcpLocalDeprecated?.();
|
||||
unlistenMcpLocalMigrated?.();
|
||||
};
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -2282,6 +2282,7 @@
|
||||
"wayfernPlanCheckUnavailable": "Could not check your plan right now. Try again in a moment.",
|
||||
"wayfernCustomFingerprintRequiresPlan": "Your plan does not include custom fingerprints.",
|
||||
"wayfernGenerationUnavailable": "Could not create a fingerprint right now. Try again later.",
|
||||
"profileGenerationLimitReached": "You have created 100 profiles in the last hour, which is the limit on Pro. Wait about {{minutes}} minutes before creating another one.",
|
||||
"wayfernBrowserBusy": "The browser was busy applying a fingerprint. Try again.",
|
||||
"wayfernBrowserExited": "The browser closed while starting. {{detail}}",
|
||||
"profileExportFailed": "This profile could not be exported. {{detail}}",
|
||||
@@ -2580,6 +2581,10 @@
|
||||
"title": "Browser automation paused",
|
||||
"description": "Browser automation runs on one device at a time, and another signed-in device is currently using it. Sign out there, then relaunch the profile."
|
||||
},
|
||||
"profileGenerationLimit": {
|
||||
"title": "Hourly profile limit reached",
|
||||
"description": "Your Pro plan allows 100 new profiles per hour. Creation is paused until the hour rolls over, so your account is not blocked."
|
||||
},
|
||||
"wayfernConfigDialog": {
|
||||
"titleView": "View Fingerprint Settings - {{name}} ({{browser}})",
|
||||
"titleConfigure": "Configure Fingerprint Settings - {{name}} ({{browser}})",
|
||||
|
||||
@@ -2292,6 +2292,7 @@
|
||||
"wayfernPlanCheckUnavailable": "No se pudo comprobar tu plan ahora mismo. Inténtalo de nuevo en un momento.",
|
||||
"wayfernCustomFingerprintRequiresPlan": "Tu plan no incluye huellas digitales personalizadas.",
|
||||
"wayfernGenerationUnavailable": "No se pudo crear una huella digital ahora mismo. Inténtalo de nuevo más tarde.",
|
||||
"profileGenerationLimitReached": "Has creado 100 perfiles en la última hora, el límite del plan Pro. Espera unos {{minutes}} minutos antes de crear otro.",
|
||||
"wayfernBrowserBusy": "El navegador estaba ocupado aplicando una huella digital. Inténtalo de nuevo.",
|
||||
"wayfernBrowserExited": "El navegador se cerró mientras se iniciaba. {{detail}}",
|
||||
"profileExportFailed": "No se pudo exportar este perfil. {{detail}}",
|
||||
@@ -2590,6 +2591,10 @@
|
||||
"title": "Automatización del navegador en pausa",
|
||||
"description": "La automatización del navegador funciona en un solo dispositivo a la vez y otro dispositivo con la sesión abierta la está usando. Cierra la sesión allí y vuelve a iniciar el perfil."
|
||||
},
|
||||
"profileGenerationLimit": {
|
||||
"title": "Límite de perfiles por hora alcanzado",
|
||||
"description": "Tu plan Pro permite 100 perfiles nuevos por hora. La creación está en pausa hasta que avance la hora, para que tu cuenta no se bloquee."
|
||||
},
|
||||
"wayfernConfigDialog": {
|
||||
"titleView": "Ver configuración de huella - {{name}} ({{browser}})",
|
||||
"titleConfigure": "Configurar huella - {{name}} ({{browser}})",
|
||||
|
||||
@@ -2292,6 +2292,7 @@
|
||||
"wayfernPlanCheckUnavailable": "Impossible de vérifier votre forfait pour le moment. Réessayez dans un instant.",
|
||||
"wayfernCustomFingerprintRequiresPlan": "Votre forfait n'inclut pas les empreintes personnalisées.",
|
||||
"wayfernGenerationUnavailable": "Impossible de créer une empreinte pour le moment. Réessayez plus tard.",
|
||||
"profileGenerationLimitReached": "Vous avez créé 100 profils au cours de la dernière heure, la limite de l'offre Pro. Attendez environ {{minutes}} minutes avant d'en créer un autre.",
|
||||
"wayfernBrowserBusy": "Le navigateur était occupé à appliquer une empreinte. Réessayez.",
|
||||
"wayfernBrowserExited": "Le navigateur s'est fermé pendant le démarrage. {{detail}}",
|
||||
"profileExportFailed": "Ce profil n'a pas pu être exporté. {{detail}}",
|
||||
@@ -2590,6 +2591,10 @@
|
||||
"title": "Automatisation du navigateur en pause",
|
||||
"description": "L'automatisation du navigateur fonctionne sur un seul appareil à la fois, et un autre appareil connecté l'utilise actuellement. Déconnectez-vous sur cet appareil, puis relancez le profil."
|
||||
},
|
||||
"profileGenerationLimit": {
|
||||
"title": "Limite horaire de profils atteinte",
|
||||
"description": "Votre offre Pro permet 100 nouveaux profils par heure. La création est suspendue jusqu'au renouvellement de la fenêtre, afin que votre compte ne soit pas bloqué."
|
||||
},
|
||||
"wayfernConfigDialog": {
|
||||
"titleView": "Voir les paramètres d'empreinte - {{name}} ({{browser}})",
|
||||
"titleConfigure": "Configurer les paramètres d'empreinte - {{name}} ({{browser}})",
|
||||
|
||||
@@ -2282,6 +2282,7 @@
|
||||
"wayfernPlanCheckUnavailable": "現在、ご利用のプランを確認できません。しばらくしてからもう一度お試しください。",
|
||||
"wayfernCustomFingerprintRequiresPlan": "ご利用のプランにはカスタムフィンガープリントが含まれていません。",
|
||||
"wayfernGenerationUnavailable": "現在、フィンガープリントを作成できません。しばらくしてから再度お試しください。",
|
||||
"profileGenerationLimitReached": "過去1時間に100件のプロファイルを作成しました。これはProプランの上限です。次の作成まで約{{minutes}}分お待ちください。",
|
||||
"wayfernBrowserBusy": "ブラウザーがフィンガープリントを適用中でした。もう一度お試しください。",
|
||||
"wayfernBrowserExited": "ブラウザーが起動中に終了しました。{{detail}}",
|
||||
"profileExportFailed": "このプロファイルを書き出せませんでした。{{detail}}",
|
||||
@@ -2580,6 +2581,10 @@
|
||||
"title": "ブラウザの自動化が一時停止しました",
|
||||
"description": "ブラウザの自動化は同時に1台のデバイスでのみ利用できます。現在、サインイン中の別のデバイスが使用しています。そのデバイスからサインアウトして、プロファイルを再起動してください。"
|
||||
},
|
||||
"profileGenerationLimit": {
|
||||
"title": "1時間あたりのプロファイル上限に達しました",
|
||||
"description": "Proプランでは1時間に100件の新規プロファイルを作成できます。アカウントがブロックされないよう、次の時間枠まで作成を一時停止します。"
|
||||
},
|
||||
"wayfernConfigDialog": {
|
||||
"titleView": "フィンガープリント設定を表示 - {{name}} ({{browser}})",
|
||||
"titleConfigure": "フィンガープリント設定 - {{name}} ({{browser}})",
|
||||
|
||||
@@ -2282,6 +2282,7 @@
|
||||
"wayfernPlanCheckUnavailable": "지금은 요금제를 확인할 수 없습니다. 잠시 후 다시 시도하세요.",
|
||||
"wayfernCustomFingerprintRequiresPlan": "현재 요금제에는 사용자 지정 지문이 포함되어 있지 않습니다.",
|
||||
"wayfernGenerationUnavailable": "지금은 지문을 만들 수 없습니다. 나중에 다시 시도하세요.",
|
||||
"profileGenerationLimitReached": "지난 1시간 동안 프로필 100개를 만들었습니다. Pro 요금제의 한도입니다. 약 {{minutes}}분 후에 다시 시도하세요.",
|
||||
"wayfernBrowserBusy": "브라우저가 지문을 적용하는 중이었습니다. 다시 시도하세요.",
|
||||
"wayfernBrowserExited": "브라우저가 시작되는 도중에 종료되었습니다. {{detail}}",
|
||||
"profileExportFailed": "이 프로필을 내보낼 수 없습니다. {{detail}}",
|
||||
@@ -2580,6 +2581,10 @@
|
||||
"title": "브라우저 자동화가 일시 중지됨",
|
||||
"description": "브라우저 자동화는 한 번에 기기 한 대에서만 실행되며, 현재 로그인된 다른 기기가 사용 중입니다. 해당 기기에서 로그아웃한 후 프로필을 다시 실행하세요."
|
||||
},
|
||||
"profileGenerationLimit": {
|
||||
"title": "시간당 프로필 한도에 도달했습니다",
|
||||
"description": "Pro 요금제는 시간당 100개의 새 프로필을 허용합니다. 계정이 차단되지 않도록 다음 시간대까지 생성을 일시 중지합니다."
|
||||
},
|
||||
"wayfernConfigDialog": {
|
||||
"titleView": "핑거프린트 설정 보기 - {{name}} ({{browser}})",
|
||||
"titleConfigure": "핑거프린트 설정 구성 - {{name}} ({{browser}})",
|
||||
|
||||
@@ -2292,6 +2292,7 @@
|
||||
"wayfernPlanCheckUnavailable": "Não foi possível verificar o seu plano agora. Tente novamente em instantes.",
|
||||
"wayfernCustomFingerprintRequiresPlan": "O seu plano não inclui impressões digitais personalizadas.",
|
||||
"wayfernGenerationUnavailable": "Não foi possível criar uma impressão digital agora. Tente novamente mais tarde.",
|
||||
"profileGenerationLimitReached": "Você criou 100 perfis na última hora, o limite do plano Pro. Aguarde cerca de {{minutes}} minutos antes de criar outro.",
|
||||
"wayfernBrowserBusy": "O navegador estava ocupado com a aplicação de uma impressão digital. Tente novamente.",
|
||||
"wayfernBrowserExited": "O navegador fechou durante a inicialização. {{detail}}",
|
||||
"profileExportFailed": "Não foi possível exportar este perfil. {{detail}}",
|
||||
@@ -2590,6 +2591,10 @@
|
||||
"title": "Automação do navegador pausada",
|
||||
"description": "A automação do navegador funciona em um dispositivo por vez e outro dispositivo com a sessão ativa está usando-a. Saia da conta nesse dispositivo e inicie o perfil novamente."
|
||||
},
|
||||
"profileGenerationLimit": {
|
||||
"title": "Limite de perfis por hora atingido",
|
||||
"description": "O seu plano Pro permite 100 novos perfis por hora. A criação fica pausada até a janela reiniciar, para que a sua conta não seja bloqueada."
|
||||
},
|
||||
"wayfernConfigDialog": {
|
||||
"titleView": "Ver configurações de impressão digital - {{name}} ({{browser}})",
|
||||
"titleConfigure": "Configurar impressão digital - {{name}} ({{browser}})",
|
||||
|
||||
@@ -2302,6 +2302,7 @@
|
||||
"wayfernPlanCheckUnavailable": "Сейчас не удалось проверить ваш тариф. Попробуйте снова через минуту.",
|
||||
"wayfernCustomFingerprintRequiresPlan": "Ваш тариф не включает пользовательские отпечатки.",
|
||||
"wayfernGenerationUnavailable": "Сейчас не удалось создать отпечаток. Повторите попытку позже.",
|
||||
"profileGenerationLimitReached": "За последний час создано 100 профилей — это лимит тарифа Pro. Подождите около {{minutes}} мин. перед созданием следующего.",
|
||||
"wayfernBrowserBusy": "Браузер был занят применением отпечатка. Попробуйте снова.",
|
||||
"wayfernBrowserExited": "Браузер закрылся во время запуска. {{detail}}",
|
||||
"profileExportFailed": "Не удалось экспортировать этот профиль. {{detail}}",
|
||||
@@ -2600,6 +2601,10 @@
|
||||
"title": "Автоматизация браузера приостановлена",
|
||||
"description": "Автоматизация браузера работает только на одном устройстве одновременно, и сейчас её использует другое устройство с активным входом. Выйдите из аккаунта на нём и запустите профиль снова."
|
||||
},
|
||||
"profileGenerationLimit": {
|
||||
"title": "Достигнут часовой лимит профилей",
|
||||
"description": "Тариф Pro позволяет создавать 100 новых профилей в час. Создание приостановлено до начала нового часа, чтобы аккаунт не заблокировали."
|
||||
},
|
||||
"wayfernConfigDialog": {
|
||||
"titleView": "Просмотр настроек отпечатка - {{name}} ({{browser}})",
|
||||
"titleConfigure": "Настроить отпечаток - {{name}} ({{browser}})",
|
||||
|
||||
@@ -2282,6 +2282,7 @@
|
||||
"wayfernPlanCheckUnavailable": "Planınız şu anda kontrol edilemedi. Kısa süre sonra yeniden deneyin.",
|
||||
"wayfernCustomFingerprintRequiresPlan": "Planınız özel parmak izlerini içermiyor.",
|
||||
"wayfernGenerationUnavailable": "Şu anda parmak izi oluşturulamadı. Daha sonra tekrar deneyin.",
|
||||
"profileGenerationLimitReached": "Son bir saatte 100 profil oluşturdunuz; bu, Pro planının sınırıdır. Yenisini oluşturmadan önce yaklaşık {{minutes}} dakika bekleyin.",
|
||||
"wayfernBrowserBusy": "Tarayıcı bir parmak izi uygulamakla meşguldü. Tekrar deneyin.",
|
||||
"wayfernBrowserExited": "Tarayıcı başlatılırken kapandı. {{detail}}",
|
||||
"profileExportFailed": "Bu profil dışa aktarılamadı. {{detail}}",
|
||||
@@ -2580,6 +2581,10 @@
|
||||
"title": "Tarayıcı otomasyonu duraklatıldı",
|
||||
"description": "Tarayıcı otomasyonu aynı anda yalnızca tek bir cihazda çalışır ve şu anda oturum açmış başka bir cihaz tarafından kullanılıyor. O cihazdan çıkış yapın, ardından profili yeniden başlatın."
|
||||
},
|
||||
"profileGenerationLimit": {
|
||||
"title": "Saatlik profil sınırına ulaşıldı",
|
||||
"description": "Pro planınız saatte 100 yeni profile izin verir. Hesabınız engellenmesin diye oluşturma, yeni saat dilimine kadar duraklatıldı."
|
||||
},
|
||||
"wayfernConfigDialog": {
|
||||
"titleView": "Parmak İzi Ayarlarını Görüntüle - {{name}} ({{browser}})",
|
||||
"titleConfigure": "Parmak İzi Ayarlarını Yapılandır - {{name}} ({{browser}})",
|
||||
|
||||
@@ -2282,6 +2282,7 @@
|
||||
"wayfernPlanCheckUnavailable": "Hiện không thể kiểm tra gói của bạn. Hãy thử lại sau giây lát.",
|
||||
"wayfernCustomFingerprintRequiresPlan": "Gói của bạn không bao gồm dấu vân tay tùy chỉnh.",
|
||||
"wayfernGenerationUnavailable": "Hiện không thể tạo dấu vân tay. Vui lòng thử lại sau.",
|
||||
"profileGenerationLimitReached": "Bạn đã tạo 100 hồ sơ trong giờ qua, đây là giới hạn của gói Pro. Hãy đợi khoảng {{minutes}} phút trước khi tạo hồ sơ tiếp theo.",
|
||||
"wayfernBrowserBusy": "Trình duyệt đang bận áp dụng dấu vân tay. Hãy thử lại.",
|
||||
"wayfernBrowserExited": "Trình duyệt đã đóng khi đang khởi động. {{detail}}",
|
||||
"profileExportFailed": "Không thể xuất hồ sơ này. {{detail}}",
|
||||
@@ -2580,6 +2581,10 @@
|
||||
"title": "Tự động hóa trình duyệt đã tạm dừng",
|
||||
"description": "Tự động hóa trình duyệt chỉ chạy trên một thiết bị tại một thời điểm và một thiết bị khác đang đăng nhập hiện đang sử dụng nó. Hãy đăng xuất trên thiết bị đó, rồi khởi chạy lại cấu hình."
|
||||
},
|
||||
"profileGenerationLimit": {
|
||||
"title": "Đã đạt giới hạn hồ sơ mỗi giờ",
|
||||
"description": "Gói Pro cho phép tạo 100 hồ sơ mới mỗi giờ. Việc tạo hồ sơ tạm dừng cho đến khi sang giờ mới để tài khoản của bạn không bị khóa."
|
||||
},
|
||||
"wayfernConfigDialog": {
|
||||
"titleView": "Xem cài đặt vân tay - {{name}} ({{browser}})",
|
||||
"titleConfigure": "Cấu hình cài đặt vân tay - {{name}} ({{browser}})",
|
||||
|
||||
@@ -2282,6 +2282,7 @@
|
||||
"wayfernPlanCheckUnavailable": "目前无法检查你的套餐。请稍后再试。",
|
||||
"wayfernCustomFingerprintRequiresPlan": "你的套餐不包含自定义指纹。",
|
||||
"wayfernGenerationUnavailable": "目前无法创建指纹。请稍后再试。",
|
||||
"profileGenerationLimitReached": "过去一小时内你已创建 100 个配置文件,已达 Pro 套餐上限。请等待约 {{minutes}} 分钟后再创建。",
|
||||
"wayfernBrowserBusy": "浏览器正忙于应用指纹。请重试。",
|
||||
"wayfernBrowserExited": "浏览器在启动过程中关闭了。{{detail}}",
|
||||
"profileExportFailed": "无法导出此配置文件。{{detail}}",
|
||||
@@ -2580,6 +2581,10 @@
|
||||
"title": "浏览器自动化已暂停",
|
||||
"description": "浏览器自动化一次只能在一台设备上运行,目前有另一台已登录的设备正在使用。请在该设备上退出登录,然后重新启动此配置文件。"
|
||||
},
|
||||
"profileGenerationLimit": {
|
||||
"title": "已达到每小时的配置文件上限",
|
||||
"description": "Pro 套餐每小时可新建 100 个配置文件。为避免账号被封禁,新建操作将暂停到下一个小时。"
|
||||
},
|
||||
"wayfernConfigDialog": {
|
||||
"titleView": "查看指纹设置 - {{name}} ({{browser}})",
|
||||
"titleConfigure": "配置指纹设置 - {{name}} ({{browser}})",
|
||||
|
||||
+14
-45
@@ -217,6 +217,9 @@ export type BackendErrorCode =
|
||||
| "WAYFERN_PLAN_CHECK_UNAVAILABLE"
|
||||
| "WAYFERN_CUSTOM_FINGERPRINT_REQUIRES_PLAN"
|
||||
| "WAYFERN_GENERATION_UNAVAILABLE"
|
||||
// Donut's own hourly cap on Pro accounts, refused locally before the request
|
||||
// reaches Wayfern. `retryAfterSeconds` says when the window reopens.
|
||||
| "PROFILE_GENERATION_LIMIT_REACHED"
|
||||
| "WAYFERN_BROWSER_BUSY"
|
||||
| "WAYFERN_BROWSER_EXITED"
|
||||
| "PROFILE_EXPORT_FAILED"
|
||||
@@ -235,23 +238,6 @@ export type BackendErrorCode =
|
||||
| "NO_E2E_PASSWORD_SET"
|
||||
| "TRASH_ENTRY_NOT_FOUND"
|
||||
| "TRASH_RESTORE_CONFLICT"
|
||||
// Moving the data directory. Every refusal is its own code because every one
|
||||
// of them has a different fix: close the browsers, wait for the sync, pick
|
||||
// somewhere else, free some space.
|
||||
| "DATA_ROOT_SAME_AS_CURRENT"
|
||||
| "DATA_ROOT_DESTINATION_INSIDE_SOURCE"
|
||||
| "DATA_ROOT_BROWSER_RUNNING"
|
||||
| "DATA_ROOT_SYNC_IN_PROGRESS"
|
||||
// `params.required` / `params.available`, both in bytes as strings.
|
||||
| "DATA_ROOT_INSUFFICIENT_SPACE"
|
||||
| "DATA_ROOT_DESTINATION_NOT_WRITABLE"
|
||||
| "DATA_ROOT_DESTINATION_NOT_EMPTY"
|
||||
| "DATA_ROOT_MOVE_IN_PROGRESS"
|
||||
| "DATA_ROOT_COPY_FAILED"
|
||||
// The copy did not match the source, so nothing was deleted. Its own code
|
||||
// rather than a copy failure: the user's data is still where it was, and
|
||||
// saying so is the whole point of verifying before deleting.
|
||||
| "DATA_ROOT_VERIFY_FAILED"
|
||||
// Synchronised windows.
|
||||
| "SYNC_SESSION_NOT_FOUND"
|
||||
| "SYNC_FOLLOWER_NOT_FOUND"
|
||||
@@ -771,6 +757,17 @@ export function translateBackendError(t: TFunction, err: unknown): string {
|
||||
return t("backendErrors.wayfernCustomFingerprintRequiresPlan");
|
||||
case "WAYFERN_GENERATION_UNAVAILABLE":
|
||||
return t("backendErrors.wayfernGenerationUnavailable");
|
||||
case "PROFILE_GENERATION_LIMIT_REACHED": {
|
||||
const seconds = Number.parseInt(
|
||||
parsed.params?.retryAfterSeconds ?? "",
|
||||
10,
|
||||
);
|
||||
return t("backendErrors.profileGenerationLimitReached", {
|
||||
minutes: Number.isFinite(seconds)
|
||||
? Math.max(1, Math.ceil(seconds / 60))
|
||||
: 60,
|
||||
});
|
||||
}
|
||||
case "WAYFERN_BROWSER_BUSY":
|
||||
return t("backendErrors.wayfernBrowserBusy");
|
||||
case "WAYFERN_BROWSER_EXITED":
|
||||
@@ -817,34 +814,6 @@ export function translateBackendError(t: TFunction, err: unknown): string {
|
||||
return t("backendErrors.trashEntryNotFound");
|
||||
case "TRASH_RESTORE_CONFLICT":
|
||||
return t("backendErrors.trashRestoreConflict");
|
||||
case "DATA_ROOT_SAME_AS_CURRENT":
|
||||
return t("backendErrors.dataRootSameAsCurrent");
|
||||
case "DATA_ROOT_DESTINATION_INSIDE_SOURCE":
|
||||
return t("backendErrors.dataRootDestinationInsideSource");
|
||||
case "DATA_ROOT_BROWSER_RUNNING":
|
||||
return t("backendErrors.dataRootBrowserRunning");
|
||||
case "DATA_ROOT_SYNC_IN_PROGRESS":
|
||||
return t("backendErrors.dataRootSyncInProgress");
|
||||
// The figures arrive already written out. This module is imported by a
|
||||
// plain `node --test` run, so it stays free of project imports, and the
|
||||
// one place that knows the byte counts formats them.
|
||||
case "DATA_ROOT_INSUFFICIENT_SPACE":
|
||||
return t("backendErrors.dataRootInsufficientSpace", {
|
||||
required: parsed.params?.required ?? "",
|
||||
available: parsed.params?.available ?? "",
|
||||
});
|
||||
case "DATA_ROOT_DESTINATION_NOT_WRITABLE":
|
||||
return t("backendErrors.dataRootDestinationNotWritable");
|
||||
case "DATA_ROOT_DESTINATION_NOT_EMPTY":
|
||||
return t("backendErrors.dataRootDestinationNotEmpty");
|
||||
case "DATA_ROOT_MOVE_IN_PROGRESS":
|
||||
return t("backendErrors.dataRootMoveInProgress");
|
||||
case "DATA_ROOT_COPY_FAILED":
|
||||
return t("backendErrors.dataRootCopyFailed", {
|
||||
detail: parsed.params?.detail ?? "",
|
||||
});
|
||||
case "DATA_ROOT_VERIFY_FAILED":
|
||||
return t("backendErrors.dataRootVerifyFailed");
|
||||
case "SYNC_SESSION_NOT_FOUND":
|
||||
return t("backendErrors.syncSessionNotFound");
|
||||
case "SYNC_FOLLOWER_NOT_FOUND":
|
||||
|
||||
@@ -676,28 +676,6 @@ export interface SyncSessionInfo {
|
||||
/** How the follower windows are placed on the host display. */
|
||||
export type SyncWindowLayout = "grid" | "columns" | "cascade";
|
||||
|
||||
// Data directory
|
||||
export interface DataRootInfo {
|
||||
active_path: string;
|
||||
configured_path: string | null;
|
||||
default_path: string;
|
||||
size_bytes: number;
|
||||
file_count: number;
|
||||
overridden_by_environment: boolean;
|
||||
restart_required: boolean;
|
||||
app_directory_name: string;
|
||||
active_path_missing: boolean;
|
||||
}
|
||||
|
||||
export interface DataRootMoveProgress {
|
||||
phase: "scanning" | "copying" | "verifying" | "cleaning" | "done";
|
||||
copied_files: number;
|
||||
total_files: number;
|
||||
copied_bytes: number;
|
||||
total_bytes: number;
|
||||
destination: string;
|
||||
}
|
||||
|
||||
// Traffic stats types
|
||||
export interface BandwidthDataPoint {
|
||||
timestamp: number;
|
||||
|
||||
Reference in new issue
Block a user