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