mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-04-29 15:26:05 +02:00
fix: properly handle permissions on macos
This commit is contained in:
Vendored
+1
@@ -15,6 +15,7 @@
|
||||
"donutbrowser",
|
||||
"dpkg",
|
||||
"dtolnay",
|
||||
"dyld",
|
||||
"elif",
|
||||
"esbuild",
|
||||
"eslintcache",
|
||||
|
||||
+3
-1
@@ -38,6 +38,7 @@
|
||||
"@tauri-apps/plugin-dialog": "^2.2.2",
|
||||
"@tauri-apps/plugin-fs": "~2.3.0",
|
||||
"@tauri-apps/plugin-opener": "^2.2.7",
|
||||
"ahooks": "^3.8.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
@@ -47,7 +48,8 @@
|
||||
"react-dom": "^19.1.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"sonner": "^2.0.5",
|
||||
"tailwind-merge": "^3.3.0"
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tauri-plugin-macos-permissions-api": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
|
||||
Generated
+75
@@ -53,6 +53,9 @@ importers:
|
||||
'@tauri-apps/plugin-opener':
|
||||
specifier: ^2.2.7
|
||||
version: 2.2.7
|
||||
ahooks:
|
||||
specifier: ^3.8.5
|
||||
version: 3.8.5(react@19.1.0)
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
@@ -83,6 +86,9 @@ importers:
|
||||
tailwind-merge:
|
||||
specifier: ^3.3.0
|
||||
version: 3.3.0
|
||||
tauri-plugin-macos-permissions-api:
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0
|
||||
devDependencies:
|
||||
'@biomejs/biome':
|
||||
specifier: 1.9.4
|
||||
@@ -255,6 +261,10 @@ packages:
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@babel/runtime@7.27.6':
|
||||
resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -1690,6 +1700,12 @@ packages:
|
||||
resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
ahooks@3.8.5:
|
||||
resolution: {integrity: sha512-Y+MLoJpBXVdjsnnBjE5rOSPkQ4DK+8i5aPDzLJdIOsCpo/fiAeXcBY1Y7oWgtOK0TpOz0gFa/XcyO1UGdoqLcw==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
ajv@6.12.6:
|
||||
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
||||
|
||||
@@ -1940,6 +1956,9 @@ packages:
|
||||
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
dayjs@1.11.13:
|
||||
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
|
||||
|
||||
debug@3.2.7:
|
||||
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
||||
peerDependencies:
|
||||
@@ -2409,6 +2428,9 @@ packages:
|
||||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
intersection-observer@0.12.2:
|
||||
resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==}
|
||||
|
||||
into-stream@6.0.0:
|
||||
resolution: {integrity: sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -2556,6 +2578,10 @@ packages:
|
||||
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
|
||||
hasBin: true
|
||||
|
||||
js-cookie@3.0.5:
|
||||
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
@@ -2694,6 +2720,9 @@ packages:
|
||||
lodash.merge@4.6.2:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
|
||||
lodash@4.17.21:
|
||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||
|
||||
log-update@6.1.0:
|
||||
resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -2985,6 +3014,9 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^19.1.0
|
||||
|
||||
react-fast-compare@3.2.2:
|
||||
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
|
||||
|
||||
react-icons@5.5.0:
|
||||
resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==}
|
||||
peerDependencies:
|
||||
@@ -3054,6 +3086,9 @@ packages:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
resize-observer-polyfill@1.5.1:
|
||||
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
|
||||
|
||||
resolve-from@4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -3110,6 +3145,10 @@ packages:
|
||||
scheduler@0.26.0:
|
||||
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
|
||||
|
||||
screenfull@5.2.0:
|
||||
resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
semver@6.3.1:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
@@ -3330,6 +3369,9 @@ packages:
|
||||
resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tauri-plugin-macos-permissions-api@2.3.0:
|
||||
resolution: {integrity: sha512-pZp0jmDySysBqrGueknd1a7Rr4XEO9aXpMv9TNrT2PDHP0MSH20njieOagsFYJ5MCVb8A+wcaK0cIkjUC2dOww==}
|
||||
|
||||
tinyglobby@0.2.14:
|
||||
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -3674,6 +3716,8 @@ snapshots:
|
||||
'@babel/core': 7.27.4
|
||||
'@babel/helper-plugin-utils': 7.27.1
|
||||
|
||||
'@babel/runtime@7.27.6': {}
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
@@ -4936,6 +4980,19 @@ snapshots:
|
||||
|
||||
agent-base@7.1.3: {}
|
||||
|
||||
ahooks@3.8.5(react@19.1.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.6
|
||||
dayjs: 1.11.13
|
||||
intersection-observer: 0.12.2
|
||||
js-cookie: 3.0.5
|
||||
lodash: 4.17.21
|
||||
react: 19.1.0
|
||||
react-fast-compare: 3.2.2
|
||||
resize-observer-polyfill: 1.5.1
|
||||
screenfull: 5.2.0
|
||||
tslib: 2.8.1
|
||||
|
||||
ajv@6.12.6:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
@@ -5231,6 +5288,8 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
is-data-view: 1.0.2
|
||||
|
||||
dayjs@1.11.13: {}
|
||||
|
||||
debug@3.2.7:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@@ -5840,6 +5899,8 @@ snapshots:
|
||||
hasown: 2.0.2
|
||||
side-channel: 1.1.0
|
||||
|
||||
intersection-observer@0.12.2: {}
|
||||
|
||||
into-stream@6.0.0:
|
||||
dependencies:
|
||||
from2: 2.3.0
|
||||
@@ -5993,6 +6054,8 @@ snapshots:
|
||||
|
||||
jiti@2.4.2: {}
|
||||
|
||||
js-cookie@3.0.5: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
js-yaml@4.1.0:
|
||||
@@ -6120,6 +6183,8 @@ snapshots:
|
||||
|
||||
lodash.merge@4.6.2: {}
|
||||
|
||||
lodash@4.17.21: {}
|
||||
|
||||
log-update@6.1.0:
|
||||
dependencies:
|
||||
ansi-escapes: 7.0.0
|
||||
@@ -6414,6 +6479,8 @@ snapshots:
|
||||
react: 19.1.0
|
||||
scheduler: 0.26.0
|
||||
|
||||
react-fast-compare@3.2.2: {}
|
||||
|
||||
react-icons@5.5.0(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
@@ -6493,6 +6560,8 @@ snapshots:
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
||||
resize-observer-polyfill@1.5.1: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
@@ -6573,6 +6642,8 @@ snapshots:
|
||||
|
||||
scheduler@0.26.0: {}
|
||||
|
||||
screenfull@5.2.0: {}
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
||||
semver@7.7.2: {}
|
||||
@@ -6861,6 +6932,10 @@ snapshots:
|
||||
mkdirp: 3.0.1
|
||||
yallist: 5.0.0
|
||||
|
||||
tauri-plugin-macos-permissions-api@2.3.0:
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.5.0
|
||||
|
||||
tinyglobby@0.2.14:
|
||||
dependencies:
|
||||
fdir: 6.4.5(picomatch@4.0.2)
|
||||
|
||||
Generated
+26
@@ -1012,6 +1012,7 @@ dependencies = [
|
||||
"tauri-plugin-deep-link",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-macos-permissions",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-shell",
|
||||
"tempfile",
|
||||
@@ -2346,6 +2347,16 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||
|
||||
[[package]]
|
||||
name = "macos-accessibility-client"
|
||||
version = "0.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edf7710fbff50c24124331760978fb9086d6de6288dcdb38b25a97f8b1bdebbb"
|
||||
dependencies = [
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever"
|
||||
version = "0.11.0"
|
||||
@@ -4407,6 +4418,21 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-macos-permissions"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5607e0707d37d7b20e287cf0ce396d1efebe7b833b8e9cbd2ea4257091d9c604"
|
||||
dependencies = [
|
||||
"macos-accessibility-client",
|
||||
"objc2 0.6.1",
|
||||
"objc2-foundation 0.3.1",
|
||||
"serde",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-opener"
|
||||
version = "2.2.7"
|
||||
|
||||
@@ -27,6 +27,7 @@ tauri-plugin-fs = "2"
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-deep-link = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-macos-permissions = "2"
|
||||
directories = "6"
|
||||
reqwest = { version = "0.12", features = ["json", "stream"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
+6
-26
@@ -2,47 +2,27 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Donut Browser needs camera access to enable camera functionality in web browsers. Each website will still ask for your permission individually.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Donut Browser needs microphone access to enable microphone functionality in web browsers. Each website will still ask for your permission individually.</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Donut Browser</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Donut Browser</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.donutbrowser</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.donutbrowser</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>donutbrowser</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.3.2</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>icon.icns</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>icon.icns</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>Web Browser</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>http</string>
|
||||
<string>https</string>
|
||||
</array>
|
||||
<key>CFBundleURLIconFile</key>
|
||||
<string>icon.icns</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.productivity</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2025 Donut Browser</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.13</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -20,6 +20,11 @@
|
||||
"shell:allow-stdin-write",
|
||||
"deep-link:default",
|
||||
"dialog:default",
|
||||
"dialog:allow-open"
|
||||
"dialog:allow-open",
|
||||
"macos-permissions:default",
|
||||
"macos-permissions:allow-request-microphone-permission",
|
||||
"macos-permissions:allow-request-camera-permission",
|
||||
"macos-permissions:allow-check-microphone-permission",
|
||||
"macos-permissions:allow-check-camera-permission"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,5 +12,21 @@
|
||||
<true/>
|
||||
<key>com.apple.security.files.downloads.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.audio-output</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.microphone</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -172,6 +172,7 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_macos_permissions::init())
|
||||
.setup(|app| {
|
||||
// Create the main window programmatically
|
||||
#[allow(unused_variables)]
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { ChangeVersionDialog } from "@/components/change-version-dialog";
|
||||
import { CreateProfileDialog } from "@/components/create-profile-dialog";
|
||||
import { ImportProfileDialog } from "@/components/import-profile-dialog";
|
||||
import { PermissionDialog } from "@/components/permission-dialog";
|
||||
import { ProfilesDataTable } from "@/components/profile-data-table";
|
||||
import { ProfileSelectorDialog } from "@/components/profile-selector-dialog";
|
||||
import { ProxySettingsDialog } from "@/components/proxy-settings-dialog";
|
||||
@@ -21,6 +22,8 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications";
|
||||
import { usePermissions } from "@/hooks/use-permissions";
|
||||
import type { PermissionType } from "@/hooks/use-permissions";
|
||||
import { useUpdateNotifications } from "@/hooks/use-update-notifications";
|
||||
import { showErrorToast } from "@/lib/toast-utils";
|
||||
import type { BrowserProfile, ProxySettings } from "@/types";
|
||||
@@ -58,6 +61,11 @@ export default function Home() {
|
||||
const [currentProfileForVersionChange, setCurrentProfileForVersionChange] =
|
||||
useState<BrowserProfile | null>(null);
|
||||
const [hasCheckedStartupPrompt, setHasCheckedStartupPrompt] = useState(false);
|
||||
const [permissionDialogOpen, setPermissionDialogOpen] = useState(false);
|
||||
const [currentPermissionType, setCurrentPermissionType] =
|
||||
useState<PermissionType>("microphone");
|
||||
const { isMicrophoneAccessGranted, isCameraAccessGranted, isInitialized } =
|
||||
usePermissions();
|
||||
|
||||
// Simple profiles loader without updates check (for use as callback)
|
||||
const loadProfiles = useCallback(async () => {
|
||||
@@ -119,6 +127,13 @@ export default function Home() {
|
||||
};
|
||||
}, [loadProfilesWithUpdateCheck, checkForUpdates]);
|
||||
|
||||
// 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
|
||||
if (hasCheckedStartupPrompt) return;
|
||||
@@ -137,6 +152,42 @@ export default function Home() {
|
||||
}
|
||||
};
|
||||
|
||||
const checkAllPermissions = async () => {
|
||||
try {
|
||||
// Wait for permissions to be initialized before checking
|
||||
if (!isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if any permissions are not granted - prioritize missing permissions
|
||||
if (!isMicrophoneAccessGranted) {
|
||||
setCurrentPermissionType("microphone");
|
||||
setPermissionDialogOpen(true);
|
||||
} else if (!isCameraAccessGranted) {
|
||||
setCurrentPermissionType("camera");
|
||||
setPermissionDialogOpen(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to check permissions:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const checkNextPermission = () => {
|
||||
try {
|
||||
if (!isMicrophoneAccessGranted) {
|
||||
setCurrentPermissionType("microphone");
|
||||
setPermissionDialogOpen(true);
|
||||
} else if (!isCameraAccessGranted) {
|
||||
setCurrentPermissionType("camera");
|
||||
setPermissionDialogOpen(true);
|
||||
} else {
|
||||
setPermissionDialogOpen(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to check next permission:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const checkStartupUrls = async () => {
|
||||
try {
|
||||
const hasStartupUrl = await invoke<boolean>(
|
||||
@@ -533,6 +584,15 @@ export default function Home() {
|
||||
runningProfiles={runningProfiles}
|
||||
/>
|
||||
))}
|
||||
|
||||
<PermissionDialog
|
||||
isOpen={permissionDialogOpen}
|
||||
onClose={() => {
|
||||
setPermissionDialogOpen(false);
|
||||
}}
|
||||
permissionType={currentPermissionType}
|
||||
onPermissionGranted={checkNextPermission}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
"use client";
|
||||
|
||||
import { LoadingButton } from "@/components/loading-button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { usePermissions } from "@/hooks/use-permissions";
|
||||
import type { PermissionType } from "@/hooks/use-permissions";
|
||||
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
|
||||
import { useEffect, useState } from "react";
|
||||
import { BsCamera, BsMic } from "react-icons/bs";
|
||||
|
||||
interface PermissionDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
permissionType: PermissionType;
|
||||
onPermissionGranted?: () => void;
|
||||
}
|
||||
|
||||
export function PermissionDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
permissionType,
|
||||
onPermissionGranted,
|
||||
}: PermissionDialogProps) {
|
||||
const [isRequesting, setIsRequesting] = useState(false);
|
||||
const [isMacOS, setIsMacOS] = useState(false);
|
||||
const {
|
||||
requestPermission,
|
||||
isMicrophoneAccessGranted,
|
||||
isCameraAccessGranted,
|
||||
} = usePermissions();
|
||||
|
||||
// Check if we're on macOS and close dialog if not
|
||||
useEffect(() => {
|
||||
const userAgent = navigator.userAgent;
|
||||
const isMac = userAgent.includes("Mac");
|
||||
setIsMacOS(isMac);
|
||||
|
||||
// If not macOS, close the dialog as permissions aren't needed
|
||||
if (!isMac) {
|
||||
onClose();
|
||||
}
|
||||
}, [onClose]);
|
||||
|
||||
// Get current permission status
|
||||
const isCurrentPermissionGranted =
|
||||
permissionType === "microphone"
|
||||
? isMicrophoneAccessGranted
|
||||
: isCameraAccessGranted;
|
||||
|
||||
// Auto-close dialog when permission is granted
|
||||
useEffect(() => {
|
||||
if (isCurrentPermissionGranted && isOpen) {
|
||||
onPermissionGranted?.();
|
||||
}
|
||||
}, [isCurrentPermissionGranted, isOpen, onPermissionGranted]);
|
||||
|
||||
const getPermissionIcon = (type: PermissionType) => {
|
||||
switch (type) {
|
||||
case "microphone":
|
||||
return <BsMic className="w-8 h-8" />;
|
||||
case "camera":
|
||||
return <BsCamera className="w-8 h-8" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getPermissionTitle = (type: PermissionType) => {
|
||||
switch (type) {
|
||||
case "microphone":
|
||||
return "Microphone Access Required";
|
||||
case "camera":
|
||||
return "Camera Access Required";
|
||||
}
|
||||
};
|
||||
|
||||
const getPermissionDescription = (type: PermissionType) => {
|
||||
switch (type) {
|
||||
case "microphone":
|
||||
return "Donut Browser needs access to your microphone to enable microphone functionality in web browsers. Each website that wants to use your microphone will still ask for your permission individually.";
|
||||
case "camera":
|
||||
return "Donut Browser needs access to your camera to enable camera functionality in web browsers. Each website that wants to use your camera will still ask for your permission individually.";
|
||||
}
|
||||
};
|
||||
|
||||
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 handleRequestPermission = async () => {
|
||||
setIsRequesting(true);
|
||||
try {
|
||||
await requestPermission(permissionType);
|
||||
showSuccessToast(
|
||||
`${getPermissionTitle(permissionType).replace(
|
||||
" Required",
|
||||
"",
|
||||
)} permission requested`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to request permission:", error);
|
||||
showErrorToast("Failed to request permission");
|
||||
} finally {
|
||||
setIsRequesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Don't render if not macOS
|
||||
if (!isMacOS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader className="text-center">
|
||||
<div className="flex justify-center items-center mx-auto mb-4 w-16 h-16 bg-blue-100 rounded-full dark:bg-blue-900">
|
||||
{getPermissionIcon(permissionType)}
|
||||
</div>
|
||||
<DialogTitle className="text-xl">
|
||||
{getPermissionTitle(permissionType)}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-base">
|
||||
{getPermissionDescription(permissionType)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{isCurrentPermissionGranted && (
|
||||
<div className="p-3 bg-green-50 rounded-lg dark:bg-green-900/20">
|
||||
<p className="text-sm text-green-800 dark:text-green-200">
|
||||
✅ Permission granted! Browsers launched from Donut Browser can
|
||||
now access your {permissionType}.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isCurrentPermissionGranted && (
|
||||
<div className="p-3 bg-amber-50 rounded-lg dark:bg-amber-900/20">
|
||||
<p className="text-sm text-amber-800 dark:text-amber-200">
|
||||
⚠️ Permission not granted. Click the button below to request
|
||||
access to your {permissionType}.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
{isCurrentPermissionGranted ? "Done" : "Cancel"}
|
||||
</Button>
|
||||
|
||||
{!isCurrentPermissionGranted && (
|
||||
<LoadingButton
|
||||
isLoading={isRequesting}
|
||||
onClick={() => {
|
||||
handleRequestPermission().catch(console.error);
|
||||
}}
|
||||
className="min-w-24"
|
||||
>
|
||||
Grant Access
|
||||
</LoadingButton>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -19,10 +19,13 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { usePermissions } from "@/hooks/use-permissions";
|
||||
import type { PermissionType } from "@/hooks/use-permissions";
|
||||
import { showSuccessToast } from "@/lib/toast-utils";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { BsCamera, BsMic } from "react-icons/bs";
|
||||
|
||||
interface AppSettings {
|
||||
set_as_default_browser: boolean;
|
||||
@@ -32,6 +35,12 @@ interface AppSettings {
|
||||
auto_delete_unused_binaries: boolean;
|
||||
}
|
||||
|
||||
interface PermissionInfo {
|
||||
permission_type: PermissionType;
|
||||
isGranted: boolean;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface SettingsDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
@@ -57,18 +66,46 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isSettingDefault, setIsSettingDefault] = useState(false);
|
||||
const [isClearingCache, setIsClearingCache] = useState(false);
|
||||
const [permissions, setPermissions] = useState<PermissionInfo[]>([]);
|
||||
const [isLoadingPermissions, setIsLoadingPermissions] = useState(false);
|
||||
const [requestingPermission, setRequestingPermission] =
|
||||
useState<PermissionType | null>(null);
|
||||
const [isMacOS, setIsMacOS] = useState(false);
|
||||
|
||||
const { setTheme } = useTheme();
|
||||
const {
|
||||
requestPermission,
|
||||
isMicrophoneAccessGranted,
|
||||
isCameraAccessGranted,
|
||||
} = usePermissions();
|
||||
|
||||
const getPermissionDescription = useCallback((type: PermissionType) => {
|
||||
switch (type) {
|
||||
case "microphone":
|
||||
return "Access to microphone for browser applications";
|
||||
case "camera":
|
||||
return "Access to camera for browser applications";
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
void loadSettings();
|
||||
void checkDefaultBrowserStatus();
|
||||
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(() => {
|
||||
void checkDefaultBrowserStatus();
|
||||
}, 500); // Check every 2 seconds
|
||||
checkDefaultBrowserStatus().catch(console.error);
|
||||
}, 500); // Check every 500ms
|
||||
|
||||
// Cleanup interval on component unmount or dialog close
|
||||
return () => {
|
||||
@@ -77,6 +114,32 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||
}
|
||||
}, [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);
|
||||
try {
|
||||
@@ -90,6 +153,36 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const loadPermissions = async () => {
|
||||
setIsLoadingPermissions(true);
|
||||
try {
|
||||
if (!isMacOS) {
|
||||
// On non-macOS platforms, don't show permissions
|
||||
setPermissions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const permissionList: PermissionInfo[] = [
|
||||
{
|
||||
permission_type: "microphone",
|
||||
isGranted: isMicrophoneAccessGranted,
|
||||
description: getPermissionDescription("microphone"),
|
||||
},
|
||||
{
|
||||
permission_type: "camera",
|
||||
isGranted: isCameraAccessGranted,
|
||||
description: getPermissionDescription("camera"),
|
||||
},
|
||||
];
|
||||
|
||||
setPermissions(permissionList);
|
||||
} catch (error) {
|
||||
console.error("Failed to load permissions:", error);
|
||||
} finally {
|
||||
setIsLoadingPermissions(false);
|
||||
}
|
||||
};
|
||||
|
||||
const checkDefaultBrowserStatus = async () => {
|
||||
try {
|
||||
const isDefault = await invoke<boolean>("is_default_browser");
|
||||
@@ -127,6 +220,49 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRequestPermission = async (permissionType: PermissionType) => {
|
||||
setRequestingPermission(permissionType);
|
||||
try {
|
||||
await requestPermission(permissionType);
|
||||
showSuccessToast(
|
||||
`${getPermissionDisplayName(permissionType)} access requested`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to request permission:", error);
|
||||
} finally {
|
||||
setRequestingPermission(null);
|
||||
}
|
||||
};
|
||||
|
||||
const getPermissionIcon = (type: PermissionType) => {
|
||||
switch (type) {
|
||||
case "microphone":
|
||||
return <BsMic className="h-4 w-4" />;
|
||||
case "camera":
|
||||
return <BsCamera className="h-4 w-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="bg-green-100 text-green-800">
|
||||
Granted
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return <Badge variant="secondary">Not Granted</Badge>;
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
@@ -204,7 +340,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||
<LoadingButton
|
||||
isLoading={isSettingDefault}
|
||||
onClick={() => {
|
||||
void handleSetDefaultBrowser();
|
||||
handleSetDefaultBrowser().catch(console.error);
|
||||
}}
|
||||
disabled={isDefaultBrowser}
|
||||
variant={isDefaultBrowser ? "outline" : "default"}
|
||||
@@ -284,6 +420,69 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Permissions Section - Only show on macOS */}
|
||||
{isMacOS && (
|
||||
<div className="space-y-4">
|
||||
<Label className="text-base font-medium">
|
||||
System Permissions
|
||||
</Label>
|
||||
|
||||
{isLoadingPermissions ? (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Loading permissions...
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{permissions.map((permission) => (
|
||||
<div
|
||||
key={permission.permission_type}
|
||||
className="flex items-center justify-between p-3 border rounded-lg"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
{getPermissionIcon(permission.permission_type)}
|
||||
<div>
|
||||
<div className="text-sm font-medium">
|
||||
{getPermissionDisplayName(
|
||||
permission.permission_type,
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{permission.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
{getStatusBadge(permission.isGranted)}
|
||||
{!permission.isGranted && (
|
||||
<LoadingButton
|
||||
size="sm"
|
||||
isLoading={
|
||||
requestingPermission ===
|
||||
permission.permission_type
|
||||
}
|
||||
onClick={() => {
|
||||
handleRequestPermission(
|
||||
permission.permission_type,
|
||||
).catch(console.error);
|
||||
}}
|
||||
>
|
||||
Grant
|
||||
</LoadingButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
These permissions allow browsers launched from Donut Browser to
|
||||
access system resources. Each website will still ask for your
|
||||
permission individually.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Advanced Section */}
|
||||
<div className="space-y-4">
|
||||
<Label className="text-base font-medium">Advanced</Label>
|
||||
@@ -291,7 +490,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||
<LoadingButton
|
||||
isLoading={isClearingCache}
|
||||
onClick={() => {
|
||||
void handleClearCache();
|
||||
handleClearCache().catch(console.error);
|
||||
}}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
@@ -314,7 +513,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||
<LoadingButton
|
||||
isLoading={isSaving}
|
||||
onClick={() => {
|
||||
void handleSave();
|
||||
handleSave().catch(console.error);
|
||||
}}
|
||||
disabled={isLoading || !hasChanges}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
// Platform-specific imports
|
||||
let macOSPermissions:
|
||||
| typeof import("tauri-plugin-macos-permissions-api")
|
||||
| null = null;
|
||||
|
||||
// Dynamically import macOS permissions only when needed
|
||||
const loadMacOSPermissions = async () => {
|
||||
if (macOSPermissions) return macOSPermissions;
|
||||
|
||||
try {
|
||||
macOSPermissions = await import("tauri-plugin-macos-permissions-api");
|
||||
return macOSPermissions;
|
||||
} catch (error) {
|
||||
console.warn("Failed to load macOS permissions API:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export type PermissionType = "microphone" | "camera";
|
||||
|
||||
export interface UsePermissionsReturn {
|
||||
requestPermission: (type: PermissionType) => Promise<void>;
|
||||
isMicrophoneAccessGranted: boolean;
|
||||
isCameraAccessGranted: boolean;
|
||||
isInitialized: boolean;
|
||||
}
|
||||
|
||||
export function usePermissions(): UsePermissionsReturn {
|
||||
const [isMicrophoneAccessGranted, setIsMicrophoneAccessGranted] =
|
||||
useState(false);
|
||||
const [isCameraAccessGranted, setIsCameraAccessGranted] = useState(false);
|
||||
const [currentPlatform, setCurrentPlatform] = useState<string | null>(null);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Check permissions status
|
||||
const checkPermissions = useCallback(async () => {
|
||||
if (!currentPlatform) return;
|
||||
|
||||
if (currentPlatform !== "macos") {
|
||||
// Windows/Linux - assume permissions are granted
|
||||
setIsMicrophoneAccessGranted(true);
|
||||
setIsCameraAccessGranted(true);
|
||||
setIsInitialized(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// macOS - use the permissions API
|
||||
try {
|
||||
const permissions = await loadMacOSPermissions();
|
||||
if (permissions) {
|
||||
const [micGranted, camGranted] = await Promise.all([
|
||||
permissions.checkMicrophonePermission(),
|
||||
permissions.checkCameraPermission(),
|
||||
]);
|
||||
|
||||
setIsMicrophoneAccessGranted(micGranted);
|
||||
setIsCameraAccessGranted(camGranted);
|
||||
setIsInitialized(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to check permissions on macOS:", error);
|
||||
setIsInitialized(true);
|
||||
}
|
||||
}, [currentPlatform]);
|
||||
|
||||
// Request permission
|
||||
const requestPermission = useCallback(
|
||||
async (type: PermissionType): Promise<void> => {
|
||||
if (!currentPlatform || currentPlatform !== "macos") return;
|
||||
|
||||
// macOS - use the permissions API
|
||||
try {
|
||||
const permissions = await loadMacOSPermissions();
|
||||
if (!permissions) return;
|
||||
|
||||
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") {
|
||||
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();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to request ${type} permission on macOS:`, error);
|
||||
}
|
||||
},
|
||||
[currentPlatform],
|
||||
);
|
||||
|
||||
// Initialize platform detection and start interval checking
|
||||
useEffect(() => {
|
||||
const initializePlatform = async () => {
|
||||
try {
|
||||
// Detect platform - on macOS we need permissions, on others we don't
|
||||
const userAgent = navigator.userAgent;
|
||||
let platformName = "unknown";
|
||||
|
||||
if (userAgent.includes("Mac")) {
|
||||
platformName = "macos";
|
||||
} else if (userAgent.includes("Win")) {
|
||||
platformName = "windows";
|
||||
} else if (userAgent.includes("Linux")) {
|
||||
platformName = "linux";
|
||||
}
|
||||
|
||||
setCurrentPlatform(platformName);
|
||||
} catch (error) {
|
||||
console.error("Failed to detect platform:", error);
|
||||
// Fallback - assume non-macOS
|
||||
setCurrentPlatform("unknown");
|
||||
}
|
||||
};
|
||||
|
||||
initializePlatform().catch(console.error);
|
||||
}, []);
|
||||
|
||||
// Set up interval checking when platform is determined
|
||||
useEffect(() => {
|
||||
if (!currentPlatform) return;
|
||||
|
||||
// Initial check
|
||||
void checkPermissions();
|
||||
|
||||
// Set up 500ms interval for checking permissions
|
||||
intervalRef.current = setInterval(() => {
|
||||
void checkPermissions();
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [currentPlatform, checkPermissions]);
|
||||
|
||||
return {
|
||||
requestPermission,
|
||||
isMicrophoneAccessGranted,
|
||||
isCameraAccessGranted,
|
||||
isInitialized,
|
||||
};
|
||||
}
|
||||
@@ -40,3 +40,17 @@ export interface AppVersionInfo {
|
||||
version: string;
|
||||
is_nightly: boolean;
|
||||
}
|
||||
|
||||
export type PermissionType = "microphone" | "camera" | "location";
|
||||
|
||||
export type PermissionStatus =
|
||||
| "granted"
|
||||
| "denied"
|
||||
| "not_determined"
|
||||
| "restricted";
|
||||
|
||||
export interface PermissionInfo {
|
||||
permission_type: PermissionType;
|
||||
status: PermissionStatus;
|
||||
description: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user