mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-12 09:47:51 +02:00
refactor: better custom name
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user