diff --git a/CHANGELOG.md b/CHANGELOG.md index c7184ae..7476d66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ ### Maintenance -- chore: versiom bump +- chore: version bump - chore: update flake.nix for v0.24.3 [skip ci] (#383) diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 3ea5ce5..a8977b7 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -21,6 +21,17 @@ "core:window:allow-minimize", "core:window:allow-toggle-maximize", "opener:default", + { + "identifier": "opener:allow-open-url", + "allow": [ + { + "url": "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone" + }, + { + "url": "x-apple.systempreferences:com.apple.preference.security?Privacy_Camera" + } + ] + }, "fs:default", "shell:allow-execute", "shell:allow-kill", diff --git a/src/components/settings-dialog.tsx b/src/components/settings-dialog.tsx index 98bc4ca..c2a8998 100644 --- a/src/components/settings-dialog.tsx +++ b/src/components/settings-dialog.tsx @@ -2,6 +2,7 @@ import { invoke } from "@tauri-apps/api/core"; import { writeText as writeClipboardText } from "@tauri-apps/plugin-clipboard-manager"; +import { openUrl } from "@tauri-apps/plugin-opener"; import Color from "color"; import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -368,19 +369,36 @@ export function SettingsDialog({ async (permissionType: PermissionType) => { setRequestingPermission(permissionType); try { - await requestPermission(permissionType); - showSuccessToast( - t("settings.permissions.accessRequested", { - permission: getPermissionDisplayName(permissionType), - }), + const granted = await requestPermission(permissionType); + if (granted) { + showSuccessToast( + permissionType === "microphone" + ? t("permissionDialog.grantedToastMicrophone") + : t("permissionDialog.grantedToastCamera"), + ); + return; + } + + await openUrl( + `x-apple.systempreferences:com.apple.preference.security?${ + permissionType === "microphone" + ? "Privacy_Microphone" + : "Privacy_Camera" + }`, + ); + showErrorToast( + permissionType === "microphone" + ? t("permissionDialog.stillNotGrantedMicrophone") + : t("permissionDialog.stillNotGrantedCamera"), ); } catch (error) { console.error("Failed to request permission:", error); + showErrorToast(t("permissionDialog.requestFailed")); } finally { setRequestingPermission(null); } }, - [getPermissionDisplayName, requestPermission, t], + [requestPermission, t], ); const handleSave = useCallback(async () => { diff --git a/src/hooks/use-permissions.ts b/src/hooks/use-permissions.ts index 624ef9a..6448a6d 100644 --- a/src/hooks/use-permissions.ts +++ b/src/hooks/use-permissions.ts @@ -21,7 +21,7 @@ const loadMacOSPermissions = async () => { export type PermissionType = "microphone" | "camera"; interface UsePermissionsReturn { - requestPermission: (type: PermissionType) => Promise; + requestPermission: (type: PermissionType) => Promise; isMicrophoneAccessGranted: boolean; isCameraAccessGranted: boolean; isInitialized: boolean; @@ -68,51 +68,44 @@ export function usePermissions(): UsePermissionsReturn { // Request permission const requestPermission = useCallback( - async (type: PermissionType): Promise => { - if (!currentPlatform || currentPlatform !== "macos") return; + async (type: PermissionType): Promise => { + // Non-macOS platforms do not require this permission gate. + if (!currentPlatform || currentPlatform !== "macos") return true; // macOS - use the permissions API try { const permissions = await loadMacOSPermissions(); - if (!permissions) return; + if (!permissions) return false; + + const readPermission = async () => { + const granted = + type === "microphone" + ? await permissions.checkMicrophonePermission() + : await permissions.checkCameraPermission(); + if (type === "microphone") { + setIsMicrophoneAccessGranted(granted); + } else { + setIsCameraAccessGranted(granted); + } + return granted; + }; if (type === "microphone") { await permissions.requestMicrophonePermission(); - - // Poll for permission status change - const pollMicPermission = async () => { - const granted = await permissions.checkMicrophonePermission(); - setIsMicrophoneAccessGranted(granted); - - if (!granted) { - setTimeout(() => { - void pollMicPermission(); - }, 1000); - } - }; - - await pollMicPermission(); - } - - if (type === "camera") { + } else { await permissions.requestCameraPermission(); - - // Poll for permission status change - const pollCamPermission = async () => { - const granted = await permissions.checkCameraPermission(); - setIsCameraAccessGranted(granted); - - if (!granted) { - setTimeout(() => { - void pollCamPermission(); - }, 1000); - } - }; - - await pollCamPermission(); } + + for (let attempt = 0; attempt < 8; attempt += 1) { + const granted = await readPermission(); + if (granted) return true; + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + + return readPermission(); } catch (error) { console.error(`Failed to request ${type} permission on macOS:`, error); + return false; } }, [currentPlatform],