Files
donutbrowser/src/components/proxy-check-button.tsx
T
2026-07-31 01:04:58 +04:00

213 lines
6.6 KiB
TypeScript

"use client";
import { invoke } from "@tauri-apps/api/core";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { FiCheck } from "react-icons/fi";
import { toast } from "sonner";
import { FlagIcon } from "@/components/flag-icon";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { formatRelativeTime } from "@/lib/flag-utils";
import { MOTION_EASE_OUT } from "@/lib/motion";
import type { ProxyCheckResult, StoredProxy } from "@/types";
interface ProxyCheckButtonProps {
proxy: StoredProxy;
profileId: string;
checkingProfileId: string | null;
cachedResult?: ProxyCheckResult;
onCheckComplete?: (result: ProxyCheckResult) => void;
onCheckFailed?: (result: ProxyCheckResult) => void;
disabled?: boolean;
setCheckingProfileId?: (id: string | null) => void;
}
export function ProxyCheckButton({
proxy,
profileId,
checkingProfileId,
cachedResult,
onCheckComplete,
onCheckFailed,
disabled = false,
setCheckingProfileId,
}: ProxyCheckButtonProps) {
const { t } = useTranslation();
const reduceMotion = useReducedMotion();
const [localResult, setLocalResult] = React.useState<
ProxyCheckResult | undefined
>(cachedResult);
React.useEffect(() => {
setLocalResult(cachedResult);
}, [cachedResult]);
const handleCheck = React.useCallback(async () => {
if (checkingProfileId === profileId) return;
setCheckingProfileId?.(profileId);
try {
const result = await invoke<ProxyCheckResult>("check_proxy_validity", {
proxyId: proxy.id,
proxySettings: proxy.proxy_settings,
});
setLocalResult(result);
onCheckComplete?.(result);
// Show toast with location
const locationParts: string[] = [];
if (result.city) locationParts.push(result.city);
if (result.country) locationParts.push(result.country);
const location =
locationParts.length > 0
? locationParts.join(", ")
: t("proxyCheck.unknownLocation");
toast.success(
<div className="flex flex-col">
{t("proxyCheck.locationToast")}
<div className="flex items-center whitespace-nowrap">
{location}
{result.country_code && (
<FlagIcon
countryCode={result.country_code}
className="ml-1 text-sm"
/>
)}
</div>
</div>,
);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
toast.error(t("proxyCheck.failed", { error: errorMessage }));
// Save failed check result
const failedResult: ProxyCheckResult = {
ip: "",
city: undefined,
country: undefined,
country_code: undefined,
timestamp: Math.floor(Date.now() / 1000),
is_valid: false,
};
setLocalResult(failedResult);
onCheckFailed?.(failedResult);
} finally {
setCheckingProfileId?.(null);
}
}, [
proxy,
profileId,
checkingProfileId,
onCheckComplete,
onCheckFailed,
setCheckingProfileId,
t,
]);
const isCurrentlyChecking = checkingProfileId === profileId;
const result = localResult;
const statusKey = isCurrentlyChecking
? "checking"
: result?.is_valid && result.country_code
? "valid-location"
: result && !result.is_valid
? "invalid"
: "idle";
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="size-7 p-0"
onClick={handleCheck}
disabled={isCurrentlyChecking || disabled}
>
<AnimatePresence initial={false} mode="wait">
<motion.span
key={statusKey}
initial={{ opacity: 0, scale: reduceMotion ? 1 : 0.9 }}
animate={{
opacity: 1,
scale: 1,
transition: {
duration: reduceMotion ? 0.15 : 0.16,
ease: MOTION_EASE_OUT,
},
}}
exit={{
opacity: 0,
scale: reduceMotion ? 1 : 0.9,
transition: {
duration: reduceMotion ? 0.15 : 0.1,
ease: MOTION_EASE_OUT,
},
}}
className="inline-flex size-3 items-center justify-center"
>
{isCurrentlyChecking ? (
<span className="size-3 animate-spin rounded-full border border-current border-t-transparent" />
) : result?.is_valid && result.country_code ? (
<span className="relative inline-flex items-center justify-center">
<FlagIcon
countryCode={result.country_code}
className="h-2.5"
/>
<FiCheck className="absolute right-[-4px] bottom-[-6px]" />
</span>
) : result && !result.is_valid ? (
<span className="text-sm text-destructive-text"></span>
) : (
<FiCheck className="size-3" />
)}
</motion.span>
</AnimatePresence>
</Button>
</TooltipTrigger>
<TooltipContent>
{isCurrentlyChecking ? (
<p>{t("proxyCheck.tooltipChecking")}</p>
) : result?.is_valid ? (
<div className="space-y-1">
<p className="flex items-center gap-1">
{result.country_code && (
<FlagIcon countryCode={result.country_code} />
)}
{[result.city, result.country].filter(Boolean).join(", ") ||
t("proxyCheck.unknownLocation")}
</p>
<p className="text-xs text-primary-foreground">
{t("proxyCheck.tooltipIp", { ip: result.ip })}
</p>
<p className="text-xs text-primary-foreground">
{t("proxyCheck.tooltipChecked", {
time: formatRelativeTime(result.timestamp),
})}
</p>
</div>
) : result && !result.is_valid ? (
<div>
<p>{t("proxyCheck.tooltipFailedTitle")}</p>
<p className="text-xs text-primary-foreground">
{t("proxyCheck.tooltipFailed", {
time: formatRelativeTime(result.timestamp),
})}
</p>
</div>
) : (
<p>{t("proxyCheck.tooltipDefault")}</p>
)}
</TooltipContent>
</Tooltip>
);
}