mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-04-22 11:56:22 +02:00
refactor: share browser state logic across data table and selector dialog
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user