refactor: better custom name

This commit is contained in:
zhom
2026-03-02 11:29:17 +04:00
parent 8a96d18e46
commit 97b1225d40
17 changed files with 525 additions and 64 deletions
+10 -10
View File
@@ -5,6 +5,7 @@ import { listen } from "@tauri-apps/api/event";
import { getCurrent } from "@tauri-apps/plugin-deep-link";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { CamoufoxConfigDialog } from "@/components/camoufox-config-dialog";
import { CloneProfileDialog } from "@/components/clone-profile-dialog";
import { CommercialTrialModal } from "@/components/commercial-trial-modal";
import { CookieCopyDialog } from "@/components/cookie-copy-dialog";
import { CookieManagementDialog } from "@/components/cookie-management-dialog";
@@ -168,6 +169,7 @@ export default function Home() {
const [pendingUrls, setPendingUrls] = useState<PendingUrl[]>([]);
const [currentProfileForCamoufoxConfig, setCurrentProfileForCamoufoxConfig] =
useState<BrowserProfile | null>(null);
const [cloneProfile, setCloneProfile] = useState<BrowserProfile | null>(null);
const [hasCheckedStartupPrompt, setHasCheckedStartupPrompt] = useState(false);
const [launchOnLoginDialogOpen, setLaunchOnLoginDialogOpen] = useState(false);
const [windowResizeWarningOpen, setWindowResizeWarningOpen] = useState(false);
@@ -585,16 +587,8 @@ export default function Home() {
}
}, []);
const handleCloneProfile = useCallback(async (profile: BrowserProfile) => {
try {
await invoke<BrowserProfile>("clone_profile", {
profileId: profile.id,
});
} catch (err: unknown) {
console.error("Failed to clone profile:", err);
const errorMessage = err instanceof Error ? err.message : String(err);
showErrorToast(`Failed to clone profile: ${errorMessage}`);
}
const handleCloneProfile = useCallback((profile: BrowserProfile) => {
setCloneProfile(profile);
}, []);
const handleDeleteProfile = useCallback(async (profile: BrowserProfile) => {
@@ -1139,6 +1133,12 @@ export default function Home() {
onPermissionGranted={checkNextPermission}
/>
<CloneProfileDialog
isOpen={!!cloneProfile}
onClose={() => setCloneProfile(null)}
profile={cloneProfile}
/>
<CamoufoxConfigDialog
isOpen={camoufoxConfigDialogOpen}
onClose={() => {
+109
View File
@@ -0,0 +1,109 @@
"use client";
import { invoke } from "@tauri-apps/api/core";
import * as React from "react";
import { useTranslation } from "react-i18next";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { showErrorToast } from "@/lib/toast-utils";
import type { BrowserProfile } from "@/types";
import { LoadingButton } from "./loading-button";
import { RippleButton } from "./ui/ripple";
interface CloneProfileDialogProps {
isOpen: boolean;
onClose: () => void;
profile: BrowserProfile | null;
onCloneComplete?: () => void;
}
export function CloneProfileDialog({
isOpen,
onClose,
profile,
onCloneComplete,
}: CloneProfileDialogProps) {
const { t } = useTranslation();
const [name, setName] = React.useState("");
const [isLoading, setIsLoading] = React.useState(false);
const inputRef = React.useRef<HTMLInputElement>(null);
React.useEffect(() => {
if (isOpen && profile) {
const defaultName = `${profile.name} (Copy)`;
setName(defaultName);
setTimeout(() => {
inputRef.current?.focus();
inputRef.current?.select();
}, 0);
} else {
setIsLoading(false);
}
}, [isOpen, profile]);
if (!profile) return null;
const handleClone = async () => {
if (!name.trim() || isLoading) return;
setIsLoading(true);
try {
await invoke<BrowserProfile>("clone_profile", {
profileId: profile.id,
name: name.trim(),
});
onClose();
onCloneComplete?.();
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
showErrorToast(`Failed to clone profile: ${errorMessage}`);
} finally {
setIsLoading(false);
}
};
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("profileInfo.clone.title")}</DialogTitle>
<DialogDescription>
{t("profileInfo.clone.description")}
</DialogDescription>
</DialogHeader>
<Input
ref={inputRef}
value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") void handleClone();
}}
placeholder={t("profileInfo.clone.namePlaceholder")}
disabled={isLoading}
/>
<DialogFooter>
<RippleButton
variant="outline"
onClick={onClose}
disabled={isLoading}
>
{t("common.buttons.cancel")}
</RippleButton>
<LoadingButton
onClick={() => void handleClone()}
isLoading={isLoading}
disabled={!name.trim()}
>
{t("profileInfo.clone.button")}
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+18 -2
View File
@@ -2279,6 +2279,7 @@ export function ProfilesDataTable({
},
{
id: "settings",
size: 40,
cell: ({ row, table }) => {
const meta = table.options.meta as TableMeta;
const profile = row.original;
@@ -2341,7 +2342,14 @@ export function ProfilesDataTable({
<TableRow key={headerGroup.id} className="overflow-visible">
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
<TableHead
key={header.id}
style={{
width: header.column.columnDef.size
? `${header.column.getSize()}px`
: undefined,
}}
>
{header.isPlaceholder
? null
: flexRender(
@@ -2374,7 +2382,15 @@ export function ProfilesDataTable({
)}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="overflow-visible">
<TableCell
key={cell.id}
className="overflow-visible"
style={{
width: cell.column.columnDef.size
? `${cell.column.getSize()}px`
: undefined,
}}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
+44 -42
View File
@@ -398,36 +398,63 @@ export function ProfileInfoDialog({
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<ProfileIcon className="w-5 h-5" />
{profile.name}
</DialogTitle>
<DialogTitle>{t("profileInfo.title")}</DialogTitle>
</DialogHeader>
<Tabs defaultValue="info">
<TabsList className="w-full">
<TabsTrigger value="info" className="flex-1">
{t("profileInfo.tabs.info")}
</TabsTrigger>
<TabsTrigger value="network" className="flex-1">
{t("profileInfo.tabs.network")}
</TabsTrigger>
<TabsTrigger value="settings" className="flex-1">
{t("profileInfo.tabs.settings")}
</TabsTrigger>
</TabsList>
<TabsContent value="info">
<div className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-3 py-2">
{infoFields.map((field) => (
<React.Fragment key={field.label}>
<span className="text-sm text-muted-foreground whitespace-nowrap">
{field.label}
</span>
<span className="text-sm">{field.value}</span>
</React.Fragment>
))}
<div className="flex flex-col items-center gap-1 py-3">
<ProfileIcon className="w-12 h-12 text-muted-foreground" />
<h3 className="text-lg font-semibold">{profile.name}</h3>
<p className="text-sm text-muted-foreground">
{getBrowserDisplayName(profile.browser)} {profile.version}
</p>
</div>
<div className="max-h-[300px] overflow-y-auto">
<div className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-3 py-2">
{infoFields.map((field) => (
<React.Fragment key={field.label}>
<span className="text-sm text-muted-foreground whitespace-nowrap">
{field.label}
</span>
<span className="text-sm">{field.value}</span>
</React.Fragment>
))}
</div>
</div>
</TabsContent>
<TabsContent value="network">
<TabsContent value="settings">
<div className="flex flex-col py-1">
{visibleActions.map((action) => (
<button
key={action.label}
type="button"
disabled={action.disabled}
onClick={action.onClick}
className={cn(
"flex items-center gap-3 px-3 py-2.5 rounded-md text-sm transition-colors text-left w-full",
"hover:bg-accent disabled:opacity-50 disabled:pointer-events-none",
action.destructive &&
"text-destructive hover:bg-destructive/10",
)}
>
{action.icon}
<span className="flex-1 flex items-center gap-2">
{action.label}
{action.proBadge && <ProBadge />}
</span>
<LuChevronRight className="w-4 h-4 text-muted-foreground" />
</button>
))}
</div>
<div className="border-t my-2" />
<div className="flex flex-col gap-3 py-2">
<div>
<h4 className="text-sm font-medium">
@@ -484,31 +511,6 @@ export function ProfileInfoDialog({
</p>
</div>
</TabsContent>
<TabsContent value="settings">
<div className="flex flex-col py-1">
{visibleActions.map((action) => (
<button
key={action.label}
type="button"
disabled={action.disabled}
onClick={action.onClick}
className={cn(
"flex items-center gap-3 px-3 py-2.5 rounded-md text-sm transition-colors text-left w-full",
"hover:bg-accent disabled:opacity-50 disabled:pointer-events-none",
action.destructive &&
"text-destructive hover:bg-destructive/10",
)}
>
{action.icon}
<span className="flex-1 flex items-center gap-2">
{action.label}
{action.proBadge && <ProBadge />}
</span>
<LuChevronRight className="w-4 h-4 text-muted-foreground" />
</button>
))}
</div>
</TabsContent>
</Tabs>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
+6 -1
View File
@@ -683,7 +683,6 @@
"title": "Profile Details",
"tabs": {
"info": "Info",
"network": "Network",
"settings": "Settings"
},
"fields": {
@@ -716,6 +715,12 @@
},
"actions": {
"manageCookies": "Manage Cookies"
},
"clone": {
"title": "Clone Profile",
"description": "Enter a name for the cloned profile",
"namePlaceholder": "Profile name",
"button": "Clone"
}
},
"extensions": {
+6 -1
View File
@@ -683,7 +683,6 @@
"title": "Detalles del Perfil",
"tabs": {
"info": "Info",
"network": "Red",
"settings": "Configuración"
},
"fields": {
@@ -716,6 +715,12 @@
},
"actions": {
"manageCookies": "Administrar Cookies"
},
"clone": {
"title": "Clonar Perfil",
"description": "Ingrese un nombre para el perfil clonado",
"namePlaceholder": "Nombre del perfil",
"button": "Clonar"
}
},
"extensions": {
+6 -1
View File
@@ -683,7 +683,6 @@
"title": "Détails du Profil",
"tabs": {
"info": "Info",
"network": "Réseau",
"settings": "Paramètres"
},
"fields": {
@@ -716,6 +715,12 @@
},
"actions": {
"manageCookies": "Gérer les Cookies"
},
"clone": {
"title": "Cloner le Profil",
"description": "Entrez un nom pour le profil cloné",
"namePlaceholder": "Nom du profil",
"button": "Cloner"
}
},
"extensions": {
+6 -1
View File
@@ -683,7 +683,6 @@
"title": "プロフィール詳細",
"tabs": {
"info": "情報",
"network": "ネットワーク",
"settings": "設定"
},
"fields": {
@@ -716,6 +715,12 @@
},
"actions": {
"manageCookies": "Cookieを管理"
},
"clone": {
"title": "プロフィールを複製",
"description": "複製されたプロフィールの名前を入力してください",
"namePlaceholder": "プロフィール名",
"button": "複製"
}
},
"extensions": {
+6 -1
View File
@@ -683,7 +683,6 @@
"title": "Detalhes do Perfil",
"tabs": {
"info": "Info",
"network": "Rede",
"settings": "Configurações"
},
"fields": {
@@ -716,6 +715,12 @@
},
"actions": {
"manageCookies": "Gerenciar Cookies"
},
"clone": {
"title": "Clonar Perfil",
"description": "Digite um nome para o perfil clonado",
"namePlaceholder": "Nome do perfil",
"button": "Clonar"
}
},
"extensions": {
+6 -1
View File
@@ -683,7 +683,6 @@
"title": "Детали профиля",
"tabs": {
"info": "Информация",
"network": "Сеть",
"settings": "Настройки"
},
"fields": {
@@ -716,6 +715,12 @@
},
"actions": {
"manageCookies": "Управление Cookie"
},
"clone": {
"title": "Клонировать профиль",
"description": "Введите имя для клонированного профиля",
"namePlaceholder": "Имя профиля",
"button": "Клонировать"
}
},
"extensions": {
+6 -1
View File
@@ -683,7 +683,6 @@
"title": "配置文件详情",
"tabs": {
"info": "信息",
"network": "网络",
"settings": "设置"
},
"fields": {
@@ -716,6 +715,12 @@
},
"actions": {
"manageCookies": "管理 Cookie"
},
"clone": {
"title": "克隆配置文件",
"description": "输入克隆配置文件的名称",
"namePlaceholder": "配置文件名称",
"button": "克隆"
}
},
"extensions": {