mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-09-24 02:10:49 +02:00
chore: linting
This commit is contained in:
+2
-16
@@ -6,17 +6,13 @@
|
|||||||
"useIgnoreFile": false
|
"useIgnoreFile": false
|
||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"ignoreUnknown": false,
|
"ignoreUnknown": false
|
||||||
"ignore": []
|
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"indentStyle": "space",
|
"indentStyle": "space",
|
||||||
"indentWidth": 2
|
"indentWidth": 2
|
||||||
},
|
},
|
||||||
"organizeImports": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"linter": {
|
"linter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
@@ -25,17 +21,7 @@
|
|||||||
"useHookAtTopLevel": "error"
|
"useHookAtTopLevel": "error"
|
||||||
},
|
},
|
||||||
"nursery": {
|
"nursery": {
|
||||||
"useGoogleFontDisplay": "error",
|
"useUniqueElementIds": "off"
|
||||||
"noDocumentImportInPage": "error",
|
|
||||||
"noHeadElement": "error",
|
|
||||||
"noHeadImportInDocument": "error",
|
|
||||||
"noImgElement": "off",
|
|
||||||
"useComponentExportOnlyModules": {
|
|
||||||
"level": "error",
|
|
||||||
"options": {
|
|
||||||
"allowExportNames": ["metadata", "badgeVariants", "buttonVariants"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"a11y": {
|
"a11y": {
|
||||||
"useSemanticElements": "off"
|
"useSemanticElements": "off"
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
"shadcn:add": "pnpm dlx shadcn@latest add",
|
"shadcn:add": "pnpm dlx shadcn@latest add",
|
||||||
"prepare": "husky && husky install",
|
"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: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",
|
"format": "pnpm format:js && pnpm format:rust",
|
||||||
"cargo": "cd src-tauri && cargo",
|
"cargo": "cd src-tauri && cargo",
|
||||||
"unused-exports:js": "ts-unused-exports tsconfig.json",
|
"unused-exports:js": "ts-unused-exports tsconfig.json",
|
||||||
|
|||||||
+119
-112
@@ -1,5 +1,11 @@
|
|||||||
"use client";
|
"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 { ChangeVersionDialog } from "@/components/change-version-dialog";
|
||||||
import { CreateProfileDialog } from "@/components/create-profile-dialog";
|
import { CreateProfileDialog } from "@/components/create-profile-dialog";
|
||||||
import { ImportProfileDialog } from "@/components/import-profile-dialog";
|
import { ImportProfileDialog } from "@/components/import-profile-dialog";
|
||||||
@@ -22,18 +28,12 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications";
|
import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications";
|
||||||
import { usePermissions } from "@/hooks/use-permissions";
|
|
||||||
import type { PermissionType } 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 { useUpdateNotifications } from "@/hooks/use-update-notifications";
|
||||||
import { useVersionUpdater } from "@/hooks/use-version-updater";
|
import { useVersionUpdater } from "@/hooks/use-version-updater";
|
||||||
import { showErrorToast } from "@/lib/toast-utils";
|
import { showErrorToast } from "@/lib/toast-utils";
|
||||||
import type { BrowserProfile, ProxySettings } from "@/types";
|
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 =
|
type BrowserTypeString =
|
||||||
| "mullvad-browser"
|
| "mullvad-browser"
|
||||||
@@ -69,22 +69,6 @@ export default function Home() {
|
|||||||
const { isMicrophoneAccessGranted, isCameraAccessGranted, isInitialized } =
|
const { isMicrophoneAccessGranted, isCameraAccessGranted, isInitialized } =
|
||||||
usePermissions();
|
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
|
// Check for missing binaries and offer to download them
|
||||||
const checkMissingBinaries = useCallback(async () => {
|
const checkMissingBinaries = useCallback(async () => {
|
||||||
try {
|
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
|
// Version updater for handling version fetching progress events and auto-updates
|
||||||
useVersionUpdater();
|
useVersionUpdater();
|
||||||
|
|
||||||
@@ -165,42 +185,9 @@ export default function Home() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to check current URL:", error);
|
console.error("Failed to check current URL:", error);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [handleUrlOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
const checkStartupPrompt = useCallback(async () => {
|
||||||
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 () => {
|
|
||||||
// Only check once during app startup to prevent reopening after dismissing notifications
|
// Only check once during app startup to prevent reopening after dismissing notifications
|
||||||
if (hasCheckedStartupPrompt) return;
|
if (hasCheckedStartupPrompt) return;
|
||||||
|
|
||||||
@@ -216,9 +203,9 @@ export default function Home() {
|
|||||||
console.error("Failed to check startup prompt:", error);
|
console.error("Failed to check startup prompt:", error);
|
||||||
setHasCheckedStartupPrompt(true);
|
setHasCheckedStartupPrompt(true);
|
||||||
}
|
}
|
||||||
};
|
}, [hasCheckedStartupPrompt]);
|
||||||
|
|
||||||
const checkAllPermissions = async () => {
|
const checkAllPermissions = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
// Wait for permissions to be initialized before checking
|
// Wait for permissions to be initialized before checking
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
@@ -236,9 +223,9 @@ export default function Home() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to check permissions:", error);
|
console.error("Failed to check permissions:", error);
|
||||||
}
|
}
|
||||||
};
|
}, [isMicrophoneAccessGranted, isCameraAccessGranted, isInitialized]);
|
||||||
|
|
||||||
const checkNextPermission = () => {
|
const checkNextPermission = useCallback(() => {
|
||||||
try {
|
try {
|
||||||
if (!isMicrophoneAccessGranted) {
|
if (!isMicrophoneAccessGranted) {
|
||||||
setCurrentPermissionType("microphone");
|
setCurrentPermissionType("microphone");
|
||||||
@@ -252,9 +239,9 @@ export default function Home() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to check next permission:", error);
|
console.error("Failed to check next permission:", error);
|
||||||
}
|
}
|
||||||
};
|
}, [isMicrophoneAccessGranted, isCameraAccessGranted]);
|
||||||
|
|
||||||
const checkStartupUrls = async () => {
|
const checkStartupUrls = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const hasStartupUrl = await invoke<boolean>(
|
const hasStartupUrl = await invoke<boolean>(
|
||||||
"check_and_handle_startup_url",
|
"check_and_handle_startup_url",
|
||||||
@@ -265,9 +252,9 @@ export default function Home() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to check startup URLs:", error);
|
console.error("Failed to check startup URLs:", error);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const listenForUrlEvents = async () => {
|
const listenForUrlEvents = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
// Listen for URL open events from the deep link handler (when app is already running)
|
// Listen for URL open events from the deep link handler (when app is already running)
|
||||||
await listen<string>("url-open-request", (event) => {
|
await listen<string>("url-open-request", (event) => {
|
||||||
@@ -295,27 +282,7 @@ export default function Home() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to setup URL listener:", error);
|
console.error("Failed to setup URL listener:", error);
|
||||||
}
|
}
|
||||||
};
|
}, [handleUrlOpen]);
|
||||||
|
|
||||||
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 }]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const openProxyDialog = useCallback((profile: BrowserProfile | null) => {
|
const openProxyDialog = useCallback((profile: BrowserProfile | null) => {
|
||||||
setCurrentProfileForProxy(profile);
|
setCurrentProfileForProxy(profile);
|
||||||
@@ -459,31 +426,6 @@ export default function Home() {
|
|||||||
[loadProfiles, checkBrowserStatus, isUpdating],
|
[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(
|
const handleDeleteProfile = useCallback(
|
||||||
async (profile: BrowserProfile) => {
|
async (profile: BrowserProfile) => {
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -551,6 +493,71 @@ export default function Home() {
|
|||||||
[loadProfiles],
|
[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 (
|
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">
|
<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">
|
<main className="flex flex-col row-start-2 gap-8 items-center w-full max-w-3xl">
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
"use client";
|
"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 { FaDownload, FaTimes } from "react-icons/fa";
|
||||||
import { LuRefreshCw } from "react-icons/lu";
|
import { LuRefreshCw } from "react-icons/lu";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
interface AppUpdateInfo {
|
interface AppUpdateInfo {
|
||||||
current_version: string;
|
current_version: string;
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
"use client";
|
"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 { LoadingButton } from "@/components/loading-button";
|
||||||
import { ReleaseTypeSelector } from "@/components/release-type-selector";
|
import { ReleaseTypeSelector } from "@/components/release-type-selector";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
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 { useBrowserDownload } from "@/hooks/use-browser-download";
|
||||||
import { getBrowserDisplayName } from "@/lib/browser-utils";
|
import { getBrowserDisplayName } from "@/lib/browser-utils";
|
||||||
import type { BrowserProfile, BrowserReleaseTypes } from "@/types";
|
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 {
|
interface ChangeVersionDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -50,17 +50,7 @@ export function ChangeVersionDialog({
|
|||||||
isVersionDownloaded,
|
isVersionDownloaded,
|
||||||
} = useBrowserDownload();
|
} = useBrowserDownload();
|
||||||
|
|
||||||
useEffect(() => {
|
const loadReleaseTypes = useCallback(async (browser: string) => {
|
||||||
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) => {
|
|
||||||
setIsLoadingReleaseTypes(true);
|
setIsLoadingReleaseTypes(true);
|
||||||
try {
|
try {
|
||||||
const releaseTypes = await invoke<BrowserReleaseTypes>(
|
const releaseTypes = await invoke<BrowserReleaseTypes>(
|
||||||
@@ -73,7 +63,7 @@ export function ChangeVersionDialog({
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoadingReleaseTypes(false);
|
setIsLoadingReleaseTypes(false);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@@ -93,7 +83,7 @@ export function ChangeVersionDialog({
|
|||||||
}
|
}
|
||||||
}, [selectedReleaseType, profile]);
|
}, [selectedReleaseType, profile]);
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = useCallback(async () => {
|
||||||
if (!profile || !selectedReleaseType) return;
|
if (!profile || !selectedReleaseType) return;
|
||||||
|
|
||||||
const version =
|
const version =
|
||||||
@@ -103,9 +93,9 @@ export function ChangeVersionDialog({
|
|||||||
if (!version) return;
|
if (!version) return;
|
||||||
|
|
||||||
await downloadBrowser(profile.browser, version);
|
await downloadBrowser(profile.browser, version);
|
||||||
};
|
}, [profile, selectedReleaseType, downloadBrowser, releaseTypes]);
|
||||||
|
|
||||||
const handleVersionChange = async () => {
|
const handleVersionChange = useCallback(async () => {
|
||||||
if (!profile || !selectedReleaseType) return;
|
if (!profile || !selectedReleaseType) return;
|
||||||
|
|
||||||
const version =
|
const version =
|
||||||
@@ -127,7 +117,7 @@ export function ChangeVersionDialog({
|
|||||||
} finally {
|
} finally {
|
||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
}
|
}
|
||||||
};
|
}, [profile, selectedReleaseType, releaseTypes, onVersionChanged, onClose]);
|
||||||
|
|
||||||
const selectedVersion =
|
const selectedVersion =
|
||||||
selectedReleaseType === "stable"
|
selectedReleaseType === "stable"
|
||||||
@@ -142,6 +132,16 @@ export function ChangeVersionDialog({
|
|||||||
isVersionDownloaded(selectedVersion) &&
|
isVersionDownloaded(selectedVersion) &&
|
||||||
(!showDowngradeWarning || acknowledgeDowngrade);
|
(!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;
|
if (!profile) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
"use client";
|
"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 { LoadingButton } from "@/components/loading-button";
|
||||||
import { ReleaseTypeSelector } from "@/components/release-type-selector";
|
import { ReleaseTypeSelector } from "@/components/release-type-selector";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -33,9 +36,6 @@ import type {
|
|||||||
BrowserReleaseTypes,
|
BrowserReleaseTypes,
|
||||||
ProxySettings,
|
ProxySettings,
|
||||||
} from "@/types";
|
} from "@/types";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Alert, AlertDescription } from "./ui/alert";
|
import { Alert, AlertDescription } from "./ui/alert";
|
||||||
|
|
||||||
type BrowserTypeString =
|
type BrowserTypeString =
|
||||||
@@ -102,12 +102,6 @@ export function CreateProfileDialog({
|
|||||||
isBrowserSupported,
|
isBrowserSupported,
|
||||||
} = useBrowserSupport();
|
} = useBrowserSupport();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen) {
|
|
||||||
void loadExistingProfiles();
|
|
||||||
}
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (supportedBrowsers.length > 0) {
|
if (supportedBrowsers.length > 0) {
|
||||||
// Set default browser to first supported browser
|
// Set default browser to first supported browser
|
||||||
@@ -119,15 +113,6 @@ export function CreateProfileDialog({
|
|||||||
}
|
}
|
||||||
}, [supportedBrowsers]);
|
}, [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
|
// Set default release type when release types are loaded
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedReleaseType && Object.keys(releaseTypes).length > 0) {
|
if (!selectedReleaseType && Object.keys(releaseTypes).length > 0) {
|
||||||
@@ -142,16 +127,16 @@ export function CreateProfileDialog({
|
|||||||
}
|
}
|
||||||
}, [releaseTypes, selectedReleaseType, selectedBrowser]);
|
}, [releaseTypes, selectedReleaseType, selectedBrowser]);
|
||||||
|
|
||||||
const loadExistingProfiles = async () => {
|
const loadExistingProfiles = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const profiles = await invoke<BrowserProfile[]>("list_browser_profiles");
|
const profiles = await invoke<BrowserProfile[]>("list_browser_profiles");
|
||||||
setExistingProfiles(profiles);
|
setExistingProfiles(profiles);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load existing profiles:", error);
|
console.error("Failed to load existing profiles:", error);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const loadReleaseTypes = async (browser: string) => {
|
const loadReleaseTypes = useCallback(async (browser: string) => {
|
||||||
try {
|
try {
|
||||||
setIsLoadingReleaseTypes(true);
|
setIsLoadingReleaseTypes(true);
|
||||||
const types = await invoke<BrowserReleaseTypes>(
|
const types = await invoke<BrowserReleaseTypes>(
|
||||||
@@ -167,9 +152,9 @@ export function CreateProfileDialog({
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoadingReleaseTypes(false);
|
setIsLoadingReleaseTypes(false);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = useCallback(async () => {
|
||||||
if (!selectedBrowser || !selectedReleaseType) return;
|
if (!selectedBrowser || !selectedReleaseType) return;
|
||||||
|
|
||||||
const version =
|
const version =
|
||||||
@@ -179,26 +164,29 @@ export function CreateProfileDialog({
|
|||||||
if (!version) return;
|
if (!version) return;
|
||||||
|
|
||||||
await downloadBrowser(selectedBrowser, version);
|
await downloadBrowser(selectedBrowser, version);
|
||||||
};
|
}, [selectedBrowser, selectedReleaseType, downloadBrowser, releaseTypes]);
|
||||||
|
|
||||||
const validateProfileName = (name: string): string | null => {
|
const validateProfileName = useCallback(
|
||||||
const trimmedName = name.trim();
|
(name: string): string | null => {
|
||||||
|
const trimmedName = name.trim();
|
||||||
|
|
||||||
if (!trimmedName) {
|
if (!trimmedName) {
|
||||||
return "Profile name cannot be empty";
|
return "Profile name cannot be empty";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for duplicate names (case insensitive)
|
// Check for duplicate names (case insensitive)
|
||||||
const isDuplicate = existingProfiles.some(
|
const isDuplicate = existingProfiles.some(
|
||||||
(profile) => profile.name.toLowerCase() === trimmedName.toLowerCase(),
|
(profile) => profile.name.toLowerCase() === trimmedName.toLowerCase(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isDuplicate) {
|
if (isDuplicate) {
|
||||||
return "A profile with this name already exists";
|
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
|
// Helper to determine if proxy should be disabled for the selected browser
|
||||||
const isProxyDisabled = selectedBrowser === "tor-browser";
|
const isProxyDisabled = selectedBrowser === "tor-browser";
|
||||||
@@ -210,7 +198,7 @@ export function CreateProfileDialog({
|
|||||||
}
|
}
|
||||||
}, [selectedBrowser, proxyEnabled]);
|
}, [selectedBrowser, proxyEnabled]);
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = useCallback(async () => {
|
||||||
if (!profileName.trim() || !selectedBrowser || !selectedReleaseType) return;
|
if (!profileName.trim() || !selectedBrowser || !selectedReleaseType) return;
|
||||||
|
|
||||||
// Validate profile name
|
// Validate profile name
|
||||||
@@ -265,7 +253,23 @@ export function CreateProfileDialog({
|
|||||||
} finally {
|
} finally {
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
}
|
}
|
||||||
};
|
}, [
|
||||||
|
profileName,
|
||||||
|
selectedBrowser,
|
||||||
|
selectedReleaseType,
|
||||||
|
onCreateProfile,
|
||||||
|
proxyEnabled,
|
||||||
|
isProxyDisabled,
|
||||||
|
onClose,
|
||||||
|
proxyHost,
|
||||||
|
proxyPassword,
|
||||||
|
proxyPort,
|
||||||
|
proxyType,
|
||||||
|
proxyUsername,
|
||||||
|
releaseTypes.nightly,
|
||||||
|
releaseTypes.stable,
|
||||||
|
validateProfileName,
|
||||||
|
]);
|
||||||
|
|
||||||
const nameError = profileName.trim()
|
const nameError = profileName.trim()
|
||||||
? validateProfileName(profileName)
|
? validateProfileName(profileName)
|
||||||
@@ -285,6 +289,21 @@ export function CreateProfileDialog({
|
|||||||
(!proxyEnabled || isProxyDisabled || (proxyHost && proxyPort)) &&
|
(!proxyEnabled || isProxyDisabled || (proxyHost && proxyPort)) &&
|
||||||
!nameError;
|
!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 (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
<DialogContent className="max-w-md max-h-[80vh] my-8 flex flex-col">
|
<DialogContent className="max-w-md max-h-[80vh] my-8 flex flex-col">
|
||||||
|
|||||||
@@ -48,7 +48,6 @@
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import {
|
import {
|
||||||
LuCheckCheck,
|
LuCheckCheck,
|
||||||
LuDownload,
|
LuDownload,
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
"use client";
|
"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 { LoadingButton } from "@/components/loading-button";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -21,11 +26,6 @@ import {
|
|||||||
import { useBrowserSupport } from "@/hooks/use-browser-support";
|
import { useBrowserSupport } from "@/hooks/use-browser-support";
|
||||||
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
|
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
|
||||||
import type { DetectedProfile } from "@/types";
|
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 {
|
interface ImportProfileDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -63,13 +63,7 @@ export function ImportProfileDialog({
|
|||||||
const { supportedBrowsers, isLoading: isLoadingSupport } =
|
const { supportedBrowsers, isLoading: isLoadingSupport } =
|
||||||
useBrowserSupport();
|
useBrowserSupport();
|
||||||
|
|
||||||
useEffect(() => {
|
const loadDetectedProfiles = useCallback(async () => {
|
||||||
if (isOpen) {
|
|
||||||
void loadDetectedProfiles();
|
|
||||||
}
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
const loadDetectedProfiles = async () => {
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const profiles = await invoke<DetectedProfile[]>(
|
const profiles = await invoke<DetectedProfile[]>(
|
||||||
@@ -96,7 +90,7 @@ export function ImportProfileDialog({
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleBrowseFolder = async () => {
|
const handleBrowseFolder = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -115,7 +109,7 @@ export function ImportProfileDialog({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAutoDetectImport = async () => {
|
const handleAutoDetectImport = useCallback(async () => {
|
||||||
if (!selectedDetectedProfile || !autoDetectProfileName.trim()) {
|
if (!selectedDetectedProfile || !autoDetectProfileName.trim()) {
|
||||||
toast.error("Please select a profile and provide a name");
|
toast.error("Please select a profile and provide a name");
|
||||||
return;
|
return;
|
||||||
@@ -152,9 +146,15 @@ export function ImportProfileDialog({
|
|||||||
} finally {
|
} finally {
|
||||||
setIsImporting(false);
|
setIsImporting(false);
|
||||||
}
|
}
|
||||||
};
|
}, [
|
||||||
|
selectedDetectedProfile,
|
||||||
|
autoDetectProfileName,
|
||||||
|
detectedProfiles,
|
||||||
|
onImportComplete,
|
||||||
|
onClose,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleManualImport = async () => {
|
const handleManualImport = useCallback(async () => {
|
||||||
if (
|
if (
|
||||||
!manualBrowserType ||
|
!manualBrowserType ||
|
||||||
!manualProfilePath.trim() ||
|
!manualProfilePath.trim() ||
|
||||||
@@ -187,7 +187,13 @@ export function ImportProfileDialog({
|
|||||||
} finally {
|
} finally {
|
||||||
setIsImporting(false);
|
setIsImporting(false);
|
||||||
}
|
}
|
||||||
};
|
}, [
|
||||||
|
manualBrowserType,
|
||||||
|
manualProfilePath,
|
||||||
|
manualProfileName,
|
||||||
|
onImportComplete,
|
||||||
|
onClose,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setSelectedDetectedProfile(null);
|
setSelectedDetectedProfile(null);
|
||||||
@@ -222,6 +228,12 @@ export function ImportProfileDialog({
|
|||||||
(p) => p.path === selectedDetectedProfile,
|
(p) => p.path === selectedDetectedProfile,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
void loadDetectedProfiles();
|
||||||
|
}
|
||||||
|
}, [isOpen, loadDetectedProfiles]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
<DialogContent className="max-w-2xl max-h-[80vh] my-8 flex flex-col">
|
<DialogContent className="max-w-2xl max-h-[80vh] my-8 flex flex-col">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { LuLoaderCircle } from "react-icons/lu";
|
import { LuLoaderCircle } from "react-icons/lu";
|
||||||
import { type ButtonProps, Button as UIButton } from "./ui/button";
|
import { type ButtonProps, Button as UIButton } from "./ui/button";
|
||||||
|
|
||||||
type Props = ButtonProps & {
|
type Props = ButtonProps & {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
"aria-label"?: string;
|
"aria-label"?: string;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { BsCamera, BsMic } from "react-icons/bs";
|
||||||
import { LoadingButton } from "@/components/loading-button";
|
import { LoadingButton } from "@/components/loading-button";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -10,11 +12,9 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { usePermissions } from "@/hooks/use-permissions";
|
|
||||||
import type { PermissionType } 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 { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { BsCamera, BsMic } from "react-icons/bs";
|
|
||||||
|
|
||||||
interface PermissionDialogProps {
|
interface PermissionDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
"use client";
|
"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 { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -33,18 +45,6 @@ import {
|
|||||||
import { useTableSorting } from "@/hooks/use-table-sorting";
|
import { useTableSorting } from "@/hooks/use-table-sorting";
|
||||||
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
|
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
|
||||||
import type { BrowserProfile } from "@/types";
|
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 { Input } from "./ui/input";
|
||||||
import { Label } from "./ui/label";
|
import { Label } from "./ui/label";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
"use client";
|
"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 { LoadingButton } from "@/components/loading-button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -25,10 +29,6 @@ import {
|
|||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
|
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
|
||||||
import type { BrowserProfile } from "@/types";
|
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 {
|
interface ProfileSelectorDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -48,13 +48,42 @@ export function ProfileSelectorDialog({
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isLaunching, setIsLaunching] = useState(false);
|
const [isLaunching, setIsLaunching] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
// Helper function to determine if a profile can be used for opening links
|
||||||
if (isOpen) {
|
const canUseProfileForLinks = useCallback(
|
||||||
void loadProfiles();
|
(
|
||||||
}
|
profile: BrowserProfile,
|
||||||
}, [isOpen]);
|
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);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const profileList = await invoke<BrowserProfile[]>(
|
const profileList = await invoke<BrowserProfile[]>(
|
||||||
@@ -99,39 +128,7 @@ export function ProfileSelectorDialog({
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [runningProfiles, canUseProfileForLinks]);
|
||||||
|
|
||||||
// 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;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to get tooltip content for profiles
|
// Helper function to get tooltip content for profiles
|
||||||
const getProfileTooltipContent = (profile: BrowserProfile): string => {
|
const getProfileTooltipContent = (profile: BrowserProfile): string => {
|
||||||
@@ -156,7 +153,7 @@ export function ProfileSelectorDialog({
|
|||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenUrl = async () => {
|
const handleOpenUrl = useCallback(async () => {
|
||||||
if (!selectedProfile || !url) return;
|
if (!selectedProfile || !url) return;
|
||||||
|
|
||||||
setIsLaunching(true);
|
setIsLaunching(true);
|
||||||
@@ -171,14 +168,14 @@ export function ProfileSelectorDialog({
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLaunching(false);
|
setIsLaunching(false);
|
||||||
}
|
}
|
||||||
};
|
}, [selectedProfile, url, onClose]);
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = useCallback(() => {
|
||||||
setSelectedProfile(null);
|
setSelectedProfile(null);
|
||||||
onClose();
|
onClose();
|
||||||
};
|
}, [onClose]);
|
||||||
|
|
||||||
const handleCopyUrl = async () => {
|
const handleCopyUrl = useCallback(async () => {
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -188,7 +185,7 @@ export function ProfileSelectorDialog({
|
|||||||
console.error("Failed to copy URL:", error);
|
console.error("Failed to copy URL:", error);
|
||||||
toast.error("Failed to copy URL to clipboard");
|
toast.error("Failed to copy URL to clipboard");
|
||||||
}
|
}
|
||||||
};
|
}, [url]);
|
||||||
|
|
||||||
const selectedProfileData = profiles.find((p) => p.name === selectedProfile);
|
const selectedProfileData = profiles.find((p) => p.name === selectedProfile);
|
||||||
|
|
||||||
@@ -208,6 +205,12 @@ export function ProfileSelectorDialog({
|
|||||||
return getProfileTooltipContent(selectedProfileData);
|
return getProfileTooltipContent(selectedProfileData);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
void loadProfiles();
|
||||||
|
}
|
||||||
|
}, [isOpen, loadProfiles]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
<DialogContent className="max-w-md">
|
<DialogContent className="max-w-md">
|
||||||
@@ -253,86 +256,84 @@ export function ProfileSelectorDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<Select
|
||||||
<Select
|
value={selectedProfile ?? undefined}
|
||||||
value={selectedProfile ?? undefined}
|
onValueChange={setSelectedProfile}
|
||||||
onValueChange={setSelectedProfile}
|
>
|
||||||
>
|
<SelectTrigger>
|
||||||
<SelectTrigger>
|
<SelectValue placeholder="Choose a profile" />
|
||||||
<SelectValue placeholder="Choose a profile" />
|
</SelectTrigger>
|
||||||
</SelectTrigger>
|
<SelectContent>
|
||||||
<SelectContent>
|
{profiles.map((profile) => {
|
||||||
{profiles.map((profile) => {
|
const isRunning = runningProfiles.has(profile.name);
|
||||||
const isRunning = runningProfiles.has(profile.name);
|
const canUseForLinks = canUseProfileForLinks(
|
||||||
const canUseForLinks = canUseProfileForLinks(
|
profile,
|
||||||
profile,
|
profiles,
|
||||||
profiles,
|
runningProfiles,
|
||||||
runningProfiles,
|
);
|
||||||
);
|
const tooltipContent = getProfileTooltipContent(profile);
|
||||||
const tooltipContent = getProfileTooltipContent(profile);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip key={profile.name}>
|
<Tooltip key={profile.name}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<SelectItem
|
<SelectItem
|
||||||
value={profile.name}
|
value={profile.name}
|
||||||
disabled={!canUseForLinks}
|
disabled={!canUseForLinks}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-2 ${
|
||||||
|
!canUseForLinks ? "opacity-50" : ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div className="flex gap-3 items-center px-2 py-1 rounded-lg cursor-pointer hover:bg-accent">
|
||||||
className={`flex items-center gap-2 ${
|
<div className="flex gap-2 items-center">
|
||||||
!canUseForLinks ? "opacity-50" : ""
|
{(() => {
|
||||||
}`}
|
const IconComponent = getBrowserIcon(
|
||||||
>
|
profile.browser,
|
||||||
<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">
|
return IconComponent ? (
|
||||||
{(() => {
|
<IconComponent className="w-4 h-4" />
|
||||||
const IconComponent = getBrowserIcon(
|
) : null;
|
||||||
profile.browser,
|
})()}
|
||||||
);
|
</div>
|
||||||
return IconComponent ? (
|
<div className="flex-1 text-right">
|
||||||
<IconComponent className="w-4 h-4" />
|
<div className="font-medium">
|
||||||
) : null;
|
{profile.name}
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 text-right">
|
|
||||||
<div className="font-medium">
|
|
||||||
{profile.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</SelectItem>
|
<Badge variant="secondary" className="text-xs">
|
||||||
</TooltipTrigger>
|
{getBrowserDisplayName(profile.browser)}
|
||||||
{tooltipContent && (
|
</Badge>
|
||||||
<TooltipContent>{tooltipContent}</TooltipContent>
|
{profile.proxy?.enabled && (
|
||||||
)}
|
<Badge variant="outline" className="text-xs">
|
||||||
</Tooltip>
|
Proxy
|
||||||
);
|
</Badge>
|
||||||
})}
|
)}
|
||||||
</SelectContent>
|
{isRunning && (
|
||||||
</Select>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import {
|
import {
|
||||||
@@ -23,7 +24,6 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
interface ProxySettings {
|
interface ProxySettings {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { LuCheck, LuChevronsUpDown, LuDownload } from "react-icons/lu";
|
||||||
import { LoadingButton } from "@/components/loading-button";
|
import { LoadingButton } from "@/components/loading-button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -17,9 +19,6 @@ import {
|
|||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { BrowserReleaseTypes } from "@/types";
|
import type { BrowserReleaseTypes } from "@/types";
|
||||||
import { useState } from "react";
|
|
||||||
import { LuDownload } from "react-icons/lu";
|
|
||||||
import { LuCheck, LuChevronsUpDown } from "react-icons/lu";
|
|
||||||
|
|
||||||
interface ReleaseTypeSelectorProps {
|
interface ReleaseTypeSelectorProps {
|
||||||
selectedReleaseType: "stable" | "nightly" | null;
|
selectedReleaseType: "stable" | "nightly" | null;
|
||||||
|
|||||||
+125
-116
@@ -1,5 +1,9 @@
|
|||||||
"use client";
|
"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 { LoadingButton } from "@/components/loading-button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -19,13 +23,9 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { usePermissions } from "@/hooks/use-permissions";
|
|
||||||
import type { PermissionType } 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 { 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 {
|
interface AppSettings {
|
||||||
set_as_default_browser: boolean;
|
set_as_default_browser: boolean;
|
||||||
@@ -73,6 +73,35 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|||||||
isCameraAccessGranted,
|
isCameraAccessGranted,
|
||||||
} = usePermissions();
|
} = 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) => {
|
const getPermissionDescription = useCallback((type: PermissionType) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "microphone":
|
case "microphone":
|
||||||
@@ -81,60 +110,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|||||||
return "Access to camera for browser applications";
|
return "Access to camera for browser applications";
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
const loadSettings = useCallback(async () => {
|
||||||
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 () => {
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const appSettings = await invoke<AppSettings>("get_app_settings");
|
const appSettings = await invoke<AppSettings>("get_app_settings");
|
||||||
@@ -145,9 +121,9 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const loadPermissions = async () => {
|
const loadPermissions = useCallback(async () => {
|
||||||
setIsLoadingPermissions(true);
|
setIsLoadingPermissions(true);
|
||||||
try {
|
try {
|
||||||
if (!isMacOS) {
|
if (!isMacOS) {
|
||||||
@@ -175,18 +151,23 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoadingPermissions(false);
|
setIsLoadingPermissions(false);
|
||||||
}
|
}
|
||||||
};
|
}, [
|
||||||
|
getPermissionDescription,
|
||||||
|
isCameraAccessGranted,
|
||||||
|
isMacOS,
|
||||||
|
isMicrophoneAccessGranted,
|
||||||
|
]);
|
||||||
|
|
||||||
const checkDefaultBrowserStatus = async () => {
|
const checkDefaultBrowserStatus = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const isDefault = await invoke<boolean>("is_default_browser");
|
const isDefault = await invoke<boolean>("is_default_browser");
|
||||||
setIsDefaultBrowser(isDefault);
|
setIsDefaultBrowser(isDefault);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to check default browser status:", error);
|
console.error("Failed to check default browser status:", error);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleSetDefaultBrowser = async () => {
|
const handleSetDefaultBrowser = useCallback(async () => {
|
||||||
setIsSettingDefault(true);
|
setIsSettingDefault(true);
|
||||||
try {
|
try {
|
||||||
await invoke("set_as_default_browser");
|
await invoke("set_as_default_browser");
|
||||||
@@ -196,9 +177,9 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsSettingDefault(false);
|
setIsSettingDefault(false);
|
||||||
}
|
}
|
||||||
};
|
}, [checkDefaultBrowserStatus]);
|
||||||
|
|
||||||
const handleClearCache = async () => {
|
const handleClearCache = useCallback(async () => {
|
||||||
setIsClearingCache(true);
|
setIsClearingCache(true);
|
||||||
try {
|
try {
|
||||||
await invoke("clear_all_version_cache_and_refetch");
|
await invoke("clear_all_version_cache_and_refetch");
|
||||||
@@ -217,52 +198,25 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsClearingCache(false);
|
setIsClearingCache(false);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleRequestPermission = async (permissionType: PermissionType) => {
|
const handleRequestPermission = useCallback(
|
||||||
setRequestingPermission(permissionType);
|
async (permissionType: PermissionType) => {
|
||||||
try {
|
setRequestingPermission(permissionType);
|
||||||
await requestPermission(permissionType);
|
try {
|
||||||
showSuccessToast(
|
await requestPermission(permissionType);
|
||||||
`${getPermissionDisplayName(permissionType)} access requested`,
|
showSuccessToast(
|
||||||
);
|
`${getPermissionDisplayName(permissionType)} access requested`,
|
||||||
} catch (error) {
|
);
|
||||||
console.error("Failed to request permission:", error);
|
} catch (error) {
|
||||||
} finally {
|
console.error("Failed to request permission:", error);
|
||||||
setRequestingPermission(null);
|
} finally {
|
||||||
}
|
setRequestingPermission(null);
|
||||||
};
|
}
|
||||||
|
},
|
||||||
const getPermissionIcon = (type: PermissionType) => {
|
[getPermissionDisplayName, requestPermission],
|
||||||
switch (type) {
|
);
|
||||||
case "microphone":
|
const handleSave = useCallback(async () => {
|
||||||
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 () => {
|
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
await invoke("save_app_settings", { settings });
|
await invoke("save_app_settings", { settings });
|
||||||
@@ -274,11 +228,66 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
};
|
}, [onClose, setTheme, settings]);
|
||||||
|
|
||||||
const updateSetting = (key: keyof AppSettings, value: boolean | string) => {
|
const updateSetting = useCallback(
|
||||||
setSettings((prev) => ({ ...prev, [key]: value }));
|
(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)
|
// Check if settings have changed (excluding default browser setting)
|
||||||
const hasChanges =
|
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 type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Slot } from "@radix-ui/react-slot";
|
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 type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Slot } from "@radix-ui/react-slot";
|
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 type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|||||||
@@ -39,8 +39,9 @@ export function WindowDragArea() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<button
|
||||||
className="fixed top-0 right-0 left-0 h-10 z-9999"
|
type="button"
|
||||||
|
className="fixed top-0 right-0 left-0 h-10 bg-transparent border-0 pointer-events-none z-9999"
|
||||||
style={{
|
style={{
|
||||||
// Ensure it's above all other content
|
// Ensure it's above all other content
|
||||||
zIndex: 9999,
|
zIndex: 9999,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use client";
|
"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 { AppUpdateToast } from "@/components/app-update-toast";
|
||||||
import { showToast } from "@/lib/toast-utils";
|
import { showToast } from "@/lib/toast-utils";
|
||||||
import type { AppUpdateInfo } from "@/types";
|
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() {
|
export function useAppUpdateNotifications() {
|
||||||
const [updateInfo, setUpdateInfo] = useState<AppUpdateInfo | null>(null);
|
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 { getBrowserDisplayName } from "@/lib/browser-utils";
|
||||||
import {
|
import {
|
||||||
dismissToast,
|
dismissToast,
|
||||||
@@ -5,9 +8,6 @@ import {
|
|||||||
showErrorToast,
|
showErrorToast,
|
||||||
showSuccessToast,
|
showSuccessToast,
|
||||||
} from "@/lib/toast-utils";
|
} 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 {
|
interface GithubRelease {
|
||||||
tag_name: string;
|
tag_name: string;
|
||||||
@@ -52,47 +52,7 @@ export function useBrowserDownload() {
|
|||||||
const [downloadProgress, setDownloadProgress] =
|
const [downloadProgress, setDownloadProgress] =
|
||||||
useState<DownloadProgress | null>(null);
|
useState<DownloadProgress | null>(null);
|
||||||
|
|
||||||
// Listen for download progress events
|
const formatTime = useCallback((seconds: number): string => {
|
||||||
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 => {
|
|
||||||
if (seconds < 60) {
|
if (seconds < 60) {
|
||||||
return `${Math.round(seconds)}s`;
|
return `${Math.round(seconds)}s`;
|
||||||
}
|
}
|
||||||
@@ -104,15 +64,15 @@ export function useBrowserDownload() {
|
|||||||
const hours = Math.floor(seconds / 3600);
|
const hours = Math.floor(seconds / 3600);
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
return `${hours}h ${minutes}m`;
|
return `${hours}h ${minutes}m`;
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const formatBytes = (bytes: number): string => {
|
const formatBytes = useCallback((bytes: number): string => {
|
||||||
if (bytes === 0) return "0 B";
|
if (bytes === 0) return "0 B";
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
const sizes = ["B", "KB", "MB", "GB"];
|
const sizes = ["B", "KB", "MB", "GB"];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;
|
return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const loadVersions = useCallback(async (browserStr: string) => {
|
const loadVersions = useCallback(async (browserStr: string) => {
|
||||||
const browserName = getBrowserDisplayName(browserStr);
|
const browserName = getBrowserDisplayName(browserStr);
|
||||||
@@ -268,6 +228,46 @@ export function useBrowserDownload() {
|
|||||||
[downloadedVersions],
|
[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 {
|
return {
|
||||||
availableVersions,
|
availableVersions,
|
||||||
downloadedVersions,
|
downloadedVersions,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { TableSortingSettings } from "@/types";
|
|
||||||
import type { SortingState } from "@tanstack/react-table";
|
import type { SortingState } from "@tanstack/react-table";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import type { TableSortingSettings } from "@/types";
|
||||||
|
|
||||||
export function useTableSorting() {
|
export function useTableSorting() {
|
||||||
const [sortingSettings, setSortingSettings] = useState<TableSortingSettings>({
|
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 { invoke } from "@tauri-apps/api/core";
|
||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
|
import { getBrowserDisplayName } from "@/lib/browser-utils";
|
||||||
|
import { dismissToast, showToast } from "@/lib/toast-utils";
|
||||||
|
|
||||||
interface UpdateNotification {
|
interface UpdateNotification {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -34,48 +34,6 @@ export function useUpdateNotifications(
|
|||||||
const isCheckingForUpdates = useRef(false);
|
const isCheckingForUpdates = useRef(false);
|
||||||
const activeDownloads = useRef<Set<string>>(new Set()); // Track "browser-version" keys
|
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(
|
const handleAutoUpdate = useCallback(
|
||||||
async (browser: string, newVersion: string, notificationId: string) => {
|
async (browser: string, newVersion: string, notificationId: string) => {
|
||||||
const downloadKey = `${browser}-${newVersion}`;
|
const downloadKey = `${browser}-${newVersion}`;
|
||||||
@@ -212,6 +170,48 @@ export function useUpdateNotifications(
|
|||||||
[onProfilesUpdated],
|
[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 {
|
return {
|
||||||
notifications,
|
notifications,
|
||||||
isUpdating,
|
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 { getBrowserDisplayName } from "@/lib/browser-utils";
|
||||||
import {
|
import {
|
||||||
dismissToast,
|
dismissToast,
|
||||||
@@ -6,9 +9,6 @@ import {
|
|||||||
showSuccessToast,
|
showSuccessToast,
|
||||||
showUnifiedVersionUpdateToast,
|
showUnifiedVersionUpdateToast,
|
||||||
} from "@/lib/toast-utils";
|
} 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 {
|
interface VersionUpdateProgress {
|
||||||
current_browser: string;
|
current_browser: string;
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
* Centralized helpers for browser name mapping, icons, etc.
|
* Centralized helpers for browser name mapping, icons, etc.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ZenBrowser } from "@/components/icons/zen-browser";
|
|
||||||
import { FaChrome, FaFirefox } from "react-icons/fa";
|
import { FaChrome, FaFirefox } from "react-icons/fa";
|
||||||
import { SiBrave, SiMullvad, SiTorbrowser } from "react-icons/si";
|
import { SiBrave, SiMullvad, SiTorbrowser } from "react-icons/si";
|
||||||
|
import { ZenBrowser } from "@/components/icons/zen-browser";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map internal browser names to display names
|
* Map internal browser names to display names
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { UnifiedToast } from "@/components/custom-toast";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { toast as sonnerToast } from "sonner";
|
import { toast as sonnerToast } from "sonner";
|
||||||
|
import { UnifiedToast } from "@/components/custom-toast";
|
||||||
|
|
||||||
interface BaseToastProps {
|
interface BaseToastProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user