Skip to content

Commit 18c39a3

Browse files
committed
feat: improve microphone permission flow in onboarding
Added native mic permission request bridge and updated Welcome UI to handle permission states properly.
1 parent 25dc02e commit 18c39a3

2 files changed

Lines changed: 59 additions & 13 deletions

File tree

‎src-tauri/src/lib.rs‎

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -289,14 +289,48 @@ fn update_tray_lang(app: AppHandle, lang: String) {
289289
}
290290

291291
#[tauri::command]
292-
async fn check_microphone_permission() -> Result<bool, String> {
292+
async fn check_microphone_permission() -> Result<i32, String> {
293293
#[cfg(target_os = "macos")]
294294
{
295-
use objc2_av_foundation::{AVCaptureDevice, AVMediaTypeAudio, AVAuthorizationStatus};
295+
use objc2_av_foundation::{AVCaptureDevice, AVMediaTypeAudio};
296296
unsafe {
297297
if let Some(media_type) = AVMediaTypeAudio {
298+
// 0: NotDetermined, 1: Restricted, 2: Denied, 3: Authorized
298299
let status = AVCaptureDevice::authorizationStatusForMediaType(media_type);
299-
Ok(status == AVAuthorizationStatus::Authorized)
300+
Ok(status as i32)
301+
} else {
302+
Err("Could not get audio media type constant".to_string())
303+
}
304+
}
305+
}
306+
#[cfg(not(target_os = "macos"))]
307+
{
308+
Ok(3) // Authorized
309+
}
310+
}
311+
312+
#[tauri::command]
313+
async fn request_microphone_permission() -> Result<bool, String> {
314+
#[cfg(target_os = "macos")]
315+
{
316+
use objc2_av_foundation::{AVCaptureDevice, AVMediaTypeAudio};
317+
use objc2::rc::Retained;
318+
use objc2::block::StackBlock;
319+
320+
unsafe {
321+
if let Some(media_type) = AVMediaTypeAudio {
322+
let (tx, rx) = std::sync::mpsc::channel();
323+
let handler = StackBlock::new(move |granted: bool| {
324+
let _ = tx.send(granted);
325+
});
326+
327+
AVCaptureDevice::requestAccessForMediaType_completionHandler(media_type, &handler);
328+
329+
// Wait for the block to execute (it usually shows a dialog)
330+
match rx.recv_timeout(std::time::Duration::from_secs(300)) {
331+
Ok(granted) => Ok(granted),
332+
Err(_) => Err("Permission request timed out".to_string()),
333+
}
300334
} else {
301335
Err("Could not get audio media type constant".to_string())
302336
}

‎src/app/welcome/page.tsx‎

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export default function WelcomePage() {
1414
const [language, setLanguage] = useState<'ru' | 'en'>('ru');
1515
const [isLoaded, setIsLoaded] = useState(false);
1616
const [accGranted, setAccGranted] = useState<boolean | null>(null);
17-
const [micGranted, setMicGranted] = useState<boolean | null>(null);
17+
const [micStatus, setMicStatus] = useState<number | null>(null); // 0: NotDetermined, 1: Restricted, 2: Denied, 3: Authorized
1818

1919
useEffect(() => {
2020
setIsLoaded(true);
@@ -34,8 +34,8 @@ export default function WelcomePage() {
3434
try {
3535
const acc = await invoke<boolean>('check_accessibility');
3636
setAccGranted(acc);
37-
const mic = await invoke<boolean>('check_microphone_permission');
38-
setMicGranted(mic);
37+
const mic = await invoke<number>('check_microphone_permission');
38+
setMicStatus(mic);
3939
} catch (e) {
4040
console.error(e);
4141
}
@@ -61,6 +61,14 @@ export default function WelcomePage() {
6161
}
6262
};
6363

64+
const handleRequestMic = async () => {
65+
try {
66+
await invoke('request_microphone_permission');
67+
} catch (e) {
68+
console.error('Mic request failed:', e);
69+
}
70+
};
71+
6472
const tabs: { id: Tab; icon: React.ReactNode; label: string }[] = [
6573
{ id: 'welcome', icon: <Info className="w-[12px] h-[12px]" />, label: language === 'ru' ? 'Старт' : 'Home' },
6674
{ id: 'perms', icon: <ShieldCheck className="w-[12px] h-[12px]" />, label: language === 'ru' ? 'Права' : 'Privacy' },
@@ -165,18 +173,22 @@ export default function WelcomePage() {
165173
<div className="text-[9px] font-black text-white/20 uppercase tracking-[0.3em] ml-2 mb-2">{C.welcome.permTitle}</div>
166174
{[
167175
{
176+
id: 'acc',
168177
icon: <Accessibility className="text-orange-400" size={17} />,
169178
title: C.welcome.permAccess,
170179
desc: C.welcome.permAccessDesc,
171-
cmd: 'open_accessibility_settings',
172-
granted: accGranted
180+
action: () => invoke('open_accessibility_settings'),
181+
granted: accGranted,
182+
btnLabel: language === 'ru' ? 'ВЫДАТЬ' : 'SET'
173183
},
174184
{
185+
id: 'mic',
175186
icon: <Mic2 className="text-emerald-400" size={17} />,
176187
title: C.welcome.permMic,
177-
desc: C.welcome.permMicDesc,
178-
cmd: 'open_microphone_settings',
179-
granted: micGranted
188+
desc: micStatus === 0 ? (language === 'ru' ? 'Будет запрошен при первой записи' : 'Will be requested on first record') : C.welcome.permMicDesc,
189+
action: micStatus === 0 ? handleRequestMic : () => invoke('open_microphone_settings'),
190+
granted: micStatus === 3,
191+
btnLabel: micStatus === 0 ? (language === 'ru' ? 'ЗАПРОС' : 'ASK') : (language === 'ru' ? 'НАСТРОЙКИ' : 'SETTINGS')
180192
}
181193
].map((p, i) => (
182194
<div key={i} className={`flex items-center gap-4 p-4 rounded-xl bg-white/5 border transition-all group ${p.granted ? 'border-green-500/50 hover:bg-green-500/5' : 'border-white/5 hover:bg-white/10'}`}>
@@ -188,10 +200,10 @@ export default function WelcomePage() {
188200
<div className="text-[11px] text-white/30 leading-snug mt-1 font-bold">{p.desc}</div>
189201
</div>
190202
<button
191-
onClick={() => invoke(p.cmd)}
203+
onClick={p.action}
192204
className={`h-8 px-3 rounded-lg text-[10px] font-black transition-all border uppercase tracking-widest ${p.granted ? 'bg-green-500/10 text-green-500 border-green-500/30' : 'bg-white/5 hover:bg-orange-600 active:scale-95 text-white/70 hover:text-white border-white/10'}`}
193205
>
194-
{p.granted ? (language === 'ru' ? 'ВЫДАНО' : 'GRANTED') : (language === 'ru' ? 'ВЫДАТЬ' : 'SET')}
206+
{p.granted ? (language === 'ru' ? 'ВЫДАНО' : 'GRANTED') : p.btnLabel}
195207
</button>
196208
</div>
197209
))}

0 commit comments

Comments
 (0)