chore: linting for both js and rs

This commit is contained in:
zhom
2025-05-29 23:57:54 +04:00
parent cf57b2a043
commit 5c02b59af4
33 changed files with 836 additions and 608 deletions
+10 -20
View File
@@ -6,7 +6,6 @@ import { ProfilesDataTable } from "@/components/profile-data-table";
import { ProfileSelectorDialog } from "@/components/profile-selector-dialog";
import { ProxySettingsDialog } from "@/components/proxy-settings-dialog";
import { SettingsDialog } from "@/components/settings-dialog";
import { useUpdateNotifications } from "@/components/update-notification";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
@@ -14,12 +13,13 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useUpdateNotifications } from "@/hooks/use-update-notifications";
import { showErrorToast } from "@/lib/toast-utils";
import type { BrowserProfile, ProxySettings } from "@/types";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { useCallback, useEffect, useRef, useState } from "react";
import { GoGear, GoPlus } from "react-icons/go";
import { showErrorToast } from "@/components/custom-toast";
type BrowserTypeString =
| "mullvad-browser"
@@ -177,26 +177,14 @@ export default function Home() {
});
console.log("Smart URL opening succeeded:", result);
// URL was handled successfully
} catch (error: any) {
} catch (error: unknown) {
console.log(
"Smart URL opening failed or requires profile selection:",
error
);
// Check if it's the special error cases
if (error === "show_selector") {
// Show profile selector
setPendingUrls((prev) => [...prev, { id: Date.now().toString(), url }]);
} else if (error === "no_profiles") {
// No profiles available, show error message
setError(
"No profiles available. Please create a profile first before opening URLs."
);
} else {
// Some other error occurred
console.error("Failed to open URL:", error);
setError(`Failed to open URL: ${error}`);
}
// Show profile selector for manual selection
setPendingUrls((prev) => [...prev, { id: Date.now().toString(), url }]);
}
};
@@ -260,7 +248,9 @@ export default function Home() {
await loadProfiles();
} catch (error) {
setError(`Failed to create profile: ${error as any}`);
setError(
`Failed to create profile: ${error instanceof Error ? error.message : String(error)}`
);
throw error;
}
},
@@ -347,9 +337,9 @@ export default function Home() {
if (profiles.length === 0 || !isClient) return;
const interval = setInterval(() => {
profiles.forEach((profile) => {
for (const profile of profiles) {
void checkBrowserStatus(profile);
});
}
}, 500);
return () => {
+1 -1
View File
@@ -16,8 +16,8 @@ import { VersionSelector } from "@/components/version-selector";
import { useBrowserDownload } from "@/hooks/use-browser-download";
import type { BrowserProfile } from "@/types";
import { invoke } from "@tauri-apps/api/core";
import { LuTriangleAlert } from "react-icons/lu";
import { useEffect, useState } from "react";
import { LuTriangleAlert } from "react-icons/lu";
interface ChangeVersionDialogProps {
isOpen: boolean;
+1 -204
View File
@@ -43,12 +43,11 @@
*/
import React from "react";
import { toast as sonnerToast } from "sonner";
import {
LuCheckCheck,
LuTriangleAlert,
LuDownload,
LuRefreshCw,
LuTriangleAlert,
} from "react-icons/lu";
interface BaseToastProps {
@@ -123,7 +122,6 @@ function getToastIcon(type: ToastProps["type"], stage?: string) {
return (
<LuRefreshCw className="h-4 w-4 text-blue-500 animate-spin flex-shrink-0" />
);
case "loading":
default:
return (
<div className="animate-spin rounded-full h-4 w-4 border-2 border-blue-500 border-t-transparent flex-shrink-0" />
@@ -214,204 +212,3 @@ export function UnifiedToast(props: ToastProps) {
</div>
);
}
// Unified toast function
export function showToast(props: ToastProps & { id?: string }) {
const toastId = props.id ?? `toast-${props.type}-${Date.now()}`;
// Improved duration logic - make toasts disappear more quickly
let duration: number;
if (props.duration !== undefined) {
duration = props.duration;
} else {
switch (props.type) {
case "loading":
case "fetching":
duration = 10000; // 10 seconds instead of infinite
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") {
duration = 3000; // Shorter duration for completed downloads
} else {
duration = 8000; // 8 seconds for extracting/verifying
}
break;
case "version-update":
duration = 15000; // 15 seconds instead of infinite
break;
case "success":
duration = 3000; // Shorter success duration
break;
case "error":
duration = 5000; // Reasonable error duration
break;
default:
duration = 4000;
}
}
if (props.type === "success") {
sonnerToast.success(<UnifiedToast {...props} />, {
id: toastId,
duration,
style: {
background: "transparent",
border: "none",
boxShadow: "none",
padding: 0,
},
});
} else if (props.type === "error") {
sonnerToast.error(<UnifiedToast {...props} />, {
id: toastId,
duration,
style: {
background: "transparent",
border: "none",
boxShadow: "none",
padding: 0,
},
});
} else {
sonnerToast.custom((id) => <UnifiedToast {...props} />, {
id: toastId,
duration,
style: {
background: "transparent",
border: "none",
boxShadow: "none",
padding: 0,
},
});
}
return toastId;
}
// Convenience functions for common use cases
export function showLoadingToast(
title: string,
options?: {
id?: string;
description?: string;
duration?: number;
}
) {
return showToast({
type: "loading",
title,
...options,
});
}
export function showDownloadToast(
browserName: string,
version: string,
stage: "downloading" | "extracting" | "verifying" | "completed",
progress?: { percentage: number; speed?: string; eta?: string },
options?: { suppressCompletionToast?: boolean }
) {
const title =
stage === "completed"
? `${browserName} ${version} downloaded successfully!`
: stage === "downloading"
? `Downloading ${browserName} ${version}`
: stage === "extracting"
? `Extracting ${browserName} ${version}`
: `Verifying ${browserName} ${version}`;
// Don't show completion toast if suppressed (for auto-update scenarios)
if (stage === "completed" && options?.suppressCompletionToast) {
dismissToast(`download-${browserName.toLowerCase()}-${version}`);
return;
}
return showToast({
type: "download",
title,
stage,
progress,
id: `download-${browserName.toLowerCase()}-${version}`,
});
}
export function showVersionUpdateToast(
title: string,
options?: {
id?: string;
description?: string;
progress?: {
current: number;
total: number;
found: number;
};
duration?: number;
}
) {
return showToast({
type: "version-update",
title,
...options,
});
}
export function showFetchingToast(
browserName: string,
options?: {
id?: string;
description?: string;
duration?: number;
}
) {
return showToast({
type: "fetching",
title: `Checking for new ${browserName} versions...`,
description:
options?.description ?? "Fetching latest release information...",
browserName,
...options,
});
}
export function showSuccessToast(
title: string,
options?: {
id?: string;
description?: string;
duration?: number;
}
) {
return showToast({
type: "success",
title,
...options,
});
}
export function showErrorToast(
title: string,
options?: {
id?: string;
description?: string;
duration?: number;
}
) {
return showToast({
type: "error",
title,
...options,
});
}
// Generic helper for dismissing toasts
export function dismissToast(id: string) {
sonnerToast.dismiss(id);
}
// Dismiss all toasts
export function dismissAllToasts() {
sonnerToast.dismiss();
}
+15 -5
View File
@@ -31,6 +31,8 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useTableSorting } from "@/hooks/use-table-sorting";
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
import type { BrowserProfile } from "@/types";
import {
type ColumnDef,
@@ -40,14 +42,12 @@ import {
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { LuChevronDown, LuChevronUp } from "react-icons/lu";
import { IoEllipsisHorizontal } from "react-icons/io5";
import * as React from "react";
import { CiCircleCheck } from "react-icons/ci";
import { useTableSorting } from "@/hooks/use-table-sorting";
import { IoEllipsisHorizontal } from "react-icons/io5";
import { LuChevronDown, LuChevronUp } from "react-icons/lu";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
interface ProfilesDataTableProps {
data: BrowserProfile[];
@@ -392,7 +392,17 @@ export function ProfilesDataTable({
},
},
],
[isClient, runningProfiles, isUpdating, data]
[
isClient,
runningProfiles,
isUpdating,
data,
onLaunchProfile,
onKillProfile,
onProxySettings,
onDeleteProfile,
onChangeVersion,
]
);
const table = useReactTable({
+2 -2
View File
@@ -23,12 +23,12 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
import type { BrowserProfile } from "@/types";
import { invoke } from "@tauri-apps/api/core";
import { LuCopy } from "react-icons/lu";
import { useEffect, useState } from "react";
import { LuCopy } from "react-icons/lu";
import { toast } from "sonner";
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
interface ProfileSelectorDialogProps {
isOpen: boolean;
+1 -1
View File
@@ -1,8 +1,8 @@
"use client";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { LuCheck } from "react-icons/lu";
import type * as React from "react";
import { LuCheck } from "react-icons/lu";
import { cn } from "@/lib/utils";
+1 -1
View File
@@ -1,8 +1,8 @@
"use client";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { LuCheck, LuChevronRight, LuCircle } from "react-icons/lu";
import type * as React from "react";
import { LuCheck, LuChevronRight, LuCircle } from "react-icons/lu";
import { cn } from "@/lib/utils";
+1 -1
View File
@@ -1,8 +1,8 @@
"use client";
import * as SelectPrimitive from "@radix-ui/react-select";
import { LuCheck, LuChevronDown, LuChevronUp } from "react-icons/lu";
import type * as React from "react";
import { LuCheck, LuChevronDown, LuChevronUp } from "react-icons/lu";
import { cn } from "@/lib/utils";
+5 -202
View File
@@ -2,14 +2,12 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { invoke } from "@tauri-apps/api/core";
import { toast } from "sonner";
import { useCallback, useEffect, useState } from "react";
import { FaDownload, FaTimes } from "react-icons/fa";
import { showToast } from "@/components/custom-toast";
import { Button } from "@/components/ui/button";
import { getBrowserDisplayName } from "@/lib/browser-utils";
import React from "react";
import { FaDownload, FaTimes } from "react-icons/fa";
import { LuDownload } from "react-icons/lu";
interface UpdateNotification {
id: string;
@@ -28,7 +26,7 @@ interface UpdateNotificationProps {
isUpdating?: boolean;
}
function UpdateNotificationComponent({
export function UpdateNotificationComponent({
notification,
onUpdate,
onDismiss,
@@ -108,198 +106,3 @@ function UpdateNotificationComponent({
</div>
);
}
export function useUpdateNotifications() {
const [notifications, setNotifications] = useState<UpdateNotification[]>([]);
const [updatingBrowsers, setUpdatingBrowsers] = useState<Set<string>>(
new Set()
);
const [isClient, setIsClient] = useState(false);
// Ensure we're on the client side to prevent hydration mismatches
useEffect(() => {
setIsClient(true);
}, []);
const checkForUpdates = useCallback(async () => {
if (!isClient) return; // Only run on client side
try {
const updates = await invoke<UpdateNotification[]>(
"check_for_browser_updates"
);
setNotifications(updates);
// Show toasts for new notifications - we'll define handleUpdate and handleDismiss separately
// to avoid circular dependencies
} catch (error) {
console.error("Failed to check for updates:", error);
}
}, [isClient]);
const handleUpdate = useCallback(
async (browser: string, newVersion: string) => {
try {
setUpdatingBrowsers((prev) => new Set(prev).add(browser));
const browserDisplayName = getBrowserDisplayName(browser);
// Dismiss all notifications for this browser first
const browserNotifications = notifications.filter(
(n) => n.browser === browser
);
for (const notification of browserNotifications) {
toast.dismiss(notification.id);
await invoke("dismiss_update_notification", {
notificationId: notification.id,
});
}
try {
// Check if browser already exists before downloading
const isDownloaded = await invoke<boolean>("check_browser_exists", {
browserStr: browser,
version: newVersion,
});
if (isDownloaded) {
// Browser already exists, skip download and go straight to profile update
console.log(
`${browserDisplayName} ${newVersion} already exists, skipping download`
);
} else {
// Mark download as auto-update in the backend for toast suppression
await invoke("mark_auto_update_download", {
browser,
version: newVersion,
});
// Download the browser (progress will be handled by use-browser-download hook)
await invoke("download_browser", {
browserStr: browser,
version: newVersion,
});
}
// Complete the update with auto-update of profile versions
const updatedProfiles = await invoke<string[]>(
"complete_browser_update_with_auto_update",
{
browser,
newVersion,
}
);
// Show success message based on whether profiles were updated
if (updatedProfiles.length > 0) {
const profileText =
updatedProfiles.length === 1
? `Profile "${updatedProfiles[0]}" has been updated`
: `${updatedProfiles.length} profiles have been updated`;
showToast({
type: "success",
title: `${browserDisplayName} update completed`,
description: `${profileText} to version ${newVersion}. Running profiles were not updated and can be updated manually.`,
duration: 5000,
});
} else {
showToast({
type: "success",
title: `${browserDisplayName} update ready`,
description:
"All affected profiles are currently running. Stop them and manually update their versions to use the new version.",
duration: 5000,
});
}
} catch (downloadError) {
console.error("Failed to download browser:", downloadError);
// Clean up auto-update tracking on error
try {
await invoke("remove_auto_update_download", {
browser,
version: newVersion,
});
} catch (e) {
console.error("Failed to clean up auto-update tracking:", e);
}
showToast({
type: "error",
title: `Failed to download ${browserDisplayName} ${newVersion}`,
description: String(downloadError),
duration: 6000,
});
throw downloadError;
}
// Refresh notifications to clear any remaining ones
await checkForUpdates();
} catch (error) {
console.error("Failed to start update:", error);
const browserDisplayName = getBrowserDisplayName(browser);
showToast({
type: "error",
title: `Failed to update ${browserDisplayName}`,
description: String(error),
duration: 6000,
});
} finally {
setUpdatingBrowsers((prev) => {
const next = new Set(prev);
next.delete(browser);
return next;
});
}
},
[notifications, checkForUpdates]
);
const handleDismiss = useCallback(
async (notificationId: string) => {
if (!isClient) return; // Only run on client side
try {
toast.dismiss(notificationId);
await invoke("dismiss_update_notification", { notificationId });
await checkForUpdates();
} catch (error) {
console.error("Failed to dismiss notification:", error);
}
},
[checkForUpdates, isClient]
);
// Separate effect to show toasts when notifications change
useEffect(() => {
if (!isClient) return;
notifications.forEach((notification) => {
const isUpdating = updatingBrowsers.has(notification.browser);
toast.custom(
() => (
<UpdateNotificationComponent
notification={notification}
onUpdate={handleUpdate}
onDismiss={handleDismiss}
isUpdating={isUpdating}
/>
),
{
id: notification.id,
duration: Number.POSITIVE_INFINITY, // Persistent until user action
position: "top-right",
// Remove transparent styling to fix background issue
style: undefined,
}
);
});
}, [notifications, updatingBrowsers, handleUpdate, handleDismiss, isClient]);
return {
notifications,
checkForUpdates,
isUpdating: (browser: string) => updatingBrowsers.has(browser),
};
}
+1 -1
View File
@@ -17,8 +17,8 @@ import {
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { LuDownload } from "react-icons/lu";
import { useState } from "react";
import { LuDownload } from "react-icons/lu";
import { LuCheck, LuChevronsUpDown } from "react-icons/lu";
import { ScrollArea } from "./ui/scroll-area";
+2 -2
View File
@@ -11,10 +11,10 @@ import {
} from "@/components/ui/card";
import { useVersionUpdater } from "@/hooks/use-version-updater";
import {
LuRefreshCw,
LuClock,
LuCheckCheck,
LuCircleAlert,
LuClock,
LuRefreshCw,
} from "react-icons/lu";
export function VersionUpdateSettings() {
+14 -16
View File
@@ -1,15 +1,15 @@
import { getBrowserDisplayName } from "@/lib/browser-utils";
import {
dismissToast,
showDownloadToast,
showErrorToast,
showFetchingToast,
showSuccessToast,
} from "@/lib/toast-utils";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import {
showDownloadToast,
showFetchingToast,
showSuccessToast,
showErrorToast,
dismissToast,
} from "../components/custom-toast";
import { getBrowserDisplayName } from "@/lib/browser-utils";
interface GithubRelease {
tag_name: string;
@@ -192,15 +192,15 @@ export function useBrowserDownload() {
const formatTime = (seconds: number): string => {
if (seconds < 60) {
return `${Math.round(seconds)}s`;
} else if (seconds < 3600) {
}
if (seconds < 3600) {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.round(seconds % 60);
return `${minutes}m ${remainingSeconds}s`;
} else {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${hours}h ${minutes}m`;
}
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${hours}h ${minutes}m`;
};
const formatBytes = (bytes: number): string => {
@@ -208,9 +208,7 @@ export function useBrowserDownload() {
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${Number.parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${
sizes[i]
}`;
return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;
};
const loadVersions = useCallback(async (browserStr: string) => {
+2 -2
View File
@@ -1,7 +1,7 @@
import { invoke } from "@tauri-apps/api/core";
import { useCallback, useEffect, useState } from "react";
import type { TableSortingSettings } from "@/types";
import type { SortingState } from "@tanstack/react-table";
import { invoke } from "@tauri-apps/api/core";
import { useCallback, useEffect, useState } from "react";
export function useTableSorting() {
const [sortingSettings, setSortingSettings] = useState<TableSortingSettings>({
+211
View File
@@ -0,0 +1,211 @@
import { UpdateNotificationComponent } from "@/components/update-notification";
import { getBrowserDisplayName } from "@/lib/browser-utils";
import { showToast } from "@/lib/toast-utils";
import { invoke } from "@tauri-apps/api/core";
import React, { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
interface UpdateNotification {
id: string;
browser: string;
current_version: string;
new_version: string;
affected_profiles: string[];
is_stable_update: boolean;
timestamp: number;
}
export function useUpdateNotifications() {
const [notifications, setNotifications] = useState<UpdateNotification[]>([]);
const [updatingBrowsers, setUpdatingBrowsers] = useState<Set<string>>(
new Set()
);
const [isClient, setIsClient] = useState(false);
// Ensure we're on the client side to prevent hydration mismatches
useEffect(() => {
setIsClient(true);
}, []);
const checkForUpdates = useCallback(async () => {
if (!isClient) return; // Only run on client side
try {
const updates = await invoke<UpdateNotification[]>(
"check_for_browser_updates"
);
setNotifications(updates);
// Show toasts for new notifications - we'll define handleUpdate and handleDismiss separately
// to avoid circular dependencies
} catch (error) {
console.error("Failed to check for updates:", error);
}
}, [isClient]);
const handleUpdate = useCallback(
async (browser: string, newVersion: string) => {
try {
setUpdatingBrowsers((prev) => new Set(prev).add(browser));
const browserDisplayName = getBrowserDisplayName(browser);
// Dismiss all notifications for this browser first
const browserNotifications = notifications.filter(
(n) => n.browser === browser
);
for (const notification of browserNotifications) {
toast.dismiss(notification.id);
await invoke("dismiss_update_notification", {
notificationId: notification.id,
});
}
try {
// Check if browser already exists before downloading
const isDownloaded = await invoke<boolean>("check_browser_exists", {
browserStr: browser,
version: newVersion,
});
if (isDownloaded) {
// Browser already exists, skip download and go straight to profile update
console.log(
`${browserDisplayName} ${newVersion} already exists, skipping download`
);
} else {
// Mark download as auto-update in the backend for toast suppression
await invoke("mark_auto_update_download", {
browser,
version: newVersion,
});
// Download the browser (progress will be handled by use-browser-download hook)
await invoke("download_browser", {
browserStr: browser,
version: newVersion,
});
}
// Complete the update with auto-update of profile versions
const updatedProfiles = await invoke<string[]>(
"complete_browser_update_with_auto_update",
{
browser,
newVersion,
}
);
// Show success message based on whether profiles were updated
if (updatedProfiles.length > 0) {
const profileText =
updatedProfiles.length === 1
? `Profile "${updatedProfiles[0]}" has been updated`
: `${updatedProfiles.length} profiles have been updated`;
showToast({
type: "success",
title: `${browserDisplayName} update completed`,
description: `${profileText} to version ${newVersion}. Running profiles were not updated and can be updated manually.`,
duration: 5000,
});
} else {
showToast({
type: "success",
title: `${browserDisplayName} update ready`,
description:
"All affected profiles are currently running. Stop them and manually update their versions to use the new version.",
duration: 5000,
});
}
} catch (downloadError) {
console.error("Failed to download browser:", downloadError);
// Clean up auto-update tracking on error
try {
await invoke("remove_auto_update_download", {
browser,
version: newVersion,
});
} catch (e) {
console.error("Failed to clean up auto-update tracking:", e);
}
showToast({
type: "error",
title: `Failed to download ${browserDisplayName} ${newVersion}`,
description: String(downloadError),
duration: 6000,
});
throw downloadError;
}
// Refresh notifications to clear any remaining ones
await checkForUpdates();
} catch (error) {
console.error("Failed to start update:", error);
const browserDisplayName = getBrowserDisplayName(browser);
showToast({
type: "error",
title: `Failed to update ${browserDisplayName}`,
description: String(error),
duration: 6000,
});
} finally {
setUpdatingBrowsers((prev) => {
const next = new Set(prev);
next.delete(browser);
return next;
});
}
},
[notifications, checkForUpdates]
);
const handleDismiss = useCallback(
async (notificationId: string) => {
if (!isClient) return; // Only run on client side
try {
toast.dismiss(notificationId);
await invoke("dismiss_update_notification", { notificationId });
await checkForUpdates();
} catch (error) {
console.error("Failed to dismiss notification:", error);
}
},
[checkForUpdates, isClient]
);
// Separate effect to show toasts when notifications change
useEffect(() => {
if (!isClient) return;
for (const notification of notifications) {
const isUpdating = updatingBrowsers.has(notification.browser);
toast.custom(
() => (
<UpdateNotificationComponent
notification={notification}
onUpdate={handleUpdate}
onDismiss={handleDismiss}
isUpdating={isUpdating}
/>
),
{
id: notification.id,
duration: Number.POSITIVE_INFINITY, // Persistent until user action
position: "top-right",
// Remove transparent styling to fix background issue
style: undefined,
}
);
}
}, [notifications, updatingBrowsers, handleUpdate, handleDismiss, isClient]);
return {
notifications,
checkForUpdates,
isUpdating: (browser: string) => updatingBrowsers.has(browser),
};
}
+15 -15
View File
@@ -1,13 +1,13 @@
import { getBrowserDisplayName } from "@/lib/browser-utils";
import {
dismissToast,
showLoadingToast,
showVersionUpdateToast,
} from "@/lib/toast-utils";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import {
showVersionUpdateToast,
showLoadingToast,
dismissToast,
} from "../components/custom-toast";
import { getBrowserDisplayName } from "@/lib/browser-utils";
interface VersionUpdateProgress {
current_browser: string;
@@ -158,7 +158,7 @@ export function useVersionUpdater() {
).length;
if (failedUpdates > 0) {
toast.warning(`Update completed with some errors`, {
toast.warning("Update completed with some errors", {
description: `${totalNewVersions} new versions found, ${failedUpdates} browsers failed to update`,
duration: 5000,
});
@@ -226,11 +226,11 @@ export function useVersionUpdater() {
if (hours > 0) {
return `${hours}h ${minutes}m`;
} else if (minutes > 0) {
return `${minutes}m`;
} else {
return "< 1m";
}
if (minutes > 0) {
return `${minutes}m`;
}
return "< 1m";
}, []);
const formatLastUpdateTime = useCallback(
@@ -245,11 +245,11 @@ export function useVersionUpdater() {
if (diffHours > 0) {
return `${diffHours}h ${diffMinutes}m ago`;
} else if (diffMinutes > 0) {
return `${diffMinutes}m ago`;
} else {
return "Just now";
}
if (diffMinutes > 0) {
return `${diffMinutes}m ago`;
}
return "Just now";
},
[]
);
+1 -1
View File
@@ -3,8 +3,8 @@
* Centralized helpers for browser name mapping, icons, etc.
*/
import { SiMullvad, SiBrave, SiTorbrowser } from "react-icons/si";
import { FaChrome, FaFirefox } from "react-icons/fa";
import { SiBrave, SiMullvad, SiTorbrowser } from "react-icons/si";
/**
* Map internal browser names to display names
+256
View File
@@ -0,0 +1,256 @@
import { UnifiedToast } from "@/components/custom-toast";
import React from "react";
import { toast as sonnerToast } from "sonner";
// Define toast types locally
export interface BaseToastProps {
id?: string;
title: string;
description?: string;
duration?: number;
}
export interface LoadingToastProps extends BaseToastProps {
type: "loading";
}
export interface SuccessToastProps extends BaseToastProps {
type: "success";
}
export interface ErrorToastProps extends BaseToastProps {
type: "error";
}
export interface DownloadToastProps extends BaseToastProps {
type: "download";
stage?: "downloading" | "extracting" | "verifying" | "completed";
progress?: {
percentage: number;
speed?: string;
eta?: string;
};
}
export interface VersionUpdateToastProps extends BaseToastProps {
type: "version-update";
progress?: {
current: number;
total: number;
found: number;
};
}
export interface FetchingToastProps extends BaseToastProps {
type: "fetching";
browserName?: string;
}
export type ToastProps =
| LoadingToastProps
| SuccessToastProps
| ErrorToastProps
| DownloadToastProps
| VersionUpdateToastProps
| FetchingToastProps;
// Unified toast function
export function showToast(props: ToastProps & { id?: string }) {
const toastId = props.id ?? `toast-${props.type}-${Date.now()}`;
// Improved duration logic - make toasts disappear more quickly
let duration: number;
if (props.duration !== undefined) {
duration = props.duration;
} else {
switch (props.type) {
case "loading":
case "fetching":
duration = 10000; // 10 seconds instead of infinite
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") {
duration = 3000; // Shorter duration for completed downloads
} else {
duration = 8000; // 8 seconds for extracting/verifying
}
break;
case "version-update":
duration = 15000; // 15 seconds instead of infinite
break;
case "success":
duration = 3000; // Shorter success duration
break;
case "error":
duration = 5000; // Reasonable error duration
break;
default:
duration = 4000;
}
}
if (props.type === "success") {
sonnerToast.success(React.createElement(UnifiedToast, props), {
id: toastId,
duration,
style: {
background: "transparent",
border: "none",
boxShadow: "none",
padding: 0,
},
});
} else if (props.type === "error") {
sonnerToast.error(React.createElement(UnifiedToast, props), {
id: toastId,
duration,
style: {
background: "transparent",
border: "none",
boxShadow: "none",
padding: 0,
},
});
} else {
sonnerToast.custom((id) => React.createElement(UnifiedToast, props), {
id: toastId,
duration,
style: {
background: "transparent",
border: "none",
boxShadow: "none",
padding: 0,
},
});
}
return toastId;
}
// Convenience functions for common use cases
export function showLoadingToast(
title: string,
options?: {
id?: string;
description?: string;
duration?: number;
}
) {
return showToast({
type: "loading",
title,
...options,
});
}
export function showDownloadToast(
browserName: string,
version: string,
stage: "downloading" | "extracting" | "verifying" | "completed",
progress?: { percentage: number; speed?: string; eta?: string },
options?: { suppressCompletionToast?: boolean }
) {
const title =
stage === "completed"
? `${browserName} ${version} downloaded successfully!`
: stage === "downloading"
? `Downloading ${browserName} ${version}`
: stage === "extracting"
? `Extracting ${browserName} ${version}`
: `Verifying ${browserName} ${version}`;
// Don't show completion toast if suppressed (for auto-update scenarios)
if (stage === "completed" && options?.suppressCompletionToast) {
dismissToast(`download-${browserName.toLowerCase()}-${version}`);
return;
}
return showToast({
type: "download",
title,
stage,
progress,
id: `download-${browserName.toLowerCase()}-${version}`,
});
}
export function showVersionUpdateToast(
title: string,
options?: {
id?: string;
description?: string;
progress?: {
current: number;
total: number;
found: number;
};
duration?: number;
}
) {
return showToast({
type: "version-update",
title,
...options,
});
}
export function showFetchingToast(
browserName: string,
options?: {
id?: string;
description?: string;
duration?: number;
}
) {
return showToast({
type: "fetching",
title: `Checking for new ${browserName} versions...`,
description:
options?.description ?? "Fetching latest release information...",
browserName,
...options,
});
}
export function showSuccessToast(
title: string,
options?: {
id?: string;
description?: string;
duration?: number;
}
) {
return showToast({
type: "success",
title,
...options,
});
}
export function showErrorToast(
title: string,
options?: {
id?: string;
description?: string;
duration?: number;
}
) {
return showToast({
type: "error",
title,
...options,
});
}
// Generic helper for dismissing toasts
export function dismissToast(id: string) {
sonnerToast.dismiss(id);
}
// Dismiss all toasts
export function dismissAllToasts() {
sonnerToast.dismiss();
}