mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-05-31 12:29:32 +02:00
refactor: cleanup, korean translation
This commit is contained in:
+1
-1
@@ -1174,7 +1174,7 @@ export default function Home() {
|
||||
failed_count: payload.failed_count ?? 0,
|
||||
phase: payload.phase,
|
||||
},
|
||||
{ id: toastId },
|
||||
{ id: toastId, profileId: payload.profile_id },
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@ export function DeleteConfirmationDialog({
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
|
||||
@@ -73,6 +73,7 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { parseBackendError, translateBackendError } from "@/lib/backend-errors";
|
||||
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
|
||||
import type { Extension, ExtensionGroup } from "@/types";
|
||||
import { DeleteConfirmationDialog } from "./delete-confirmation-dialog";
|
||||
@@ -308,7 +309,11 @@ export function ExtensionManagementDialog({
|
||||
);
|
||||
void loadData();
|
||||
} catch (err) {
|
||||
showErrorToast(err instanceof Error ? err.message : String(err));
|
||||
showErrorToast(
|
||||
parseBackendError(err)
|
||||
? translateBackendError(t, err)
|
||||
: t("proxies.management.updateSyncFailed"),
|
||||
);
|
||||
} finally {
|
||||
setIsTogglingExtSync((prev) => ({ ...prev, [ext.id]: false }));
|
||||
}
|
||||
@@ -331,7 +336,11 @@ export function ExtensionManagementDialog({
|
||||
);
|
||||
void loadData();
|
||||
} catch (err) {
|
||||
showErrorToast(err instanceof Error ? err.message : String(err));
|
||||
showErrorToast(
|
||||
parseBackendError(err)
|
||||
? translateBackendError(t, err)
|
||||
: t("proxies.management.updateSyncFailed"),
|
||||
);
|
||||
} finally {
|
||||
setIsTogglingGroupSync((prev) => ({ ...prev, [group.id]: false }));
|
||||
}
|
||||
@@ -589,9 +598,15 @@ export function ExtensionManagementDialog({
|
||||
}),
|
||||
),
|
||||
);
|
||||
const failed = results.filter((r) => r.status === "rejected").length;
|
||||
if (failed > 0) {
|
||||
showErrorToast(t("proxies.management.updateSyncFailed"));
|
||||
const firstRejection = results.find((r) => r.status === "rejected") as
|
||||
| PromiseRejectedResult
|
||||
| undefined;
|
||||
if (firstRejection) {
|
||||
showErrorToast(
|
||||
parseBackendError(firstRejection.reason)
|
||||
? translateBackendError(t, firstRejection.reason)
|
||||
: t("proxies.management.updateSyncFailed"),
|
||||
);
|
||||
} else {
|
||||
showSuccessToast(
|
||||
targetEnabled
|
||||
@@ -614,9 +629,15 @@ export function ExtensionManagementDialog({
|
||||
}),
|
||||
),
|
||||
);
|
||||
const failed = results.filter((r) => r.status === "rejected").length;
|
||||
if (failed > 0) {
|
||||
showErrorToast(t("proxies.management.updateSyncFailed"));
|
||||
const firstRejection = results.find((r) => r.status === "rejected") as
|
||||
| PromiseRejectedResult
|
||||
| undefined;
|
||||
if (firstRejection) {
|
||||
showErrorToast(
|
||||
parseBackendError(firstRejection.reason)
|
||||
? translateBackendError(t, firstRejection.reason)
|
||||
: t("proxies.management.updateSyncFailed"),
|
||||
);
|
||||
} else {
|
||||
showSuccessToast(
|
||||
targetEnabled
|
||||
|
||||
@@ -57,6 +57,7 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { parseBackendError, translateBackendError } from "@/lib/backend-errors";
|
||||
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
|
||||
import type { GroupWithCount, ProfileGroup } from "@/types";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
@@ -262,8 +263,8 @@ export function GroupManagementDialog({
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle sync:", error);
|
||||
showErrorToast(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
parseBackendError(error)
|
||||
? translateBackendError(t, error)
|
||||
: t("proxies.management.updateSyncFailed"),
|
||||
);
|
||||
} finally {
|
||||
@@ -529,9 +530,15 @@ export function GroupManagementDialog({
|
||||
}),
|
||||
),
|
||||
);
|
||||
const failed = results.filter((r) => r.status === "rejected").length;
|
||||
if (failed > 0) {
|
||||
showErrorToast(t("proxies.management.updateSyncFailed"));
|
||||
const firstRejection = results.find((r) => r.status === "rejected") as
|
||||
| PromiseRejectedResult
|
||||
| undefined;
|
||||
if (firstRejection) {
|
||||
showErrorToast(
|
||||
parseBackendError(firstRejection.reason)
|
||||
? translateBackendError(t, firstRejection.reason)
|
||||
: t("proxies.management.updateSyncFailed"),
|
||||
);
|
||||
} else {
|
||||
showSuccessToast(
|
||||
targetEnabled
|
||||
|
||||
@@ -120,6 +120,7 @@ export function IntegrationsDialog({
|
||||
const [isMcpStarting, setIsMcpStarting] = useState(false);
|
||||
const [agents, setAgents] = useState<McpAgentInfo[]>([]);
|
||||
const [busyAgentIds, setBusyAgentIds] = useState<Set<string>>(new Set());
|
||||
const [apiPortDraft, setApiPortDraft] = useState<string>("10108");
|
||||
|
||||
const { termsAccepted } = useWayfernTerms();
|
||||
|
||||
@@ -127,6 +128,7 @@ export function IntegrationsDialog({
|
||||
try {
|
||||
const loaded = await invoke<AppSettings>("get_app_settings");
|
||||
setSettings(loaded);
|
||||
setApiPortDraft(String(loaded.api_port ?? ""));
|
||||
} catch (e) {
|
||||
console.error("Failed to load settings:", e);
|
||||
}
|
||||
@@ -370,13 +372,24 @@ export function IntegrationsDialog({
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
value={settings.api_port}
|
||||
value={apiPortDraft}
|
||||
onChange={(e) => {
|
||||
setApiPortDraft(e.target.value);
|
||||
const val = Number.parseInt(e.target.value, 10);
|
||||
if (!Number.isNaN(val)) {
|
||||
if (
|
||||
!Number.isNaN(val) &&
|
||||
val >= 1 &&
|
||||
val <= 65535
|
||||
) {
|
||||
setSettings({ ...settings, api_port: val });
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
const val = Number.parseInt(apiPortDraft, 10);
|
||||
if (Number.isNaN(val) || val < 1 || val > 65535) {
|
||||
setApiPortDraft(String(settings.api_port));
|
||||
}
|
||||
}}
|
||||
className="w-24 font-mono"
|
||||
min={1}
|
||||
max={65535}
|
||||
|
||||
@@ -12,7 +12,7 @@ type Props = ButtonProps & {
|
||||
export const LoadingButton = ({ isLoading, className, ...props }: Props) => {
|
||||
return (
|
||||
<UIButton
|
||||
className={cn("grid place-items-center", className)}
|
||||
className={cn("inline-flex items-center justify-center", className)}
|
||||
{...props}
|
||||
disabled={props.disabled || isLoading}
|
||||
>
|
||||
|
||||
@@ -582,8 +582,9 @@ function ProfileInfoLayout({
|
||||
|
||||
const deleteAction = findAction("delete");
|
||||
const fingerprintAction = findAction("fingerprint");
|
||||
const cookiesAction =
|
||||
findAction("manage cookies") ?? findAction("copy cookies");
|
||||
const cookiesManageAction = findAction("manage cookies");
|
||||
const cookiesCopyAction = findAction("copy cookies");
|
||||
const cookiesAction = cookiesManageAction ?? cookiesCopyAction;
|
||||
const extensionAction = findAction("extension");
|
||||
const syncAction = findAction("sync");
|
||||
const _launchHookAction = findAction("hook") ?? findAction("launch hook");
|
||||
@@ -905,6 +906,7 @@ function ProfileInfoLayout({
|
||||
profile={profile}
|
||||
isRunning={isRunning}
|
||||
isDisabled={isDisabled}
|
||||
onCopyCookies={cookiesCopyAction?.onClick}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
@@ -1435,11 +1437,14 @@ function ExtensionsSectionInline({
|
||||
function CookiesSectionInline({
|
||||
profile,
|
||||
isRunning,
|
||||
isDisabled,
|
||||
onCopyCookies,
|
||||
t,
|
||||
}: {
|
||||
profile: BrowserProfile;
|
||||
isRunning: boolean;
|
||||
isDisabled: boolean;
|
||||
onCopyCookies?: () => void;
|
||||
t: (key: string, options?: Record<string, unknown>) => string;
|
||||
}) {
|
||||
type CookieStats = {
|
||||
@@ -1483,9 +1488,23 @@ function CookiesSectionInline({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 min-h-0 flex-1">
|
||||
<div className="flex items-center gap-2 text-sm font-semibold">
|
||||
<LuCookie className="size-4" />
|
||||
{t("profileInfo.sections.cookies")}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 text-sm font-semibold">
|
||||
<LuCookie className="size-4" />
|
||||
{t("profileInfo.sections.cookies")}
|
||||
</div>
|
||||
{onCopyCookies && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 gap-1.5"
|
||||
disabled={isDisabled}
|
||||
onClick={onCopyCookies}
|
||||
>
|
||||
<LuCopy className="size-3.5" />
|
||||
{t("profiles.actions.copyCookies")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("profileInfo.sectionDesc.cookies")}
|
||||
|
||||
@@ -67,6 +67,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useProxyEvents } from "@/hooks/use-proxy-events";
|
||||
import { useVpnEvents } from "@/hooks/use-vpn-events";
|
||||
import { parseBackendError, translateBackendError } from "@/lib/backend-errors";
|
||||
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ProxyCheckResult, StoredProxy, VpnConfig } from "@/types";
|
||||
@@ -394,8 +395,8 @@ export function ProxyManagementDialog({
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle sync:", error);
|
||||
showErrorToast(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
parseBackendError(error)
|
||||
? translateBackendError(t, error)
|
||||
: t("proxies.management.updateSyncFailed"),
|
||||
);
|
||||
} finally {
|
||||
@@ -458,8 +459,8 @@ export function ProxyManagementDialog({
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle VPN sync:", error);
|
||||
showErrorToast(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
parseBackendError(error)
|
||||
? translateBackendError(t, error)
|
||||
: t("proxies.management.updateSyncFailed"),
|
||||
);
|
||||
} finally {
|
||||
@@ -1010,9 +1011,15 @@ export function ProxyManagementDialog({
|
||||
}),
|
||||
),
|
||||
);
|
||||
const failed = results.filter((r) => r.status === "rejected").length;
|
||||
if (failed > 0) {
|
||||
showErrorToast(t("proxies.management.updateSyncFailed"));
|
||||
const firstRejection = results.find((r) => r.status === "rejected") as
|
||||
| PromiseRejectedResult
|
||||
| undefined;
|
||||
if (firstRejection) {
|
||||
showErrorToast(
|
||||
parseBackendError(firstRejection.reason)
|
||||
? translateBackendError(t, firstRejection.reason)
|
||||
: t("proxies.management.updateSyncFailed"),
|
||||
);
|
||||
} else {
|
||||
showSuccessToast(
|
||||
targetEnabled
|
||||
@@ -1039,9 +1046,15 @@ export function ProxyManagementDialog({
|
||||
}),
|
||||
),
|
||||
);
|
||||
const failed = results.filter((r) => r.status === "rejected").length;
|
||||
if (failed > 0) {
|
||||
showErrorToast(t("vpns.management.updateSyncFailed"));
|
||||
const firstRejection = results.find((r) => r.status === "rejected") as
|
||||
| PromiseRejectedResult
|
||||
| undefined;
|
||||
if (firstRejection) {
|
||||
showErrorToast(
|
||||
parseBackendError(firstRejection.reason)
|
||||
? translateBackendError(t, firstRejection.reason)
|
||||
: t("proxies.management.updateSyncFailed"),
|
||||
);
|
||||
} else {
|
||||
showSuccessToast(
|
||||
targetEnabled
|
||||
@@ -1055,7 +1068,7 @@ export function ProxyManagementDialog({
|
||||
return (
|
||||
<>
|
||||
<Dialog open={isOpen} onOpenChange={onClose} subPage={subPage}>
|
||||
<DialogContent className="max-w-[min(95vw,1600px)] max-h-[90vh] flex flex-col">
|
||||
<DialogContent className="max-w-4xl max-h-[85vh] flex flex-col">
|
||||
{!subPage && (
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("proxies.management.title")}</DialogTitle>
|
||||
@@ -1170,7 +1183,7 @@ export function ProxyManagementDialog({
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<Table className="min-w-max">
|
||||
<Table className="w-full">
|
||||
<TableHeader className="sticky top-0 z-10 bg-background">
|
||||
{proxiesTable.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
@@ -1251,7 +1264,7 @@ export function ProxyManagementDialog({
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<Table className="min-w-max">
|
||||
<Table className="w-full">
|
||||
<TableHeader className="sticky top-0 z-10 bg-background">
|
||||
{vpnsTable.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
|
||||
@@ -464,6 +464,7 @@ export function SettingsDialog({
|
||||
| "fr"
|
||||
| "zh"
|
||||
| "ja"
|
||||
| "ko"
|
||||
| "ru"),
|
||||
);
|
||||
setOriginalLanguage(selectedLanguage);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FiWifi } from "react-icons/fi";
|
||||
import { LuLayers, LuPuzzle, LuShield, LuUsers } from "react-icons/lu";
|
||||
import { LoadingButton } from "@/components/loading-button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -19,6 +22,8 @@ interface UnsyncedEntityCounts {
|
||||
proxies: number;
|
||||
groups: number;
|
||||
vpns: number;
|
||||
extensions: number;
|
||||
extension_groups: number;
|
||||
}
|
||||
|
||||
interface SyncAllDialogProps {
|
||||
@@ -67,27 +72,55 @@ export function SyncAllDialog({ isOpen, onClose }: SyncAllDialogProps) {
|
||||
}
|
||||
}, [onClose, t]);
|
||||
|
||||
const totalCount =
|
||||
(counts?.proxies ?? 0) + (counts?.groups ?? 0) + (counts?.vpns ?? 0);
|
||||
const items = useMemo(() => {
|
||||
if (!counts) return [];
|
||||
return [
|
||||
{
|
||||
key: "proxies",
|
||||
count: counts.proxies,
|
||||
label: t("syncAll.labels.proxies"),
|
||||
Icon: FiWifi,
|
||||
},
|
||||
{
|
||||
key: "vpns",
|
||||
count: counts.vpns,
|
||||
label: t("syncAll.labels.vpns"),
|
||||
Icon: LuShield,
|
||||
},
|
||||
{
|
||||
key: "groups",
|
||||
count: counts.groups,
|
||||
label: t("syncAll.labels.groups"),
|
||||
Icon: LuUsers,
|
||||
},
|
||||
{
|
||||
key: "extensions",
|
||||
count: counts.extensions,
|
||||
label: t("syncAll.labels.extensions"),
|
||||
Icon: LuPuzzle,
|
||||
},
|
||||
{
|
||||
key: "extensionGroups",
|
||||
count: counts.extension_groups,
|
||||
label: t("syncAll.labels.extensionGroups"),
|
||||
Icon: LuLayers,
|
||||
},
|
||||
].filter((item) => item.count > 0);
|
||||
}, [counts, t]);
|
||||
|
||||
// Don't show if there's nothing to sync
|
||||
const totalCount = items.reduce((sum, item) => sum + item.count, 0);
|
||||
|
||||
// Don't render anything when there's nothing to sync — the parent
|
||||
// mounts this dialog eagerly after login, so silent-close is correct.
|
||||
if (!isLoading && totalCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
if (counts?.proxies && counts.proxies > 0) {
|
||||
parts.push(t("syncAll.proxies", { count: counts.proxies }));
|
||||
}
|
||||
if (counts?.groups && counts.groups > 0) {
|
||||
parts.push(t("syncAll.groups", { count: counts.groups }));
|
||||
}
|
||||
if (counts?.vpns && counts.vpns > 0) {
|
||||
parts.push(t("syncAll.vpns", { count: counts.vpns }));
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen && totalCount > 0} onOpenChange={onClose}>
|
||||
<Dialog
|
||||
open={isOpen && (isLoading || totalCount > 0)}
|
||||
onOpenChange={onClose}
|
||||
>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("syncAll.title")}</DialogTitle>
|
||||
@@ -99,10 +132,26 @@ export function SyncAllDialog({ isOpen, onClose }: SyncAllDialogProps) {
|
||||
<div className="size-6 rounded-full border-2 border-current animate-spin border-t-transparent" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("syncAll.itemsList", { items: parts.join(", ") })}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2 py-2">
|
||||
{items.map(({ key, count, label, Icon }) => (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center gap-3 rounded-lg border border-border/60 bg-card/50 p-3 transition-colors hover:bg-card"
|
||||
>
|
||||
<div className="flex size-9 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary">
|
||||
<Icon className="size-4" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 text-sm font-medium truncate">
|
||||
{label}
|
||||
</div>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="shrink-0 tabular-nums px-2"
|
||||
>
|
||||
{count}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -11,19 +11,18 @@ const MotionThumb = motion.create(SwitchPrimitive.Thumb);
|
||||
type AnimatedSwitchProps = React.ComponentProps<typeof SwitchPrimitive.Root>;
|
||||
|
||||
/**
|
||||
* Toggle switch with a thumb that slides between the off (left) and on
|
||||
* (right) positions and squashes wider while pressed. Animated via Framer
|
||||
* Motion — no layout shift when the parent's width changes, and the
|
||||
* pressed state is purely visual so external onCheckedChange semantics
|
||||
* stay identical to a Radix Switch.
|
||||
* Switch whose thumb actually slides between off and on. The Root flips
|
||||
* its flex alignment on `data-state=checked`, which moves the Thumb's
|
||||
* layout box; Framer Motion's `layout` prop tweens between the two
|
||||
* positions. The thumb also squashes wider while pressed.
|
||||
*/
|
||||
function AnimatedSwitch({ className, ...props }: AnimatedSwitchProps) {
|
||||
return (
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="animated-switch"
|
||||
className={cn(
|
||||
"peer relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent",
|
||||
"bg-input data-[state=checked]:bg-primary",
|
||||
"peer relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center justify-start rounded-full border border-transparent px-[2px]",
|
||||
"bg-input data-[state=checked]:bg-primary data-[state=checked]:justify-end",
|
||||
"transition-colors duration-200 ease-out",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
@@ -39,8 +38,7 @@ function AnimatedSwitch({ className, ...props }: AnimatedSwitchProps) {
|
||||
)}
|
||||
layout
|
||||
transition={{ type: "spring", stiffness: 700, damping: 32, mass: 0.5 }}
|
||||
whileTap={{ width: 22 }}
|
||||
style={{ marginLeft: 2, marginRight: 2 }}
|
||||
whileTap={{ width: 20 }}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import en from "./locales/en.json";
|
||||
import es from "./locales/es.json";
|
||||
import fr from "./locales/fr.json";
|
||||
import ja from "./locales/ja.json";
|
||||
import ko from "./locales/ko.json";
|
||||
import pt from "./locales/pt.json";
|
||||
import ru from "./locales/ru.json";
|
||||
import zh from "./locales/zh.json";
|
||||
@@ -16,6 +17,7 @@ export const SUPPORTED_LANGUAGES = [
|
||||
{ code: "fr", name: "French", nativeName: "Français" },
|
||||
{ code: "zh", name: "Chinese", nativeName: "中文" },
|
||||
{ code: "ja", name: "Japanese", nativeName: "日本語" },
|
||||
{ code: "ko", name: "Korean", nativeName: "한국어" },
|
||||
{ code: "ru", name: "Russian", nativeName: "Русский" },
|
||||
] as const;
|
||||
|
||||
@@ -61,6 +63,7 @@ const resources = {
|
||||
fr: { translation: fr },
|
||||
zh: { translation: zh },
|
||||
ja: { translation: ja },
|
||||
ko: { translation: ko },
|
||||
ru: { translation: ru },
|
||||
};
|
||||
|
||||
|
||||
@@ -1070,16 +1070,16 @@
|
||||
"syncAll": {
|
||||
"title": "Enable Sync for Existing Items",
|
||||
"description": "You have items that are not being synced. Would you like to enable sync for all of them?",
|
||||
"itemsList": "Items not synced: {{items}}",
|
||||
"proxies": "{{count}} proxy",
|
||||
"proxies_plural": "{{count}} proxies",
|
||||
"groups": "{{count}} group",
|
||||
"groups_plural": "{{count}} groups",
|
||||
"vpns": "{{count}} VPN",
|
||||
"vpns_plural": "{{count}} VPNs",
|
||||
"enableAll": "Enable All",
|
||||
"skip": "Skip",
|
||||
"success": "Sync enabled for all items"
|
||||
"success": "Sync enabled for all items",
|
||||
"labels": {
|
||||
"proxies": "Proxies",
|
||||
"vpns": "VPNs",
|
||||
"groups": "Groups",
|
||||
"extensions": "Extensions",
|
||||
"extensionGroups": "Extension Groups"
|
||||
}
|
||||
},
|
||||
"crossOs": {
|
||||
"viewOnly": "This profile was created on {{os}} and is not supported on this system",
|
||||
@@ -1788,6 +1788,14 @@
|
||||
"profileLocked": "Profile is locked. Enter the password first.",
|
||||
"invalidProfileId": "Invalid profile id",
|
||||
"passwordTooShort": "Password must be at least {{min}} characters",
|
||||
"proxyNotFound": "Proxy not found",
|
||||
"groupNotFound": "Group not found",
|
||||
"vpnNotFound": "VPN not found",
|
||||
"extensionNotFound": "Extension not found",
|
||||
"extensionGroupNotFound": "Extension group not found",
|
||||
"cannotModifyCloudManagedProxy": "Cannot modify sync for a cloud-managed proxy",
|
||||
"syncLockedByProfile": "Sync cannot be disabled while this is used by synced profiles",
|
||||
"syncNotConfigured": "Sync is not configured. Sign in or configure a self-hosted server first.",
|
||||
"internal": "Something went wrong: {{detail}}",
|
||||
"invalidLaunchHookUrl": "Invalid launch hook URL. Use a full http:// or https:// URL.",
|
||||
"cookieDbLocked": "Could not read cookies — the database is locked. Close the browser and try again.",
|
||||
|
||||
@@ -1070,16 +1070,16 @@
|
||||
"syncAll": {
|
||||
"title": "Activar sincronización para elementos existentes",
|
||||
"description": "Tienes elementos que no se están sincronizando. ¿Te gustaría activar la sincronización para todos?",
|
||||
"itemsList": "Elementos no sincronizados: {{items}}",
|
||||
"proxies": "{{count}} proxy",
|
||||
"proxies_plural": "{{count}} proxies",
|
||||
"groups": "{{count}} grupo",
|
||||
"groups_plural": "{{count}} grupos",
|
||||
"vpns": "{{count}} VPN",
|
||||
"vpns_plural": "{{count}} VPNs",
|
||||
"enableAll": "Activar todos",
|
||||
"skip": "Omitir",
|
||||
"success": "Sincronización activada para todos los elementos"
|
||||
"success": "Sincronización activada para todos los elementos",
|
||||
"labels": {
|
||||
"proxies": "Proxies",
|
||||
"vpns": "VPN",
|
||||
"groups": "Grupos",
|
||||
"extensions": "Extensiones",
|
||||
"extensionGroups": "Grupos de extensiones"
|
||||
}
|
||||
},
|
||||
"crossOs": {
|
||||
"viewOnly": "Este perfil fue creado en {{os}} y no es compatible con este sistema",
|
||||
@@ -1788,6 +1788,14 @@
|
||||
"profileLocked": "El perfil está bloqueado. Introduce la contraseña primero.",
|
||||
"invalidProfileId": "ID de perfil no válido",
|
||||
"passwordTooShort": "La contraseña debe tener al menos {{min}} caracteres",
|
||||
"proxyNotFound": "Proxy no encontrado",
|
||||
"groupNotFound": "Grupo no encontrado",
|
||||
"vpnNotFound": "VPN no encontrada",
|
||||
"extensionNotFound": "Extensión no encontrada",
|
||||
"extensionGroupNotFound": "Grupo de extensiones no encontrado",
|
||||
"cannotModifyCloudManagedProxy": "No se puede modificar la sincronización de un proxy gestionado en la nube",
|
||||
"syncLockedByProfile": "No se puede desactivar la sincronización mientras se usa en perfiles sincronizados",
|
||||
"syncNotConfigured": "La sincronización no está configurada. Inicia sesión o configura un servidor propio.",
|
||||
"internal": "Algo salió mal: {{detail}}",
|
||||
"invalidLaunchHookUrl": "URL del hook de inicio no válida. Usa una URL completa http:// o https://.",
|
||||
"cookieDbLocked": "No se pudieron leer las cookies — la base de datos está bloqueada. Cierra el navegador e inténtalo de nuevo.",
|
||||
|
||||
@@ -1070,16 +1070,16 @@
|
||||
"syncAll": {
|
||||
"title": "Activer la synchronisation pour les éléments existants",
|
||||
"description": "Vous avez des éléments qui ne sont pas synchronisés. Voulez-vous activer la synchronisation pour tous ?",
|
||||
"itemsList": "Éléments non synchronisés : {{items}}",
|
||||
"proxies": "{{count}} proxy",
|
||||
"proxies_plural": "{{count}} proxies",
|
||||
"groups": "{{count}} groupe",
|
||||
"groups_plural": "{{count}} groupes",
|
||||
"vpns": "{{count}} VPN",
|
||||
"vpns_plural": "{{count}} VPNs",
|
||||
"enableAll": "Tout activer",
|
||||
"skip": "Ignorer",
|
||||
"success": "Synchronisation activée pour tous les éléments"
|
||||
"success": "Synchronisation activée pour tous les éléments",
|
||||
"labels": {
|
||||
"proxies": "Proxies",
|
||||
"vpns": "VPN",
|
||||
"groups": "Groupes",
|
||||
"extensions": "Extensions",
|
||||
"extensionGroups": "Groupes d'extensions"
|
||||
}
|
||||
},
|
||||
"crossOs": {
|
||||
"viewOnly": "Ce profil a été créé sur {{os}} et n'est pas pris en charge sur ce système",
|
||||
@@ -1788,6 +1788,14 @@
|
||||
"profileLocked": "Le profil est verrouillé. Entrez d'abord le mot de passe.",
|
||||
"invalidProfileId": "Identifiant de profil non valide",
|
||||
"passwordTooShort": "Le mot de passe doit comporter au moins {{min}} caractères",
|
||||
"proxyNotFound": "Proxy introuvable",
|
||||
"groupNotFound": "Groupe introuvable",
|
||||
"vpnNotFound": "VPN introuvable",
|
||||
"extensionNotFound": "Extension introuvable",
|
||||
"extensionGroupNotFound": "Groupe d'extensions introuvable",
|
||||
"cannotModifyCloudManagedProxy": "Impossible de modifier la synchronisation d'un proxy géré dans le cloud",
|
||||
"syncLockedByProfile": "La synchronisation ne peut pas être désactivée tant qu'elle est utilisée par des profils synchronisés",
|
||||
"syncNotConfigured": "La synchronisation n'est pas configurée. Connectez-vous ou configurez un serveur auto-hébergé.",
|
||||
"internal": "Une erreur s'est produite : {{detail}}",
|
||||
"invalidLaunchHookUrl": "URL du hook de lancement invalide. Utilisez une URL http:// ou https:// complète.",
|
||||
"cookieDbLocked": "Impossible de lire les cookies — la base de données est verrouillée. Fermez le navigateur et réessayez.",
|
||||
|
||||
@@ -1070,16 +1070,16 @@
|
||||
"syncAll": {
|
||||
"title": "既存アイテムの同期を有効にする",
|
||||
"description": "同期されていないアイテムがあります。すべての同期を有効にしますか?",
|
||||
"itemsList": "未同期アイテム: {{items}}",
|
||||
"proxies": "{{count}}個のプロキシ",
|
||||
"proxies_plural": "{{count}}個のプロキシ",
|
||||
"groups": "{{count}}個のグループ",
|
||||
"groups_plural": "{{count}}個のグループ",
|
||||
"vpns": "{{count}}個のVPN",
|
||||
"vpns_plural": "{{count}}個のVPN",
|
||||
"enableAll": "すべて有効にする",
|
||||
"skip": "スキップ",
|
||||
"success": "すべてのアイテムの同期が有効になりました"
|
||||
"success": "すべてのアイテムの同期が有効になりました",
|
||||
"labels": {
|
||||
"proxies": "プロキシ",
|
||||
"vpns": "VPN",
|
||||
"groups": "グループ",
|
||||
"extensions": "拡張機能",
|
||||
"extensionGroups": "拡張機能グループ"
|
||||
}
|
||||
},
|
||||
"crossOs": {
|
||||
"viewOnly": "このプロファイルは{{os}}で作成されたもので、このシステムではサポートされていません",
|
||||
@@ -1788,6 +1788,14 @@
|
||||
"profileLocked": "プロファイルはロックされています。先にパスワードを入力してください。",
|
||||
"invalidProfileId": "無効なプロファイルIDです",
|
||||
"passwordTooShort": "パスワードは {{min}} 文字以上必要です",
|
||||
"proxyNotFound": "プロキシが見つかりません",
|
||||
"groupNotFound": "グループが見つかりません",
|
||||
"vpnNotFound": "VPNが見つかりません",
|
||||
"extensionNotFound": "拡張機能が見つかりません",
|
||||
"extensionGroupNotFound": "拡張機能グループが見つかりません",
|
||||
"cannotModifyCloudManagedProxy": "クラウド管理のプロキシの同期は変更できません",
|
||||
"syncLockedByProfile": "同期済みプロファイルで使用中のため、同期を無効にできません",
|
||||
"syncNotConfigured": "同期が設定されていません。サインインするか、セルフホストサーバーを設定してください。",
|
||||
"internal": "問題が発生しました: {{detail}}",
|
||||
"invalidLaunchHookUrl": "起動フックURLが無効です。完全な http:// または https:// URL を使用してください。",
|
||||
"cookieDbLocked": "Cookie を読み取れません — データベースがロックされています。ブラウザを閉じてから再試行してください。",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1070,16 +1070,16 @@
|
||||
"syncAll": {
|
||||
"title": "Ativar sincronização para itens existentes",
|
||||
"description": "Você tem itens que não estão sendo sincronizados. Gostaria de ativar a sincronização para todos?",
|
||||
"itemsList": "Itens não sincronizados: {{items}}",
|
||||
"proxies": "{{count}} proxy",
|
||||
"proxies_plural": "{{count}} proxies",
|
||||
"groups": "{{count}} grupo",
|
||||
"groups_plural": "{{count}} grupos",
|
||||
"vpns": "{{count}} VPN",
|
||||
"vpns_plural": "{{count}} VPNs",
|
||||
"enableAll": "Ativar todos",
|
||||
"skip": "Pular",
|
||||
"success": "Sincronização ativada para todos os itens"
|
||||
"success": "Sincronização ativada para todos os itens",
|
||||
"labels": {
|
||||
"proxies": "Proxies",
|
||||
"vpns": "VPNs",
|
||||
"groups": "Grupos",
|
||||
"extensions": "Extensões",
|
||||
"extensionGroups": "Grupos de extensões"
|
||||
}
|
||||
},
|
||||
"crossOs": {
|
||||
"viewOnly": "Este perfil foi criado em {{os}} e não é compatível com este sistema",
|
||||
@@ -1788,6 +1788,14 @@
|
||||
"profileLocked": "O perfil está bloqueado. Digite a senha primeiro.",
|
||||
"invalidProfileId": "ID de perfil inválido",
|
||||
"passwordTooShort": "A senha deve ter pelo menos {{min}} caracteres",
|
||||
"proxyNotFound": "Proxy não encontrado",
|
||||
"groupNotFound": "Grupo não encontrado",
|
||||
"vpnNotFound": "VPN não encontrada",
|
||||
"extensionNotFound": "Extensão não encontrada",
|
||||
"extensionGroupNotFound": "Grupo de extensões não encontrado",
|
||||
"cannotModifyCloudManagedProxy": "Não é possível modificar a sincronização de um proxy gerenciado na nuvem",
|
||||
"syncLockedByProfile": "A sincronização não pode ser desativada enquanto estiver em uso por perfis sincronizados",
|
||||
"syncNotConfigured": "A sincronização não está configurada. Faça login ou configure um servidor auto-hospedado.",
|
||||
"internal": "Algo deu errado: {{detail}}",
|
||||
"invalidLaunchHookUrl": "URL do hook de inicialização inválida. Use uma URL completa http:// ou https://.",
|
||||
"cookieDbLocked": "Não foi possível ler os cookies — o banco de dados está bloqueado. Feche o navegador e tente novamente.",
|
||||
|
||||
@@ -1070,16 +1070,16 @@
|
||||
"syncAll": {
|
||||
"title": "Включить синхронизацию для существующих элементов",
|
||||
"description": "У вас есть элементы, которые не синхронизируются. Хотите включить синхронизацию для всех?",
|
||||
"itemsList": "Несинхронизированные элементы: {{items}}",
|
||||
"proxies": "{{count}} прокси",
|
||||
"proxies_plural": "{{count}} прокси",
|
||||
"groups": "{{count}} группа",
|
||||
"groups_plural": "{{count}} групп",
|
||||
"vpns": "{{count}} VPN",
|
||||
"vpns_plural": "{{count}} VPN",
|
||||
"enableAll": "Включить все",
|
||||
"skip": "Пропустить",
|
||||
"success": "Синхронизация включена для всех элементов"
|
||||
"success": "Синхронизация включена для всех элементов",
|
||||
"labels": {
|
||||
"proxies": "Прокси",
|
||||
"vpns": "VPN",
|
||||
"groups": "Группы",
|
||||
"extensions": "Расширения",
|
||||
"extensionGroups": "Группы расширений"
|
||||
}
|
||||
},
|
||||
"crossOs": {
|
||||
"viewOnly": "Этот профиль был создан на {{os}} и не поддерживается в этой системе",
|
||||
@@ -1788,6 +1788,14 @@
|
||||
"profileLocked": "Профиль заблокирован. Сначала введите пароль.",
|
||||
"invalidProfileId": "Недействительный идентификатор профиля",
|
||||
"passwordTooShort": "Пароль должен быть не короче {{min}} символов",
|
||||
"proxyNotFound": "Прокси не найден",
|
||||
"groupNotFound": "Группа не найдена",
|
||||
"vpnNotFound": "VPN не найден",
|
||||
"extensionNotFound": "Расширение не найдено",
|
||||
"extensionGroupNotFound": "Группа расширений не найдена",
|
||||
"cannotModifyCloudManagedProxy": "Невозможно изменить синхронизацию для облачного прокси",
|
||||
"syncLockedByProfile": "Невозможно отключить синхронизацию, пока используется синхронизированными профилями",
|
||||
"syncNotConfigured": "Синхронизация не настроена. Войдите или настройте собственный сервер.",
|
||||
"internal": "Что-то пошло не так: {{detail}}",
|
||||
"invalidLaunchHookUrl": "Неверный URL хука запуска. Используйте полный URL http:// или https://.",
|
||||
"cookieDbLocked": "Не удалось прочитать куки — база данных заблокирована. Закройте браузер и попробуйте снова.",
|
||||
|
||||
@@ -1070,16 +1070,16 @@
|
||||
"syncAll": {
|
||||
"title": "为现有项目启用同步",
|
||||
"description": "您有未同步的项目。是否要为所有项目启用同步?",
|
||||
"itemsList": "未同步项目: {{items}}",
|
||||
"proxies": "{{count}} 个代理",
|
||||
"proxies_plural": "{{count}} 个代理",
|
||||
"groups": "{{count}} 个分组",
|
||||
"groups_plural": "{{count}} 个分组",
|
||||
"vpns": "{{count}} 个 VPN",
|
||||
"vpns_plural": "{{count}} 个 VPN",
|
||||
"enableAll": "全部启用",
|
||||
"skip": "跳过",
|
||||
"success": "已为所有项目启用同步"
|
||||
"success": "已为所有项目启用同步",
|
||||
"labels": {
|
||||
"proxies": "代理",
|
||||
"vpns": "VPN",
|
||||
"groups": "分组",
|
||||
"extensions": "扩展",
|
||||
"extensionGroups": "扩展分组"
|
||||
}
|
||||
},
|
||||
"crossOs": {
|
||||
"viewOnly": "此配置文件在 {{os}} 上创建,不受此系统支持",
|
||||
@@ -1788,6 +1788,14 @@
|
||||
"profileLocked": "配置文件已锁定。请先输入密码。",
|
||||
"invalidProfileId": "配置文件 ID 无效",
|
||||
"passwordTooShort": "密码至少需要 {{min}} 个字符",
|
||||
"proxyNotFound": "未找到代理",
|
||||
"groupNotFound": "未找到分组",
|
||||
"vpnNotFound": "未找到 VPN",
|
||||
"extensionNotFound": "未找到扩展",
|
||||
"extensionGroupNotFound": "未找到扩展分组",
|
||||
"cannotModifyCloudManagedProxy": "无法修改云管理代理的同步",
|
||||
"syncLockedByProfile": "在被已同步的配置文件使用时无法禁用同步",
|
||||
"syncNotConfigured": "同步未配置。请先登录或配置自托管服务器。",
|
||||
"internal": "出现问题:{{detail}}",
|
||||
"invalidLaunchHookUrl": "启动钩子 URL 无效。请使用完整的 http:// 或 https:// URL。",
|
||||
"cookieDbLocked": "无法读取 Cookie — 数据库已锁定。请关闭浏览器后重试。",
|
||||
|
||||
@@ -20,6 +20,14 @@ export type BackendErrorCode =
|
||||
| "COOKIE_DB_LOCKED"
|
||||
| "COOKIE_DB_UNAVAILABLE"
|
||||
| "SELF_HOSTED_REQUIRES_LOGOUT"
|
||||
| "PROXY_NOT_FOUND"
|
||||
| "GROUP_NOT_FOUND"
|
||||
| "VPN_NOT_FOUND"
|
||||
| "EXTENSION_NOT_FOUND"
|
||||
| "EXTENSION_GROUP_NOT_FOUND"
|
||||
| "CANNOT_MODIFY_CLOUD_MANAGED_PROXY"
|
||||
| "SYNC_LOCKED_BY_PROFILE"
|
||||
| "SYNC_NOT_CONFIGURED"
|
||||
| "INTERNAL_ERROR";
|
||||
|
||||
export interface BackendError {
|
||||
@@ -96,6 +104,22 @@ export function translateBackendError(t: TFunction, err: unknown): string {
|
||||
return t("backendErrors.cookieDbUnavailable");
|
||||
case "SELF_HOSTED_REQUIRES_LOGOUT":
|
||||
return t("backendErrors.selfHostedRequiresLogout");
|
||||
case "PROXY_NOT_FOUND":
|
||||
return t("backendErrors.proxyNotFound");
|
||||
case "GROUP_NOT_FOUND":
|
||||
return t("backendErrors.groupNotFound");
|
||||
case "VPN_NOT_FOUND":
|
||||
return t("backendErrors.vpnNotFound");
|
||||
case "EXTENSION_NOT_FOUND":
|
||||
return t("backendErrors.extensionNotFound");
|
||||
case "EXTENSION_GROUP_NOT_FOUND":
|
||||
return t("backendErrors.extensionGroupNotFound");
|
||||
case "CANNOT_MODIFY_CLOUD_MANAGED_PROXY":
|
||||
return t("backendErrors.cannotModifyCloudManagedProxy");
|
||||
case "SYNC_LOCKED_BY_PROFILE":
|
||||
return t("backendErrors.syncLockedByProfile");
|
||||
case "SYNC_NOT_CONFIGURED":
|
||||
return t("backendErrors.syncNotConfigured");
|
||||
case "INTERNAL_ERROR":
|
||||
return t("backendErrors.internal", {
|
||||
detail: parsed.params?.detail ?? "",
|
||||
|
||||
+11
-1
@@ -1,3 +1,4 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import React from "react";
|
||||
import { type ExternalToast, toast as sonnerToast } from "sonner";
|
||||
import { UnifiedToast } from "@/components/custom-toast";
|
||||
@@ -259,7 +260,7 @@ export function showSyncProgressToast(
|
||||
failed_count: number;
|
||||
phase: string;
|
||||
},
|
||||
options?: { id?: string },
|
||||
options?: { id?: string; profileId?: string },
|
||||
) {
|
||||
return showToast({
|
||||
type: "sync-progress",
|
||||
@@ -268,6 +269,15 @@ export function showSyncProgressToast(
|
||||
id: options?.id,
|
||||
duration: Number.POSITIVE_INFINITY,
|
||||
onCancel: () => {
|
||||
if (options?.profileId) {
|
||||
// Fire-and-forget — backend flips the cancel flag for the in-flight
|
||||
// upload/download loops to drain.
|
||||
void invoke("cancel_profile_sync", {
|
||||
profileId: options.profileId,
|
||||
}).catch((err: unknown) => {
|
||||
console.error("Failed to cancel sync:", err);
|
||||
});
|
||||
}
|
||||
if (options?.id) {
|
||||
dismissToast(options.id);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user