feat: better proxy management

This commit is contained in:
zhom
2026-02-16 10:03:27 +04:00
parent 777be9b9dc
commit bb8356eeef
19 changed files with 1066 additions and 324 deletions
+240 -209
View File
@@ -3,7 +3,7 @@
import { invoke } from "@tauri-apps/api/core";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { LuEye, LuEyeOff } from "react-icons/lu";
import { LuEye, LuEyeOff, LuLock } from "react-icons/lu";
import { LoadingButton } from "@/components/loading-button";
import { Button } from "@/components/ui/button";
import {
@@ -57,9 +57,10 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
const [isSendingCode, setIsSendingCode] = useState(false);
const [isVerifying, setIsVerifying] = useState(false);
// Default to self-hosted tab if self-hosted is configured and not cloud-logged-in
const [activeTab, setActiveTab] = useState<string>("cloud");
const isConnected = Boolean(serverUrl && token);
const loadSettings = useCallback(async () => {
setIsLoading(true);
try {
@@ -82,10 +83,15 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
}
}, [isOpen, loadSettings]);
// If self-hosted is configured and not cloud-logged-in, default to self-hosted tab
// Auto-select the appropriate tab based on connection state
useEffect(() => {
if (!isCloudLoading && !isLoggedIn && serverUrl && token) {
if (isCloudLoading) return;
if (isLoggedIn) {
setActiveTab("cloud");
} else if (serverUrl && token) {
setActiveTab("self-hosted");
} else {
setActiveTab("cloud");
}
}, [isCloudLoading, isLoggedIn, serverUrl, token]);
@@ -173,13 +179,15 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
} catch (e) {
console.error("Failed to restart sync service:", e);
}
// Auto-close dialog after successful login
onClose();
} catch (error) {
console.error("OTP verification failed:", error);
showErrorToast(String(error));
} finally {
setIsVerifying(false);
}
}, [email, otpCode, verifyOtp, t]);
}, [email, otpCode, verifyOtp, t, onClose]);
const handleCloudLogout = useCallback(async () => {
try {
@@ -197,7 +205,9 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
}
}, [logout, t]);
const isConnected = Boolean(serverUrl && token);
// Determine which tabs are available
const cloudBlocked = !isLoggedIn && isConnected;
const selfHostedBlocked = isLoggedIn;
return (
<Dialog open={isOpen} onOpenChange={onClose}>
@@ -207,233 +217,254 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
<DialogDescription>{t("sync.description")}</DialogDescription>
</DialogHeader>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="w-full">
<TabsTrigger value="cloud" className="flex-1">
{t("sync.cloud.tabLabel")}
</TabsTrigger>
<TabsTrigger value="self-hosted" className="flex-1">
{t("sync.cloud.selfHostedTabLabel")}
</TabsTrigger>
</TabsList>
{/* If cloud is logged in, don't show tabs at all - just show cloud account */}
{isLoggedIn && user ? (
<div className="grid gap-4 py-4">
<div className="flex gap-2 items-center text-sm">
<div className="w-2 h-2 rounded-full bg-green-500" />
{t("sync.cloud.connected")}
</div>
<TabsContent value="cloud">
{isCloudLoading ? (
<div className="flex justify-center py-8">
<div className="w-6 h-6 rounded-full border-2 border-current animate-spin border-t-transparent" />
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">
{t("sync.cloud.email")}
</span>
<span>{user.email}</span>
</div>
) : isLoggedIn && user ? (
<div className="grid gap-4 py-4">
<div className="flex gap-2 items-center text-sm">
<div className="w-2 h-2 rounded-full bg-green-500" />
{t("sync.cloud.connected")}
<div className="flex justify-between">
<span className="text-muted-foreground">
{t("sync.cloud.plan")}
</span>
<span className="capitalize">
{user.plan}
{user.planPeriod ? ` (${user.planPeriod})` : ""}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">
{t("sync.cloud.profiles")}
</span>
<span>
{t("sync.cloud.profileUsage", {
used: user.cloudProfilesUsed,
limit: user.profileLimit,
})}
</span>
</div>
{user.proxyBandwidthLimitMb > 0 && (
<div className="flex justify-between">
<span className="text-muted-foreground">Proxy Bandwidth</span>
<span>
{user.proxyBandwidthUsedMb} / {user.proxyBandwidthLimitMb}{" "}
MB
</span>
</div>
)}
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">
{t("sync.cloud.email")}
</span>
<span>{user.email}</span>
<div className="flex gap-2 pt-2">
<Button variant="outline" className="flex-1" asChild>
<a
href="https://donutbrowser.com/account"
target="_blank"
rel="noopener noreferrer"
>
{t("sync.cloud.manageAccount")}
</a>
</Button>
<Button
variant="outline"
className="flex-1"
onClick={handleCloudLogout}
>
{t("sync.cloud.logout")}
</Button>
</div>
</div>
) : (
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="w-full">
<TabsTrigger
value="cloud"
className="flex-1"
disabled={cloudBlocked}
>
<span className="flex items-center gap-2">
{t("sync.cloud.tabLabel")}
{cloudBlocked && (
<LuLock className="w-3 h-3 text-muted-foreground" />
)}
</span>
</TabsTrigger>
<TabsTrigger
value="self-hosted"
className="flex-1"
disabled={selfHostedBlocked}
>
<span className="flex items-center gap-2">
{t("sync.cloud.selfHostedTabLabel")}
{selfHostedBlocked && (
<LuLock className="w-3 h-3 text-muted-foreground" />
)}
</span>
</TabsTrigger>
</TabsList>
<TabsContent value="cloud">
{isCloudLoading ? (
<div className="flex justify-center py-8">
<div className="w-6 h-6 rounded-full border-2 border-current animate-spin border-t-transparent" />
</div>
) : (
<div className="grid gap-4 py-4">
<div className="space-y-2">
<Label htmlFor="cloud-email">{t("sync.cloud.email")}</Label>
<div className="flex gap-2">
<Input
id="cloud-email"
type="email"
placeholder={t("sync.cloud.emailPlaceholder")}
value={email}
onChange={(e) => setEmail(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !codeSent) {
void handleSendCode();
}
}}
/>
<LoadingButton
onClick={handleSendCode}
isLoading={isSendingCode}
disabled={!email || codeSent}
variant="outline"
>
{t("sync.cloud.sendCode")}
</LoadingButton>
</div>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">
{t("sync.cloud.plan")}
</span>
<span className="capitalize">
{user.plan}
{user.planPeriod ? ` (${user.planPeriod})` : ""}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">
{t("sync.cloud.profiles")}
</span>
<span>
{t("sync.cloud.profileUsage", {
used: user.cloudProfilesUsed,
limit: user.profileLimit,
})}
</span>
</div>
{user.proxyBandwidthLimitMb > 0 && (
<div className="flex justify-between">
<span className="text-muted-foreground">
Proxy Bandwidth
</span>
<span>
{user.proxyBandwidthUsedMb} /{" "}
{user.proxyBandwidthLimitMb} MB
</span>
{codeSent && (
<div className="space-y-2">
<Label htmlFor="cloud-otp">
{t("sync.cloud.verificationCode")}
</Label>
<Input
id="cloud-otp"
placeholder={t("sync.cloud.codePlaceholder")}
value={otpCode}
onChange={(e) => setOtpCode(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
void handleVerifyOtp();
}
}}
/>
<LoadingButton
onClick={handleVerifyOtp}
isLoading={isVerifying}
disabled={!otpCode}
className="w-full"
>
{isVerifying
? t("sync.cloud.loggingIn")
: t("sync.cloud.verifyAndLogin")}
</LoadingButton>
</div>
)}
</div>
)}
</TabsContent>
<div className="flex gap-2 pt-2">
<Button variant="outline" className="flex-1" asChild>
<a
href="https://donutbrowser.com/account"
target="_blank"
rel="noopener noreferrer"
>
{t("sync.cloud.manageAccount")}
</a>
</Button>
<Button
variant="outline"
className="flex-1"
onClick={handleCloudLogout}
>
{t("sync.cloud.logout")}
</Button>
<TabsContent value="self-hosted">
{isLoading ? (
<div className="flex justify-center py-8">
<div className="w-6 h-6 rounded-full border-2 border-current animate-spin border-t-transparent" />
</div>
</div>
) : (
<div className="grid gap-4 py-4">
<div className="space-y-2">
<Label htmlFor="cloud-email">{t("sync.cloud.email")}</Label>
<div className="flex gap-2">
<Input
id="cloud-email"
type="email"
placeholder={t("sync.cloud.emailPlaceholder")}
value={email}
onChange={(e) => setEmail(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !codeSent) {
void handleSendCode();
}
}}
/>
<LoadingButton
onClick={handleSendCode}
isLoading={isSendingCode}
disabled={!email || codeSent}
variant="outline"
>
{t("sync.cloud.sendCode")}
</LoadingButton>
</div>
</div>
{codeSent && (
) : (
<div className="grid gap-4 py-4">
<div className="space-y-2">
<Label htmlFor="cloud-otp">
{t("sync.cloud.verificationCode")}
<Label htmlFor="sync-server-url">
{t("sync.serverUrl")}
</Label>
<Input
id="cloud-otp"
placeholder={t("sync.cloud.codePlaceholder")}
value={otpCode}
onChange={(e) => setOtpCode(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
void handleVerifyOtp();
}
}}
id="sync-server-url"
placeholder={t("sync.serverUrlPlaceholder")}
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
/>
<LoadingButton
onClick={handleVerifyOtp}
isLoading={isVerifying}
disabled={!otpCode}
className="w-full"
>
{isVerifying
? t("sync.cloud.loggingIn")
: t("sync.cloud.verifyAndLogin")}
</LoadingButton>
</div>
)}
</div>
)}
</TabsContent>
<TabsContent value="self-hosted">
{isLoading ? (
<div className="flex justify-center py-8">
<div className="w-6 h-6 rounded-full border-2 border-current animate-spin border-t-transparent" />
</div>
) : (
<div className="grid gap-4 py-4">
<div className="space-y-2">
<Label htmlFor="sync-server-url">{t("sync.serverUrl")}</Label>
<Input
id="sync-server-url"
placeholder={t("sync.serverUrlPlaceholder")}
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="sync-token">{t("sync.token")}</Label>
<div className="relative">
<Input
id="sync-token"
type={showToken ? "text" : "password"}
placeholder={t("sync.tokenPlaceholder")}
value={token}
onChange={(e) => setToken(e.target.value)}
className="pr-10"
/>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => setShowToken(!showToken)}
className="absolute right-3 top-1/2 p-1 rounded-sm transition-colors transform -translate-y-1/2 hover:bg-accent"
aria-label={showToken ? "Hide token" : "Show token"}
>
{showToken ? (
<LuEyeOff className="w-4 h-4 text-muted-foreground hover:text-foreground" />
) : (
<LuEye className="w-4 h-4 text-muted-foreground hover:text-foreground" />
)}
</button>
</TooltipTrigger>
<TooltipContent>
{showToken ? "Hide token" : "Show token"}
</TooltipContent>
</Tooltip>
<div className="space-y-2">
<Label htmlFor="sync-token">{t("sync.token")}</Label>
<div className="relative">
<Input
id="sync-token"
type={showToken ? "text" : "password"}
placeholder={t("sync.tokenPlaceholder")}
value={token}
onChange={(e) => setToken(e.target.value)}
className="pr-10"
/>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => setShowToken(!showToken)}
className="absolute right-3 top-1/2 p-1 rounded-sm transition-colors transform -translate-y-1/2 hover:bg-accent"
aria-label={showToken ? "Hide token" : "Show token"}
>
{showToken ? (
<LuEyeOff className="w-4 h-4 text-muted-foreground hover:text-foreground" />
) : (
<LuEye className="w-4 h-4 text-muted-foreground hover:text-foreground" />
)}
</button>
</TooltipTrigger>
<TooltipContent>
{showToken ? "Hide token" : "Show token"}
</TooltipContent>
</Tooltip>
</div>
</div>
</div>
{isConnected && (
<div className="flex gap-2 items-center text-sm text-muted-foreground">
<div className="w-2 h-2 rounded-full bg-green-500" />
{t("sync.status.connected")}
</div>
)}
</div>
)}
<DialogFooter className="flex gap-2">
{isConnected && (
<div className="flex gap-2 items-center text-sm text-muted-foreground">
<div className="w-2 h-2 rounded-full bg-green-500" />
{t("sync.status.connected")}
</div>
<Button
variant="outline"
onClick={handleDisconnect}
disabled={isSaving}
>
Disconnect
</Button>
)}
</div>
)}
<DialogFooter className="flex gap-2">
{isConnected && (
<Button
variant="outline"
onClick={handleDisconnect}
disabled={isSaving}
onClick={handleTestConnection}
disabled={isTesting || !serverUrl}
>
Disconnect
{isTesting ? "Testing..." : "Test Connection"}
</Button>
)}
<Button
variant="outline"
onClick={handleTestConnection}
disabled={isTesting || !serverUrl}
>
{isTesting ? "Testing..." : "Test Connection"}
</Button>
<LoadingButton
onClick={handleSave}
isLoading={isSaving}
disabled={!serverUrl || !token}
>
Save
</LoadingButton>
</DialogFooter>
</TabsContent>
</Tabs>
<LoadingButton
onClick={handleSave}
isLoading={isSaving}
disabled={!serverUrl || !token}
>
Save
</LoadingButton>
</DialogFooter>
</TabsContent>
</Tabs>
)}
</DialogContent>
</Dialog>
);