mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-07-01 19:05:31 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a77b733a31 | |||
| c10c3b0f95 | |||
| 4b16341401 | |||
| 016d423d2c | |||
| 0596cc4009 |
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "donutbrowser",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"version": "0.9.3",
|
||||
"version": "0.9.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
|
||||
Generated
+1
-1
@@ -1021,7 +1021,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "donutbrowser"
|
||||
version = "0.9.3"
|
||||
version = "0.9.4"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "donutbrowser"
|
||||
version = "0.9.3"
|
||||
version = "0.9.4"
|
||||
description = "Simple Yet Powerful Anti-Detect Browser"
|
||||
authors = ["zhom@github"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Donut Browser",
|
||||
"version": "0.9.3",
|
||||
"version": "0.9.4",
|
||||
"identifier": "com.donutbrowser",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications"
|
||||
import type { PermissionType } from "@/hooks/use-permissions";
|
||||
import { usePermissions } from "@/hooks/use-permissions";
|
||||
import { useUpdateNotifications } from "@/hooks/use-update-notifications";
|
||||
import { useVersionUpdater } from "@/hooks/use-version-updater";
|
||||
import { showErrorToast, showToast } from "@/lib/toast-utils";
|
||||
import type { BrowserProfile, CamoufoxConfig, GroupWithCount } from "@/types";
|
||||
|
||||
@@ -40,6 +41,8 @@ interface PendingUrl {
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
// Mount global version update listener/toasts
|
||||
useVersionUpdater();
|
||||
const [isInitializing, setIsInitializing] = useState(true);
|
||||
const [profiles, setProfiles] = useState<BrowserProfile[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -108,7 +108,7 @@ export function CreateProfileDialog({
|
||||
|
||||
const handleTabChange = (value: string) => {
|
||||
if (value === "regular") {
|
||||
setSelectedBrowser(null);
|
||||
setSelectedBrowser("firefox");
|
||||
} else if (value === "anti-detect") {
|
||||
setSelectedBrowser("camoufox");
|
||||
}
|
||||
@@ -179,6 +179,8 @@ export function CreateProfileDialog({
|
||||
{ browserStr: browser },
|
||||
);
|
||||
|
||||
await loadDownloadedVersions(browser);
|
||||
|
||||
// Only update state if this browser is still the one we're loading
|
||||
if (loadingBrowserRef.current === browser) {
|
||||
// Filter to enforce stable-only creation, except Firefox Developer (nightly-only)
|
||||
@@ -198,12 +200,29 @@ export function CreateProfileDialog({
|
||||
filtered.stable = rawReleaseTypes.stable;
|
||||
setReleaseTypes(filtered);
|
||||
}
|
||||
|
||||
// Load downloaded versions for this browser
|
||||
await loadDownloadedVersions(browser);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load release types for ${browser}:`, error);
|
||||
|
||||
// Fallback: still load downloaded versions and derive release type from them if possible
|
||||
try {
|
||||
const downloaded = await loadDownloadedVersions(browser);
|
||||
if (loadingBrowserRef.current === browser && downloaded.length > 0) {
|
||||
const latest = downloaded[0];
|
||||
const fallback: BrowserReleaseTypes = {};
|
||||
if (browser === "firefox-developer") {
|
||||
fallback.nightly = latest;
|
||||
} else {
|
||||
fallback.stable = latest;
|
||||
}
|
||||
setReleaseTypes(fallback);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Failed to load downloaded versions for ${browser}:`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
// Clear loading state only if we're still loading this browser
|
||||
if (loadingBrowserRef.current === browser) {
|
||||
@@ -387,20 +406,6 @@ export function CreateProfileDialog({
|
||||
isBrowserVersionAvailable,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(
|
||||
selectedBrowser,
|
||||
selectedBrowser && isBrowserCurrentlyDownloading(selectedBrowser),
|
||||
selectedBrowser && isBrowserVersionAvailable(selectedBrowser),
|
||||
selectedBrowser && getBestAvailableVersion(selectedBrowser),
|
||||
);
|
||||
}, [
|
||||
selectedBrowser,
|
||||
isBrowserCurrentlyDownloading,
|
||||
isBrowserVersionAvailable,
|
||||
getBestAvailableVersion,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="w-full max-h-[90vh] flex flex-col">
|
||||
|
||||
@@ -168,7 +168,7 @@ export function UnifiedToast(props: ToastProps) {
|
||||
const progress = "progress" in props ? props.progress : undefined;
|
||||
|
||||
return (
|
||||
<div className="flex items-start p-4 w-full max-w-md rounded-lg border shadow-lg bg-card border-border text-card-foreground">
|
||||
<div className="flex items-start p-3 w-96 rounded-lg border shadow-lg bg-card border-border text-card-foreground">
|
||||
<div className="mr-3 mt-0.5">{getToastIcon(type, stage)}</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-semibold leading-tight text-foreground">
|
||||
|
||||
@@ -25,13 +25,6 @@ import {
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -67,8 +60,9 @@ import {
|
||||
import { trimName } from "@/lib/name-utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { BrowserProfile, StoredProxy } from "@/types";
|
||||
import { LoadingButton } from "./loading-button";
|
||||
import { Input } from "./ui/input";
|
||||
import { Label } from "./ui/label";
|
||||
// Label no longer needed after removing dialog-based renaming
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
interface ProfilesDataTableProps {
|
||||
@@ -107,6 +101,8 @@ export function ProfilesDataTable({
|
||||
React.useState<BrowserProfile | null>(null);
|
||||
const [newProfileName, setNewProfileName] = React.useState("");
|
||||
const [renameError, setRenameError] = React.useState<string | null>(null);
|
||||
const [isRenamingSaving, setIsRenamingSaving] = React.useState(false);
|
||||
const renameContainerRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const [profileToDelete, setProfileToDelete] =
|
||||
React.useState<BrowserProfile | null>(null);
|
||||
const [isDeleting, setIsDeleting] = React.useState(false);
|
||||
@@ -265,10 +261,11 @@ export function ProfilesDataTable({
|
||||
[browserState.isClient, sorting, updateSorting],
|
||||
);
|
||||
|
||||
const handleRename = async () => {
|
||||
const handleRename = React.useCallback(async () => {
|
||||
if (!profileToRename || !newProfileName.trim()) return;
|
||||
|
||||
try {
|
||||
setIsRenamingSaving(true);
|
||||
await onRenameProfile(profileToRename.name, newProfileName.trim());
|
||||
setProfileToRename(null);
|
||||
setNewProfileName("");
|
||||
@@ -277,8 +274,31 @@ export function ProfilesDataTable({
|
||||
setRenameError(
|
||||
error instanceof Error ? error.message : "Failed to rename profile",
|
||||
);
|
||||
} finally {
|
||||
setIsRenamingSaving(false);
|
||||
}
|
||||
};
|
||||
}, [profileToRename, newProfileName, onRenameProfile]);
|
||||
|
||||
// Cancel inline rename on outside click
|
||||
React.useEffect(() => {
|
||||
if (!profileToRename) return;
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target as Node | null;
|
||||
if (
|
||||
target &&
|
||||
renameContainerRef.current &&
|
||||
!renameContainerRef.current.contains(target)
|
||||
) {
|
||||
setProfileToRename(null);
|
||||
setNewProfileName("");
|
||||
setRenameError(null);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [profileToRename]);
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!profileToDelete) return;
|
||||
@@ -622,20 +642,103 @@ export function ProfilesDataTable({
|
||||
enableSorting: true,
|
||||
sortingFn: "alphanumeric",
|
||||
cell: ({ row }) => {
|
||||
const profile = row.original as BrowserProfile;
|
||||
const rawName: string = row.getValue("name");
|
||||
const name = getBrowserDisplayName(rawName);
|
||||
const isEditing = profileToRename?.name === profile.name;
|
||||
|
||||
if (name.length < 20) {
|
||||
return <div className="font-medium text-left">{name}</div>;
|
||||
if (isEditing) {
|
||||
const isSaveDisabled =
|
||||
isRenamingSaving ||
|
||||
newProfileName.trim().length === 0 ||
|
||||
newProfileName.trim() === profile.name;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={renameContainerRef}
|
||||
className="overflow-visible relative"
|
||||
>
|
||||
<Input
|
||||
autoFocus
|
||||
value={newProfileName}
|
||||
onChange={(e) => {
|
||||
setNewProfileName(e.target.value);
|
||||
if (renameError) setRenameError(null);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
void handleRename();
|
||||
} else if (e.key === "Escape") {
|
||||
setProfileToRename(null);
|
||||
setNewProfileName("");
|
||||
setRenameError(null);
|
||||
}
|
||||
}}
|
||||
className="inline-block w-full"
|
||||
/>
|
||||
<div className="flex absolute right-0 top-full z-50 gap-1 translate-y-[30%] bg-primary-foreground opacity-100">
|
||||
<LoadingButton
|
||||
isLoading={isRenamingSaving}
|
||||
size="sm"
|
||||
variant="default"
|
||||
disabled={isSaveDisabled}
|
||||
className="cursor-pointer"
|
||||
onClick={() => void handleRename()}
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const display =
|
||||
name.length < 20 ? (
|
||||
<div className="font-medium text-left">{name}</div>
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>{trimName(name, 20)}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{name}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const isRunning =
|
||||
browserState.isClient && runningProfiles.has(profile.name);
|
||||
const isLaunching = launchingProfiles.has(profile.name);
|
||||
const isStopping = stoppingProfiles.has(profile.name);
|
||||
const isBrowserUpdating = isUpdating(profile.browser);
|
||||
const isDisabled =
|
||||
isRunning || isLaunching || isStopping || isBrowserUpdating;
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>{trimName(name, 20)}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{name}</TooltipContent>
|
||||
</Tooltip>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"p-2 mr-auto w-full text-left bg-transparent rounded border-none",
|
||||
isDisabled
|
||||
? "opacity-60 cursor-not-allowed"
|
||||
: "cursor-pointer hover:bg-accent/50",
|
||||
)}
|
||||
onClick={() => {
|
||||
if (isDisabled) return;
|
||||
setProfileToRename(profile);
|
||||
setNewProfileName(profile.name);
|
||||
setRenameError(null);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (isDisabled) return;
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
setProfileToRename(profile);
|
||||
setNewProfileName(profile.name);
|
||||
setRenameError(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{display}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -750,7 +853,7 @@ export function ProfilesDataTable({
|
||||
<PopoverTrigger asChild>
|
||||
<span
|
||||
className={cn(
|
||||
"flex gap-2 items-center px-1 rounded",
|
||||
"flex gap-2 items-center p-2 rounded",
|
||||
isDisabled
|
||||
? "opacity-60 cursor-not-allowed pointer-events-none"
|
||||
: "cursor-pointer hover:bg-accent/50",
|
||||
@@ -872,15 +975,7 @@ export function ProfilesDataTable({
|
||||
Configure Fingerprint
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setProfileToRename(profile);
|
||||
setNewProfileName(profile.name);
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Rename
|
||||
</DropdownMenuItem>
|
||||
{/* Rename removed from menu; inline on name click */}
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setProfileToDelete(profile);
|
||||
@@ -917,6 +1012,11 @@ export function ProfilesDataTable({
|
||||
openProxySelectorFor,
|
||||
proxyOverrides,
|
||||
handleProxySelection,
|
||||
profileToRename,
|
||||
newProfileName,
|
||||
renameError,
|
||||
isRenamingSaving,
|
||||
handleRename,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -944,7 +1044,7 @@ export function ProfilesDataTable({
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
<TableRow key={headerGroup.id} className="overflow-visible">
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
@@ -966,10 +1066,10 @@ export function ProfilesDataTable({
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
className="hover:bg-accent/50"
|
||||
className="overflow-visible hover:bg-accent/50"
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
<TableCell key={cell.id} className="overflow-visible">
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
@@ -991,55 +1091,6 @@ export function ProfilesDataTable({
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
|
||||
<Dialog
|
||||
open={profileToRename !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setProfileToRename(null);
|
||||
setNewProfileName("");
|
||||
setRenameError(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Rename Profile</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 gap-4 items-center">
|
||||
<Label htmlFor="name" className="text-right">
|
||||
Name
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={newProfileName}
|
||||
onChange={(e) => {
|
||||
setNewProfileName(e.target.value);
|
||||
}}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
{renameError && (
|
||||
<p className="text-sm text-red-600">{renameError}</p>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<RippleButton
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setProfileToRename(null);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</RippleButton>
|
||||
<RippleButton onClick={() => void handleRename()}>
|
||||
Save
|
||||
</RippleButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<DeleteConfirmationDialog
|
||||
isOpen={profileToDelete !== null}
|
||||
onClose={() => setProfileToDelete(null)}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { BsCamera, BsMic } from "react-icons/bs";
|
||||
@@ -25,13 +24,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import type { PermissionType } from "@/hooks/use-permissions";
|
||||
import { usePermissions } from "@/hooks/use-permissions";
|
||||
import { getBrowserDisplayName } from "@/lib/browser-utils";
|
||||
import {
|
||||
dismissToast,
|
||||
showErrorToast,
|
||||
showSuccessToast,
|
||||
showUnifiedVersionUpdateToast,
|
||||
} from "@/lib/toast-utils";
|
||||
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
interface AppSettings {
|
||||
@@ -45,14 +38,7 @@ interface PermissionInfo {
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface VersionUpdateProgress {
|
||||
current_browser: string;
|
||||
total_browsers: number;
|
||||
completed_browsers: number;
|
||||
new_versions_found: number;
|
||||
browser_new_versions: number;
|
||||
status: string; // "updating", "completed", "error"
|
||||
}
|
||||
// Version update progress toasts are handled globally via useVersionUpdater
|
||||
|
||||
interface SettingsDialogProps {
|
||||
isOpen: boolean;
|
||||
@@ -265,83 +251,9 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||
checkDefaultBrowserStatus().catch(console.error);
|
||||
}, 500); // Check every 500ms
|
||||
|
||||
// Listen for version update progress events
|
||||
let unlistenFn: (() => void) | null = null;
|
||||
const setupVersionUpdateListener = async () => {
|
||||
try {
|
||||
unlistenFn = await listen<VersionUpdateProgress>(
|
||||
"version-update-progress",
|
||||
(event) => {
|
||||
const progress = event.payload;
|
||||
|
||||
if (progress.status === "updating") {
|
||||
// Show unified progress toast
|
||||
const currentBrowserName = progress.current_browser
|
||||
? getBrowserDisplayName(progress.current_browser)
|
||||
: undefined;
|
||||
|
||||
showUnifiedVersionUpdateToast(
|
||||
"Checking for browser updates...",
|
||||
{
|
||||
description: currentBrowserName
|
||||
? `Fetching ${currentBrowserName} release information...`
|
||||
: "Initializing version check...",
|
||||
progress: {
|
||||
current: progress.completed_browsers,
|
||||
total: progress.total_browsers,
|
||||
found: progress.new_versions_found,
|
||||
current_browser: currentBrowserName,
|
||||
},
|
||||
},
|
||||
);
|
||||
} else if (progress.status === "completed") {
|
||||
dismissToast("unified-version-update");
|
||||
|
||||
if (progress.new_versions_found > 0) {
|
||||
showSuccessToast("Browser versions updated successfully", {
|
||||
duration: 5000,
|
||||
description:
|
||||
"Auto-downloads will start shortly for available updates.",
|
||||
});
|
||||
} else {
|
||||
showSuccessToast("No new browser versions found", {
|
||||
duration: 3000,
|
||||
description: "All browser versions are up to date",
|
||||
});
|
||||
}
|
||||
} else if (progress.status === "error") {
|
||||
dismissToast("unified-version-update");
|
||||
|
||||
showErrorToast("Failed to update browser versions", {
|
||||
duration: 6000,
|
||||
description: "Check your internet connection and try again",
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Failed to setup version update progress listener:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
setupVersionUpdateListener();
|
||||
|
||||
// Cleanup interval and listener on component unmount or dialog close
|
||||
// Cleanup interval on component unmount or dialog close
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
if (unlistenFn) {
|
||||
try {
|
||||
unlistenFn();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Failed to cleanup version update progress listener:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [isOpen, loadPermissions, checkDefaultBrowserStatus, loadSettings]);
|
||||
|
||||
Reference in New Issue
Block a user