diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx index b516786..153d018 100644 --- a/src/components/profile-data-table.tsx +++ b/src/components/profile-data-table.tsx @@ -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(null); - const [isClient, setIsClient] = React.useState(false); + const [storedProxies, setStoredProxies] = React.useState([]); // 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) => { - 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 (
- + + + - - {!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}
); @@ -262,91 +243,67 @@ export function ProfilesDataTable({ { accessorKey: "name", header: ({ column }) => { - const isSorted = column.getIsSorted(); return ( ); }, enableSorting: true, sortingFn: "alphanumeric", cell: ({ row }) => { - const profile = row.original; - return profile.name.length > 15 ? ( - - - {profile.name.slice(0, 15)}... - - {profile.name} - - ) : ( - profile.name - ); + const name: string = row.getValue("name"); + return
{name}
; }, }, { accessorKey: "browser", header: ({ column }) => { - const isSorted = column.getIsSorted(); return ( ); }, cell: ({ row }) => { const browser: string = row.getValue("browser"); const IconComponent = getBrowserIcon(browser); - const browserDisplayName = getBrowserDisplayName(browser); - return browserDisplayName.length > 15 ? ( - - -
- {IconComponent && } - {browserDisplayName.slice(0, 15)}... -
-
- {browserDisplayName} -
- ) : ( -
+ return ( +
{IconComponent && } - {browserDisplayName} + {getBrowserDisplayName(browser)}
); }, 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 ( - - -
- {profileHasProxy && ( - - )} - - {proxyDisplayName.length > 10 ? ( - - {proxyDisplayName.slice(0, 10)}... - - ) : ( - - {profile.browser === "tor-browser" - ? "Not supported" - : proxyDisplayName} - - )} -
-
- {tooltipText} -
+
+ + + + + + + {tooltipText} + +
); }, }, - // 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 (
@@ -437,7 +397,7 @@ export function ProfilesDataTable({ -
+ void handleOpenUrl()} @@ -375,7 +309,7 @@ export function ProfileSelectorDialog({ > Open -
+
{getTooltipContent() && ( {getTooltipContent()} diff --git a/src/hooks/use-browser-support.ts b/src/hooks/use-browser-support.ts index c035aa6..856a4cb 100644 --- a/src/hooks/use-browser-support.ts +++ b/src/hooks/use-browser-support.ts @@ -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([]); @@ -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, + 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, + }; +}