mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-04-23 20:36:09 +02:00
chore: linting
This commit is contained in:
+2
-16
@@ -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"
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
+119
-112
@@ -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<BrowserProfile[]>(
|
||||
"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<BrowserProfile[]>(
|
||||
"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<string>("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<boolean>(
|
||||
"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<string>("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<string>("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 (
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen gap-8 font-[family-name:var(--font-geist-sans)] bg-white dark:bg-black">
|
||||
<main className="flex flex-col row-start-2 gap-8 items-center w-full max-w-3xl">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<BrowserReleaseTypes>(
|
||||
@@ -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 (
|
||||
|
||||
@@ -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<BrowserProfile[]>("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<BrowserReleaseTypes>(
|
||||
@@ -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 (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-md max-h-[80vh] my-8 flex flex-col">
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
* ```
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
LuCheckCheck,
|
||||
LuDownload,
|
||||
|
||||
@@ -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<DetectedProfile[]>(
|
||||
@@ -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 (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] my-8 flex flex-col">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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<string>,
|
||||
): 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<BrowserProfile[]>(
|
||||
@@ -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<string>,
|
||||
): 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 (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-md">
|
||||
@@ -253,86 +256,84 @@ export function ProfileSelectorDialog({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Select
|
||||
value={selectedProfile ?? undefined}
|
||||
onValueChange={setSelectedProfile}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Choose a profile" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{profiles.map((profile) => {
|
||||
const isRunning = runningProfiles.has(profile.name);
|
||||
const canUseForLinks = canUseProfileForLinks(
|
||||
profile,
|
||||
profiles,
|
||||
runningProfiles,
|
||||
);
|
||||
const tooltipContent = getProfileTooltipContent(profile);
|
||||
<Select
|
||||
value={selectedProfile ?? undefined}
|
||||
onValueChange={setSelectedProfile}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Choose a profile" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{profiles.map((profile) => {
|
||||
const isRunning = runningProfiles.has(profile.name);
|
||||
const canUseForLinks = canUseProfileForLinks(
|
||||
profile,
|
||||
profiles,
|
||||
runningProfiles,
|
||||
);
|
||||
const tooltipContent = getProfileTooltipContent(profile);
|
||||
|
||||
return (
|
||||
<Tooltip key={profile.name}>
|
||||
<TooltipTrigger asChild>
|
||||
<SelectItem
|
||||
value={profile.name}
|
||||
disabled={!canUseForLinks}
|
||||
return (
|
||||
<Tooltip key={profile.name}>
|
||||
<TooltipTrigger asChild>
|
||||
<SelectItem
|
||||
value={profile.name}
|
||||
disabled={!canUseForLinks}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center gap-2 ${
|
||||
!canUseForLinks ? "opacity-50" : ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center gap-2 ${
|
||||
!canUseForLinks ? "opacity-50" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex gap-3 items-center px-2 py-1 rounded-lg cursor-pointer hover:bg-accent">
|
||||
<div className="flex gap-2 items-center">
|
||||
{(() => {
|
||||
const IconComponent = getBrowserIcon(
|
||||
profile.browser,
|
||||
);
|
||||
return IconComponent ? (
|
||||
<IconComponent className="w-4 h-4" />
|
||||
) : null;
|
||||
})()}
|
||||
</div>
|
||||
<div className="flex-1 text-right">
|
||||
<div className="font-medium">
|
||||
{profile.name}
|
||||
</div>
|
||||
<div className="flex gap-3 items-center px-2 py-1 rounded-lg cursor-pointer hover:bg-accent">
|
||||
<div className="flex gap-2 items-center">
|
||||
{(() => {
|
||||
const IconComponent = getBrowserIcon(
|
||||
profile.browser,
|
||||
);
|
||||
return IconComponent ? (
|
||||
<IconComponent className="w-4 h-4" />
|
||||
) : null;
|
||||
})()}
|
||||
</div>
|
||||
<div className="flex-1 text-right">
|
||||
<div className="font-medium">
|
||||
{profile.name}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{getBrowserDisplayName(profile.browser)}
|
||||
</Badge>
|
||||
{profile.proxy?.enabled && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Proxy
|
||||
</Badge>
|
||||
)}
|
||||
{isRunning && (
|
||||
<Badge variant="default" className="text-xs">
|
||||
Running
|
||||
</Badge>
|
||||
)}
|
||||
{!canUseForLinks && (
|
||||
<Badge
|
||||
variant="destructive"
|
||||
className="text-xs"
|
||||
>
|
||||
Unavailable
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
</TooltipTrigger>
|
||||
{tooltipContent && (
|
||||
<TooltipContent>{tooltipContent}</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{getBrowserDisplayName(profile.browser)}
|
||||
</Badge>
|
||||
{profile.proxy?.enabled && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Proxy
|
||||
</Badge>
|
||||
)}
|
||||
{isRunning && (
|
||||
<Badge variant="default" className="text-xs">
|
||||
Running
|
||||
</Badge>
|
||||
)}
|
||||
{!canUseForLinks && (
|
||||
<Badge
|
||||
variant="destructive"
|
||||
className="text-xs"
|
||||
>
|
||||
Unavailable
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
</TooltipTrigger>
|
||||
{tooltipContent && (
|
||||
<TooltipContent>{tooltipContent}</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+125
-116
@@ -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 <BsMic className="w-4 h-4" />;
|
||||
case "camera":
|
||||
return <BsCamera className="w-4 h-4" />;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getPermissionDisplayName = useCallback((type: PermissionType) => {
|
||||
switch (type) {
|
||||
case "microphone":
|
||||
return "Microphone";
|
||||
case "camera":
|
||||
return "Camera";
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getStatusBadge = useCallback((isGranted: boolean) => {
|
||||
if (isGranted) {
|
||||
return (
|
||||
<Badge variant="default" className="text-green-800 bg-green-100">
|
||||
Granted
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return <Badge variant="secondary">Not Granted</Badge>;
|
||||
}, []);
|
||||
|
||||
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<AppSettings>("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<boolean>("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 <BsMic className="w-4 h-4" />;
|
||||
case "camera":
|
||||
return <BsCamera className="w-4 h-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getPermissionDisplayName = (type: PermissionType) => {
|
||||
switch (type) {
|
||||
case "microphone":
|
||||
return "Microphone";
|
||||
case "camera":
|
||||
return "Camera";
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (isGranted: boolean) => {
|
||||
if (isGranted) {
|
||||
return (
|
||||
<Badge variant="default" className="text-green-800 bg-green-100">
|
||||
Granted
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return <Badge variant="secondary">Not Granted</Badge>;
|
||||
};
|
||||
|
||||
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 =
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -39,8 +39,9 @@ export function WindowDragArea() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed top-0 right-0 left-0 h-10 z-9999"
|
||||
<button
|
||||
type="button"
|
||||
className="fixed top-0 right-0 left-0 h-10 bg-transparent border-0 pointer-events-none z-9999"
|
||||
style={{
|
||||
// Ensure it's above all other content
|
||||
zIndex: 9999,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
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 { AppUpdateToast } from "@/components/app-update-toast";
|
||||
import { showToast } from "@/lib/toast-utils";
|
||||
import type { AppUpdateInfo } from "@/types";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function useAppUpdateNotifications() {
|
||||
const [updateInfo, setUpdateInfo] = useState<AppUpdateInfo | null>(null);
|
||||
|
||||
@@ -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<DownloadProgress | null>(null);
|
||||
|
||||
// Listen for download progress events
|
||||
useEffect(() => {
|
||||
const unlisten = listen<DownloadProgress>("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<DownloadProgress>("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,
|
||||
|
||||
@@ -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<TableSortingSettings>({
|
||||
|
||||
@@ -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<Set<string>>(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<UpdateNotification[]>(
|
||||
"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<UpdateNotification[]>(
|
||||
"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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user