chore: linting

This commit is contained in:
zhom
2025-06-22 06:04:02 +04:00
parent e090881917
commit 3d3a3b3816
26 changed files with 607 additions and 574 deletions
+2 -16
View File
@@ -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
View File
@@ -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
View File
@@ -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">
+2 -3
View File
@@ -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;
+19 -19
View File
@@ -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 (
+59 -40
View File
@@ -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">
-1
View File
@@ -48,7 +48,6 @@
* ``` * ```
*/ */
import React from "react";
import { import {
LuCheckCheck, LuCheckCheck,
LuDownload, LuDownload,
+29 -17
View File
@@ -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
View File
@@ -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;
+3 -3
View File
@@ -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;
+12 -12
View File
@@ -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";
+125 -124
View File
@@ -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 -1
View File
@@ -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;
+2 -3
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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";
+3 -2
View File
@@ -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,
+4 -4
View File
@@ -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);
+47 -47
View File
@@ -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 -1
View File
@@ -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>({
+44 -44
View File
@@ -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,
+3 -3
View File
@@ -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;
+1 -1
View File
@@ -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 -1
View File
@@ -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;