mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-06 07:03:52 +02:00
refactor: cleanup
This commit is contained in:
@@ -77,11 +77,16 @@ export function CookieCopyDialog({
|
||||
);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Never offer a selected profile as a source — you can't copy a profile's
|
||||
// cookies onto itself, and including it here would leave the user in a
|
||||
// dead-end state (source picked = target list empty = copy button disabled).
|
||||
const eligibleSourceProfiles = useMemo(() => {
|
||||
return profiles.filter(
|
||||
(p) => p.browser === "wayfern" || p.browser === "camoufox",
|
||||
(p) =>
|
||||
!selectedProfiles.includes(p.id) &&
|
||||
(p.browser === "wayfern" || p.browser === "camoufox"),
|
||||
);
|
||||
}, [profiles]);
|
||||
}, [profiles, selectedProfiles]);
|
||||
|
||||
const targetProfiles = useMemo(() => {
|
||||
return profiles.filter(
|
||||
@@ -148,22 +153,21 @@ export function CookieCopyDialog({
|
||||
const toggleDomain = useCallback(
|
||||
(domain: string, cookies: UnifiedCookie[]) => {
|
||||
setSelection((prev) => {
|
||||
const current = prev[domain];
|
||||
const allSelected = current.allSelected;
|
||||
|
||||
if (allSelected) {
|
||||
// `prev[domain]` is `undefined` for any domain not yet interacted with
|
||||
// and after the user fully deselects it (toggleCookie deletes the
|
||||
// entry on empty). Treat missing as "not selected".
|
||||
if (prev[domain]?.allSelected) {
|
||||
const newSelection = { ...prev };
|
||||
delete newSelection[domain];
|
||||
return newSelection;
|
||||
} else {
|
||||
return {
|
||||
...prev,
|
||||
[domain]: {
|
||||
allSelected: true,
|
||||
cookies: new Set(cookies.map((c) => c.name)),
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
[domain]: {
|
||||
allSelected: true,
|
||||
cookies: new Set(cookies.map((c) => c.name)),
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
[],
|
||||
@@ -503,9 +507,13 @@ function DomainRow({
|
||||
onToggleCookie,
|
||||
onToggleExpand,
|
||||
}: DomainRowProps) {
|
||||
// `selection[domain.domain]` is `undefined` for domains the user hasn't
|
||||
// touched yet (initial state after loading cookies is `{}`) and for any
|
||||
// domain the user fully deselected (toggleCookie deletes the entry on
|
||||
// empty). Default to "no cookies selected" instead of crashing.
|
||||
const domainSelection = selection[domain.domain];
|
||||
const isAllSelected = domainSelection.allSelected;
|
||||
const selectedCount = domainSelection.cookies.size;
|
||||
const isAllSelected = domainSelection?.allSelected ?? false;
|
||||
const selectedCount = domainSelection?.cookies.size ?? 0;
|
||||
const isPartial =
|
||||
selectedCount > 0 && selectedCount < domain.cookie_count && !isAllSelected;
|
||||
|
||||
@@ -540,7 +548,8 @@ function DomainRow({
|
||||
{isExpanded && (
|
||||
<div className="ml-8 pl-2 border-l space-y-1">
|
||||
{domain.cookies.map((cookie) => {
|
||||
const isSelected = domainSelection.cookies.has(cookie.name);
|
||||
const isSelected =
|
||||
domainSelection?.cookies.has(cookie.name) ?? false;
|
||||
return (
|
||||
<div
|
||||
key={`${domain.domain}-${cookie.name}`}
|
||||
|
||||
@@ -309,8 +309,11 @@ export function CookieManagementDialog({
|
||||
const toggleDomain = useCallback(
|
||||
(domain: string, cookies: UnifiedCookie[]) => {
|
||||
setExportSelection((prev) => {
|
||||
const current = prev[domain];
|
||||
if (current.allSelected) {
|
||||
// `prev[domain]` is `undefined` when the domain was previously fully
|
||||
// deselected (entries are deleted on empty — see toggleCookie). Treat
|
||||
// missing as "not selected" so re-enabling falls through to the add
|
||||
// branch instead of crashing on `.allSelected`.
|
||||
if (prev[domain]?.allSelected) {
|
||||
const next = { ...prev };
|
||||
delete next[domain];
|
||||
return next;
|
||||
@@ -592,8 +595,8 @@ function ExportDomainRow({
|
||||
onToggleExpand,
|
||||
}: ExportDomainRowProps) {
|
||||
const domainSelection = selection[domain.domain];
|
||||
const isAllSelected = domainSelection.allSelected;
|
||||
const selectedCount = domainSelection.cookies.size;
|
||||
const isAllSelected = domainSelection?.allSelected ?? false;
|
||||
const selectedCount = domainSelection?.cookies.size ?? 0;
|
||||
const isPartial =
|
||||
selectedCount > 0 && selectedCount < domain.cookie_count && !isAllSelected;
|
||||
|
||||
@@ -628,7 +631,8 @@ function ExportDomainRow({
|
||||
{isExpanded && (
|
||||
<div className="ml-7 pl-2 border-l space-y-0.5">
|
||||
{domain.cookies.map((cookie) => {
|
||||
const isSelected = domainSelection.cookies.has(cookie.name);
|
||||
const isSelected =
|
||||
domainSelection?.cookies.has(cookie.name) ?? false;
|
||||
return (
|
||||
<div
|
||||
key={`${domain.domain}-${cookie.name}`}
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
ProfileBypassRulesDialog,
|
||||
ProfileDnsBlocklistDialog,
|
||||
ProfileInfoDialog,
|
||||
ProfileLaunchHookDialog,
|
||||
} from "@/components/profile-info-dialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -937,6 +938,8 @@ export function ProfilesDataTable({
|
||||
React.useState<BrowserProfile | null>(null);
|
||||
const [dnsBlocklistProfile, setDnsBlocklistProfile] =
|
||||
React.useState<BrowserProfile | null>(null);
|
||||
const [launchHookProfile, setLaunchHookProfile] =
|
||||
React.useState<BrowserProfile | null>(null);
|
||||
const [launchingProfiles, setLaunchingProfiles] = React.useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
@@ -2680,6 +2683,9 @@ export function ProfilesDataTable({
|
||||
onOpenDnsBlocklist={(profile) => {
|
||||
setDnsBlocklistProfile(profile);
|
||||
}}
|
||||
onOpenLaunchHook={(profile) => {
|
||||
setLaunchHookProfile(profile);
|
||||
}}
|
||||
onCloneProfile={onCloneProfile}
|
||||
onLaunchWithSync={onLaunchWithSync}
|
||||
onDeleteProfile={(profile) => {
|
||||
@@ -2770,6 +2776,14 @@ export function ProfilesDataTable({
|
||||
profileId={dnsBlocklistProfile?.id ?? null}
|
||||
currentLevel={dnsBlocklistProfile?.dns_blocklist ?? null}
|
||||
/>
|
||||
<ProfileLaunchHookDialog
|
||||
isOpen={launchHookProfile !== null}
|
||||
onClose={() => {
|
||||
setLaunchHookProfile(null);
|
||||
}}
|
||||
profileId={launchHookProfile?.id ?? null}
|
||||
currentLaunchHook={launchHookProfile?.launch_hook ?? null}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
LuFingerprint,
|
||||
LuGlobe,
|
||||
LuGroup,
|
||||
LuLink,
|
||||
LuPlus,
|
||||
LuPuzzle,
|
||||
LuRefreshCw,
|
||||
@@ -66,6 +67,7 @@ interface ProfileInfoDialogProps {
|
||||
onAssignExtensionGroup?: (profileIds: string[]) => void;
|
||||
onOpenBypassRules?: (profile: BrowserProfile) => void;
|
||||
onOpenDnsBlocklist?: (profile: BrowserProfile) => void;
|
||||
onOpenLaunchHook?: (profile: BrowserProfile) => void;
|
||||
onCloneProfile?: (profile: BrowserProfile) => void;
|
||||
onDeleteProfile?: (profile: BrowserProfile) => void;
|
||||
onLaunchWithSync?: (profile: BrowserProfile) => void;
|
||||
@@ -113,6 +115,7 @@ export function ProfileInfoDialog({
|
||||
onAssignExtensionGroup,
|
||||
onOpenBypassRules,
|
||||
onOpenDnsBlocklist,
|
||||
onOpenLaunchHook,
|
||||
onCloneProfile,
|
||||
onDeleteProfile,
|
||||
onLaunchWithSync,
|
||||
@@ -128,8 +131,6 @@ export function ProfileInfoDialog({
|
||||
const [extensionGroupName, setExtensionGroupName] = React.useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [launchHookValue, setLaunchHookValue] = React.useState("");
|
||||
const [isSavingLaunchHook, setIsSavingLaunchHook] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isOpen || !profile?.group_id) {
|
||||
@@ -171,12 +172,6 @@ export function ProfileInfoDialog({
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isOpen) {
|
||||
setLaunchHookValue(profile?.launch_hook ?? "");
|
||||
}
|
||||
}, [isOpen, profile?.launch_hook]);
|
||||
|
||||
if (!profile) return null;
|
||||
|
||||
const ProfileIcon = getProfileIcon(profile);
|
||||
@@ -225,23 +220,14 @@ export function ProfileInfoDialog({
|
||||
const hasTags = profile.tags && profile.tags.length > 0;
|
||||
const hasNote = !!profile.note;
|
||||
const showCrossOs = isCrossOsProfile(profile);
|
||||
const trimmedLaunchHook = launchHookValue.trim();
|
||||
const savedLaunchHook = profile.launch_hook ?? "";
|
||||
|
||||
const handleSaveLaunchHook = async () => {
|
||||
setIsSavingLaunchHook(true);
|
||||
try {
|
||||
await invoke("update_profile_launch_hook", {
|
||||
profileId: profile.id,
|
||||
launchHook: trimmedLaunchHook || null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to update launch hook:", error);
|
||||
} finally {
|
||||
setIsSavingLaunchHook(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Items in the settings tab `actions` list MUST only open another dialog
|
||||
// (or trigger a navigation/action that closes this one). Do NOT put inline
|
||||
// settings UI — inputs, toggles, save buttons — directly in this dialog's
|
||||
// settings tab. Each setting belongs in its own focused dialog (see
|
||||
// `ProfileLaunchHookDialog`, `ProfileBypassRulesDialog`,
|
||||
// `ProfileDnsBlocklistDialog` for the pattern). The settings tab is purely
|
||||
// a navigation hub.
|
||||
interface ActionItem {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
@@ -360,6 +346,14 @@ export function ProfileInfoDialog({
|
||||
handleAction(() => onOpenDnsBlocklist?.(profile));
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <LuLink className="w-4 h-4" />,
|
||||
label: t("profiles.actions.launchHook"),
|
||||
onClick: () => {
|
||||
handleAction(() => onOpenLaunchHook?.(profile));
|
||||
},
|
||||
hidden: !onOpenLaunchHook,
|
||||
},
|
||||
{
|
||||
icon: <LuTrash2 className="w-4 h-4" />,
|
||||
label: t("profiles.actions.delete"),
|
||||
@@ -575,31 +569,6 @@ export function ProfileInfoDialog({
|
||||
<TabsContent value="settings">
|
||||
<div className="overflow-y-auto max-h-[calc(80vh-12rem)]">
|
||||
<div className="flex flex-col gap-3 py-1">
|
||||
<div className="rounded-md bg-muted/50 border px-3 py-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("profileInfo.launchHook.label")}
|
||||
</p>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Input
|
||||
value={launchHookValue}
|
||||
onChange={(e) => {
|
||||
setLaunchHookValue(e.target.value);
|
||||
}}
|
||||
placeholder={t("profileInfo.launchHook.placeholder")}
|
||||
disabled={isSavingLaunchHook}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => void handleSaveLaunchHook()}
|
||||
disabled={
|
||||
isSavingLaunchHook ||
|
||||
trimmedLaunchHook === savedLaunchHook
|
||||
}
|
||||
>
|
||||
{t("common.buttons.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
{visibleActions.map((action) => (
|
||||
<button
|
||||
@@ -639,6 +608,80 @@ export function ProfileInfoDialog({
|
||||
);
|
||||
}
|
||||
|
||||
interface ProfileLaunchHookDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
profileId: string | null;
|
||||
currentLaunchHook: string | null;
|
||||
}
|
||||
|
||||
export function ProfileLaunchHookDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
profileId,
|
||||
currentLaunchHook,
|
||||
}: ProfileLaunchHookDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [value, setValue] = React.useState(currentLaunchHook ?? "");
|
||||
const [isSaving, setIsSaving] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isOpen) {
|
||||
setValue(currentLaunchHook ?? "");
|
||||
}
|
||||
}, [isOpen, currentLaunchHook]);
|
||||
|
||||
const trimmed = value.trim();
|
||||
const saved = currentLaunchHook ?? "";
|
||||
const isDirty = trimmed !== saved;
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!profileId) return;
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await invoke("update_profile_launch_hook", {
|
||||
profileId,
|
||||
launchHook: trimmed || null,
|
||||
});
|
||||
onClose();
|
||||
} catch (err) {
|
||||
console.error("Failed to update launch hook:", err);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("profileInfo.launchHook.title")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("profileInfo.launchHook.description")}
|
||||
</p>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
placeholder={t("profileInfo.launchHook.placeholder")}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={() => void handleSave()}
|
||||
disabled={isSaving || !isDirty}
|
||||
className="w-full"
|
||||
>
|
||||
{t("common.buttons.save")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
interface ProfileDnsBlocklistDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
|
||||
Reference in New Issue
Block a user