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
+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}>