mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-07-25 13:50:51 +02:00
refactor: cleanup
This commit is contained in:
+5
-41
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" && (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -541,11 +541,6 @@
|
||||
"unknownError": "不明なエラーが発生しました。もう一度お試しください。"
|
||||
},
|
||||
"browser": {
|
||||
"firefox": "Firefox",
|
||||
"firefoxDeveloper": "Firefox Developer Edition",
|
||||
"chromium": "Chromium",
|
||||
"brave": "Brave",
|
||||
"zen": "Zen Browser",
|
||||
"camoufox": "Camoufox",
|
||||
"wayfern": "Wayfern"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -541,11 +541,6 @@
|
||||
"unknownError": "Произошла неизвестная ошибка. Попробуйте снова."
|
||||
},
|
||||
"browser": {
|
||||
"firefox": "Firefox",
|
||||
"firefoxDeveloper": "Firefox Developer Edition",
|
||||
"chromium": "Chromium",
|
||||
"brave": "Brave",
|
||||
"zen": "Zen Browser",
|
||||
"camoufox": "Camoufox",
|
||||
"wayfern": "Wayfern"
|
||||
},
|
||||
|
||||
@@ -541,11 +541,6 @@
|
||||
"unknownError": "发生未知错误。请重试。"
|
||||
},
|
||||
"browser": {
|
||||
"firefox": "Firefox",
|
||||
"firefoxDeveloper": "Firefox 开发者版",
|
||||
"chromium": "Chromium",
|
||||
"brave": "Brave",
|
||||
"zen": "Zen Browser",
|
||||
"camoufox": "Camoufox",
|
||||
"wayfern": "Wayfern"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -161,6 +161,7 @@ export interface DetectedProfile {
|
||||
name: string;
|
||||
path: string;
|
||||
description: string;
|
||||
mapped_browser: string;
|
||||
}
|
||||
|
||||
export interface BrowserReleaseTypes {
|
||||
|
||||
Reference in New Issue
Block a user