mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-09-28 04:02:19 +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";
|
} 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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user