Remove 290 lines of unused rdev listener code from the Linux hotkey platform module, drop the rdev dependency from Cargo.toml, and clean up capsule_window_visible init leftover. Update doc comments and frontend i18n to reflect the unified fcitx5-only path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
207 lines
8.1 KiB
TypeScript
207 lines
8.1 KiB
TypeScript
// 权限/连通性面板:麦克风 / 辅助功能 / 全局热键 / Windows IME / 网络。
|
|
// 内含三个状态 Pill + 适配器名称翻译辅助函数。
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Icon } from '../../components/Icon';
|
|
import {
|
|
checkAccessibilityPermission,
|
|
checkMicrophonePermission,
|
|
getHotkeyStatus,
|
|
getWindowsImeStatus,
|
|
openSystemSettings,
|
|
requestAccessibilityPermission,
|
|
requestMicrophonePermission,
|
|
} from '../../lib/ipc';
|
|
import type {
|
|
HotkeyCapability,
|
|
HotkeyStatus,
|
|
PermissionStatus,
|
|
WindowsImeStatus,
|
|
} from '../../lib/types';
|
|
import { useHotkeySettings } from '../../state/HotkeySettingsContext';
|
|
import i18n from '../../i18n';
|
|
import { Btn, Card, Pill } from '../_atoms';
|
|
import { SettingRow } from './shared';
|
|
|
|
export function PermissionsSection() {
|
|
const { t } = useTranslation();
|
|
const [accessibility, setAccessibility] = useState<PermissionStatus | 'loading'>('loading');
|
|
const [microphone, setMicrophone] = useState<PermissionStatus | 'loading'>('loading');
|
|
const [hotkey, setHotkey] = useState<HotkeyStatus | null>(null);
|
|
const [windowsIme, setWindowsIme] = useState<WindowsImeStatus | null>(null);
|
|
const { capability } = useHotkeySettings();
|
|
|
|
const refreshPermissions = async () => {
|
|
const [a, m] = await Promise.all([
|
|
checkAccessibilityPermission(),
|
|
checkMicrophonePermission(),
|
|
]);
|
|
setAccessibility(a);
|
|
setMicrophone(m);
|
|
};
|
|
|
|
const refreshHotkey = async () => {
|
|
setHotkey(await getHotkeyStatus());
|
|
};
|
|
|
|
const refreshWindowsIme = async () => {
|
|
setWindowsIme(await getWindowsImeStatus());
|
|
};
|
|
|
|
useEffect(() => {
|
|
refreshPermissions();
|
|
refreshHotkey();
|
|
refreshWindowsIme();
|
|
const hotkeyId = window.setInterval(refreshHotkey, 1000);
|
|
// 麦克风检查会短暂打开输入流,避免每秒探测导致隐私指示器频繁闪烁。
|
|
const permissionId = window.setInterval(refreshPermissions, 10000);
|
|
const onFocus = () => {
|
|
refreshPermissions();
|
|
refreshHotkey();
|
|
refreshWindowsIme();
|
|
};
|
|
window.addEventListener('focus', onFocus);
|
|
return () => {
|
|
window.clearInterval(hotkeyId);
|
|
window.clearInterval(permissionId);
|
|
window.removeEventListener('focus', onFocus);
|
|
};
|
|
}, []);
|
|
|
|
const reRequestAccessibility = async () => {
|
|
await requestAccessibilityPermission();
|
|
refreshPermissions();
|
|
};
|
|
|
|
const reRequestMicrophone = async () => {
|
|
if (microphone === 'denied' || microphone === 'restricted') {
|
|
await openSystemSettings('microphone');
|
|
refreshPermissions();
|
|
return;
|
|
}
|
|
const status = await requestMicrophonePermission();
|
|
setMicrophone(status);
|
|
if (status === 'denied' || status === 'restricted') {
|
|
await openSystemSettings('microphone');
|
|
}
|
|
refreshPermissions();
|
|
};
|
|
|
|
const desc = capability?.requiresAccessibilityPermission
|
|
? t('settings.permissions.descAcc')
|
|
: t('settings.permissions.descNoAcc');
|
|
|
|
return (
|
|
<Card>
|
|
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 4 }}>{t('settings.permissions.title')}</div>
|
|
<div style={{ fontSize: 11.5, color: 'var(--ol-ink-4)', marginBottom: 6 }}>
|
|
{desc}
|
|
</div>
|
|
<SettingRow label={t('settings.permissions.micLabel')} desc={t('settings.permissions.micDesc')}>
|
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center', justifyContent: 'flex-end', width: '100%' }}>
|
|
<PermissionPill status={microphone} />
|
|
{microphone !== 'granted' && microphone !== 'notApplicable' && microphone !== 'loading' && (
|
|
<Btn variant="ghost" size="sm" onClick={reRequestMicrophone}>
|
|
{microphone === 'denied' || microphone === 'restricted' ? t('settings.permissions.openSystem') : t('settings.permissions.grant')}
|
|
</Btn>
|
|
)}
|
|
</div>
|
|
</SettingRow>
|
|
{capability?.requiresAccessibilityPermission && (
|
|
<SettingRow label={t('settings.permissions.accLabel')} desc={t('settings.permissions.accDesc')}>
|
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
|
<PermissionPill status={accessibility} />
|
|
{accessibility !== 'granted' && accessibility !== 'notApplicable' && (
|
|
<Btn variant="ghost" size="sm" onClick={reRequestAccessibility}>
|
|
{t('settings.permissions.grant')}
|
|
</Btn>
|
|
)}
|
|
</div>
|
|
</SettingRow>
|
|
)}
|
|
<SettingRow
|
|
label={t('settings.permissions.hotkeyLabel')}
|
|
desc={capability ? t('settings.permissions.hotkeyDescWithAdapter', { adapter: adapterDisplayName(capability.adapter) }) : t('settings.permissions.hotkeyDescPlain')}
|
|
>
|
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center', minWidth: 0, justifyContent: 'flex-end', width: '100%' }}>
|
|
{hotkey?.message && (
|
|
<span style={{ fontSize: 11.5, color: 'var(--ol-ink-4)', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
{hotkey.message}
|
|
</span>
|
|
)}
|
|
<HotkeyStatusPill status={hotkey} />
|
|
</div>
|
|
</SettingRow>
|
|
{windowsIme?.state !== 'notWindows' && (
|
|
<SettingRow
|
|
label={t('settings.permissions.windowsImeLabel')}
|
|
desc={t('settings.permissions.windowsImeDesc')}
|
|
>
|
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center', minWidth: 0, justifyContent: 'flex-end', width: '100%' }}>
|
|
{windowsIme && (
|
|
<span style={{ fontSize: 11.5, color: 'var(--ol-ink-4)', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
{t(`settings.permissions.windowsIme.${windowsIme.state}`)}
|
|
</span>
|
|
)}
|
|
<WindowsImeStatusPill status={windowsIme} />
|
|
</div>
|
|
</SettingRow>
|
|
)}
|
|
<SettingRow label={t('settings.permissions.networkLabel')} desc={t('settings.permissions.networkDesc')}>
|
|
<div style={{ display: 'flex', justifyContent: 'flex-end', width: '100%' }}>
|
|
<Pill tone="ok"><Icon name="check" size={11} />{t('settings.permissions.networkOk')}</Pill>
|
|
</div>
|
|
</SettingRow>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function PermissionPill({ status }: { status: PermissionStatus | 'loading' }) {
|
|
const { t } = useTranslation();
|
|
if (status === 'loading') {
|
|
return <Pill tone="default">{t('settings.permissions.checking')}</Pill>;
|
|
}
|
|
if (status === 'granted') {
|
|
return <Pill tone="ok"><Icon name="check" size={11} />{t('settings.permissions.granted')}</Pill>;
|
|
}
|
|
if (status === 'notApplicable') {
|
|
return <Pill tone="default">{t('settings.permissions.notApplicable')}</Pill>;
|
|
}
|
|
if (status === 'denied' || status === 'restricted') {
|
|
return <Pill tone="outline">{t('settings.permissions.denied')}</Pill>;
|
|
}
|
|
return <Pill tone="outline">{t('settings.permissions.indeterminate')}</Pill>;
|
|
}
|
|
|
|
function HotkeyStatusPill({ status }: { status: HotkeyStatus | null }) {
|
|
const { t } = useTranslation();
|
|
if (!status) {
|
|
return <Pill tone="default">{t('settings.permissions.checking')}</Pill>;
|
|
}
|
|
if (status.state === 'installed') {
|
|
return <Pill tone="ok"><Icon name="check" size={11} />{t('settings.permissions.hotkeyInstalled')}</Pill>;
|
|
}
|
|
if (status.state === 'starting') {
|
|
return <Pill tone="default">{t('settings.permissions.hotkeyStarting')}</Pill>;
|
|
}
|
|
return <Pill tone="outline">{t('settings.permissions.hotkeyFailed')}</Pill>;
|
|
}
|
|
|
|
function WindowsImeStatusPill({ status }: { status: WindowsImeStatus | null }) {
|
|
const { t } = useTranslation();
|
|
if (!status) {
|
|
return <Pill tone="default">{t('settings.permissions.checking')}</Pill>;
|
|
}
|
|
if (status.state === 'installed') {
|
|
return <Pill tone="ok"><Icon name="check" size={11} />{t('settings.permissions.windowsImeInstalled')}</Pill>;
|
|
}
|
|
return <Pill tone="outline">{t('settings.permissions.windowsImeUnavailable')}</Pill>;
|
|
}
|
|
|
|
function adapterDisplayName(adapter: HotkeyCapability['adapter'] | HotkeyStatus['adapter']) {
|
|
if (adapter === 'macEventTap') return i18n.t('hotkey.adapter.macEventTap');
|
|
if (adapter === 'windowsLowLevel') return i18n.t('hotkey.adapter.windowsLowLevel');
|
|
return i18n.t('hotkey.adapter.fcitx5');
|
|
}
|