From 3d3a3b3816c2915aa7edcfc1918823de78782bf4 Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Sun, 22 Jun 2025 06:04:02 +0400 Subject: [PATCH] chore: linting --- biome.json | 18 +- package.json | 2 +- src/app/page.tsx | 231 ++++++++++--------- src/components/app-update-toast.tsx | 5 +- src/components/change-version-dialog.tsx | 38 ++-- src/components/create-profile-dialog.tsx | 99 ++++---- src/components/custom-toast.tsx | 1 - src/components/import-profile-dialog.tsx | 46 ++-- src/components/loading-button.tsx | 1 + src/components/permission-dialog.tsx | 6 +- src/components/profile-data-table.tsx | 24 +- src/components/profile-selector-dialog.tsx | 249 +++++++++++---------- src/components/proxy-settings-dialog.tsx | 2 +- src/components/release-type-selector.tsx | 5 +- src/components/settings-dialog.tsx | 241 ++++++++++---------- src/components/ui/alert.tsx | 2 +- src/components/ui/badge.tsx | 2 +- src/components/ui/button.tsx | 2 +- src/components/window-drag-area.tsx | 5 +- src/hooks/use-app-update-notifications.tsx | 8 +- src/hooks/use-browser-download.ts | 94 ++++---- src/hooks/use-table-sorting.ts | 2 +- src/hooks/use-update-notifications.tsx | 88 ++++---- src/hooks/use-version-updater.ts | 6 +- src/lib/browser-utils.ts | 2 +- src/lib/toast-utils.ts | 2 +- 26 files changed, 607 insertions(+), 574 deletions(-) diff --git a/biome.json b/biome.json index 078b377..cf83ccd 100644 --- a/biome.json +++ b/biome.json @@ -6,17 +6,13 @@ "useIgnoreFile": false }, "files": { - "ignoreUnknown": false, - "ignore": [] + "ignoreUnknown": false }, "formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2 }, - "organizeImports": { - "enabled": true - }, "linter": { "enabled": true, "rules": { @@ -25,17 +21,7 @@ "useHookAtTopLevel": "error" }, "nursery": { - "useGoogleFontDisplay": "error", - "noDocumentImportInPage": "error", - "noHeadElement": "error", - "noHeadImportInDocument": "error", - "noImgElement": "off", - "useComponentExportOnlyModules": { - "level": "error", - "options": { - "allowExportNames": ["metadata", "badgeVariants", "buttonVariants"] - } - } + "useUniqueElementIds": "off" }, "a11y": { "useSemanticElements": "off" diff --git a/package.json b/package.json index 570f8c4..91dbce0 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "shadcn:add": "pnpm dlx shadcn@latest add", "prepare": "husky && husky install", "format:rust": "cd src-tauri && cargo clippy --fix --allow-dirty --all-targets --all-features -- -D warnings -D clippy::all && cargo fmt --all", - "format:js": "biome check src/ --fix", + "format:js": "biome check src/ --write --unsafe", "format": "pnpm format:js && pnpm format:rust", "cargo": "cd src-tauri && cargo", "unused-exports:js": "ts-unused-exports tsconfig.json", diff --git a/src/app/page.tsx b/src/app/page.tsx index 926a421..ef4d7d9 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,11 @@ "use client"; +import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; +import { getCurrent } from "@tauri-apps/plugin-deep-link"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { FaDownload } from "react-icons/fa"; +import { GoGear, GoKebabHorizontal, GoPlus } from "react-icons/go"; import { ChangeVersionDialog } from "@/components/change-version-dialog"; import { CreateProfileDialog } from "@/components/create-profile-dialog"; import { ImportProfileDialog } from "@/components/import-profile-dialog"; @@ -22,18 +28,12 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications"; -import { usePermissions } from "@/hooks/use-permissions"; import type { PermissionType } from "@/hooks/use-permissions"; +import { usePermissions } from "@/hooks/use-permissions"; import { useUpdateNotifications } from "@/hooks/use-update-notifications"; import { useVersionUpdater } from "@/hooks/use-version-updater"; import { showErrorToast } 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 { getCurrent } from "@tauri-apps/plugin-deep-link"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { FaDownload } from "react-icons/fa"; -import { GoGear, GoKebabHorizontal, GoPlus } from "react-icons/go"; type BrowserTypeString = | "mullvad-browser" @@ -69,22 +69,6 @@ export default function Home() { const { isMicrophoneAccessGranted, isCameraAccessGranted, isInitialized } = usePermissions(); - // Simple profiles loader without updates check (for use as callback) - const loadProfiles = useCallback(async () => { - try { - const profileList = await invoke( - "list_browser_profiles", - ); - setProfiles(profileList); - - // Check for missing binaries after loading profiles - await checkMissingBinaries(); - } catch (err: unknown) { - console.error("Failed to load profiles:", err); - setError(`Failed to load profiles: ${JSON.stringify(err)}`); - } - }, []); - // Check for missing binaries and offer to download them const checkMissingBinaries = useCallback(async () => { try { @@ -129,6 +113,42 @@ export default function Home() { } }, []); + // Simple profiles loader without updates check (for use as callback) + const loadProfiles = useCallback(async () => { + try { + const profileList = await invoke( + "list_browser_profiles", + ); + setProfiles(profileList); + + // Check for missing binaries after loading profiles + await checkMissingBinaries(); + } catch (err: unknown) { + console.error("Failed to load profiles:", err); + setError(`Failed to load profiles: ${JSON.stringify(err)}`); + } + }, [checkMissingBinaries]); + + const handleUrlOpen = useCallback(async (url: string) => { + try { + // Use smart profile selection + const result = await invoke("smart_open_url", { + url, + }); + console.log("Smart URL opening succeeded:", result); + // URL was handled successfully, no need to show selector + } catch (error: unknown) { + console.log( + "Smart URL opening failed or requires profile selection:", + error, + ); + + // Show profile selector for manual selection + // Replace any existing pending URL with the new one + setPendingUrls([{ id: Date.now().toString(), url }]); + } + }, []); + // Version updater for handling version fetching progress events and auto-updates useVersionUpdater(); @@ -165,42 +185,9 @@ export default function Home() { } catch (error) { console.error("Failed to check current URL:", error); } - }, []); + }, [handleUrlOpen]); - useEffect(() => { - void loadProfilesWithUpdateCheck(); - - // Check for startup default browser prompt - void checkStartupPrompt(); - - // Listen for URL open events - void listenForUrlEvents(); - - // Check for startup URLs (when app was launched as default browser) - void checkStartupUrls(); - void checkCurrentUrl(); - - // Set up periodic update checks (every 30 minutes) - const updateInterval = setInterval( - () => { - void checkForUpdates(); - }, - 30 * 60 * 1000, - ); - - return () => { - clearInterval(updateInterval); - }; - }, [loadProfilesWithUpdateCheck, checkForUpdates, checkCurrentUrl]); - - // Check permissions when they are initialized - useEffect(() => { - if (isInitialized) { - void checkAllPermissions(); - } - }, [isInitialized]); - - const checkStartupPrompt = async () => { + const checkStartupPrompt = useCallback(async () => { // Only check once during app startup to prevent reopening after dismissing notifications if (hasCheckedStartupPrompt) return; @@ -216,9 +203,9 @@ export default function Home() { console.error("Failed to check startup prompt:", error); setHasCheckedStartupPrompt(true); } - }; + }, [hasCheckedStartupPrompt]); - const checkAllPermissions = async () => { + const checkAllPermissions = useCallback(async () => { try { // Wait for permissions to be initialized before checking if (!isInitialized) { @@ -236,9 +223,9 @@ export default function Home() { } catch (error) { console.error("Failed to check permissions:", error); } - }; + }, [isMicrophoneAccessGranted, isCameraAccessGranted, isInitialized]); - const checkNextPermission = () => { + const checkNextPermission = useCallback(() => { try { if (!isMicrophoneAccessGranted) { setCurrentPermissionType("microphone"); @@ -252,9 +239,9 @@ export default function Home() { } catch (error) { console.error("Failed to check next permission:", error); } - }; + }, [isMicrophoneAccessGranted, isCameraAccessGranted]); - const checkStartupUrls = async () => { + const checkStartupUrls = useCallback(async () => { try { const hasStartupUrl = await invoke( "check_and_handle_startup_url", @@ -265,9 +252,9 @@ export default function Home() { } catch (error) { console.error("Failed to check startup URLs:", error); } - }; + }, []); - const listenForUrlEvents = async () => { + const listenForUrlEvents = useCallback(async () => { try { // Listen for URL open events from the deep link handler (when app is already running) await listen("url-open-request", (event) => { @@ -295,27 +282,7 @@ export default function Home() { } catch (error) { console.error("Failed to setup URL listener:", error); } - }; - - const handleUrlOpen = async (url: string) => { - try { - // Use smart profile selection - const result = await invoke("smart_open_url", { - url, - }); - console.log("Smart URL opening succeeded:", result); - // URL was handled successfully, no need to show selector - } catch (error: unknown) { - console.log( - "Smart URL opening failed or requires profile selection:", - error, - ); - - // Show profile selector for manual selection - // Replace any existing pending URL with the new one - setPendingUrls([{ id: Date.now().toString(), url }]); - } - }; + }, [handleUrlOpen]); const openProxyDialog = useCallback((profile: BrowserProfile | null) => { setCurrentProfileForProxy(profile); @@ -459,31 +426,6 @@ export default function Home() { [loadProfiles, checkBrowserStatus, isUpdating], ); - useEffect(() => { - if (profiles.length === 0) return; - - const interval = setInterval(() => { - for (const profile of profiles) { - void checkBrowserStatus(profile); - } - }, 500); - - return () => { - clearInterval(interval); - }; - }, [profiles, checkBrowserStatus]); - - useEffect(() => { - runningProfilesRef.current = runningProfiles; - }, [runningProfiles]); - - useEffect(() => { - if (error) { - showErrorToast(error); - setError(null); - } - }, [error]); - const handleDeleteProfile = useCallback( async (profile: BrowserProfile) => { setError(null); @@ -551,6 +493,71 @@ export default function Home() { [loadProfiles], ); + useEffect(() => { + void loadProfilesWithUpdateCheck(); + + // Check for startup default browser prompt + void checkStartupPrompt(); + + // Listen for URL open events + void listenForUrlEvents(); + + // Check for startup URLs (when app was launched as default browser) + void checkStartupUrls(); + void checkCurrentUrl(); + + // Set up periodic update checks (every 30 minutes) + const updateInterval = setInterval( + () => { + void checkForUpdates(); + }, + 30 * 60 * 1000, + ); + + return () => { + clearInterval(updateInterval); + }; + }, [ + loadProfilesWithUpdateCheck, + checkForUpdates, + checkCurrentUrl, + checkStartupPrompt, + listenForUrlEvents, + checkStartupUrls, + ]); + + useEffect(() => { + if (profiles.length === 0) return; + + const interval = setInterval(() => { + for (const profile of profiles) { + void checkBrowserStatus(profile); + } + }, 500); + + return () => { + clearInterval(interval); + }; + }, [profiles, checkBrowserStatus]); + + useEffect(() => { + runningProfilesRef.current = runningProfiles; + }, [runningProfiles]); + + useEffect(() => { + if (error) { + showErrorToast(error); + setError(null); + } + }, [error]); + + // Check permissions when they are initialized + useEffect(() => { + if (isInitialized) { + void checkAllPermissions(); + } + }, [isInitialized, checkAllPermissions]); + return (
diff --git a/src/components/app-update-toast.tsx b/src/components/app-update-toast.tsx index 9f2ddcb..958e31f 100644 --- a/src/components/app-update-toast.tsx +++ b/src/components/app-update-toast.tsx @@ -1,10 +1,9 @@ "use client"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import React from "react"; import { FaDownload, FaTimes } from "react-icons/fa"; import { LuRefreshCw } from "react-icons/lu"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; interface AppUpdateInfo { current_version: string; diff --git a/src/components/change-version-dialog.tsx b/src/components/change-version-dialog.tsx index f633a93..ddf6ee8 100644 --- a/src/components/change-version-dialog.tsx +++ b/src/components/change-version-dialog.tsx @@ -1,5 +1,8 @@ "use client"; +import { invoke } from "@tauri-apps/api/core"; +import { useCallback, useEffect, useState } from "react"; +import { LuTriangleAlert } from "react-icons/lu"; import { LoadingButton } from "@/components/loading-button"; import { ReleaseTypeSelector } from "@/components/release-type-selector"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; @@ -16,9 +19,6 @@ import { Label } from "@/components/ui/label"; import { useBrowserDownload } from "@/hooks/use-browser-download"; import { getBrowserDisplayName } from "@/lib/browser-utils"; import type { BrowserProfile, BrowserReleaseTypes } from "@/types"; -import { invoke } from "@tauri-apps/api/core"; -import { useEffect, useState } from "react"; -import { LuTriangleAlert } from "react-icons/lu"; interface ChangeVersionDialogProps { isOpen: boolean; @@ -50,17 +50,7 @@ export function ChangeVersionDialog({ isVersionDownloaded, } = useBrowserDownload(); - useEffect(() => { - if (isOpen && profile) { - // Set current release type based on profile - setSelectedReleaseType(profile.release_type as "stable" | "nightly"); - setAcknowledgeDowngrade(false); - void loadReleaseTypes(profile.browser); - void loadDownloadedVersions(profile.browser); - } - }, [isOpen, profile, loadDownloadedVersions]); - - const loadReleaseTypes = async (browser: string) => { + const loadReleaseTypes = useCallback(async (browser: string) => { setIsLoadingReleaseTypes(true); try { const releaseTypes = await invoke( @@ -73,7 +63,7 @@ export function ChangeVersionDialog({ } finally { setIsLoadingReleaseTypes(false); } - }; + }, []); useEffect(() => { if ( @@ -93,7 +83,7 @@ export function ChangeVersionDialog({ } }, [selectedReleaseType, profile]); - const handleDownload = async () => { + const handleDownload = useCallback(async () => { if (!profile || !selectedReleaseType) return; const version = @@ -103,9 +93,9 @@ export function ChangeVersionDialog({ if (!version) return; await downloadBrowser(profile.browser, version); - }; + }, [profile, selectedReleaseType, downloadBrowser, releaseTypes]); - const handleVersionChange = async () => { + const handleVersionChange = useCallback(async () => { if (!profile || !selectedReleaseType) return; const version = @@ -127,7 +117,7 @@ export function ChangeVersionDialog({ } finally { setIsUpdating(false); } - }; + }, [profile, selectedReleaseType, releaseTypes, onVersionChanged, onClose]); const selectedVersion = selectedReleaseType === "stable" @@ -142,6 +132,16 @@ export function ChangeVersionDialog({ isVersionDownloaded(selectedVersion) && (!showDowngradeWarning || acknowledgeDowngrade); + useEffect(() => { + if (isOpen && profile) { + // Set current release type based on profile + setSelectedReleaseType(profile.release_type as "stable" | "nightly"); + setAcknowledgeDowngrade(false); + void loadReleaseTypes(profile.browser); + void loadDownloadedVersions(profile.browser); + } + }, [isOpen, profile, loadDownloadedVersions, loadReleaseTypes]); + if (!profile) return null; return ( diff --git a/src/components/create-profile-dialog.tsx b/src/components/create-profile-dialog.tsx index da27c15..f170e5c 100644 --- a/src/components/create-profile-dialog.tsx +++ b/src/components/create-profile-dialog.tsx @@ -1,5 +1,8 @@ "use client"; +import { invoke } from "@tauri-apps/api/core"; +import { useCallback, useEffect, useState } from "react"; +import { toast } from "sonner"; import { LoadingButton } from "@/components/loading-button"; import { ReleaseTypeSelector } from "@/components/release-type-selector"; import { Button } from "@/components/ui/button"; @@ -33,9 +36,6 @@ import type { BrowserReleaseTypes, ProxySettings, } from "@/types"; -import { invoke } from "@tauri-apps/api/core"; -import { useEffect, useState } from "react"; -import { toast } from "sonner"; import { Alert, AlertDescription } from "./ui/alert"; type BrowserTypeString = @@ -102,12 +102,6 @@ export function CreateProfileDialog({ isBrowserSupported, } = useBrowserSupport(); - useEffect(() => { - if (isOpen) { - void loadExistingProfiles(); - } - }, [isOpen]); - useEffect(() => { if (supportedBrowsers.length > 0) { // Set default browser to first supported browser @@ -119,15 +113,6 @@ export function CreateProfileDialog({ } }, [supportedBrowsers]); - useEffect(() => { - if (isOpen && selectedBrowser) { - // Reset selected release type when browser changes - setSelectedReleaseType(null); - void loadReleaseTypes(selectedBrowser); - void loadDownloadedVersions(selectedBrowser); - } - }, [isOpen, selectedBrowser, loadDownloadedVersions]); - // Set default release type when release types are loaded useEffect(() => { if (!selectedReleaseType && Object.keys(releaseTypes).length > 0) { @@ -142,16 +127,16 @@ export function CreateProfileDialog({ } }, [releaseTypes, selectedReleaseType, selectedBrowser]); - const loadExistingProfiles = async () => { + const loadExistingProfiles = useCallback(async () => { try { const profiles = await invoke("list_browser_profiles"); setExistingProfiles(profiles); } catch (error) { console.error("Failed to load existing profiles:", error); } - }; + }, []); - const loadReleaseTypes = async (browser: string) => { + const loadReleaseTypes = useCallback(async (browser: string) => { try { setIsLoadingReleaseTypes(true); const types = await invoke( @@ -167,9 +152,9 @@ export function CreateProfileDialog({ } finally { setIsLoadingReleaseTypes(false); } - }; + }, []); - const handleDownload = async () => { + const handleDownload = useCallback(async () => { if (!selectedBrowser || !selectedReleaseType) return; const version = @@ -179,26 +164,29 @@ export function CreateProfileDialog({ if (!version) return; await downloadBrowser(selectedBrowser, version); - }; + }, [selectedBrowser, selectedReleaseType, downloadBrowser, releaseTypes]); - const validateProfileName = (name: string): string | null => { - const trimmedName = name.trim(); + const validateProfileName = useCallback( + (name: string): string | null => { + const trimmedName = name.trim(); - if (!trimmedName) { - return "Profile name cannot be empty"; - } + if (!trimmedName) { + return "Profile name cannot be empty"; + } - // Check for duplicate names (case insensitive) - const isDuplicate = existingProfiles.some( - (profile) => profile.name.toLowerCase() === trimmedName.toLowerCase(), - ); + // Check for duplicate names (case insensitive) + const isDuplicate = existingProfiles.some( + (profile) => profile.name.toLowerCase() === trimmedName.toLowerCase(), + ); - if (isDuplicate) { - return "A profile with this name already exists"; - } + if (isDuplicate) { + return "A profile with this name already exists"; + } - return null; - }; + return null; + }, + [existingProfiles], + ); // Helper to determine if proxy should be disabled for the selected browser const isProxyDisabled = selectedBrowser === "tor-browser"; @@ -210,7 +198,7 @@ export function CreateProfileDialog({ } }, [selectedBrowser, proxyEnabled]); - const handleCreate = async () => { + const handleCreate = useCallback(async () => { if (!profileName.trim() || !selectedBrowser || !selectedReleaseType) return; // Validate profile name @@ -265,7 +253,23 @@ export function CreateProfileDialog({ } finally { setIsCreating(false); } - }; + }, [ + profileName, + selectedBrowser, + selectedReleaseType, + onCreateProfile, + proxyEnabled, + isProxyDisabled, + onClose, + proxyHost, + proxyPassword, + proxyPort, + proxyType, + proxyUsername, + releaseTypes.nightly, + releaseTypes.stable, + validateProfileName, + ]); const nameError = profileName.trim() ? validateProfileName(profileName) @@ -285,6 +289,21 @@ export function CreateProfileDialog({ (!proxyEnabled || isProxyDisabled || (proxyHost && proxyPort)) && !nameError; + useEffect(() => { + if (isOpen) { + void loadExistingProfiles(); + } + }, [isOpen, loadExistingProfiles]); + + useEffect(() => { + if (isOpen && selectedBrowser) { + // Reset selected release type when browser changes + setSelectedReleaseType(null); + void loadReleaseTypes(selectedBrowser); + void loadDownloadedVersions(selectedBrowser); + } + }, [isOpen, selectedBrowser, loadDownloadedVersions, loadReleaseTypes]); + return ( diff --git a/src/components/custom-toast.tsx b/src/components/custom-toast.tsx index 99bbdaa..03d202d 100644 --- a/src/components/custom-toast.tsx +++ b/src/components/custom-toast.tsx @@ -48,7 +48,6 @@ * ``` */ -import React from "react"; import { LuCheckCheck, LuDownload, diff --git a/src/components/import-profile-dialog.tsx b/src/components/import-profile-dialog.tsx index ee7cae0..cf81f71 100644 --- a/src/components/import-profile-dialog.tsx +++ b/src/components/import-profile-dialog.tsx @@ -1,5 +1,10 @@ "use client"; +import { invoke } from "@tauri-apps/api/core"; +import { open } from "@tauri-apps/plugin-dialog"; +import { useCallback, useEffect, useState } from "react"; +import { FaFolder } from "react-icons/fa"; +import { toast } from "sonner"; import { LoadingButton } from "@/components/loading-button"; import { Button } from "@/components/ui/button"; import { @@ -21,11 +26,6 @@ import { import { useBrowserSupport } from "@/hooks/use-browser-support"; import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils"; import type { DetectedProfile } from "@/types"; -import { invoke } from "@tauri-apps/api/core"; -import { open } from "@tauri-apps/plugin-dialog"; -import { useEffect, useState } from "react"; -import { FaFolder } from "react-icons/fa"; -import { toast } from "sonner"; interface ImportProfileDialogProps { isOpen: boolean; @@ -63,13 +63,7 @@ export function ImportProfileDialog({ const { supportedBrowsers, isLoading: isLoadingSupport } = useBrowserSupport(); - useEffect(() => { - if (isOpen) { - void loadDetectedProfiles(); - } - }, [isOpen]); - - const loadDetectedProfiles = async () => { + const loadDetectedProfiles = useCallback(async () => { setIsLoading(true); try { const profiles = await invoke( @@ -96,7 +90,7 @@ export function ImportProfileDialog({ } finally { setIsLoading(false); } - }; + }, []); const handleBrowseFolder = async () => { try { @@ -115,7 +109,7 @@ export function ImportProfileDialog({ } }; - const handleAutoDetectImport = async () => { + const handleAutoDetectImport = useCallback(async () => { if (!selectedDetectedProfile || !autoDetectProfileName.trim()) { toast.error("Please select a profile and provide a name"); return; @@ -152,9 +146,15 @@ export function ImportProfileDialog({ } finally { setIsImporting(false); } - }; + }, [ + selectedDetectedProfile, + autoDetectProfileName, + detectedProfiles, + onImportComplete, + onClose, + ]); - const handleManualImport = async () => { + const handleManualImport = useCallback(async () => { if ( !manualBrowserType || !manualProfilePath.trim() || @@ -187,7 +187,13 @@ export function ImportProfileDialog({ } finally { setIsImporting(false); } - }; + }, [ + manualBrowserType, + manualProfilePath, + manualProfileName, + onImportComplete, + onClose, + ]); const handleClose = () => { setSelectedDetectedProfile(null); @@ -222,6 +228,12 @@ export function ImportProfileDialog({ (p) => p.path === selectedDetectedProfile, ); + useEffect(() => { + if (isOpen) { + void loadDetectedProfiles(); + } + }, [isOpen, loadDetectedProfiles]); + return ( diff --git a/src/components/loading-button.tsx b/src/components/loading-button.tsx index 2cbf71d..78f169b 100644 --- a/src/components/loading-button.tsx +++ b/src/components/loading-button.tsx @@ -1,5 +1,6 @@ import { LuLoaderCircle } from "react-icons/lu"; import { type ButtonProps, Button as UIButton } from "./ui/button"; + type Props = ButtonProps & { isLoading: boolean; "aria-label"?: string; diff --git a/src/components/permission-dialog.tsx b/src/components/permission-dialog.tsx index 225f829..23e825e 100644 --- a/src/components/permission-dialog.tsx +++ b/src/components/permission-dialog.tsx @@ -1,5 +1,7 @@ "use client"; +import { useEffect, useState } from "react"; +import { BsCamera, BsMic } from "react-icons/bs"; import { LoadingButton } from "@/components/loading-button"; import { Button } from "@/components/ui/button"; import { @@ -10,11 +12,9 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { usePermissions } from "@/hooks/use-permissions"; import type { PermissionType } from "@/hooks/use-permissions"; +import { usePermissions } from "@/hooks/use-permissions"; import { showErrorToast, showSuccessToast } from "@/lib/toast-utils"; -import { useEffect, useState } from "react"; -import { BsCamera, BsMic } from "react-icons/bs"; interface PermissionDialogProps { isOpen: boolean; diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx index f1a54fb..9dd684d 100644 --- a/src/components/profile-data-table.tsx +++ b/src/components/profile-data-table.tsx @@ -1,5 +1,17 @@ "use client"; +import { + type ColumnDef, + flexRender, + getCoreRowModel, + getSortedRowModel, + type SortingState, + useReactTable, +} from "@tanstack/react-table"; +import * as React from "react"; +import { CiCircleCheck } from "react-icons/ci"; +import { IoEllipsisHorizontal } from "react-icons/io5"; +import { LuChevronDown, LuChevronUp } from "react-icons/lu"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -33,18 +45,6 @@ import { import { useTableSorting } from "@/hooks/use-table-sorting"; import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils"; import type { BrowserProfile } from "@/types"; -import { - type ColumnDef, - type SortingState, - flexRender, - getCoreRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; -import * as React from "react"; -import { CiCircleCheck } from "react-icons/ci"; -import { IoEllipsisHorizontal } from "react-icons/io5"; -import { LuChevronDown, LuChevronUp } from "react-icons/lu"; import { Input } from "./ui/input"; import { Label } from "./ui/label"; diff --git a/src/components/profile-selector-dialog.tsx b/src/components/profile-selector-dialog.tsx index a97e3fb..6c4b2ae 100644 --- a/src/components/profile-selector-dialog.tsx +++ b/src/components/profile-selector-dialog.tsx @@ -1,5 +1,9 @@ "use client"; +import { invoke } from "@tauri-apps/api/core"; +import { useCallback, useEffect, useState } from "react"; +import { LuCopy } from "react-icons/lu"; +import { toast } from "sonner"; import { LoadingButton } from "@/components/loading-button"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -25,10 +29,6 @@ import { } from "@/components/ui/tooltip"; import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils"; import type { BrowserProfile } from "@/types"; -import { invoke } from "@tauri-apps/api/core"; -import { useEffect, useState } from "react"; -import { LuCopy } from "react-icons/lu"; -import { toast } from "sonner"; interface ProfileSelectorDialogProps { isOpen: boolean; @@ -48,13 +48,42 @@ export function ProfileSelectorDialog({ const [isLoading, setIsLoading] = useState(false); const [isLaunching, setIsLaunching] = useState(false); - useEffect(() => { - if (isOpen) { - void loadProfiles(); - } - }, [isOpen]); + // Helper function to determine if a profile can be used for opening links + const canUseProfileForLinks = useCallback( + ( + profile: BrowserProfile, + allProfiles: BrowserProfile[], + runningProfiles: Set, + ): boolean => { + const isRunning = runningProfiles.has(profile.name); - const loadProfiles = async () => { + // For TOR browser: Check if any TOR browser is running + if (profile.browser === "tor-browser") { + const runningTorProfiles = allProfiles.filter( + (p) => p.browser === "tor-browser" && runningProfiles.has(p.name), + ); + + // If no TOR browser is running, allow any TOR profile + if (runningTorProfiles.length === 0) { + return true; + } + + // If TOR browser(s) are running, only allow the running one(s) + return isRunning; + } + + // For Mullvad browser: never allow if running + if (profile.browser === "mullvad-browser" && isRunning) { + return false; + } + + // For other browsers: always allow + return true; + }, + [], + ); + + const loadProfiles = useCallback(async () => { setIsLoading(true); try { const profileList = await invoke( @@ -99,39 +128,7 @@ export function ProfileSelectorDialog({ } finally { setIsLoading(false); } - }; - - // Helper function to determine if a profile can be used for opening links - const canUseProfileForLinks = ( - profile: BrowserProfile, - allProfiles: BrowserProfile[], - runningProfiles: Set, - ): boolean => { - const isRunning = runningProfiles.has(profile.name); - - // For TOR browser: Check if any TOR browser is running - if (profile.browser === "tor-browser") { - const runningTorProfiles = allProfiles.filter( - (p) => p.browser === "tor-browser" && runningProfiles.has(p.name), - ); - - // If no TOR browser is running, allow any TOR profile - if (runningTorProfiles.length === 0) { - return true; - } - - // If TOR browser(s) are running, only allow the running one(s) - return isRunning; - } - - // For Mullvad browser: never allow if running - if (profile.browser === "mullvad-browser" && isRunning) { - return false; - } - - // For other browsers: always allow - return true; - }; + }, [runningProfiles, canUseProfileForLinks]); // Helper function to get tooltip content for profiles const getProfileTooltipContent = (profile: BrowserProfile): string => { @@ -156,7 +153,7 @@ export function ProfileSelectorDialog({ return ""; }; - const handleOpenUrl = async () => { + const handleOpenUrl = useCallback(async () => { if (!selectedProfile || !url) return; setIsLaunching(true); @@ -171,14 +168,14 @@ export function ProfileSelectorDialog({ } finally { setIsLaunching(false); } - }; + }, [selectedProfile, url, onClose]); - const handleCancel = () => { + const handleCancel = useCallback(() => { setSelectedProfile(null); onClose(); - }; + }, [onClose]); - const handleCopyUrl = async () => { + const handleCopyUrl = useCallback(async () => { if (!url) return; try { @@ -188,7 +185,7 @@ export function ProfileSelectorDialog({ console.error("Failed to copy URL:", error); toast.error("Failed to copy URL to clipboard"); } - }; + }, [url]); const selectedProfileData = profiles.find((p) => p.name === selectedProfile); @@ -208,6 +205,12 @@ export function ProfileSelectorDialog({ return getProfileTooltipContent(selectedProfileData); }; + useEffect(() => { + if (isOpen) { + void loadProfiles(); + } + }, [isOpen, loadProfiles]); + return ( @@ -253,86 +256,84 @@ export function ProfileSelectorDialog({
) : ( - <> - + + + + + {profiles.map((profile) => { + const isRunning = runningProfiles.has(profile.name); + const canUseForLinks = canUseProfileForLinks( + profile, + profiles, + runningProfiles, + ); + const tooltipContent = getProfileTooltipContent(profile); - return ( - - - + + +
-
-
-
- {(() => { - const IconComponent = getBrowserIcon( - profile.browser, - ); - return IconComponent ? ( - - ) : null; - })()} -
-
-
- {profile.name} -
+
+
+ {(() => { + const IconComponent = getBrowserIcon( + profile.browser, + ); + return IconComponent ? ( + + ) : null; + })()} +
+
+
+ {profile.name}
- - {getBrowserDisplayName(profile.browser)} - - {profile.proxy?.enabled && ( - - Proxy - - )} - {isRunning && ( - - Running - - )} - {!canUseForLinks && ( - - Unavailable - - )}
- - - {tooltipContent && ( - {tooltipContent} - )} - - ); - })} - - - + + {getBrowserDisplayName(profile.browser)} + + {profile.proxy?.enabled && ( + + Proxy + + )} + {isRunning && ( + + Running + + )} + {!canUseForLinks && ( + + Unavailable + + )} +
+ + + {tooltipContent && ( + {tooltipContent} + )} + + ); + })} + + )}
diff --git a/src/components/proxy-settings-dialog.tsx b/src/components/proxy-settings-dialog.tsx index 7a0cd0a..a495723 100644 --- a/src/components/proxy-settings-dialog.tsx +++ b/src/components/proxy-settings-dialog.tsx @@ -1,5 +1,6 @@ "use client"; +import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { @@ -23,7 +24,6 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; -import { useEffect, useState } from "react"; interface ProxySettings { enabled: boolean; diff --git a/src/components/release-type-selector.tsx b/src/components/release-type-selector.tsx index 56990ed..22bad43 100644 --- a/src/components/release-type-selector.tsx +++ b/src/components/release-type-selector.tsx @@ -1,5 +1,7 @@ "use client"; +import { useState } from "react"; +import { LuCheck, LuChevronsUpDown, LuDownload } from "react-icons/lu"; import { LoadingButton } from "@/components/loading-button"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -17,9 +19,6 @@ import { } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; import type { BrowserReleaseTypes } from "@/types"; -import { useState } from "react"; -import { LuDownload } from "react-icons/lu"; -import { LuCheck, LuChevronsUpDown } from "react-icons/lu"; interface ReleaseTypeSelectorProps { selectedReleaseType: "stable" | "nightly" | null; diff --git a/src/components/settings-dialog.tsx b/src/components/settings-dialog.tsx index 32d5c39..d863c78 100644 --- a/src/components/settings-dialog.tsx +++ b/src/components/settings-dialog.tsx @@ -1,5 +1,9 @@ "use client"; +import { invoke } from "@tauri-apps/api/core"; +import { useTheme } from "next-themes"; +import { useCallback, useEffect, useState } from "react"; +import { BsCamera, BsMic } from "react-icons/bs"; import { LoadingButton } from "@/components/loading-button"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -19,13 +23,9 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { usePermissions } from "@/hooks/use-permissions"; import type { PermissionType } from "@/hooks/use-permissions"; +import { usePermissions } from "@/hooks/use-permissions"; import { showErrorToast, showSuccessToast } from "@/lib/toast-utils"; -import { invoke } from "@tauri-apps/api/core"; -import { useTheme } from "next-themes"; -import { useCallback, useEffect, useState } from "react"; -import { BsCamera, BsMic } from "react-icons/bs"; interface AppSettings { set_as_default_browser: boolean; @@ -73,6 +73,35 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) { isCameraAccessGranted, } = usePermissions(); + const getPermissionIcon = useCallback((type: PermissionType) => { + switch (type) { + case "microphone": + return ; + case "camera": + return ; + } + }, []); + + const getPermissionDisplayName = useCallback((type: PermissionType) => { + switch (type) { + case "microphone": + return "Microphone"; + case "camera": + return "Camera"; + } + }, []); + + const getStatusBadge = useCallback((isGranted: boolean) => { + if (isGranted) { + return ( + + Granted + + ); + } + return Not Granted; + }, []); + const getPermissionDescription = useCallback((type: PermissionType) => { switch (type) { case "microphone": @@ -81,60 +110,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) { return "Access to camera for browser applications"; } }, []); - - useEffect(() => { - if (isOpen) { - loadSettings().catch(console.error); - checkDefaultBrowserStatus().catch(console.error); - - // Check if we're on macOS - const userAgent = navigator.userAgent; - const isMac = userAgent.includes("Mac"); - setIsMacOS(isMac); - - if (isMac) { - loadPermissions().catch(console.error); - } - - // Set up interval to check default browser status - const intervalId = setInterval(() => { - checkDefaultBrowserStatus().catch(console.error); - }, 500); // Check every 500ms - - // Cleanup interval on component unmount or dialog close - return () => { - clearInterval(intervalId); - }; - } - }, [isOpen]); - - // Update permissions when the permission states change - useEffect(() => { - if (isMacOS) { - const permissionList: PermissionInfo[] = [ - { - permission_type: "microphone", - isGranted: isMicrophoneAccessGranted, - description: getPermissionDescription("microphone"), - }, - { - permission_type: "camera", - isGranted: isCameraAccessGranted, - description: getPermissionDescription("camera"), - }, - ]; - setPermissions(permissionList); - } else { - setPermissions([]); - } - }, [ - isMacOS, - isMicrophoneAccessGranted, - isCameraAccessGranted, - getPermissionDescription, - ]); - - const loadSettings = async () => { + const loadSettings = useCallback(async () => { setIsLoading(true); try { const appSettings = await invoke("get_app_settings"); @@ -145,9 +121,9 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) { } finally { setIsLoading(false); } - }; + }, []); - const loadPermissions = async () => { + const loadPermissions = useCallback(async () => { setIsLoadingPermissions(true); try { if (!isMacOS) { @@ -175,18 +151,23 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) { } finally { setIsLoadingPermissions(false); } - }; + }, [ + getPermissionDescription, + isCameraAccessGranted, + isMacOS, + isMicrophoneAccessGranted, + ]); - const checkDefaultBrowserStatus = async () => { + const checkDefaultBrowserStatus = useCallback(async () => { try { const isDefault = await invoke("is_default_browser"); setIsDefaultBrowser(isDefault); } catch (error) { console.error("Failed to check default browser status:", error); } - }; + }, []); - const handleSetDefaultBrowser = async () => { + const handleSetDefaultBrowser = useCallback(async () => { setIsSettingDefault(true); try { await invoke("set_as_default_browser"); @@ -196,9 +177,9 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) { } finally { setIsSettingDefault(false); } - }; + }, [checkDefaultBrowserStatus]); - const handleClearCache = async () => { + const handleClearCache = useCallback(async () => { setIsClearingCache(true); try { await invoke("clear_all_version_cache_and_refetch"); @@ -217,52 +198,25 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) { } finally { setIsClearingCache(false); } - }; + }, []); - const handleRequestPermission = async (permissionType: PermissionType) => { - setRequestingPermission(permissionType); - try { - await requestPermission(permissionType); - showSuccessToast( - `${getPermissionDisplayName(permissionType)} access requested`, - ); - } catch (error) { - console.error("Failed to request permission:", error); - } finally { - setRequestingPermission(null); - } - }; - - const getPermissionIcon = (type: PermissionType) => { - switch (type) { - case "microphone": - return ; - case "camera": - return ; - } - }; - - const getPermissionDisplayName = (type: PermissionType) => { - switch (type) { - case "microphone": - return "Microphone"; - case "camera": - return "Camera"; - } - }; - - const getStatusBadge = (isGranted: boolean) => { - if (isGranted) { - return ( - - Granted - - ); - } - return Not Granted; - }; - - const handleSave = async () => { + const handleRequestPermission = useCallback( + async (permissionType: PermissionType) => { + setRequestingPermission(permissionType); + try { + await requestPermission(permissionType); + showSuccessToast( + `${getPermissionDisplayName(permissionType)} access requested`, + ); + } catch (error) { + console.error("Failed to request permission:", error); + } finally { + setRequestingPermission(null); + } + }, + [getPermissionDisplayName, requestPermission], + ); + const handleSave = useCallback(async () => { setIsSaving(true); try { await invoke("save_app_settings", { settings }); @@ -274,11 +228,66 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) { } finally { setIsSaving(false); } - }; + }, [onClose, setTheme, settings]); - const updateSetting = (key: keyof AppSettings, value: boolean | string) => { - setSettings((prev) => ({ ...prev, [key]: value })); - }; + const updateSetting = useCallback( + (key: keyof AppSettings, value: boolean | string) => { + setSettings((prev) => ({ ...prev, [key]: value })); + }, + [], + ); + + useEffect(() => { + if (isOpen) { + loadSettings().catch(console.error); + checkDefaultBrowserStatus().catch(console.error); + + // Check if we're on macOS + const userAgent = navigator.userAgent; + const isMac = userAgent.includes("Mac"); + setIsMacOS(isMac); + + if (isMac) { + loadPermissions().catch(console.error); + } + + // Set up interval to check default browser status + const intervalId = setInterval(() => { + checkDefaultBrowserStatus().catch(console.error); + }, 500); // Check every 500ms + + // Cleanup interval on component unmount or dialog close + return () => { + clearInterval(intervalId); + }; + } + }, [isOpen, loadPermissions, checkDefaultBrowserStatus, loadSettings]); + + // Update permissions when the permission states change + useEffect(() => { + if (isMacOS) { + const permissionList: PermissionInfo[] = [ + { + permission_type: "microphone", + isGranted: isMicrophoneAccessGranted, + description: getPermissionDescription("microphone"), + }, + { + permission_type: "camera", + isGranted: isCameraAccessGranted, + description: getPermissionDescription("camera"), + }, + ]; + setPermissions(permissionList); + } else { + setPermissions([]); + } + }, [ + isMacOS, + isMicrophoneAccessGranted, + isCameraAccessGranted, + getPermissionDescription, + ]); // Check if settings have changed (excluding default browser setting) const hasChanges = diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx index d4700bd..ef36696 100644 --- a/src/components/ui/alert.tsx +++ b/src/components/ui/alert.tsx @@ -1,4 +1,4 @@ -import { type VariantProps, cva } from "class-variance-authority"; +import { cva, type VariantProps } from "class-variance-authority"; import type * as React from "react"; import { cn } from "@/lib/utils"; diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index dac19eb..d9ebd4a 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -1,5 +1,5 @@ import { Slot } from "@radix-ui/react-slot"; -import { type VariantProps, cva } from "class-variance-authority"; +import { cva, type VariantProps } from "class-variance-authority"; import type * as React from "react"; import { cn } from "@/lib/utils"; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 1d0f7c5..f11b70f 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,5 +1,5 @@ import { Slot } from "@radix-ui/react-slot"; -import { type VariantProps, cva } from "class-variance-authority"; +import { cva, type VariantProps } from "class-variance-authority"; import type * as React from "react"; import { cn } from "@/lib/utils"; diff --git a/src/components/window-drag-area.tsx b/src/components/window-drag-area.tsx index 44daf51..c6e8c9c 100644 --- a/src/components/window-drag-area.tsx +++ b/src/components/window-drag-area.tsx @@ -39,8 +39,9 @@ export function WindowDragArea() { } return ( -
(null); diff --git a/src/hooks/use-browser-download.ts b/src/hooks/use-browser-download.ts index 9f10cb8..5bba2a7 100644 --- a/src/hooks/use-browser-download.ts +++ b/src/hooks/use-browser-download.ts @@ -1,3 +1,6 @@ +import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; +import { useCallback, useEffect, useState } from "react"; import { getBrowserDisplayName } from "@/lib/browser-utils"; import { dismissToast, @@ -5,9 +8,6 @@ import { showErrorToast, 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"; interface GithubRelease { tag_name: string; @@ -52,47 +52,7 @@ export function useBrowserDownload() { const [downloadProgress, setDownloadProgress] = useState(null); - // Listen for download progress events - useEffect(() => { - const unlisten = listen("download-progress", (event) => { - const progress = event.payload; - setDownloadProgress(progress); - - const browserName = getBrowserDisplayName(progress.browser); - - // Show toast with progress - if (progress.stage === "downloading") { - const speedMBps = ( - progress.speed_bytes_per_sec / - (1024 * 1024) - ).toFixed(1); - const etaText = progress.eta_seconds - ? formatTime(progress.eta_seconds) - : "calculating..."; - - showDownloadToast(browserName, progress.version, "downloading", { - percentage: progress.percentage, - speed: speedMBps, - eta: etaText, - }); - } else if (progress.stage === "extracting") { - showDownloadToast(browserName, progress.version, "extracting"); - } else if (progress.stage === "verifying") { - showDownloadToast(browserName, progress.version, "verifying"); - } else if (progress.stage === "completed") { - showDownloadToast(browserName, progress.version, "completed"); - setDownloadProgress(null); - } - }); - - return () => { - void unlisten.then((fn) => { - fn(); - }); - }; - }, []); - - const formatTime = (seconds: number): string => { + const formatTime = useCallback((seconds: number): string => { if (seconds < 60) { return `${Math.round(seconds)}s`; } @@ -104,15 +64,15 @@ export function useBrowserDownload() { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); return `${hours}h ${minutes}m`; - }; + }, []); - const formatBytes = (bytes: number): string => { + const formatBytes = useCallback((bytes: number): string => { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`; - }; + }, []); const loadVersions = useCallback(async (browserStr: string) => { const browserName = getBrowserDisplayName(browserStr); @@ -268,6 +228,46 @@ export function useBrowserDownload() { [downloadedVersions], ); + // Listen for download progress events + useEffect(() => { + const unlisten = listen("download-progress", (event) => { + const progress = event.payload; + setDownloadProgress(progress); + + const browserName = getBrowserDisplayName(progress.browser); + + // Show toast with progress + if (progress.stage === "downloading") { + const speedMBps = ( + progress.speed_bytes_per_sec / + (1024 * 1024) + ).toFixed(1); + const etaText = progress.eta_seconds + ? formatTime(progress.eta_seconds) + : "calculating..."; + + showDownloadToast(browserName, progress.version, "downloading", { + percentage: progress.percentage, + speed: speedMBps, + eta: etaText, + }); + } else if (progress.stage === "extracting") { + showDownloadToast(browserName, progress.version, "extracting"); + } else if (progress.stage === "verifying") { + showDownloadToast(browserName, progress.version, "verifying"); + } else if (progress.stage === "completed") { + showDownloadToast(browserName, progress.version, "completed"); + setDownloadProgress(null); + } + }); + + return () => { + void unlisten.then((fn) => { + fn(); + }); + }; + }, [formatTime]); + return { availableVersions, downloadedVersions, diff --git a/src/hooks/use-table-sorting.ts b/src/hooks/use-table-sorting.ts index 3bbe4da..d185c59 100644 --- a/src/hooks/use-table-sorting.ts +++ b/src/hooks/use-table-sorting.ts @@ -1,7 +1,7 @@ -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"; +import type { TableSortingSettings } from "@/types"; export function useTableSorting() { const [sortingSettings, setSortingSettings] = useState({ diff --git a/src/hooks/use-update-notifications.tsx b/src/hooks/use-update-notifications.tsx index f51b157..77b68a1 100644 --- a/src/hooks/use-update-notifications.tsx +++ b/src/hooks/use-update-notifications.tsx @@ -1,7 +1,7 @@ -import { getBrowserDisplayName } from "@/lib/browser-utils"; -import { dismissToast, showToast } from "@/lib/toast-utils"; import { invoke } from "@tauri-apps/api/core"; import { useCallback, useRef, useState } from "react"; +import { getBrowserDisplayName } from "@/lib/browser-utils"; +import { dismissToast, showToast } from "@/lib/toast-utils"; interface UpdateNotification { id: string; @@ -34,48 +34,6 @@ export function useUpdateNotifications( const isCheckingForUpdates = useRef(false); const activeDownloads = useRef>(new Set()); // Track "browser-version" keys - const checkForUpdates = useCallback(async () => { - // Prevent multiple simultaneous calls - if (isCheckingForUpdates.current) { - console.log("Already checking for updates, skipping duplicate call"); - return; - } - - isCheckingForUpdates.current = true; - - try { - const updates = await invoke( - "check_for_browser_updates", - ); - - // Filter out already processed notifications - const newUpdates = updates.filter((notification) => { - return !processedNotifications.has(notification.id); - }); - - setNotifications(newUpdates); - - // Automatically start downloads for new update notifications - for (const notification of newUpdates) { - if (!processedNotifications.has(notification.id)) { - setProcessedNotifications((prev) => - new Set(prev).add(notification.id), - ); - // Start automatic update without user interaction - void handleAutoUpdate( - notification.browser, - notification.new_version, - notification.id, - ); - } - } - } catch (error) { - console.error("Failed to check for updates:", error); - } finally { - isCheckingForUpdates.current = false; - } - }, [processedNotifications]); - const handleAutoUpdate = useCallback( async (browser: string, newVersion: string, notificationId: string) => { const downloadKey = `${browser}-${newVersion}`; @@ -212,6 +170,48 @@ export function useUpdateNotifications( [onProfilesUpdated], ); + const checkForUpdates = useCallback(async () => { + // Prevent multiple simultaneous calls + if (isCheckingForUpdates.current) { + console.log("Already checking for updates, skipping duplicate call"); + return; + } + + isCheckingForUpdates.current = true; + + try { + const updates = await invoke( + "check_for_browser_updates", + ); + + // Filter out already processed notifications + const newUpdates = updates.filter((notification) => { + return !processedNotifications.has(notification.id); + }); + + setNotifications(newUpdates); + + // Automatically start downloads for new update notifications + for (const notification of newUpdates) { + if (!processedNotifications.has(notification.id)) { + setProcessedNotifications((prev) => + new Set(prev).add(notification.id), + ); + // Start automatic update without user interaction + void handleAutoUpdate( + notification.browser, + notification.new_version, + notification.id, + ); + } + } + } catch (error) { + console.error("Failed to check for updates:", error); + } finally { + isCheckingForUpdates.current = false; + } + }, [processedNotifications, handleAutoUpdate]); + return { notifications, isUpdating, diff --git a/src/hooks/use-version-updater.ts b/src/hooks/use-version-updater.ts index 706dd70..cc4cb30 100644 --- a/src/hooks/use-version-updater.ts +++ b/src/hooks/use-version-updater.ts @@ -1,3 +1,6 @@ +import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; +import { useCallback, useEffect, useState } from "react"; import { getBrowserDisplayName } from "@/lib/browser-utils"; import { dismissToast, @@ -6,9 +9,6 @@ import { showSuccessToast, showUnifiedVersionUpdateToast, } from "@/lib/toast-utils"; -import { invoke } from "@tauri-apps/api/core"; -import { listen } from "@tauri-apps/api/event"; -import { useCallback, useEffect, useState } from "react"; interface VersionUpdateProgress { current_browser: string; diff --git a/src/lib/browser-utils.ts b/src/lib/browser-utils.ts index ce6e60f..ace6989 100644 --- a/src/lib/browser-utils.ts +++ b/src/lib/browser-utils.ts @@ -3,9 +3,9 @@ * Centralized helpers for browser name mapping, icons, etc. */ -import { ZenBrowser } from "@/components/icons/zen-browser"; import { FaChrome, FaFirefox } from "react-icons/fa"; import { SiBrave, SiMullvad, SiTorbrowser } from "react-icons/si"; +import { ZenBrowser } from "@/components/icons/zen-browser"; /** * Map internal browser names to display names diff --git a/src/lib/toast-utils.ts b/src/lib/toast-utils.ts index 1b03e58..0d52582 100644 --- a/src/lib/toast-utils.ts +++ b/src/lib/toast-utils.ts @@ -1,6 +1,6 @@ -import { UnifiedToast } from "@/components/custom-toast"; import React from "react"; import { toast as sonnerToast } from "sonner"; +import { UnifiedToast } from "@/components/custom-toast"; interface BaseToastProps { id?: string;