mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-08-08 04:08:46 +02:00
feat: add mass-proxy-assign action
This commit is contained in:
+40
-1
@@ -15,6 +15,7 @@ import { ImportProfileDialog } from "@/components/import-profile-dialog";
|
|||||||
import { PermissionDialog } from "@/components/permission-dialog";
|
import { PermissionDialog } from "@/components/permission-dialog";
|
||||||
import { ProfilesDataTable } from "@/components/profile-data-table";
|
import { ProfilesDataTable } from "@/components/profile-data-table";
|
||||||
import { ProfileSelectorDialog } from "@/components/profile-selector-dialog";
|
import { ProfileSelectorDialog } from "@/components/profile-selector-dialog";
|
||||||
|
import { ProxyAssignmentDialog } from "@/components/proxy-assignment-dialog";
|
||||||
import { ProxyManagementDialog } from "@/components/proxy-management-dialog";
|
import { ProxyManagementDialog } from "@/components/proxy-management-dialog";
|
||||||
import { SettingsDialog } from "@/components/settings-dialog";
|
import { SettingsDialog } from "@/components/settings-dialog";
|
||||||
import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications";
|
import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications";
|
||||||
@@ -62,7 +63,11 @@ export default function Home() {
|
|||||||
error: groupsError,
|
error: groupsError,
|
||||||
} = useGroupEvents();
|
} = useGroupEvents();
|
||||||
|
|
||||||
const { isLoading: proxiesLoading, error: proxiesError } = useProxyEvents();
|
const {
|
||||||
|
storedProxies,
|
||||||
|
isLoading: proxiesLoading,
|
||||||
|
error: proxiesError,
|
||||||
|
} = useProxyEvents();
|
||||||
|
|
||||||
const [createProfileDialogOpen, setCreateProfileDialogOpen] = useState(false);
|
const [createProfileDialogOpen, setCreateProfileDialogOpen] = useState(false);
|
||||||
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false);
|
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false);
|
||||||
@@ -75,10 +80,15 @@ export default function Home() {
|
|||||||
useState(false);
|
useState(false);
|
||||||
const [groupAssignmentDialogOpen, setGroupAssignmentDialogOpen] =
|
const [groupAssignmentDialogOpen, setGroupAssignmentDialogOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const [proxyAssignmentDialogOpen, setProxyAssignmentDialogOpen] =
|
||||||
|
useState(false);
|
||||||
const [selectedGroupId, setSelectedGroupId] = useState<string>("default");
|
const [selectedGroupId, setSelectedGroupId] = useState<string>("default");
|
||||||
const [selectedProfilesForGroup, setSelectedProfilesForGroup] = useState<
|
const [selectedProfilesForGroup, setSelectedProfilesForGroup] = useState<
|
||||||
string[]
|
string[]
|
||||||
>([]);
|
>([]);
|
||||||
|
const [selectedProfilesForProxy, setSelectedProfilesForProxy] = useState<
|
||||||
|
string[]
|
||||||
|
>([]);
|
||||||
const [selectedProfiles, setSelectedProfiles] = useState<string[]>([]);
|
const [selectedProfiles, setSelectedProfiles] = useState<string[]>([]);
|
||||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||||
const [pendingUrls, setPendingUrls] = useState<PendingUrl[]>([]);
|
const [pendingUrls, setPendingUrls] = useState<PendingUrl[]>([]);
|
||||||
@@ -559,12 +569,29 @@ export default function Home() {
|
|||||||
setSelectedProfiles([]);
|
setSelectedProfiles([]);
|
||||||
}, [selectedProfiles, handleAssignProfilesToGroup]);
|
}, [selectedProfiles, handleAssignProfilesToGroup]);
|
||||||
|
|
||||||
|
const handleAssignProfilesToProxy = useCallback((profileIds: string[]) => {
|
||||||
|
setSelectedProfilesForProxy(profileIds);
|
||||||
|
setProxyAssignmentDialogOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleBulkProxyAssignment = useCallback(() => {
|
||||||
|
if (selectedProfiles.length === 0) return;
|
||||||
|
handleAssignProfilesToProxy(selectedProfiles);
|
||||||
|
setSelectedProfiles([]);
|
||||||
|
}, [selectedProfiles, handleAssignProfilesToProxy]);
|
||||||
|
|
||||||
const handleGroupAssignmentComplete = useCallback(async () => {
|
const handleGroupAssignmentComplete = useCallback(async () => {
|
||||||
// No need to manually reload - useProfileEvents will handle the update
|
// No need to manually reload - useProfileEvents will handle the update
|
||||||
setGroupAssignmentDialogOpen(false);
|
setGroupAssignmentDialogOpen(false);
|
||||||
setSelectedProfilesForGroup([]);
|
setSelectedProfilesForGroup([]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleProxyAssignmentComplete = useCallback(async () => {
|
||||||
|
// No need to manually reload - useProfileEvents will handle the update
|
||||||
|
setProxyAssignmentDialogOpen(false);
|
||||||
|
setSelectedProfilesForProxy([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleGroupManagementComplete = useCallback(async () => {
|
const handleGroupManagementComplete = useCallback(async () => {
|
||||||
// No need to manually reload - useProfileEvents will handle the update
|
// No need to manually reload - useProfileEvents will handle the update
|
||||||
}, []);
|
}, []);
|
||||||
@@ -730,6 +757,7 @@ export default function Home() {
|
|||||||
onSelectedProfilesChange={setSelectedProfiles}
|
onSelectedProfilesChange={setSelectedProfiles}
|
||||||
onBulkDelete={handleBulkDelete}
|
onBulkDelete={handleBulkDelete}
|
||||||
onBulkGroupAssignment={handleBulkGroupAssignment}
|
onBulkGroupAssignment={handleBulkGroupAssignment}
|
||||||
|
onBulkProxyAssignment={handleBulkProxyAssignment}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -832,6 +860,17 @@ export default function Home() {
|
|||||||
profiles={profiles}
|
profiles={profiles}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ProxyAssignmentDialog
|
||||||
|
isOpen={proxyAssignmentDialogOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setProxyAssignmentDialogOpen(false);
|
||||||
|
}}
|
||||||
|
selectedProfiles={selectedProfilesForProxy}
|
||||||
|
onAssignmentComplete={handleProxyAssignmentComplete}
|
||||||
|
profiles={profiles}
|
||||||
|
storedProxies={storedProxies}
|
||||||
|
/>
|
||||||
|
|
||||||
<DeleteConfirmationDialog
|
<DeleteConfirmationDialog
|
||||||
isOpen={showBulkDeleteConfirmation}
|
isOpen={showBulkDeleteConfirmation}
|
||||||
onClose={() => setShowBulkDeleteConfirmation(false)}
|
onClose={() => setShowBulkDeleteConfirmation(false)}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { invoke } from "@tauri-apps/api/core";
|
|||||||
import { emit, listen } from "@tauri-apps/api/event";
|
import { emit, listen } from "@tauri-apps/api/event";
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { FiWifi } from "react-icons/fi";
|
||||||
import { IoEllipsisHorizontal } from "react-icons/io5";
|
import { IoEllipsisHorizontal } from "react-icons/io5";
|
||||||
import {
|
import {
|
||||||
LuCheck,
|
LuCheck,
|
||||||
@@ -662,6 +663,7 @@ interface ProfilesDataTableProps {
|
|||||||
onSelectedProfilesChange: Dispatch<SetStateAction<string[]>>;
|
onSelectedProfilesChange: Dispatch<SetStateAction<string[]>>;
|
||||||
onBulkDelete?: () => void;
|
onBulkDelete?: () => void;
|
||||||
onBulkGroupAssignment?: () => void;
|
onBulkGroupAssignment?: () => void;
|
||||||
|
onBulkProxyAssignment?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProfilesDataTable({
|
export function ProfilesDataTable({
|
||||||
@@ -678,6 +680,7 @@ export function ProfilesDataTable({
|
|||||||
onSelectedProfilesChange,
|
onSelectedProfilesChange,
|
||||||
onBulkDelete,
|
onBulkDelete,
|
||||||
onBulkGroupAssignment,
|
onBulkGroupAssignment,
|
||||||
|
onBulkProxyAssignment,
|
||||||
}: ProfilesDataTableProps) {
|
}: ProfilesDataTableProps) {
|
||||||
const { getTableSorting, updateSorting, isLoaded } = useTableSorting();
|
const { getTableSorting, updateSorting, isLoaded } = useTableSorting();
|
||||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||||
@@ -1928,6 +1931,15 @@ export function ProfilesDataTable({
|
|||||||
<LuUsers />
|
<LuUsers />
|
||||||
</DataTableActionBarAction>
|
</DataTableActionBarAction>
|
||||||
)}
|
)}
|
||||||
|
{onBulkProxyAssignment && (
|
||||||
|
<DataTableActionBarAction
|
||||||
|
tooltip="Assign Proxy"
|
||||||
|
onClick={onBulkProxyAssignment}
|
||||||
|
size="icon"
|
||||||
|
>
|
||||||
|
<FiWifi />
|
||||||
|
</DataTableActionBarAction>
|
||||||
|
)}
|
||||||
{onBulkDelete && (
|
{onBulkDelete && (
|
||||||
<DataTableActionBarAction
|
<DataTableActionBarAction
|
||||||
tooltip="Delete"
|
tooltip="Delete"
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { emit } from "@tauri-apps/api/event";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { LoadingButton } from "@/components/loading-button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import type { BrowserProfile, StoredProxy } from "@/types";
|
||||||
|
import { RippleButton } from "./ui/ripple";
|
||||||
|
|
||||||
|
interface ProxyAssignmentDialogProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
selectedProfiles: string[];
|
||||||
|
onAssignmentComplete: () => void;
|
||||||
|
profiles?: BrowserProfile[];
|
||||||
|
storedProxies?: StoredProxy[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProxyAssignmentDialog({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
selectedProfiles,
|
||||||
|
onAssignmentComplete,
|
||||||
|
profiles = [],
|
||||||
|
storedProxies = [],
|
||||||
|
}: ProxyAssignmentDialogProps) {
|
||||||
|
const [selectedProxyId, setSelectedProxyId] = useState<string | null>(null);
|
||||||
|
const [isAssigning, setIsAssigning] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleAssign = useCallback(async () => {
|
||||||
|
setIsAssigning(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
// Filter out TOR browser profiles as they don't support proxies
|
||||||
|
const validProfiles = selectedProfiles.filter((profileId) => {
|
||||||
|
const profile = profiles.find((p) => p.id === profileId);
|
||||||
|
return profile && profile.browser !== "tor-browser";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validProfiles.length === 0) {
|
||||||
|
setError("No valid profiles selected.");
|
||||||
|
setIsAssigning(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update each profile's proxy sequentially to avoid file locking issues
|
||||||
|
for (const profileId of validProfiles) {
|
||||||
|
await invoke("update_profile_proxy", {
|
||||||
|
profileId,
|
||||||
|
proxyId: selectedProxyId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify other parts of the app so usage counts and lists refresh
|
||||||
|
await emit("profile-updated");
|
||||||
|
onAssignmentComplete();
|
||||||
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to assign proxies to profiles:", err);
|
||||||
|
const errorMessage =
|
||||||
|
err instanceof Error
|
||||||
|
? err.message
|
||||||
|
: "Failed to assign proxies to profiles";
|
||||||
|
setError(errorMessage);
|
||||||
|
toast.error(errorMessage);
|
||||||
|
} finally {
|
||||||
|
setIsAssigning(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
selectedProfiles,
|
||||||
|
selectedProxyId,
|
||||||
|
profiles,
|
||||||
|
onAssignmentComplete,
|
||||||
|
onClose,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setSelectedProxyId(null);
|
||||||
|
setError(null);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Assign Proxy</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Assign a proxy to {selectedProfiles.length} selected profile(s).
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Selected Profiles:</Label>
|
||||||
|
<div className="p-3 bg-muted rounded-md max-h-32 overflow-y-auto">
|
||||||
|
<ul className="text-sm space-y-1">
|
||||||
|
{selectedProfiles.map((profileId) => {
|
||||||
|
const profile = profiles.find(
|
||||||
|
(p: BrowserProfile) => p.id === profileId,
|
||||||
|
);
|
||||||
|
const displayName = profile ? profile.name : profileId;
|
||||||
|
const isTorBrowser = profile?.browser === "tor-browser";
|
||||||
|
return (
|
||||||
|
<li key={profileId} className="truncate">
|
||||||
|
• {displayName}
|
||||||
|
{isTorBrowser && (
|
||||||
|
<span className="ml-2 text-xs text-muted-foreground">
|
||||||
|
(TOR - no proxy support)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="proxy-select">Assign Proxy:</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedProxyId || "none"}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setSelectedProxyId(value === "none" ? null : value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a proxy" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">No Proxy</SelectItem>
|
||||||
|
{storedProxies.map((proxy) => (
|
||||||
|
<SelectItem key={proxy.id} value={proxy.id}>
|
||||||
|
{proxy.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="p-3 text-sm text-red-600 bg-red-50 rounded-md dark:bg-red-900/20 dark:text-red-400">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<RippleButton
|
||||||
|
variant="outline"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={isAssigning}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</RippleButton>
|
||||||
|
<LoadingButton
|
||||||
|
isLoading={isAssigning}
|
||||||
|
onClick={() => void handleAssign()}
|
||||||
|
>
|
||||||
|
Assign
|
||||||
|
</LoadingButton>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user