Files
openless/openless-all/app/src/pages/settings/PermissionsSection.tsx
T
aeoformandClaude Opus 4.7 b349a4dcba fix(linux): remove dead rdev code and leftover Wayland references
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>
2026-05-16 09:01:56 +08:00

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');
}