refactor: cleanup

This commit is contained in:
zhom
2026-03-13 10:19:34 +04:00
parent 756bd69a84
commit e9b5442340
53 changed files with 1930 additions and 2096 deletions
+5 -41
View File
@@ -57,14 +57,7 @@ import type {
WayfernConfig,
} from "@/types";
type BrowserTypeString =
| "firefox"
| "firefox-developer"
| "chromium"
| "brave"
| "zen"
| "camoufox"
| "wayfern";
type BrowserTypeString = "camoufox" | "wayfern";
interface PendingUrl {
id: string;
@@ -943,37 +936,6 @@ export default function Home() {
profiles.length,
]);
// Show deprecation warning for unsupported profiles (with names)
useEffect(() => {
if (profiles.length === 0) return;
const deprecatedProfiles = profiles.filter(
(p) => p.release_type === "nightly" && p.browser !== "firefox-developer",
);
if (deprecatedProfiles.length > 0) {
const deprecatedNames = deprecatedProfiles.map((p) => p.name).join(", ");
// Use a stable id to avoid duplicate toasts on re-renders
showToast({
id: "deprecated-profiles-warning",
type: "error",
title: "Some profiles will be deprecated soon",
description: `The following profiles will be deprecated soon: ${deprecatedNames}. Nightly profiles (except Firefox Developers Edition) will be removed in upcoming versions. Please check GitHub for migration instructions.`,
duration: 15000,
action: {
label: "Learn more",
onClick: () => {
const event = new CustomEvent("url-open-request", {
detail: "https://github.com/zhom/donutbrowser/discussions/66",
});
window.dispatchEvent(event);
},
},
});
}
}, [profiles]);
// Show warning for non-wayfern/camoufox profiles (support ending March 15, 2026)
useEffect(() => {
if (profiles.length === 0) return;
@@ -1163,6 +1125,7 @@ export default function Home() {
onClose={() => {
setImportProfileDialogOpen(false);
}}
crossOsUnlocked={crossOsUnlocked}
/>
<ProxyManagementDialog
@@ -1329,13 +1292,14 @@ export default function Home() {
onAccepted={checkTerms}
/>
{/* Commercial Trial Modal - shown once when trial expires */}
{/* Commercial Trial Modal - shown once when trial expires (skip for paid users) */}
<CommercialTrialModal
isOpen={
!termsLoading &&
termsAccepted === true &&
trialStatus?.type === "Expired" &&
!trialAcknowledged
!trialAcknowledged &&
!crossOsUnlocked
}
onClose={checkTrialStatus}
/>
+2 -2
View File
@@ -462,8 +462,8 @@ export function CookieManagementDialog({
{importResult && (
<div className="space-y-4">
<div className="p-4 rounded-lg bg-green-500/10">
<div className="font-medium text-green-600 dark:text-green-400">
<div className="p-4 rounded-lg bg-success/10">
<div className="font-medium text-success">
Successfully imported {importResult.cookies_imported}{" "}
cookies ({importResult.cookies_replaced} replaced)
</div>
+1 -1
View File
@@ -91,7 +91,7 @@ export function CreateGroupDialog({
</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">
<div className="p-3 text-sm text-destructive bg-destructive/10 rounded-md">
{error}
</div>
)}
+16 -63
View File
@@ -67,14 +67,7 @@ const getCurrentOS = (): CamoufoxOS => {
import { RippleButton } from "./ui/ripple";
type BrowserTypeString =
| "firefox"
| "firefox-developer"
| "chromium"
| "brave"
| "zen"
| "camoufox"
| "wayfern";
type BrowserTypeString = "camoufox" | "wayfern";
interface CreateProfileDialogProps {
isOpen: boolean;
@@ -103,24 +96,12 @@ interface BrowserOption {
const browserOptions: BrowserOption[] = [
{
value: "firefox",
label: "Firefox",
value: "camoufox",
label: "Camoufox",
},
{
value: "firefox-developer",
label: "Firefox Developer Edition",
},
{
value: "chromium",
label: "Chromium",
},
{
value: "brave",
label: "Brave",
},
{
value: "zen",
label: "Zen Browser",
value: "wayfern",
label: "Wayfern",
},
];
@@ -254,23 +235,9 @@ export function CreateProfileDialog({
// 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)
if (browser === "camoufox" || browser === "wayfern") {
const filtered: BrowserReleaseTypes = {};
if (rawReleaseTypes.stable)
filtered.stable = rawReleaseTypes.stable;
setReleaseTypes(filtered);
} else if (browser === "firefox-developer") {
const filtered: BrowserReleaseTypes = {};
if (rawReleaseTypes.nightly)
filtered.nightly = rawReleaseTypes.nightly;
setReleaseTypes(filtered);
} else {
const filtered: BrowserReleaseTypes = {};
if (rawReleaseTypes.stable)
filtered.stable = rawReleaseTypes.stable;
setReleaseTypes(filtered);
}
const filtered: BrowserReleaseTypes = {};
if (rawReleaseTypes.stable) filtered.stable = rawReleaseTypes.stable;
setReleaseTypes(filtered);
setReleaseTypesError(null);
}
} catch (error) {
@@ -282,11 +249,7 @@ export function CreateProfileDialog({
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;
}
fallback.stable = latest;
setReleaseTypes(fallback);
setReleaseTypesError(null);
} else if (loadingBrowserRef.current === browser) {
@@ -351,17 +314,9 @@ export function CreateProfileDialog({
// Helper function to get the best available version respecting rules
const getBestAvailableVersion = useCallback(
(browserType?: string) => {
(_browserType?: string) => {
if (!releaseTypes) return null;
// Firefox Developer Edition: nightly-only
if (browserType === "firefox-developer" && releaseTypes.nightly) {
return {
version: releaseTypes.nightly,
releaseType: "nightly" as const,
};
}
// All others: stable-only
if (releaseTypes.stable) {
return { version: releaseTypes.stable, releaseType: "stable" as const };
}
@@ -379,11 +334,9 @@ export function CreateProfileDialog({
const browserDownloaded = downloadedVersionsMap[browserType ?? ""] ?? [];
if (browserDownloaded.length > 0) {
const fallbackVersion = browserDownloaded[0];
const releaseType =
browserType === "firefox-developer" ? "nightly" : "stable";
return {
version: fallbackVersion,
releaseType: releaseType as "stable" | "nightly",
releaseType: "stable" as const,
};
}
return null;
@@ -772,8 +725,8 @@ export function CreateProfileDialog({
{!isLoadingReleaseTypes &&
!releaseTypesError &&
!getBestAvailableVersion("wayfern") && (
<div className="flex gap-3 items-center p-3 rounded-md border border-yellow-500/50 bg-yellow-500/10">
<p className="text-sm text-yellow-500">
<div className="flex gap-3 items-center p-3 rounded-md border border-warning/50 bg-warning/10">
<p className="text-sm text-warning">
Wayfern is not available on your platform
yet.
</p>
@@ -874,8 +827,8 @@ export function CreateProfileDialog({
{!isLoadingReleaseTypes &&
!releaseTypesError &&
!getBestAvailableVersion("camoufox") && (
<div className="flex gap-3 items-center p-3 rounded-md border border-yellow-500/50 bg-yellow-500/10">
<p className="text-sm text-yellow-500">
<div className="flex gap-3 items-center p-3 rounded-md border border-warning/50 bg-warning/10">
<p className="text-sm text-warning">
Camoufox is not available on your platform
yet.
</p>
@@ -933,7 +886,7 @@ export function CreateProfileDialog({
)}
{crossOsUnlocked && (
<Alert className="border-yellow-500/50 bg-yellow-500/10">
<Alert className="border-warning/50 bg-warning/10">
<AlertDescription className="text-sm">
{t("createProfile.camoufoxWarning")}
</AlertDescription>
+1 -1
View File
@@ -347,7 +347,7 @@ export function UnifiedToast(props: ToastProps) {
<>
{stage === "extracting" && (
<p className="mt-1 text-xs text-muted-foreground">
Extracting browser files...
Extracting browser files... Please do not close the app.
</p>
)}
{stage === "verifying" && (
+2 -2
View File
@@ -117,7 +117,7 @@ function DataTableActionBarAction({
<TooltipTrigger asChild>{trigger}</TooltipTrigger>
<TooltipContent
sideOffset={6}
className="border bg-accent font-semibold text-foreground dark:bg-zinc-900 [&>span]:hidden"
className="border bg-accent font-semibold text-foreground dark:bg-card [&>span]:hidden"
>
<p>{tooltip}</p>
</TooltipContent>
@@ -155,7 +155,7 @@ function DataTableActionBarSelection<TData>({
</TooltipTrigger>
<TooltipContent
sideOffset={10}
className="flex items-center gap-2 border bg-accent px-2 py-1 font-semibold text-foreground dark:bg-zinc-900 [&>span]:hidden"
className="flex items-center gap-2 border bg-accent px-2 py-1 font-semibold text-foreground dark:bg-card [&>span]:hidden"
>
<p>Clear selection</p>
<kbd className="select-none rounded border bg-background px-1.5 py-px font-mono font-normal text-[0.7rem] text-foreground shadow-xs">
+2 -2
View File
@@ -162,7 +162,7 @@ export function DeleteGroupDialog({
<RadioGroupItem value="delete" id="delete" />
<Label
htmlFor="delete"
className="text-sm text-red-600"
className="text-sm text-destructive"
>
Delete profiles along with the group
</Label>
@@ -181,7 +181,7 @@ export function DeleteGroupDialog({
)}
{error && (
<div className="p-3 text-sm text-red-600 bg-red-50 rounded-md dark:bg-red-900/20 dark:text-red-400">
<div className="p-3 text-sm text-destructive bg-destructive/10 rounded-md">
{error}
</div>
)}
+1 -1
View File
@@ -101,7 +101,7 @@ export function EditGroupDialog({
</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">
<div className="p-3 text-sm text-destructive bg-destructive/10 rounded-md">
{error}
</div>
)}
@@ -160,7 +160,7 @@ export function ExtensionGroupAssignmentDialog({
</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">
<div className="p-3 text-sm text-destructive bg-destructive/10 rounded-md">
{error}
</div>
)}
@@ -55,10 +55,10 @@ function getSyncStatusDot(
switch (status) {
case "syncing":
return { color: "bg-yellow-500", tooltip: "Syncing...", animate: true };
return { color: "bg-warning", tooltip: "Syncing...", animate: true };
case "synced":
return {
color: "bg-green-500",
color: "bg-success",
tooltip: item.last_sync
? `Synced ${new Date(item.last_sync * 1000).toLocaleString()}`
: "Synced",
@@ -66,18 +66,22 @@ function getSyncStatusDot(
};
case "waiting":
return {
color: "bg-yellow-500",
color: "bg-warning",
tooltip: "Waiting to sync",
animate: false,
};
case "error":
return {
color: "bg-red-500",
color: "bg-destructive",
tooltip: "Sync error",
animate: false,
};
default:
return { color: "bg-gray-400", tooltip: "Not synced", animate: false };
return {
color: "bg-muted-foreground",
tooltip: "Not synced",
animate: false,
};
}
}
+1 -1
View File
@@ -176,7 +176,7 @@ export function GroupAssignmentDialog({
</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">
<div className="p-3 text-sm text-destructive bg-destructive/10 rounded-md">
{error}
</div>
)}
+10 -6
View File
@@ -49,10 +49,10 @@ function getSyncStatusDot(
switch (status) {
case "syncing":
return { color: "bg-yellow-500", tooltip: "Syncing...", animate: true };
return { color: "bg-warning", tooltip: "Syncing...", animate: true };
case "synced":
return {
color: "bg-green-500",
color: "bg-success",
tooltip: group.last_sync
? `Synced ${new Date(group.last_sync * 1000).toLocaleString()}`
: "Synced",
@@ -60,18 +60,22 @@ function getSyncStatusDot(
};
case "waiting":
return {
color: "bg-yellow-500",
color: "bg-warning",
tooltip: "Waiting to sync",
animate: false,
};
case "error":
return {
color: "bg-red-500",
color: "bg-destructive",
tooltip: errorMessage ? `Sync error: ${errorMessage}` : "Sync error",
animate: false,
};
default:
return { color: "bg-gray-400", tooltip: "Not synced", animate: false };
return {
color: "bg-muted-foreground",
tooltip: "Not synced",
animate: false,
};
}
}
@@ -252,7 +256,7 @@ export function GroupManagementDialog({
</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">
<div className="p-3 text-sm text-destructive bg-destructive/10 rounded-md">
{error}
</div>
)}
+409 -294
View File
@@ -2,10 +2,12 @@
import { invoke } from "@tauri-apps/api/core";
import { open } from "@tauri-apps/plugin-dialog";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { FaFolder } from "react-icons/fa";
import { toast } from "sonner";
import { LoadingButton } from "@/components/loading-button";
import { SharedCamoufoxConfigForm } from "@/components/shared-camoufox-config-form";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -23,19 +25,29 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { WayfernConfigForm } from "@/components/wayfern-config-form";
import { useBrowserSupport } from "@/hooks/use-browser-support";
import { useProxyEvents } from "@/hooks/use-proxy-events";
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
import type { DetectedProfile } from "@/types";
import type { CamoufoxConfig, DetectedProfile, WayfernConfig } from "@/types";
import { RippleButton } from "./ui/ripple";
const getMappedBrowser = (browser: string): "camoufox" | "wayfern" => {
if (["firefox", "firefox-developer", "zen"].includes(browser))
return "camoufox";
return "wayfern";
};
interface ImportProfileDialogProps {
isOpen: boolean;
onClose: () => void;
crossOsUnlocked?: boolean;
}
export function ImportProfileDialog({
isOpen,
onClose,
crossOsUnlocked,
}: ImportProfileDialogProps) {
const [detectedProfiles, setDetectedProfiles] = useState<DetectedProfile[]>(
[],
@@ -45,6 +57,12 @@ export function ImportProfileDialog({
const [importMode, setImportMode] = useState<"auto-detect" | "manual">(
"auto-detect",
);
const [currentStep, setCurrentStep] = useState<"select" | "configure">(
"select",
);
const [camoufoxConfig, setCamoufoxConfig] = useState<CamoufoxConfig>({});
const [wayfernConfig, setWayfernConfig] = useState<WayfernConfig>({});
const [selectedProxyId, setSelectedProxyId] = useState<string | undefined>();
// Auto-detect state
const [selectedDetectedProfile, setSelectedDetectedProfile] = useState<
@@ -61,6 +79,7 @@ export function ImportProfileDialog({
const { supportedBrowsers, isLoading: isLoadingSupport } =
useBrowserSupport();
const { storedProxies } = useProxyEvents();
const importableBrowsers = supportedBrowsers;
@@ -72,14 +91,11 @@ export function ImportProfileDialog({
);
setDetectedProfiles(profiles);
// Auto-switch to manual mode if no profiles detected
if (profiles.length === 0) {
setImportMode("manual");
} else {
// Auto-select first profile if available
setSelectedDetectedProfile(profiles[0].path);
// Generate default name from the detected profile
const profile = profiles[0];
const browserName = getBrowserDisplayName(profile.browser);
const defaultName = `Imported ${browserName} Profile`;
@@ -93,6 +109,10 @@ export function ImportProfileDialog({
}
}, []);
const selectedProfile = detectedProfiles.find(
(p) => p.path === selectedDetectedProfile,
);
const handleBrowseFolder = async () => {
try {
const selected = await open({
@@ -110,40 +130,65 @@ export function ImportProfileDialog({
}
};
const handleAutoDetectImport = useCallback(async () => {
if (!selectedDetectedProfile || !autoDetectProfileName.trim()) {
toast.error("Please select a profile and provide a name");
return;
const handleImport = useCallback(async () => {
let sourcePath: string;
let browserType: string;
let newProfileName: string;
if (importMode === "auto-detect") {
if (!selectedDetectedProfile || !autoDetectProfileName.trim()) {
toast.error("Please select a profile and provide a name");
return;
}
const profile = detectedProfiles.find(
(p) => p.path === selectedDetectedProfile,
);
if (!profile) {
toast.error("Selected profile not found");
return;
}
sourcePath = profile.path;
browserType = profile.browser;
newProfileName = autoDetectProfileName.trim();
} else {
if (
!manualBrowserType ||
!manualProfilePath.trim() ||
!manualProfileName.trim()
) {
toast.error("Please fill in all fields");
return;
}
sourcePath = manualProfilePath.trim();
browserType = manualBrowserType;
newProfileName = manualProfileName.trim();
}
const profile = detectedProfiles.find(
(p) => p.path === selectedDetectedProfile,
);
if (!profile) {
toast.error("Selected profile not found");
return;
}
const mappedBrowser =
importMode === "auto-detect" && selectedProfile
? (selectedProfile.mapped_browser as "camoufox" | "wayfern")
: getMappedBrowser(browserType);
setIsImporting(true);
try {
await invoke("import_browser_profile", {
sourcePath: profile.path,
browserType: profile.browser,
newProfileName: autoDetectProfileName.trim(),
sourcePath,
browserType,
newProfileName,
proxyId: selectedProxyId ?? null,
camoufoxConfig: mappedBrowser === "camoufox" ? camoufoxConfig : null,
wayfernConfig: mappedBrowser === "wayfern" ? wayfernConfig : null,
});
toast.success(
`Successfully imported profile "${autoDetectProfileName.trim()}"`,
);
toast.success(`Successfully imported profile "${newProfileName}"`);
onClose();
} catch (error) {
console.error("Failed to import profile:", error);
const errorMessage =
error instanceof Error ? error.message : String(error);
// Check if error is about browser not being downloaded
if (errorMessage.includes("No downloaded versions found")) {
const browserDisplayName = getBrowserDisplayName(profile.browser);
const browserDisplayName = getBrowserDisplayName(browserType);
toast.error(
`${browserDisplayName} is not installed. Please download ${browserDisplayName} first from the main window, then try importing again.`,
{
@@ -157,63 +202,30 @@ export function ImportProfileDialog({
setIsImporting(false);
}
}, [
importMode,
selectedDetectedProfile,
autoDetectProfileName,
detectedProfiles,
manualBrowserType,
manualProfilePath,
manualProfileName,
selectedProxyId,
camoufoxConfig,
wayfernConfig,
onClose,
selectedProfile,
]);
const handleManualImport = useCallback(async () => {
if (
!manualBrowserType ||
!manualProfilePath.trim() ||
!manualProfileName.trim()
) {
toast.error("Please fill in all fields");
return;
}
setIsImporting(true);
try {
await invoke("import_browser_profile", {
sourcePath: manualProfilePath.trim(),
browserType: manualBrowserType,
newProfileName: manualProfileName.trim(),
});
toast.success(
`Successfully imported profile "${manualProfileName.trim()}"`,
);
onClose();
} catch (error) {
console.error("Failed to import profile:", error);
const errorMessage =
error instanceof Error ? error.message : String(error);
// Check if error is about browser not being downloaded
if (errorMessage.includes("No downloaded versions found")) {
const browserDisplayName = getBrowserDisplayName(manualBrowserType);
toast.error(
`${browserDisplayName} is not installed. Please download ${browserDisplayName} first from the main window, then try importing again.`,
{
duration: 8000,
},
);
} else {
toast.error(`Failed to import profile: ${errorMessage}`);
}
} finally {
setIsImporting(false);
}
}, [manualBrowserType, manualProfilePath, manualProfileName, onClose]);
const handleClose = () => {
setCurrentStep("select");
setCamoufoxConfig({});
setWayfernConfig({});
setSelectedProxyId(undefined);
setSelectedDetectedProfile(null);
setAutoDetectProfileName("");
setManualBrowserType(null);
setManualProfilePath("");
setManualProfileName("");
// Only reset to auto-detect if there are profiles available
if (detectedProfiles.length > 0) {
setImportMode("auto-detect");
} else {
@@ -222,7 +234,6 @@ export function ImportProfileDialog({
onClose();
};
// Update auto-detect profile name when selection changes
useEffect(() => {
if (selectedDetectedProfile) {
const profile = detectedProfiles.find(
@@ -236,9 +247,38 @@ export function ImportProfileDialog({
}
}, [selectedDetectedProfile, detectedProfiles]);
const selectedProfile = detectedProfiles.find(
(p) => p.path === selectedDetectedProfile,
);
const currentMappedBrowser = useMemo(() => {
if (importMode === "auto-detect" && selectedProfile) {
return selectedProfile.mapped_browser as "camoufox" | "wayfern";
}
if (importMode === "manual" && manualBrowserType) {
return manualBrowserType as "camoufox" | "wayfern";
}
return null;
}, [importMode, selectedProfile, manualBrowserType]);
const canProceedToNext = useMemo(() => {
if (importMode === "auto-detect") {
return (
!isLoading &&
!!selectedDetectedProfile &&
!!autoDetectProfileName.trim()
);
}
return (
!!manualBrowserType &&
!!manualProfilePath.trim() &&
!!manualProfileName.trim()
);
}, [
importMode,
isLoading,
selectedDetectedProfile,
autoDetectProfileName,
manualBrowserType,
manualProfilePath,
manualProfileName,
]);
useEffect(() => {
if (isOpen) {
@@ -254,247 +294,322 @@ export function ImportProfileDialog({
</DialogHeader>
<div className="overflow-y-auto flex-1 space-y-6 min-h-0">
{/* Mode Selection */}
<div className="flex gap-2">
<RippleButton
variant={importMode === "auto-detect" ? "default" : "outline"}
onClick={() => {
setImportMode("auto-detect");
}}
className="flex-1"
disabled={isLoading}
>
Auto-Detect
</RippleButton>
<RippleButton
variant={importMode === "manual" ? "default" : "outline"}
onClick={() => {
setImportMode("manual");
}}
className="flex-1"
disabled={isLoading}
>
Manual Import
</RippleButton>
</div>
{currentStep === "select" && (
<>
<div className="flex gap-2">
<RippleButton
variant={importMode === "auto-detect" ? "default" : "outline"}
onClick={() => {
setImportMode("auto-detect");
}}
className="flex-1"
disabled={isLoading}
>
Auto-Detect
</RippleButton>
<RippleButton
variant={importMode === "manual" ? "default" : "outline"}
onClick={() => {
setImportMode("manual");
}}
className="flex-1"
disabled={isLoading}
>
Manual Import
</RippleButton>
</div>
{/* Auto-Detect Mode */}
{importMode === "auto-detect" && (
<div className="space-y-4">
<h3 className="text-lg font-medium">Detected Browser Profiles</h3>
{isLoading ? (
<div className="py-8 text-center">
<p className="text-muted-foreground">
Scanning for browser profiles...
</p>
</div>
) : detectedProfiles.length === 0 ? (
<div className="py-8 text-center">
<p className="text-muted-foreground">
No browser profiles found on your system.
</p>
<p className="mt-2 text-sm text-muted-foreground">
Try the manual import option if you have profiles in custom
locations.
</p>
</div>
) : (
{importMode === "auto-detect" && (
<div className="space-y-4">
<div>
<Label htmlFor="detected-profile-select" className="mb-2">
Select Profile:
</Label>
<Select
value={selectedDetectedProfile ?? undefined}
onValueChange={(value) => {
setSelectedDetectedProfile(value);
}}
>
<SelectTrigger id="detected-profile-select">
<SelectValue placeholder="Choose a detected profile" />
</SelectTrigger>
<SelectContent>
{detectedProfiles.map((profile) => {
const IconComponent = getBrowserIcon(profile.browser);
return (
<SelectItem key={profile.path} value={profile.path}>
<div className="flex gap-2 items-center">
{IconComponent && (
<IconComponent className="w-4 h-4" />
)}
<div className="flex flex-col">
<span className="font-medium">
{profile.name}
</span>
</div>
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
<h3 className="text-lg font-medium">
Detected Browser Profiles
</h3>
{selectedProfile && (
<div className="p-3 rounded-lg bg-muted">
<p className="text-sm">
<span className="font-medium">Path:</span>{" "}
{selectedProfile.path}
</p>
<p className="text-sm">
<span className="font-medium">Browser:</span>{" "}
{getBrowserDisplayName(selectedProfile.browser)}
{isLoading ? (
<div className="py-8 text-center">
<p className="text-muted-foreground">
Scanning for browser profiles...
</p>
</div>
)}
) : detectedProfiles.length === 0 ? (
<div className="py-8 text-center">
<p className="text-muted-foreground">
No browser profiles found on your system.
</p>
<p className="mt-2 text-sm text-muted-foreground">
Try the manual import option if you have profiles in
custom locations.
</p>
</div>
) : (
<div className="space-y-4">
<div>
<Label
htmlFor="detected-profile-select"
className="mb-2"
>
Select Profile:
</Label>
<Select
value={selectedDetectedProfile ?? undefined}
onValueChange={(value) => {
setSelectedDetectedProfile(value);
}}
>
<SelectTrigger id="detected-profile-select">
<SelectValue placeholder="Choose a detected profile" />
</SelectTrigger>
<SelectContent>
{detectedProfiles.map((profile) => {
const IconComponent = getBrowserIcon(
profile.browser,
);
return (
<SelectItem
key={profile.path}
value={profile.path}
>
<div className="flex gap-2 items-center">
{IconComponent && (
<IconComponent className="w-4 h-4" />
)}
<div className="flex flex-col">
<span className="font-medium">
{profile.name}
</span>
</div>
<span className="text-xs text-muted-foreground">
{" "}
{getBrowserDisplayName(
profile.mapped_browser,
)}
</span>
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="auto-profile-name" className="mb-2">
New Profile Name:
</Label>
<Input
id="auto-profile-name"
value={autoDetectProfileName}
onChange={(e) => {
setAutoDetectProfileName(e.target.value);
}}
placeholder="Enter a name for the imported profile"
/>
{selectedProfile && (
<div className="p-3 rounded-lg bg-muted">
<p className="text-sm">
<span className="font-medium">Path:</span>{" "}
{selectedProfile.path}
</p>
<p className="text-sm">
<span className="font-medium">Browser:</span>{" "}
{getBrowserDisplayName(selectedProfile.browser)}
</p>
</div>
)}
<div>
<Label htmlFor="auto-profile-name" className="mb-2">
New Profile Name:
</Label>
<Input
id="auto-profile-name"
value={autoDetectProfileName}
onChange={(e) => {
setAutoDetectProfileName(e.target.value);
}}
placeholder="Enter a name for the imported profile"
/>
</div>
</div>
)}
</div>
)}
{importMode === "manual" && (
<div className="space-y-4">
<h3 className="text-lg font-medium">Manual Profile Import</h3>
<div className="space-y-4">
<div>
<Label htmlFor="manual-browser-select" className="mb-2">
Browser Type:
</Label>
<Select
value={manualBrowserType ?? undefined}
onValueChange={(value) => {
setManualBrowserType(value);
}}
disabled={isLoadingSupport}
>
<SelectTrigger id="manual-browser-select">
<SelectValue
placeholder={
isLoadingSupport
? "Loading browsers..."
: "Select browser type"
}
/>
</SelectTrigger>
<SelectContent>
{importableBrowsers.map((browser) => {
const IconComponent = getBrowserIcon(browser);
return (
<SelectItem key={browser} value={browser}>
<div className="flex gap-2 items-center">
{IconComponent && (
<IconComponent className="w-4 h-4" />
)}
<span>{getBrowserDisplayName(browser)}</span>
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="manual-profile-path" className="mb-2">
Profile Folder Path:
</Label>
<div className="flex gap-2">
<Input
id="manual-profile-path"
value={manualProfilePath}
onChange={(e) => {
setManualProfilePath(e.target.value);
}}
placeholder="Enter the full path to the profile folder"
/>
<Button
variant="outline"
size="icon"
onClick={() => void handleBrowseFolder()}
title="Browse for folder"
>
<FaFolder className="w-4 h-4" />
</Button>
</div>
<p className="mt-2 text-xs text-muted-foreground">
Example paths:
<br />
macOS: ~/Library/Application
Support/Firefox/Profiles/xxx.default
<br />
Windows: %APPDATA%\Mozilla\Firefox\Profiles\xxx.default
<br />
Linux: ~/.mozilla/firefox/xxx.default
</p>
</div>
<div>
<Label htmlFor="manual-profile-name" className="mb-2">
New Profile Name:
</Label>
<Input
id="manual-profile-name"
value={manualProfileName}
onChange={(e) => {
setManualProfileName(e.target.value);
}}
placeholder="Enter a name for the imported profile"
/>
</div>
</div>
</div>
)}
</div>
</>
)}
{/* Manual Import Mode */}
{importMode === "manual" && (
{currentStep === "configure" && currentMappedBrowser && (
<div className="space-y-4">
<h3 className="text-lg font-medium">Manual Profile Import</h3>
<Alert>
<AlertDescription>
This profile will be imported as a{" "}
<strong>{getBrowserDisplayName(currentMappedBrowser)}</strong>{" "}
profile.
</AlertDescription>
</Alert>
<div className="space-y-4">
<div>
<Label htmlFor="manual-browser-select" className="mb-2">
Browser Type:
</Label>
<Select
value={manualBrowserType ?? undefined}
onValueChange={(value) => {
setManualBrowserType(value);
}}
disabled={isLoadingSupport}
>
<SelectTrigger id="manual-browser-select">
<SelectValue
placeholder={
isLoadingSupport
? "Loading browsers..."
: "Select browser type"
}
/>
</SelectTrigger>
<SelectContent>
{importableBrowsers.map((browser) => {
const IconComponent = getBrowserIcon(browser);
return (
<SelectItem key={browser} value={browser}>
<div className="flex gap-2 items-center">
{IconComponent && (
<IconComponent className="w-4 h-4" />
)}
<span>{getBrowserDisplayName(browser)}</span>
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="manual-profile-path" className="mb-2">
Profile Folder Path:
</Label>
<div className="flex gap-2">
<Input
id="manual-profile-path"
value={manualProfilePath}
onChange={(e) => {
setManualProfilePath(e.target.value);
}}
placeholder="Enter the full path to the profile folder"
/>
<Button
variant="outline"
size="icon"
onClick={() => void handleBrowseFolder()}
title="Browse for folder"
>
<FaFolder className="w-4 h-4" />
</Button>
</div>
<p className="mt-2 text-xs text-muted-foreground">
Example paths:
<br />
macOS: ~/Library/Application
Support/Firefox/Profiles/xxx.default
<br />
Windows: %APPDATA%\Mozilla\Firefox\Profiles\xxx.default
<br />
Linux: ~/.mozilla/firefox/xxx.default
</p>
</div>
<div>
<Label htmlFor="manual-profile-name" className="mb-2">
New Profile Name:
</Label>
<Input
id="manual-profile-name"
value={manualProfileName}
onChange={(e) => {
setManualProfileName(e.target.value);
}}
placeholder="Enter a name for the imported profile"
/>
</div>
<div>
<Label className="mb-2">Proxy (Optional)</Label>
<Select
value={selectedProxyId ?? "none"}
onValueChange={(value) => {
setSelectedProxyId(value === "none" ? undefined : value);
}}
>
<SelectTrigger>
<SelectValue placeholder="No 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>
{currentMappedBrowser === "camoufox" ? (
<SharedCamoufoxConfigForm
config={camoufoxConfig}
onConfigChange={(key, value) => {
setCamoufoxConfig((prev) => ({ ...prev, [key]: value }));
}}
isCreating={true}
crossOsUnlocked={crossOsUnlocked}
limitedMode={!crossOsUnlocked}
/>
) : (
<WayfernConfigForm
config={wayfernConfig}
onConfigChange={(key, value) => {
setWayfernConfig((prev) => ({ ...prev, [key]: value }));
}}
isCreating={true}
crossOsUnlocked={crossOsUnlocked}
limitedMode={!crossOsUnlocked}
/>
)}
</div>
)}
</div>
<DialogFooter className="flex-shrink-0">
<RippleButton variant="outline" onClick={handleClose}>
Cancel
</RippleButton>
{importMode === "auto-detect" ? (
<LoadingButton
isLoading={isImporting}
onClick={() => {
void handleAutoDetectImport();
}}
disabled={
!selectedDetectedProfile ||
!autoDetectProfileName.trim() ||
isLoading
}
>
Import
</LoadingButton>
{currentStep === "select" ? (
<>
<RippleButton variant="outline" onClick={handleClose}>
Cancel
</RippleButton>
<RippleButton
disabled={!canProceedToNext}
onClick={() => {
setCurrentStep("configure");
}}
>
Next
</RippleButton>
</>
) : (
<LoadingButton
isLoading={isImporting}
onClick={() => {
void handleManualImport();
}}
disabled={
!manualBrowserType ||
!manualProfilePath.trim() ||
!manualProfileName.trim()
}
>
Import
</LoadingButton>
<>
<RippleButton
variant="outline"
onClick={() => {
setCurrentStep("select");
}}
>
Back
</RippleButton>
<LoadingButton
isLoading={isImporting}
onClick={() => {
void handleImport();
}}
>
Import
</LoadingButton>
</>
)}
</DialogFooter>
</DialogContent>
+1 -1
View File
@@ -320,7 +320,7 @@ export function IntegrationsDialog({
<p className="text-xs text-muted-foreground">
Allow AI assistants like Claude Desktop to control browsers.
{!termsAccepted && (
<span className="ml-1 text-orange-600">
<span className="ml-1 text-warning">
(Accept Wayfern terms in Settings first)
</span>
)}
+5 -5
View File
@@ -116,7 +116,7 @@ export function PermissionDialog({
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-md">
<DialogHeader className="text-center">
<div className="flex justify-center items-center mx-auto mb-4 w-16 h-16 bg-blue-100 rounded-full dark:bg-blue-900">
<div className="flex justify-center items-center mx-auto mb-4 w-16 h-16 bg-primary/15 rounded-full">
{getPermissionIcon(permissionType)}
</div>
<DialogTitle className="text-xl">
@@ -129,8 +129,8 @@ export function PermissionDialog({
<div className="space-y-4">
{isCurrentPermissionGranted && (
<div className="p-3 bg-green-50 rounded-lg dark:bg-green-900/20">
<p className="text-sm text-green-800 dark:text-green-200">
<div className="p-3 bg-success/10 rounded-lg">
<p className="text-sm text-success">
Permission granted! Browsers launched from Donut Browser can
now access your {permissionType}.
</p>
@@ -138,8 +138,8 @@ export function PermissionDialog({
)}
{!isCurrentPermissionGranted && (
<div className="p-3 bg-amber-50 rounded-lg dark:bg-amber-900/20">
<p className="text-sm text-amber-800 dark:text-amber-200">
<div className="p-3 bg-warning/10 rounded-lg">
<p className="text-sm text-warning">
Permission not granted. Click the button below to request
access to your {permissionType}.
</p>
+5 -5
View File
@@ -234,21 +234,21 @@ function getProfileSyncStatusDot(
switch (status) {
case "syncing":
return {
color: "bg-yellow-500",
color: "bg-warning",
tooltip: "Syncing...",
animate: true,
encrypted,
};
case "waiting":
return {
color: "bg-yellow-500",
color: "bg-warning",
tooltip: "Waiting to sync",
animate: false,
encrypted,
};
case "synced":
return {
color: "bg-green-500",
color: "bg-success",
tooltip: profile.last_sync
? `Synced ${new Date(profile.last_sync * 1000).toLocaleString()}`
: "Synced",
@@ -257,7 +257,7 @@ function getProfileSyncStatusDot(
};
case "error":
return {
color: "bg-red-500",
color: "bg-destructive",
tooltip: errorMessage ? `Sync error: ${errorMessage}` : "Sync error",
animate: false,
encrypted,
@@ -265,7 +265,7 @@ function getProfileSyncStatusDot(
case "disabled":
if (profile.last_sync) {
return {
color: "bg-gray-400",
color: "bg-muted-foreground",
tooltip: `Sync disabled, last sync ${formatRelativeTime(profile.last_sync)}`,
animate: false,
encrypted: false,
+1 -1
View File
@@ -276,7 +276,7 @@ export function ProxyAssignmentDialog({
</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">
<div className="p-3 text-sm text-destructive bg-destructive/10 rounded-md">
{error}
</div>
)}
+4 -4
View File
@@ -429,14 +429,14 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) {
<div className="p-4 bg-muted/30 rounded-lg space-y-2">
<div className="flex justify-between">
<span className="text-sm">Imported:</span>
<span className="text-sm font-medium text-green-600 dark:text-green-400">
<span className="text-sm font-medium text-success">
{importResult.imported_count}
</span>
</div>
{importResult.skipped_count > 0 && (
<div className="flex justify-between">
<span className="text-sm">Skipped (duplicates):</span>
<span className="text-sm font-medium text-yellow-600 dark:text-yellow-400">
<span className="text-sm font-medium text-warning">
{importResult.skipped_count}
</span>
</div>
@@ -444,7 +444,7 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) {
{importResult.errors.length > 0 && (
<div className="flex justify-between">
<span className="text-sm">Errors:</span>
<span className="text-sm font-medium text-red-600 dark:text-red-400">
<span className="text-sm font-medium text-destructive">
{importResult.errors.length}
</span>
</div>
@@ -459,7 +459,7 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) {
{importResult.errors.map((error, i) => (
<div
key={`error-${i}`}
className="text-xs text-red-600 dark:text-red-400"
className="text-xs text-destructive"
>
{error}
</div>
+9 -5
View File
@@ -59,10 +59,10 @@ function getSyncStatusDot(
switch (status) {
case "syncing":
return { color: "bg-yellow-500", tooltip: "Syncing...", animate: true };
return { color: "bg-warning", tooltip: "Syncing...", animate: true };
case "synced":
return {
color: "bg-green-500",
color: "bg-success",
tooltip: item.last_sync
? `Synced ${new Date(item.last_sync * 1000).toLocaleString()}`
: "Synced",
@@ -70,18 +70,22 @@ function getSyncStatusDot(
};
case "waiting":
return {
color: "bg-yellow-500",
color: "bg-warning",
tooltip: "Waiting to sync",
animate: false,
};
case "error":
return {
color: "bg-red-500",
color: "bg-destructive",
tooltip: errorMessage ? `Sync error: ${errorMessage}` : "Sync error",
animate: false,
};
default:
return { color: "bg-gray-400", tooltip: "Not synced", animate: false };
return {
color: "bg-muted-foreground",
tooltip: "Not synced",
animate: false,
};
}
}
+2 -2
View File
@@ -170,7 +170,7 @@ export function SettingsDialog({
const getStatusBadge = useCallback((isGranted: boolean) => {
if (isGranted) {
return (
<Badge variant="default" className="text-green-800 bg-green-100">
<Badge variant="default" className="text-success-foreground bg-success">
Granted
</Badge>
);
@@ -1018,7 +1018,7 @@ export function SettingsDialog({
</div>
) : (
<div className="space-y-1">
<p className="text-sm font-medium text-orange-600">
<p className="text-sm font-medium text-warning">
Trial expired
</p>
<p className="text-xs text-muted-foreground">
+3 -3
View File
@@ -269,7 +269,7 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
{isLoggedIn && user ? (
<div className="grid gap-4 py-4">
<div className="flex gap-2 items-center text-sm">
<div className="w-2 h-2 rounded-full bg-green-500" />
<div className="w-2 h-2 rounded-full bg-success" />
{t("sync.cloud.connected")}
</div>
@@ -530,13 +530,13 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
)}
{connectionStatus === "connected" && (
<div className="flex gap-2 items-center text-sm text-muted-foreground">
<div className="w-2 h-2 rounded-full bg-green-500" />
<div className="w-2 h-2 rounded-full bg-success" />
{t("sync.status.connected")}
</div>
)}
{connectionStatus === "error" && (
<div className="flex gap-2 items-center text-sm text-muted-foreground">
<div className="w-2 h-2 rounded-full bg-red-500" />
<div className="w-2 h-2 rounded-full bg-destructive" />
{t("sync.status.disconnected")}
</div>
)}
+1 -1
View File
@@ -75,7 +75,7 @@ export function VpnCheckButton({
{isCurrentlyChecking ? (
<div className="w-3 h-3 rounded-full border border-current animate-spin border-t-transparent" />
) : result?.is_valid ? (
<FiCheck className="w-3 h-3 text-green-500" />
<FiCheck className="w-3 h-3 text-success" />
) : result && !result.is_valid ? (
<span className="text-destructive text-sm"></span>
) : (
+5 -5
View File
@@ -295,13 +295,13 @@ export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) {
{step === "vpn-result" && vpnImportResult && (
<div className="space-y-4">
<div
className={`p-4 rounded-lg ${vpnImportResult.success ? "bg-green-500/10" : "bg-red-500/10"}`}
className={`p-4 rounded-lg ${vpnImportResult.success ? "bg-success/10" : "bg-destructive/10"}`}
>
{vpnImportResult.success ? (
<div className="flex items-center gap-3">
<LuShield className="w-8 h-8 text-green-600 dark:text-green-400" />
<LuShield className="w-8 h-8 text-success" />
<div>
<div className="font-medium text-green-600 dark:text-green-400">
<div className="font-medium text-success">
VPN Imported Successfully
</div>
<div className="text-sm text-muted-foreground">
@@ -311,10 +311,10 @@ export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) {
</div>
) : (
<div className="space-y-2">
<div className="font-medium text-red-600 dark:text-red-400">
<div className="font-medium text-destructive">
Import Failed
</div>
<div className="text-sm text-red-600 dark:text-red-400">
<div className="text-sm text-destructive">
{vpnImportResult.error}
</div>
</div>
-42
View File
@@ -3,11 +3,9 @@ import { listen } from "@tauri-apps/api/event";
import { useCallback, useEffect, useRef, useState } from "react";
import { getBrowserDisplayName } from "@/lib/browser-utils";
import {
dismissToast,
showAutoUpdateToast,
showErrorToast,
showSuccessToast,
showUnifiedVersionUpdateToast,
} from "@/lib/toast-utils";
interface VersionUpdateProgress {
@@ -76,53 +74,13 @@ export function useVersionUpdater() {
if (progress.status === "updating") {
setIsUpdating(true);
// 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,
},
onCancel: () => dismissToast("unified-version-update"),
});
} else if (progress.status === "completed") {
setIsUpdating(false);
setUpdateProgress(null);
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",
});
}
// Refresh status
void loadUpdateStatus();
} else if (progress.status === "error") {
setIsUpdating(false);
setUpdateProgress(null);
dismissToast("unified-version-update");
showErrorToast("Failed to update browser versions", {
duration: 6000,
description: "Check your internet connection and try again",
});
}
},
);
-5
View File
@@ -541,11 +541,6 @@
"unknownError": "An unknown error occurred. Please try again."
},
"browser": {
"firefox": "Firefox",
"firefoxDeveloper": "Firefox Developer Edition",
"chromium": "Chromium",
"brave": "Brave",
"zen": "Zen Browser",
"camoufox": "Camoufox",
"wayfern": "Wayfern"
},
-5
View File
@@ -541,11 +541,6 @@
"unknownError": "Ocurrió un error desconocido. Por favor intenta de nuevo."
},
"browser": {
"firefox": "Firefox",
"firefoxDeveloper": "Firefox Developer Edition",
"chromium": "Chromium",
"brave": "Brave",
"zen": "Zen Browser",
"camoufox": "Camoufox",
"wayfern": "Wayfern"
},
-5
View File
@@ -541,11 +541,6 @@
"unknownError": "Une erreur inconnue s'est produite. Veuillez réessayer."
},
"browser": {
"firefox": "Firefox",
"firefoxDeveloper": "Firefox Developer Edition",
"chromium": "Chromium",
"brave": "Brave",
"zen": "Zen Browser",
"camoufox": "Camoufox",
"wayfern": "Wayfern"
},
-5
View File
@@ -541,11 +541,6 @@
"unknownError": "不明なエラーが発生しました。もう一度お試しください。"
},
"browser": {
"firefox": "Firefox",
"firefoxDeveloper": "Firefox Developer Edition",
"chromium": "Chromium",
"brave": "Brave",
"zen": "Zen Browser",
"camoufox": "Camoufox",
"wayfern": "Wayfern"
},
-5
View File
@@ -541,11 +541,6 @@
"unknownError": "Ocorreu um erro desconhecido. Por favor, tente novamente."
},
"browser": {
"firefox": "Firefox",
"firefoxDeveloper": "Firefox Developer Edition",
"chromium": "Chromium",
"brave": "Brave",
"zen": "Zen Browser",
"camoufox": "Camoufox",
"wayfern": "Wayfern"
},
-5
View File
@@ -541,11 +541,6 @@
"unknownError": "Произошла неизвестная ошибка. Попробуйте снова."
},
"browser": {
"firefox": "Firefox",
"firefoxDeveloper": "Firefox Developer Edition",
"chromium": "Chromium",
"brave": "Brave",
"zen": "Zen Browser",
"camoufox": "Camoufox",
"wayfern": "Wayfern"
},
-5
View File
@@ -541,11 +541,6 @@
"unknownError": "发生未知错误。请重试。"
},
"browser": {
"firefox": "Firefox",
"firefoxDeveloper": "Firefox 开发者版",
"chromium": "Chromium",
"brave": "Brave",
"zen": "Zen Browser",
"camoufox": "Camoufox",
"wayfern": "Wayfern"
},
-5
View File
@@ -15,11 +15,6 @@ import {
*/
export function getBrowserDisplayName(browserType: string): string {
const browserNames: Record<string, string> = {
firefox: "Firefox",
"firefox-developer": "Firefox Developer Edition",
zen: "Zen Browser",
brave: "Brave",
chromium: "Chromium",
camoufox: "Camoufox",
wayfern: "Wayfern",
};
+48
View File
@@ -15,6 +15,10 @@ export interface ThemeColors extends Record<string, string> {
"--accent-foreground": string;
"--destructive": string;
"--destructive-foreground": string;
"--success": string;
"--success-foreground": string;
"--warning": string;
"--warning-foreground": string;
"--border": string;
"--chart-1": string;
"--chart-2": string;
@@ -50,6 +54,10 @@ export const THEMES: Theme[] = [
"--accent-foreground": "#1a1b26",
"--destructive": "#f7768e",
"--destructive-foreground": "#1a1b26",
"--success": "#9ece6a",
"--success-foreground": "#1a1b26",
"--warning": "#e0af68",
"--warning-foreground": "#1a1b26",
"--border": "#3b4261",
"--chart-1": "#7aa2f7",
"--chart-2": "#9ece6a",
@@ -78,6 +86,10 @@ export const THEMES: Theme[] = [
"--accent-foreground": "#282a36",
"--destructive": "#ff5555",
"--destructive-foreground": "#f8f8f2",
"--success": "#50fa7b",
"--success-foreground": "#282a36",
"--warning": "#ffb86c",
"--warning-foreground": "#282a36",
"--border": "#6272a4",
"--chart-1": "#bd93f9",
"--chart-2": "#50fa7b",
@@ -106,6 +118,10 @@ export const THEMES: Theme[] = [
"--accent-foreground": "#273136",
"--destructive": "#ff819f",
"--destructive-foreground": "#273136",
"--success": "#a8c97f",
"--success-foreground": "#273136",
"--warning": "#e6c07b",
"--warning-foreground": "#273136",
"--border": "#304e37",
"--chart-1": "#7eb08a",
"--chart-2": "#d2b48c",
@@ -134,6 +150,10 @@ export const THEMES: Theme[] = [
"--accent-foreground": "#f7f7f8",
"--destructive": "#ef4444",
"--destructive-foreground": "#f7f7f8",
"--success": "#22c55e",
"--success-foreground": "#17191e",
"--warning": "#f59e0b",
"--warning-foreground": "#17191e",
"--border": "#2a2e39",
"--chart-1": "#5755d9",
"--chart-2": "#0ea5e9",
@@ -162,6 +182,10 @@ export const THEMES: Theme[] = [
"--accent-foreground": "#0a0e14",
"--destructive": "#f07178",
"--destructive-foreground": "#b3b1ad",
"--success": "#c2d94c",
"--success-foreground": "#0a0e14",
"--warning": "#ffb454",
"--warning-foreground": "#0a0e14",
"--border": "#1f2430",
"--chart-1": "#39bae6",
"--chart-2": "#c2d94c",
@@ -190,6 +214,10 @@ export const THEMES: Theme[] = [
"--accent-foreground": "#fafafa",
"--destructive": "#f07178",
"--destructive-foreground": "#fafafa",
"--success": "#86b300",
"--success-foreground": "#fafafa",
"--warning": "#fa8d3e",
"--warning-foreground": "#fafafa",
"--border": "#e7eaed",
"--chart-1": "#399ee6",
"--chart-2": "#86b300",
@@ -218,6 +246,10 @@ export const THEMES: Theme[] = [
"--accent-foreground": "#eff1f5",
"--destructive": "#d20f39",
"--destructive-foreground": "#eff1f5",
"--success": "#40a02b",
"--success-foreground": "#eff1f5",
"--warning": "#df8e1d",
"--warning-foreground": "#eff1f5",
"--border": "#9ca0b0",
"--chart-1": "#1e66f5",
"--chart-2": "#40a02b",
@@ -246,6 +278,10 @@ export const THEMES: Theme[] = [
"--accent-foreground": "#303446",
"--destructive": "#e78284",
"--destructive-foreground": "#303446",
"--success": "#a6d189",
"--success-foreground": "#303446",
"--warning": "#e5c890",
"--warning-foreground": "#303446",
"--border": "#737994",
"--chart-1": "#8caaee",
"--chart-2": "#a6d189",
@@ -274,6 +310,10 @@ export const THEMES: Theme[] = [
"--accent-foreground": "#24273a",
"--destructive": "#ed8796",
"--destructive-foreground": "#24273a",
"--success": "#a6da95",
"--success-foreground": "#24273a",
"--warning": "#eed49f",
"--warning-foreground": "#24273a",
"--border": "#6e738d",
"--chart-1": "#8aadf4",
"--chart-2": "#a6da95",
@@ -302,6 +342,10 @@ export const THEMES: Theme[] = [
"--accent-foreground": "#1e1e2e",
"--destructive": "#f38ba8",
"--destructive-foreground": "#1e1e2e",
"--success": "#a6e3a1",
"--success-foreground": "#1e1e2e",
"--warning": "#f9e2af",
"--warning-foreground": "#1e1e2e",
"--border": "#585b70",
"--chart-1": "#89b4fa",
"--chart-2": "#a6e3a1",
@@ -330,6 +374,10 @@ export const THEME_VARIABLES: Array<{ key: keyof ThemeColors; label: string }> =
{ key: "--accent-foreground", label: "Accent FG" },
{ key: "--destructive", label: "Destructive" },
{ key: "--destructive-foreground", label: "Destructive FG" },
{ key: "--success", label: "Success" },
{ key: "--success-foreground", label: "Success FG" },
{ key: "--warning", label: "Warning" },
{ key: "--warning-foreground", label: "Warning FG" },
{ key: "--border", label: "Border" },
{ key: "--chart-1", label: "Chart 1" },
{ key: "--chart-2", label: "Chart 2" },
+2 -5
View File
@@ -82,13 +82,10 @@ export function showToast(props: ToastProps & { id?: string }) {
duration = 10000;
break;
case "download":
// Only keep infinite for active downloading, others get shorter durations
if ("stage" in props && props.stage === "downloading") {
duration = Number.POSITIVE_INFINITY;
} else if ("stage" in props && props.stage === "completed") {
if ("stage" in props && props.stage === "completed") {
duration = 3000;
} else {
duration = 20000;
duration = Number.POSITIVE_INFINITY;
}
break;
case "success":
+15
View File
@@ -28,6 +28,11 @@
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
@@ -63,6 +68,11 @@
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.985 0 0);
--success: oklch(0.6 0.2 145);
--success-foreground: oklch(0.985 0 0);
--warning: oklch(0.75 0.15 75);
--warning-foreground: oklch(0.141 0.005 285.823);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
@@ -102,6 +112,11 @@
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--destructive-foreground: oklch(0.985 0 0);
--success: oklch(0.7 0.2 145);
--success-foreground: oklch(0.141 0.005 285.823);
--warning: oklch(0.8 0.15 75);
--warning-foreground: oklch(0.141 0.005 285.823);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.552 0.016 285.938);
+1
View File
@@ -161,6 +161,7 @@ export interface DetectedProfile {
name: string;
path: string;
description: string;
mapped_browser: string;
}
export interface BrowserReleaseTypes {