Files
TRIPandHKLHaoBin 6f9af89583 release: OpenLess 2.0.0-Beta.4+build.20260930 (#1126)
Integrate ASR/Omni fixes, API Route, Android IME and confirmation-based vocabulary learning. Complete settings transitions and macOS single-item credential storage. Synchronize all application versions to 2.0.0-Beta.4+build.20260930.

Integrated PRs: #1116, #1120, #1121, #1123, #1124, #1067 and #1128. Linux #1117 remains independent and is excluded from automatic release.

Validated: full cross-platform CI, 18 UI motion cases, 112 frontend test files, 1245 Core tests (1 ignored) and 572 macOS native tests.

Co-authored-by: HKLHaoBin <we3q@qq.com>
2026-09-30 20:32:53 +08:00

808 lines
29 KiB
TypeScript

import { useEffect, useRef, useState, type CSSProperties } from 'react';
import { useTranslation } from 'react-i18next';
import type { TFunction } from 'i18next';
import { Icon } from '../../../src/components/Icon';
import { getSettings, setSettings } from '../../../src/lib/ipc/settings';
import type { UserPreferences } from '../../../src/lib/types';
import { Btn, Pill } from '../../../src/pages/_atoms';
import { SettingRow } from '../../../src/pages/settings/shared';
import { useLayoutStack, useConservativeLayout } from '../../../src/lib/useMobileLayout';
import {
getAndroidAccessibilityStatus,
getAndroidOverlayStatus,
getAndroidShizukuStatus,
openShizukuApp,
recoverAndroidAccessibility,
requestAndroidAccessibilityPermission,
requestAndroidOverlayPermission,
requestAndroidShizukuPermission,
} from '../lib/androidIpc';
import type {
AndroidAccessibilityStatus,
AndroidInsertStrategy,
AndroidOverlayActivationMode,
AndroidOverlayGestureAction,
AndroidOverlayStatus,
AndroidOverlayTrigger,
AndroidPreferenceKey,
AndroidShizukuStatus,
} from '../lib/androidTypes';
import { clampAndroidOverlaySize, normalizeAndroidOverlayTrigger } from '../lib/androidTypes';
type AndroidPrefsSlice = Pick<UserPreferences, AndroidPreferenceKey>;
function pickAndroidPrefs(settings: UserPreferences): AndroidPrefsSlice {
return {
androidInsertStrategy: settings.androidInsertStrategy,
androidOverlayTrigger: settings.androidOverlayTrigger,
androidOverlayActivationMode: settings.androidOverlayActivationMode,
androidOverlayLeftSwipeAction: settings.androidOverlayLeftSwipeAction,
androidOverlayCancelSwipeDirection: settings.androidOverlayCancelSwipeDirection,
androidOverlayGestureActions: settings.androidOverlayGestureActions,
androidOverlaySizeDp: settings.androidOverlaySizeDp,
};
}
let androidOverlaySaveQueue = Promise.resolve();
function enqueueAndroidOverlaySave<T>(task: () => Promise<T>): Promise<T> {
const run = androidOverlaySaveQueue.then(task);
androidOverlaySaveQueue = run.then(
() => undefined,
() => undefined,
);
return run;
}
function persistAndroidOverlayPrefs(patch: Partial<UserPreferences>): Promise<AndroidPrefsSlice> {
return enqueueAndroidOverlaySave(async () => {
const settings = await getSettings();
const next = {
...settings,
...patch,
};
await setSettings(next);
return pickAndroidPrefs(next);
});
}
type AndroidPermissionsPanelMode =
| 'all'
| 'permissionsOnly'
| 'accessibility'
| 'overlayPermission'
| 'overlayConfig';
interface AndroidPermissionsPanelProps {
mode?: AndroidPermissionsPanelMode;
}
export function AndroidPermissionsPanel({ mode = 'all' }: AndroidPermissionsPanelProps) {
const { t } = useTranslation();
const baseLayoutStack = useLayoutStack();
const conservative = useConservativeLayout();
const layoutStack = baseLayoutStack || conservative;
const [androidOverlay, setAndroidOverlay] = useState<AndroidOverlayStatus | null>(null);
const [androidAccessibility, setAndroidAccessibility] =
useState<AndroidAccessibilityStatus | null>(null);
const [androidShizuku, setAndroidShizuku] = useState<AndroidShizukuStatus | null>(null);
const [shizukuRecoveryMessageKey, setShizukuRecoveryMessageKey] = useState<string | null>(null);
const [shizukuActionMessageKey, setShizukuActionMessageKey] = useState<string | null>(null);
const [shizukuRecoveryPending, setShizukuRecoveryPending] = useState(false);
const [androidPrefs, setAndroidPrefs] = useState<Pick<
UserPreferences,
AndroidPreferenceKey
> | null>(null);
const [sizeDraft, setSizeDraft] = useState<number | null>(null);
const sizeDebounceRef = useRef<number | null>(null);
const sizePendingRef = useRef(false);
const refreshAndroid = async () => {
const [overlayResult, accessibilityResult, shizukuResult, settingsResult] =
await Promise.allSettled([
getAndroidOverlayStatus(),
getAndroidAccessibilityStatus(),
getAndroidShizukuStatus(),
getSettings(),
]);
if (overlayResult.status === 'fulfilled') {
setAndroidOverlay(overlayResult.value);
}
if (accessibilityResult.status === 'fulfilled') {
setAndroidAccessibility(accessibilityResult.value);
}
if (shizukuResult.status === 'fulfilled') {
const shizuku = shizukuResult.value;
setAndroidShizuku((prev) => {
const operational = shizuku.accessibility.operational;
if (operational && !prev?.accessibility.operational) {
setShizukuRecoveryMessageKey(null);
}
if (shizuku.lastPermissionMessageKey) {
setShizukuActionMessageKey(null);
}
return shizuku;
});
}
if (settingsResult.status === 'fulfilled') {
let migratedSettings = settingsResult.value;
if (migratedSettings.androidOverlayTrigger === 'keyboard') {
const migratedPrefs = await persistAndroidOverlayPrefs({
androidOverlayTrigger: normalizeAndroidOverlayTrigger(
migratedSettings.androidOverlayTrigger,
),
});
migratedSettings = { ...migratedSettings, ...migratedPrefs };
}
setAndroidPrefs(pickAndroidPrefs(migratedSettings));
}
};
const handleRecoverAccessibility = async () => {
if (shizukuRecoveryPending) return;
const confirmed = window.confirm(t('settings.permissions.androidShizukuRecoverConfirm'));
if (!confirmed) return;
setShizukuActionMessageKey(null);
setShizukuRecoveryPending(true);
try {
const result = await recoverAndroidAccessibility(true);
setShizukuRecoveryMessageKey(result.messageKey);
await refreshAndroid();
} finally {
setShizukuRecoveryPending(false);
}
};
const shizukuDisplayMessageKey =
androidShizuku?.lastPermissionMessageKey ??
shizukuRecoveryMessageKey ??
shizukuActionMessageKey ??
androidShizuku?.messageKey ??
null;
useEffect(() => {
void refreshAndroid();
const androidId = window.setInterval(refreshAndroid, 3000);
const onFocus = () => {
void refreshAndroid();
};
window.addEventListener('focus', onFocus);
return () => {
window.clearInterval(androidId);
window.removeEventListener('focus', onFocus);
};
}, []);
useEffect(() => {
if (sizePendingRef.current) return;
if (androidPrefs?.androidOverlaySizeDp != null) {
setSizeDraft(androidPrefs.androidOverlaySizeDp);
}
}, [androidPrefs?.androidOverlaySizeDp]);
useEffect(() => {
return () => {
if (sizeDebounceRef.current) clearTimeout(sizeDebounceRef.current);
};
}, []);
const saveAndroidOverlaySize = async (value: number) => {
const clamped = clampAndroidOverlaySize(value);
try {
const nextPrefs = await persistAndroidOverlayPrefs({ androidOverlaySizeDp: clamped });
setAndroidPrefs((prev) => (prev ? { ...prev, ...nextPrefs } : nextPrefs));
setSizeDraft(clamped);
} catch (error) {
console.error('[android] failed to save overlay size', error);
const settings = await getSettings();
const rolledBack = settings.androidOverlaySizeDp;
const safeDraft = rolledBack != null ? clampAndroidOverlaySize(rolledBack) : null;
setAndroidPrefs((prev) => (prev ? { ...prev, androidOverlaySizeDp: rolledBack } : null));
setSizeDraft(safeDraft);
} finally {
sizePendingRef.current = false;
}
};
const scheduleAndroidOverlaySizeSave = (value: number) => {
if (sizeDebounceRef.current) clearTimeout(sizeDebounceRef.current);
sizeDebounceRef.current = window.setTimeout(() => {
sizeDebounceRef.current = null;
void saveAndroidOverlaySize(value);
}, 200);
};
const flushAndroidOverlaySizeSave = (value: number) => {
const hasDebounce = sizeDebounceRef.current != null;
const hasPending = sizePendingRef.current;
if (!hasDebounce && !hasPending) return;
if (sizeDebounceRef.current) {
clearTimeout(sizeDebounceRef.current);
sizeDebounceRef.current = null;
}
const clamped = clampAndroidOverlaySize(value);
const committed = androidPrefs?.androidOverlaySizeDp;
if (committed != null && clamped === committed) {
sizePendingRef.current = false;
return;
}
void saveAndroidOverlaySize(clamped);
};
const handleAndroidOverlaySizeChange = (value: number) => {
const clamped = clampAndroidOverlaySize(value);
sizePendingRef.current = true;
setSizeDraft(clamped);
scheduleAndroidOverlaySizeSave(clamped);
};
const updateAndroidPref = async <K extends AndroidPreferenceKey>(
key: K,
value: UserPreferences[K],
) => {
if (key !== 'androidOverlaySizeDp' && sizeDebounceRef.current) {
clearTimeout(sizeDebounceRef.current);
sizeDebounceRef.current = null;
}
const patch: Partial<UserPreferences> = {
[key]:
key === 'androidOverlayTrigger'
? normalizeAndroidOverlayTrigger(value as AndroidOverlayTrigger)
: value,
};
if (key !== 'androidOverlaySizeDp') {
const committedSize = androidPrefs?.androidOverlaySizeDp;
const draftSize = sizeDraft;
if (sizePendingRef.current || (draftSize != null && draftSize !== committedSize)) {
patch.androidOverlaySizeDp = clampAndroidOverlaySize(draftSize ?? committedSize ?? 72);
}
sizePendingRef.current = false;
}
try {
const nextPrefs = await persistAndroidOverlayPrefs(patch);
setAndroidPrefs(nextPrefs);
if (patch.androidOverlaySizeDp != null) {
setSizeDraft(nextPrefs.androidOverlaySizeDp);
}
await refreshAndroid();
} catch (error) {
console.error('[android] failed to save overlay pref', error);
await refreshAndroid();
}
};
const showOverlayPermission =
mode === 'all' || mode === 'permissionsOnly' || mode === 'overlayPermission';
const showAccessibility =
mode === 'all' || mode === 'permissionsOnly' || mode === 'accessibility';
const showOverlayConfig = mode === 'all' || mode === 'overlayConfig';
const rowJustify = layoutStack ? 'flex-start' : 'flex-end';
const colAlign = layoutStack ? 'stretch' : 'flex-end';
const hintAlign = layoutStack ? 'left' : 'right';
const hintMaxWidth = layoutStack ? undefined : 300;
const hintMaxWidthSm = layoutStack ? undefined : 220;
const selectStyle = layoutStack
? { width: '100%', maxWidth: '100%' as const }
: { minWidth: 180, maxWidth: '100%' as const };
const actionRowStyle: CSSProperties = {
display: 'flex',
flexDirection: conservative ? 'column' : 'row',
gap: 8,
alignItems: conservative ? 'flex-start' : layoutStack ? 'flex-start' : 'center',
justifyContent: rowJustify,
width: '100%',
flexWrap: conservative ? 'nowrap' : 'wrap',
minWidth: 0,
};
return (
<>
{showOverlayPermission && (
<SettingRow label={t('settings.permissions.androidOverlayLabel')}>
<div className="ol-flex-row" style={actionRowStyle}>
{androidOverlay?.message && (
<span
style={{
fontSize: 11.5,
color: 'var(--ol-ink-4)',
maxWidth: hintMaxWidthSm,
textAlign: hintAlign,
}}
>
{androidOverlay.message}
</span>
)}
<AndroidOverlayStatusPill status={androidOverlay} />
{androidOverlay?.permission !== 'granted' && (
<Btn
variant="ghost"
size="sm"
onClick={() => {
void requestAndroidOverlayPermission().then(refreshAndroid);
}}
>
{t('settings.permissions.grant')}
</Btn>
)}
</div>
</SettingRow>
)}
{showAccessibility && (
<SettingRow label={t('settings.permissions.androidAccessibilityLabel')}>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 6,
alignItems: colAlign,
width: '100%',
minWidth: 0,
}}
>
<div className="ol-flex-row" style={actionRowStyle}>
{resolveAccessibilityMessage(t, androidAccessibility?.messageKey) && (
<span
style={{
fontSize: 11.5,
color: 'var(--ol-ink-4)',
maxWidth: hintMaxWidthSm,
textAlign: hintAlign,
}}
>
{resolveAccessibilityMessage(t, androidAccessibility?.messageKey)}
</span>
)}
<AndroidAccessibilityStatusPill status={androidAccessibility} />
{(!androidAccessibility?.enabled || androidAccessibility?.operational === false) && (
<Btn
variant="ghost"
size="sm"
onClick={() => {
void requestAndroidAccessibilityPermission().then(refreshAndroid);
}}
>
{t('settings.permissions.openSystem')}
</Btn>
)}
</div>
<span
style={{
fontSize: 11,
color: 'var(--ol-ink-4)',
maxWidth: hintMaxWidth,
textAlign: hintAlign,
}}
>
{t('settings.permissions.androidAccessibilityImpact')}
</span>
</div>
</SettingRow>
)}
{showAccessibility && (
<SettingRow label={t('settings.permissions.androidShizukuLabel')}>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 6,
alignItems: colAlign,
width: '100%',
minWidth: 0,
}}
>
<div className="ol-flex-row" style={actionRowStyle}>
{resolveShizukuMessage(t, shizukuDisplayMessageKey) && (
<span
style={{
fontSize: 11.5,
color: 'var(--ol-ink-4)',
maxWidth: hintMaxWidthSm,
textAlign: hintAlign,
}}
>
{resolveShizukuMessage(t, shizukuDisplayMessageKey)}
</span>
)}
<AndroidShizukuStatusPill status={androidShizuku} />
{(androidShizuku?.state === 'notInstalled' ||
androidShizuku?.state === 'notRunning' ||
androidShizuku?.state === 'binderDead') && (
<Btn
variant="ghost"
size="sm"
onClick={() => {
setShizukuRecoveryMessageKey(null);
setShizukuActionMessageKey(null);
void openShizukuApp().then((result) => {
if (!result.launched) {
setShizukuActionMessageKey(result.messageKey);
}
return refreshAndroid();
});
}}
>
{t('settings.permissions.androidShizukuOpenApp')}
</Btn>
)}
{androidShizuku?.state === 'notAuthorized' && (
<Btn
variant="ghost"
size="sm"
onClick={() => {
setShizukuRecoveryMessageKey(null);
setShizukuActionMessageKey(null);
void requestAndroidShizukuPermission().then((result) => {
if (!result.launched) {
setShizukuActionMessageKey(result.messageKey);
}
return refreshAndroid();
});
}}
>
{t('settings.permissions.androidShizukuRequestPermission')}
</Btn>
)}
{androidShizuku?.state === 'authorized' &&
!androidShizuku.accessibility.operational && (
<Btn
variant="ghost"
size="sm"
disabled={shizukuRecoveryPending}
onClick={() => {
void handleRecoverAccessibility();
}}
>
{shizukuRecoveryPending
? t('settings.permissions.checking')
: t('settings.permissions.androidShizukuRecover')}
</Btn>
)}
{shizukuRecoveryMessageKey &&
androidShizuku?.state === 'authorized' &&
!androidShizuku.accessibility.operational &&
(shizukuRecoveryMessageKey === 'partial_rollback' ||
shizukuRecoveryMessageKey === 'manual_required' ||
shizukuRecoveryMessageKey === 'oem_rollback' ||
shizukuRecoveryMessageKey === 'concurrent_change') && (
<Btn
variant="ghost"
size="sm"
onClick={() => {
void requestAndroidAccessibilityPermission().then(refreshAndroid);
}}
>
{t('settings.permissions.openSystem')}
</Btn>
)}
</div>
{androidShizuku?.state === 'authorized' && (
<span
style={{
fontSize: 11,
color: 'var(--ol-ink-4)',
maxWidth: hintMaxWidth,
textAlign: hintAlign,
}}
>
{androidShizuku.accessibility.operational
? t('settings.permissions.androidShizukuAccessibilityOperational')
: t('settings.permissions.androidShizukuAccessibilityRegistered', {
registered: androidShizuku.accessibility.registered
? t('settings.permissions.androidShizukuYes')
: t('settings.permissions.androidShizukuNo'),
operational: androidShizuku.accessibility.operational
? t('settings.permissions.androidShizukuYes')
: t('settings.permissions.androidShizukuNo'),
})}
</span>
)}
<span
style={{
fontSize: 11,
color: 'var(--ol-ink-4)',
maxWidth: hintMaxWidth,
textAlign: hintAlign,
}}
>
{t('settings.permissions.androidShizukuHint')}
</span>
</div>
</SettingRow>
)}
{showOverlayConfig && (
<>
<SettingRow label={t('settings.permissions.androidInsertStrategyLabel')}>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 6,
alignItems: colAlign,
width: '100%',
}}
>
<select
value={androidPrefs?.androidInsertStrategy ?? 'accessibility'}
onChange={(event) => {
void updateAndroidPref(
'androidInsertStrategy',
event.target.value as AndroidInsertStrategy,
);
}}
style={selectStyle}
>
<option value="accessibility">
{t('settings.permissions.androidInsertStrategy.accessibility')}
</option>
<option value="clipboard">
{t('settings.permissions.androidInsertStrategy.clipboard')}
</option>
</select>
<span style={{ fontSize: 11, color: 'var(--ol-ink-4)', textAlign: hintAlign }}>
{t(
`settings.permissions.androidInsertStrategyHint.${androidPrefs?.androidInsertStrategy ?? 'accessibility'}`,
)}
</span>
</div>
</SettingRow>
<SettingRow label={t('settings.permissions.androidOverlayTriggerLabel')}>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 6,
alignItems: colAlign,
width: '100%',
}}
>
<select
value={androidPrefs?.androidOverlayTrigger ?? 'background'}
onChange={(event) => {
void updateAndroidPref(
'androidOverlayTrigger',
event.target.value as AndroidOverlayTrigger,
);
}}
style={selectStyle}
>
<option value="background">
{t('settings.permissions.androidOverlayTrigger.background')}
</option>
<option value="keyboard" disabled>
{t('settings.permissions.androidOverlayTrigger.keyboard')}
</option>
<option value="always">
{t('settings.permissions.androidOverlayTrigger.always')}
</option>
</select>
<span style={{ fontSize: 11, color: 'var(--ol-ink-4)', textAlign: hintAlign }}>
{t(
`settings.permissions.androidOverlayTriggerHint.${androidPrefs?.androidOverlayTrigger ?? 'background'}`,
)}
</span>
<span style={{ fontSize: 11, color: 'var(--ol-ink-4)', textAlign: hintAlign }}>
{t('settings.permissions.androidOverlayTriggerDisabled.keyboard')}
</span>
</div>
</SettingRow>
<SettingRow label={t('settings.permissions.androidOverlayActivationModeLabel')}>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 6,
alignItems: colAlign,
width: '100%',
}}
>
<select
value={androidPrefs?.androidOverlayActivationMode ?? 'tap'}
onChange={(event) => {
void updateAndroidPref(
'androidOverlayActivationMode',
event.target.value as AndroidOverlayActivationMode,
);
}}
style={selectStyle}
>
<option value="tap">
{t('settings.permissions.androidOverlayActivationMode.tap')}
</option>
<option value="long_press">
{t('settings.permissions.androidOverlayActivationMode.long_press')}
</option>
</select>
<span style={{ fontSize: 11, color: 'var(--ol-ink-4)', textAlign: hintAlign }}>
{t(
`settings.permissions.androidOverlayActivationModeHint.${androidPrefs?.androidOverlayActivationMode ?? 'tap'}`,
)}
</span>
</div>
</SettingRow>
<SettingRow
label={t(
'settings.permissions.androidOverlayGestureActionsLabel',
'Overlay swipe actions',
)}
desc={t(
'settings.permissions.androidOverlayGestureActionsDesc',
'These actions apply while recording. A normal tap finishes ordinary dictation; a Quick note swipe keeps the audio permanently.',
)}
>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
gap: 8,
width: '100%',
}}
>
{(['up', 'down', 'left', 'right'] as const).map((direction) => (
<label
key={direction}
style={{ display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}
>
<span style={{ fontSize: 11, color: 'var(--ol-ink-3)' }}>
{t(
`settings.permissions.androidOverlayGestureDirection.${direction}`,
direction,
)}
</span>
<select
value={androidPrefs?.androidOverlayGestureActions?.[direction] ?? 'none'}
onChange={(event) => {
const actions = androidPrefs?.androidOverlayGestureActions ?? {
up: 'cancel',
down: 'none',
left: 'translation',
right: 'qa',
};
void updateAndroidPref('androidOverlayGestureActions', {
...actions,
[direction]: event.target.value as AndroidOverlayGestureAction,
});
}}
style={selectStyle}
>
{(
['none', 'quick_note', 'translation', 'style_pack', 'cancel', 'qa'] as const
).map((action) => (
<option key={action} value={action}>
{t(
`settings.permissions.androidOverlayGestureAction.${action}`,
action,
)}
</option>
))}
</select>
</label>
))}
</div>
</SettingRow>
<SettingRow label={t('settings.permissions.androidOverlaySizeLabel')}>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 6,
alignItems: colAlign,
width: '100%',
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 10,
width: '100%',
maxWidth: '100%',
}}
>
<input
type="range"
min={48}
max={120}
step={4}
value={sizeDraft ?? androidPrefs?.androidOverlaySizeDp ?? 72}
onChange={(event) => {
handleAndroidOverlaySizeChange(Number(event.target.value));
}}
onPointerUp={(event) => {
flushAndroidOverlaySizeSave(Number(event.currentTarget.value));
}}
onTouchEnd={(event) => {
flushAndroidOverlaySizeSave(Number(event.currentTarget.value));
}}
onBlur={(event) => {
flushAndroidOverlaySizeSave(Number(event.currentTarget.value));
}}
style={{ flex: 1, minWidth: 0 }}
/>
<span
style={{
fontSize: 12,
color: 'var(--ol-ink-3)',
minWidth: 42,
textAlign: hintAlign,
}}
>
{sizeDraft ?? androidPrefs?.androidOverlaySizeDp ?? 72} dp
</span>
</div>
<span style={{ fontSize: 11, color: 'var(--ol-ink-4)', textAlign: hintAlign }}>
{t('settings.permissions.androidOverlaySizeHint')}
</span>
</div>
</SettingRow>
</>
)}
</>
);
}
function AndroidOverlayStatusPill({ status }: { status: AndroidOverlayStatus | null }) {
const { t } = useTranslation();
if (!status) return <Pill tone="default">{t('settings.permissions.checking')}</Pill>;
if (status.permission === 'granted') {
return (
<Pill tone="ok">
<Icon name="check" size={11} />
{t('settings.permissions.granted')}
</Pill>
);
}
return <Pill tone="outline">{t('settings.permissions.denied')}</Pill>;
}
function AndroidAccessibilityStatusPill({ status }: { status: AndroidAccessibilityStatus | null }) {
const { t } = useTranslation();
if (!status) return <Pill tone="default">{t('settings.permissions.checking')}</Pill>;
if (status.enabled && status.operational === false) {
return <Pill tone="outline">{t('settings.permissions.androidAccessibilityGrantedStale')}</Pill>;
}
if (status.enabled) {
return (
<Pill tone="ok">
<Icon name="check" size={11} />
{t('settings.permissions.granted')}
</Pill>
);
}
return <Pill tone="outline">{t('settings.permissions.denied')}</Pill>;
}
function resolveShizukuMessage(t: TFunction, key: string | null | undefined): string {
if (!key) return '';
return t(`settings.permissions.androidShizukuMessages.${key}`, { defaultValue: key });
}
function resolveAccessibilityMessage(t: TFunction, key: string | null | undefined): string {
if (!key) return '';
return t(`settings.permissions.androidAccessibilityMessages.${key}`, { defaultValue: '' });
}
function AndroidShizukuStatusPill({ status }: { status: AndroidShizukuStatus | null }) {
const { t } = useTranslation();
if (!status) return <Pill tone="default">{t('settings.permissions.checking')}</Pill>;
const labelKey = `settings.permissions.androidShizukuState.${status.state}` as const;
if (status.state === 'authorized') {
return (
<Pill tone="ok">
<Icon name="check" size={11} />
{t(labelKey)}
</Pill>
);
}
if (status.state === 'notAndroid') {
return <Pill tone="default">{t(labelKey)}</Pill>;
}
return <Pill tone="outline">{t(labelKey)}</Pill>;
}