refactor: share browser state logic across data table and selector dialog

This commit is contained in:
zhom
2025-07-25 11:21:26 +04:00
parent 0b4263140d
commit 25653e166b
3 changed files with 376 additions and 289 deletions
+120 -154
View File
@@ -10,7 +10,6 @@ import {
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import * as React from "react"; import * as React from "react";
import { CiCircleCheck } from "react-icons/ci";
import { IoEllipsisHorizontal } from "react-icons/io5"; import { IoEllipsisHorizontal } from "react-icons/io5";
import { LuChevronDown, LuChevronUp } from "react-icons/lu"; import { LuChevronDown, LuChevronUp } from "react-icons/lu";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -44,6 +43,7 @@ import {
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { useBrowserState } from "@/hooks/use-browser-support";
import { useTableSorting } from "@/hooks/use-table-sorting"; import { useTableSorting } from "@/hooks/use-table-sorting";
import { import {
getBrowserDisplayName, getBrowserDisplayName,
@@ -93,7 +93,7 @@ export function ProfilesDataTable({
const [deleteConfirmationName, setDeleteConfirmationName] = const [deleteConfirmationName, setDeleteConfirmationName] =
React.useState(""); React.useState("");
const [deleteError, setDeleteError] = React.useState<string | null>(null); const [deleteError, setDeleteError] = React.useState<string | null>(null);
const [isClient, setIsClient] = React.useState(false);
const [storedProxies, setStoredProxies] = React.useState<StoredProxy[]>([]); const [storedProxies, setStoredProxies] = React.useState<StoredProxy[]>([]);
// Helper function to check if a profile has a proxy // Helper function to check if a profile has a proxy
@@ -125,10 +125,8 @@ export function ProfilesDataTable({
[storedProxies], [storedProxies],
); );
// Ensure we're on the client side to prevent hydration mismatches // Use shared browser state hook
React.useEffect(() => { const browserState = useBrowserState(data, runningProfiles, isUpdating);
setIsClient(true);
}, []);
// Load stored proxies // Load stored proxies
const loadStoredProxies = React.useCallback(async () => { const loadStoredProxies = React.useCallback(async () => {
@@ -141,10 +139,10 @@ export function ProfilesDataTable({
}, []); }, []);
React.useEffect(() => { React.useEffect(() => {
if (isClient) { if (browserState.isClient) {
void loadStoredProxies(); void loadStoredProxies();
} }
}, [isClient, loadStoredProxies]); }, [browserState.isClient, loadStoredProxies]);
// Reload proxy data when requested from parent // Reload proxy data when requested from parent
React.useEffect(() => { React.useEffect(() => {
@@ -155,21 +153,21 @@ export function ProfilesDataTable({
// Update local sorting state when settings are loaded // Update local sorting state when settings are loaded
React.useEffect(() => { React.useEffect(() => {
if (isLoaded && isClient) { if (isLoaded && browserState.isClient) {
setSorting(getTableSorting()); setSorting(getTableSorting());
} }
}, [isLoaded, getTableSorting, isClient]); }, [isLoaded, getTableSorting, browserState.isClient]);
// Handle sorting changes // Handle sorting changes
const handleSortingChange = React.useCallback( const handleSortingChange = React.useCallback(
(updater: React.SetStateAction<SortingState>) => { (updater: React.SetStateAction<SortingState>) => {
if (!isClient) return; if (!browserState.isClient) return;
const newSorting = const newSorting =
typeof updater === "function" ? updater(sorting) : updater; typeof updater === "function" ? updater(sorting) : updater;
setSorting(newSorting); setSorting(newSorting);
updateSorting(newSorting); updateSorting(newSorting);
}, },
[sorting, updateSorting, isClient], [browserState.isClient, sorting, updateSorting],
); );
const handleRename = async () => { const handleRename = async () => {
@@ -180,18 +178,16 @@ export function ProfilesDataTable({
setProfileToRename(null); setProfileToRename(null);
setNewProfileName(""); setNewProfileName("");
setRenameError(null); setRenameError(null);
} catch (err) { } catch (error) {
setRenameError(err as string); setRenameError(
error instanceof Error ? error.message : "Failed to rename profile",
);
} }
}; };
const handleDelete = async () => { const handleDelete = async () => {
if (!profileToDelete || !deleteConfirmationName.trim()) return; if (!profileToDelete || deleteConfirmationName !== profileToDelete.name) {
setDeleteError("Profile name confirmation does not match");
if (deleteConfirmationName.trim() !== profileToDelete.name) {
setDeleteError(
"Profile name doesn't match. Please type the exact name to confirm deletion.",
);
return; return;
} }
@@ -200,8 +196,10 @@ export function ProfilesDataTable({
setProfileToDelete(null); setProfileToDelete(null);
setDeleteConfirmationName(""); setDeleteConfirmationName("");
setDeleteError(null); setDeleteError(null);
} catch (err) { } catch (error) {
setDeleteError(err as string); setDeleteError(
error instanceof Error ? error.message : "Failed to delete profile",
);
} }
}; };
@@ -211,49 +209,32 @@ export function ProfilesDataTable({
id: "actions", id: "actions",
cell: ({ row }) => { cell: ({ row }) => {
const profile = row.original; const profile = row.original;
const isRunning = isClient && runningProfiles.has(profile.name); const isRunning =
const isBrowserUpdating = isClient && isUpdating(profile.browser); browserState.isClient && runningProfiles.has(profile.name);
const canLaunch = browserState.canLaunchProfile(profile);
// Check if any TOR browser profile is running const tooltipContent = browserState.getLaunchTooltipContent(profile);
const isTorBrowser = profile.browser === "tor-browser";
const anyTorRunning =
isClient &&
data.some(
(p) => p.browser === "tor-browser" && runningProfiles.has(p.name),
);
const shouldDisableTorStart =
isTorBrowser && !isRunning && anyTorRunning;
const isDisabled = shouldDisableTorStart || isBrowserUpdating;
return ( return (
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <span className="inline-flex">
variant={isRunning ? "destructive" : "default"} <Button
size="sm" variant={isRunning ? "destructive" : "default"}
disabled={!isClient || isDisabled} size="sm"
onClick={() => disabled={!canLaunch}
void (isRunning className={!canLaunch ? "opacity-50" : ""}
? onKillProfile(profile) onClick={() =>
: onLaunchProfile(profile)) void (isRunning
} ? onKillProfile(profile)
> : onLaunchProfile(profile))
{isRunning ? "Stop" : "Launch"} }
</Button> >
{isRunning ? "Stop" : "Launch"}
</Button>
</span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>{tooltipContent}</TooltipContent>
{!isClient
? "Loading..."
: isRunning
? "Click to forcefully stop the browser"
: isBrowserUpdating
? `${profile.browser} is being updated. Please wait for the update to complete.`
: shouldDisableTorStart
? "Only one TOR browser instance can run at a time. Stop the running TOR browser first."
: "Click to launch the browser"}
</TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
); );
@@ -262,91 +243,67 @@ export function ProfilesDataTable({
{ {
accessorKey: "name", accessorKey: "name",
header: ({ column }) => { header: ({ column }) => {
const isSorted = column.getIsSorted();
return ( return (
<Button <Button
variant="ghost" variant="ghost"
onClick={() => { onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc"); column.toggleSorting(column.getIsSorted() === "asc")
}} }
className="p-0 h-auto font-semibold hover:bg-transparent" className="h-auto p-0 font-semibold text-left justify-start"
> >
Profile Name
{isSorted === "asc" && <LuChevronUp className="ml-2 w-4 h-4" />} {column.getIsSorted() === "asc" ? (
{isSorted === "desc" && ( <LuChevronUp className="ml-2 h-4 w-4" />
<LuChevronDown className="ml-2 w-4 h-4" /> ) : column.getIsSorted() === "desc" ? (
)} <LuChevronDown className="ml-2 h-4 w-4" />
{!isSorted && ( ) : null}
<LuChevronDown className="ml-2 w-4 h-4 opacity-50" />
)}
</Button> </Button>
); );
}, },
enableSorting: true, enableSorting: true,
sortingFn: "alphanumeric", sortingFn: "alphanumeric",
cell: ({ row }) => { cell: ({ row }) => {
const profile = row.original; const name: string = row.getValue("name");
return profile.name.length > 15 ? ( return <div className="font-medium text-left">{name}</div>;
<Tooltip>
<TooltipTrigger asChild>
<span className="truncate">{profile.name.slice(0, 15)}...</span>
</TooltipTrigger>
<TooltipContent>{profile.name}</TooltipContent>
</Tooltip>
) : (
profile.name
);
}, },
}, },
{ {
accessorKey: "browser", accessorKey: "browser",
header: ({ column }) => { header: ({ column }) => {
const isSorted = column.getIsSorted();
return ( return (
<Button <Button
variant="ghost" variant="ghost"
onClick={() => { onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc"); column.toggleSorting(column.getIsSorted() === "asc")
}} }
className="p-0 h-auto font-semibold hover:bg-transparent" className="h-auto p-0 font-semibold text-left justify-start"
> >
Browser Browser
{isSorted === "asc" && <LuChevronUp className="ml-2 w-4 h-4" />} {column.getIsSorted() === "asc" ? (
{isSorted === "desc" && ( <LuChevronUp className="ml-2 h-4 w-4" />
<LuChevronDown className="ml-2 w-4 h-4" /> ) : column.getIsSorted() === "desc" ? (
)} <LuChevronDown className="ml-2 h-4 w-4" />
{!isSorted && ( ) : null}
<LuChevronDown className="ml-2 w-4 h-4 opacity-50" />
)}
</Button> </Button>
); );
}, },
cell: ({ row }) => { cell: ({ row }) => {
const browser: string = row.getValue("browser"); const browser: string = row.getValue("browser");
const IconComponent = getBrowserIcon(browser); const IconComponent = getBrowserIcon(browser);
const browserDisplayName = getBrowserDisplayName(browser); return (
return browserDisplayName.length > 15 ? ( <div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<div className="flex gap-2 items-center">
{IconComponent && <IconComponent className="w-4 h-4" />}
<span>{browserDisplayName.slice(0, 15)}...</span>
</div>
</TooltipTrigger>
<TooltipContent>{browserDisplayName}</TooltipContent>
</Tooltip>
) : (
<div className="flex gap-2 items-center">
{IconComponent && <IconComponent className="w-4 h-4" />} {IconComponent && <IconComponent className="w-4 h-4" />}
<span>{browserDisplayName}</span> <span>{getBrowserDisplayName(browser)}</span>
</div> </div>
); );
}, },
enableSorting: true, enableSorting: true,
sortingFn: (rowA, rowB, columnId) => { sortingFn: (rowA, rowB, columnId) => {
const browserA = getBrowserDisplayName(rowA.getValue(columnId)); const browserA: string = rowA.getValue(columnId);
const browserB = getBrowserDisplayName(rowB.getValue(columnId)); const browserB: string = rowB.getValue(columnId);
return browserA.localeCompare(browserB); return getBrowserDisplayName(browserA).localeCompare(
getBrowserDisplayName(browserB),
);
}, },
}, },
{ {
@@ -398,38 +355,41 @@ export function ProfilesDataTable({
: "No proxy configured"; : "No proxy configured";
return ( return (
<Tooltip> <div className="flex items-center gap-2">
<TooltipTrigger> <Tooltip>
<div className="flex gap-2 items-center"> <TooltipTrigger asChild>
{profileHasProxy && ( <span className="inline-flex">
<CiCircleCheck className="w-4 h-4 text-green-500" /> <Button
)} variant="outline"
size="sm"
{proxyDisplayName.length > 10 ? ( onClick={() => onProxySettings(profile)}
<span className="text-sm truncate text-muted-foreground"> disabled={
{proxyDisplayName.slice(0, 10)}... !browserState.isClient ||
</span> profile.browser === "tor-browser"
) : ( }
<span className="text-sm text-muted-foreground"> className={
{profile.browser === "tor-browser" profile.browser === "tor-browser" ? "opacity-50" : ""
? "Not supported" }
: proxyDisplayName} >
</span> {profileHasProxy ? "Configured" : "Configure"}
)} </Button>
</div> </span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>{tooltipText}</TooltipContent> <TooltipContent>{tooltipText}</TooltipContent>
</Tooltip> </Tooltip>
</div>
); );
}, },
}, },
// Update the settings column to use the confirmation dialog
{ {
id: "settings", id: "settings",
cell: ({ row }) => { cell: ({ row }) => {
const profile = row.original; const profile = row.original;
const isRunning = isClient && runningProfiles.has(profile.name); const isRunning =
const isBrowserUpdating = isClient && isUpdating(profile.browser); browserState.isClient && runningProfiles.has(profile.name);
const isBrowserUpdating =
browserState.isClient && isUpdating(profile.browser);
return ( return (
<div className="flex justify-end items-center"> <div className="flex justify-end items-center">
<DropdownMenu> <DropdownMenu>
@@ -437,7 +397,7 @@ export function ProfilesDataTable({
<Button <Button
variant="ghost" variant="ghost"
className="p-0 w-8 h-8" className="p-0 w-8 h-8"
disabled={!isClient} disabled={!browserState.isClient}
> >
<span className="sr-only">Open menu</span> <span className="sr-only">Open menu</span>
<IoEllipsisHorizontal className="w-4 h-4" /> <IoEllipsisHorizontal className="w-4 h-4" />
@@ -450,7 +410,7 @@ export function ProfilesDataTable({
onClick={() => { onClick={() => {
onProxySettings(profile); onProxySettings(profile);
}} }}
disabled={!isClient || isBrowserUpdating} disabled={!browserState.isClient || isBrowserUpdating}
> >
Configure Proxy Configure Proxy
</DropdownMenuItem> </DropdownMenuItem>
@@ -459,7 +419,9 @@ export function ProfilesDataTable({
onClick={() => { onClick={() => {
onConfigureCamoufox(profile); onConfigureCamoufox(profile);
}} }}
disabled={!isClient || isRunning || isBrowserUpdating} disabled={
!browserState.isClient || isRunning || isBrowserUpdating
}
> >
Configure Camoufox Configure Camoufox
</DropdownMenuItem> </DropdownMenuItem>
@@ -471,7 +433,9 @@ export function ProfilesDataTable({
onClick={() => { onClick={() => {
onChangeVersion(profile); onChangeVersion(profile);
}} }}
disabled={!isClient || isRunning || isBrowserUpdating} disabled={
!browserState.isClient || isRunning || isBrowserUpdating
}
> >
Switch Release Switch Release
</DropdownMenuItem> </DropdownMenuItem>
@@ -481,7 +445,9 @@ export function ProfilesDataTable({
setProfileToRename(profile); setProfileToRename(profile);
setNewProfileName(profile.name); setNewProfileName(profile.name);
}} }}
disabled={!isClient || isRunning || isBrowserUpdating} disabled={
!browserState.isClient || isRunning || isBrowserUpdating
}
> >
Rename Rename
</DropdownMenuItem> </DropdownMenuItem>
@@ -490,8 +456,9 @@ export function ProfilesDataTable({
setProfileToDelete(profile); setProfileToDelete(profile);
setDeleteConfirmationName(""); setDeleteConfirmationName("");
}} }}
className="text-red-600" disabled={
disabled={!isClient || isRunning || isBrowserUpdating} !browserState.isClient || isRunning || isBrowserUpdating
}
> >
Delete Delete
</DropdownMenuItem> </DropdownMenuItem>
@@ -503,18 +470,17 @@ export function ProfilesDataTable({
}, },
], ],
[ [
isClient,
runningProfiles, runningProfiles,
isUpdating, browserState,
data,
onLaunchProfile,
onKillProfile,
onProxySettings,
onChangeVersion,
onConfigureCamoufox,
getProxyInfo,
hasProxy, hasProxy,
getProxyDisplayName, getProxyDisplayName,
getProxyInfo,
onProxySettings,
onLaunchProfile,
onKillProfile,
onConfigureCamoufox,
onChangeVersion,
isUpdating,
], ],
); );
@@ -559,7 +525,7 @@ export function ProfilesDataTable({
))} ))}
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{table.getRowModel().rows.length ? ( {table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
<TableRow <TableRow
key={row.id} key={row.id}
+68 -134
View File
@@ -27,6 +27,7 @@ import {
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { useBrowserState } from "@/hooks/use-browser-support";
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils"; import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
import type { BrowserProfile, StoredProxy } from "@/types"; import type { BrowserProfile, StoredProxy } from "@/types";
@@ -49,6 +50,9 @@ export function ProfileSelectorDialog({
const [isLaunching, setIsLaunching] = useState(false); const [isLaunching, setIsLaunching] = useState(false);
const [storedProxies, setStoredProxies] = useState<StoredProxy[]>([]); const [storedProxies, setStoredProxies] = useState<StoredProxy[]>([]);
// Use shared browser state hook
const browserState = useBrowserState(profiles, runningProfiles);
// Helper function to check if a profile has a proxy // Helper function to check if a profile has a proxy
const hasProxy = useCallback( const hasProxy = useCallback(
(profile: BrowserProfile): boolean => { (profile: BrowserProfile): boolean => {
@@ -59,50 +63,6 @@ export function ProfileSelectorDialog({
[storedProxies], [storedProxies],
); );
// Helper function to determine if a profile can be used for opening links
const canUseProfileForLinks = useCallback(
(
profile: BrowserProfile,
allProfiles: BrowserProfile[],
runningProfiles: Set<string>,
): boolean => {
const isRunning = runningProfiles.has(profile.name);
// For TOR browser: Check if any TOR browser is running
if (profile.browser === "tor-browser") {
const runningTorProfiles = allProfiles.filter(
(p) => p.browser === "tor-browser" && runningProfiles.has(p.name),
);
// If no TOR browser is running, allow any TOR profile
if (runningTorProfiles.length === 0) {
return true;
}
// If TOR browser(s) are running, only allow the running one(s)
return isRunning;
}
// For Mullvad browser: Check if any Mullvad browser is running
if (profile.browser === "mullvad-browser") {
const runningMullvadProfiles = allProfiles.filter(
(p) => p.browser === "mullvad-browser" && runningProfiles.has(p.name),
);
// If no Mullvad browser is running, allow any Mullvad profile
if (runningMullvadProfiles.length === 0) {
return true;
}
// If Mullvad browser(s) are running, only allow the running one(s)
return isRunning;
}
return true;
},
[],
);
const loadProfiles = useCallback(async () => { const loadProfiles = useCallback(async () => {
setIsLoading(true); setIsLoading(true);
try { try {
@@ -124,52 +84,31 @@ export function ProfileSelectorDialog({
// First, try to find a running profile that can be used for opening links // First, try to find a running profile that can be used for opening links
const runningAvailableProfile = profileList.find((profile) => { const runningAvailableProfile = profileList.find((profile) => {
const isRunning = runningProfiles.has(profile.name); const isRunning = runningProfiles.has(profile.name);
return ( return isRunning && browserState.canUseProfileForLinks(profile);
isRunning &&
canUseProfileForLinks(profile, profileList, runningProfiles)
);
}); });
if (runningAvailableProfile) { if (runningAvailableProfile) {
setSelectedProfile(runningAvailableProfile.name); setSelectedProfile(runningAvailableProfile.name);
} else { } else {
// If no running profile is suitable, find the first profile that can be used for opening links // If no running profile is available, find the first available profile
const availableProfile = profileList.find((profile) => { const availableProfile = profileList.find((profile) =>
return canUseProfileForLinks(profile, profileList, runningProfiles); browserState.canUseProfileForLinks(profile),
}); );
if (availableProfile) { if (availableProfile) {
setSelectedProfile(availableProfile.name); setSelectedProfile(availableProfile.name);
} else {
// If no suitable profile found, still select the first one to show UI
setSelectedProfile(profileList[0].name);
} }
} }
} }
} catch (error) { } catch (err) {
console.error("Failed to load profiles:", error); console.error("Failed to load profiles:", err);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}, [runningProfiles, canUseProfileForLinks]); }, [runningProfiles, browserState]);
// Helper function to get tooltip content for profiles // Helper function to get tooltip content for profiles - now uses shared hook
const getProfileTooltipContent = (profile: BrowserProfile): string => { const getProfileTooltipContent = (profile: BrowserProfile): string | null => {
const isRunning = runningProfiles.has(profile.name); return browserState.getProfileTooltipContent(profile);
if (
profile.browser === "tor-browser" ||
profile.browser === "mullvad-browser"
) {
// If another TOR/Mullvad profile is running, this one is not available
return "Only 1 instance can run at a time";
}
if (isRunning) {
return "URL will open in a new tab in the existing browser window";
}
return "";
}; };
const handleOpenUrl = useCallback(async () => { const handleOpenUrl = useCallback(async () => {
@@ -211,16 +150,12 @@ export function ProfileSelectorDialog({
// Check if the selected profile can be used for opening links // Check if the selected profile can be used for opening links
const canOpenWithSelectedProfile = () => { const canOpenWithSelectedProfile = () => {
if (!selectedProfileData) return false; if (!selectedProfileData) return false;
return canUseProfileForLinks( return browserState.canUseProfileForLinks(selectedProfileData);
selectedProfileData,
profiles,
runningProfiles,
);
}; };
// Get tooltip content for disabled profiles // Get tooltip content for disabled profiles
const getTooltipContent = () => { const getTooltipContent = () => {
if (!selectedProfileData) return ""; if (!selectedProfileData) return null;
return getProfileTooltipContent(selectedProfileData); return getProfileTooltipContent(selectedProfileData);
}; };
@@ -285,65 +220,64 @@ export function ProfileSelectorDialog({
<SelectContent> <SelectContent>
{profiles.map((profile) => { {profiles.map((profile) => {
const isRunning = runningProfiles.has(profile.name); const isRunning = runningProfiles.has(profile.name);
const canUseForLinks = canUseProfileForLinks( const canUseForLinks =
profile, browserState.canUseProfileForLinks(profile);
profiles,
runningProfiles,
);
const tooltipContent = getProfileTooltipContent(profile); const tooltipContent = getProfileTooltipContent(profile);
return ( return (
<Tooltip key={profile.name}> <Tooltip key={profile.name}>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<SelectItem <span className="inline-flex">
value={profile.name} <SelectItem
disabled={!canUseForLinks} value={profile.name}
> disabled={!canUseForLinks}
<div
className={`flex items-center gap-2 ${
!canUseForLinks ? "opacity-50" : ""
}`}
> >
<div className="flex gap-3 items-center px-2 py-1 rounded-lg cursor-pointer hover:bg-accent"> <div
<div className="flex gap-2 items-center"> className={`flex items-center gap-2 ${
{(() => { !canUseForLinks ? "opacity-50" : ""
const IconComponent = getBrowserIcon( }`}
profile.browser, >
); <div className="flex gap-3 items-center px-2 py-1 rounded-lg cursor-pointer hover:bg-accent">
return IconComponent ? ( <div className="flex gap-2 items-center">
<IconComponent className="w-4 h-4" /> {(() => {
) : null; const IconComponent = getBrowserIcon(
})()} profile.browser,
</div> );
<div className="flex-1 text-right"> return IconComponent ? (
<div className="font-medium"> <IconComponent className="w-4 h-4" />
{profile.name} ) : null;
})()}
</div>
<div className="flex-1 text-right">
<div className="font-medium">
{profile.name}
</div>
</div> </div>
</div> </div>
<Badge variant="secondary" className="text-xs">
{getBrowserDisplayName(profile.browser)}
</Badge>
{hasProxy(profile) && (
<Badge variant="outline" className="text-xs">
Proxy
</Badge>
)}
{isRunning && (
<Badge variant="default" className="text-xs">
Running
</Badge>
)}
{!canUseForLinks && (
<Badge
variant="destructive"
className="text-xs"
>
Unavailable
</Badge>
)}
</div> </div>
<Badge variant="secondary" className="text-xs"> </SelectItem>
{getBrowserDisplayName(profile.browser)} </span>
</Badge>
{hasProxy(profile) && (
<Badge variant="outline" className="text-xs">
Proxy
</Badge>
)}
{isRunning && (
<Badge variant="default" className="text-xs">
Running
</Badge>
)}
{!canUseForLinks && (
<Badge
variant="destructive"
className="text-xs"
>
Unavailable
</Badge>
)}
</div>
</SelectItem>
</TooltipTrigger> </TooltipTrigger>
{tooltipContent && ( {tooltipContent && (
<TooltipContent>{tooltipContent}</TooltipContent> <TooltipContent>{tooltipContent}</TooltipContent>
@@ -363,7 +297,7 @@ export function ProfileSelectorDialog({
</Button> </Button>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div> <span className="inline-flex">
<LoadingButton <LoadingButton
isLoading={isLaunching} isLoading={isLaunching}
onClick={() => void handleOpenUrl()} onClick={() => void handleOpenUrl()}
@@ -375,7 +309,7 @@ export function ProfileSelectorDialog({
> >
Open Open
</LoadingButton> </LoadingButton>
</div> </span>
</TooltipTrigger> </TooltipTrigger>
{getTooltipContent() && ( {getTooltipContent() && (
<TooltipContent>{getTooltipContent()}</TooltipContent> <TooltipContent>{getTooltipContent()}</TooltipContent>
+188 -1
View File
@@ -1,5 +1,6 @@
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import type { BrowserProfile } from "@/types";
export function useBrowserSupport() { export function useBrowserSupport() {
const [supportedBrowsers, setSupportedBrowsers] = useState<string[]>([]); const [supportedBrowsers, setSupportedBrowsers] = useState<string[]>([]);
@@ -51,3 +52,189 @@ export function useBrowserSupport() {
checkBrowserSupport, checkBrowserSupport,
}; };
} }
/**
* Hook for managing browser state and enforcing single-instance rules for Tor and Mullvad browsers
*/
export function useBrowserState(
profiles: BrowserProfile[],
runningProfiles: Set<string>,
isUpdating?: (browser: string) => boolean,
) {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
/**
* Check if a browser type allows only one instance to run at a time
*/
const isSingleInstanceBrowser = useCallback(
(browserType: string): boolean => {
return browserType === "tor-browser" || browserType === "mullvad-browser";
},
[],
);
/**
* Check if any instance of a specific browser type is currently running
*/
const isAnyInstanceRunning = useCallback(
(browserType: string): boolean => {
if (!isClient) return false;
return profiles.some(
(p) => p.browser === browserType && runningProfiles.has(p.name),
);
},
[profiles, runningProfiles, isClient],
);
/**
* Check if a profile can be launched (not disabled by single-instance rules)
*/
const canLaunchProfile = useCallback(
(profile: BrowserProfile): boolean => {
if (!isClient) return false;
const isRunning = runningProfiles.has(profile.name);
const isBrowserUpdating = isUpdating?.(profile.browser) ?? false;
// If the profile is already running, it can always be stopped
if (isRunning) return true;
// If browser is updating, it cannot be launched
if (isBrowserUpdating) return false;
// For single-instance browsers, check if any instance is running
if (isSingleInstanceBrowser(profile.browser)) {
return !isAnyInstanceRunning(profile.browser);
}
return true;
},
[
runningProfiles,
isClient,
isUpdating,
isSingleInstanceBrowser,
isAnyInstanceRunning,
],
);
/**
* Check if a profile can be used for opening links
* This is more restrictive than canLaunchProfile as it considers running state
*/
const canUseProfileForLinks = useCallback(
(profile: BrowserProfile): boolean => {
if (!isClient) return false;
const isRunning = runningProfiles.has(profile.name);
// For single-instance browsers (Tor and Mullvad)
if (isSingleInstanceBrowser(profile.browser)) {
const runningInstancesOfType = profiles.filter(
(p) => p.browser === profile.browser && runningProfiles.has(p.name),
);
// If no instances are running, any profile of this type can be used
if (runningInstancesOfType.length === 0) {
return true;
}
// If instances are running, only the running ones can be used
return isRunning;
}
// For other browsers, any profile can be used
return true;
},
[profiles, runningProfiles, isClient, isSingleInstanceBrowser],
);
/**
* Get tooltip content for a profile's launch button
*/
const getLaunchTooltipContent = useCallback(
(profile: BrowserProfile): string => {
if (!isClient) return "Loading...";
const isRunning = runningProfiles.has(profile.name);
const isBrowserUpdating = isUpdating?.(profile.browser) ?? false;
if (isRunning) {
return "Click to forcefully stop the browser";
}
if (isBrowserUpdating) {
return `${profile.browser} is being updated. Please wait for the update to complete.`;
}
if (
isSingleInstanceBrowser(profile.browser) &&
!canLaunchProfile(profile)
) {
const browserDisplayName =
profile.browser === "tor-browser" ? "TOR" : "Mullvad";
return `Only one ${browserDisplayName} browser instance can run at a time. Stop the running ${browserDisplayName} browser first.`;
}
return "Click to launch the browser";
},
[
runningProfiles,
isClient,
isUpdating,
isSingleInstanceBrowser,
canLaunchProfile,
],
);
/**
* Get tooltip content for profile selection (for opening links)
*/
const getProfileTooltipContent = useCallback(
(profile: BrowserProfile): string | null => {
if (!isClient) return null;
const canUseForLinks = canUseProfileForLinks(profile);
if (canUseForLinks) return null;
if (isSingleInstanceBrowser(profile.browser)) {
const browserDisplayName =
profile.browser === "tor-browser" ? "TOR" : "Mullvad";
const runningInstancesOfType = profiles.filter(
(p) => p.browser === profile.browser && runningProfiles.has(p.name),
);
if (runningInstancesOfType.length > 0) {
const runningProfileNames = runningInstancesOfType
.map((p) => p.name)
.join(", ");
return `${browserDisplayName} browser is already running (${runningProfileNames}). Only one instance can run at a time.`;
}
}
return "This profile cannot be used for opening links right now.";
},
[
profiles,
runningProfiles,
isClient,
canUseProfileForLinks,
isSingleInstanceBrowser,
],
);
return {
isClient,
isSingleInstanceBrowser,
isAnyInstanceRunning,
canLaunchProfile,
canUseProfileForLinks,
getLaunchTooltipContent,
getProfileTooltipContent,
};
}