Tamper-Lock von Keyword-Scanning auf präzise Einzel-Surfaces umgebaut: blockt nur ReBreaks eigene Screens (Admin-Deaktivierung via DeviceAdminAdd, a11y-Ausschalten, VPN-Trennen/Surface), nie Listen oder fremde Apps. - Deny-Removal = Admin-only: OS graut Uninstall+Force-Stop für aktiven Device-Admin aus; einziger Bypass (Admin deaktivieren) bleibt a11y-gesperrt. Andere Apps verwalten/force-stoppen/deinstallieren bleibt komplett frei. - a11y-Onboarding: passiver Bottom-Overlay-Hinweis + Settings-Reset auf Startseite nach Aktivierung + 1s-Delay vor App-Rückkehr. - VPN-Trennen-Dialog + a11y-Ausschalten neu abgedeckt. - a11y-Service-Icon im Plugin (klar als ReBreak erkennbar). Verifiziert auf A50 per logcat: alle 4 Surfaces blocken, Listen + fremde Apps frei, keine False-Positives. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
79 lines
2.1 KiB
TypeScript
79 lines
2.1 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
import { Animated, Text, View } from 'react-native';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useColors } from '../../lib/theme';
|
|
|
|
interface DeviceProgressBarProps {
|
|
count: number;
|
|
max: number;
|
|
atLimit: boolean;
|
|
/** Optionales Label (z.B. "Mobil" / "Computer") statt generischem progress_label */
|
|
label?: string;
|
|
}
|
|
|
|
export function DeviceProgressBar({ count, max, atLimit, label }: DeviceProgressBarProps) {
|
|
const { t } = useTranslation();
|
|
const colors = useColors();
|
|
const fillAnim = useRef(new Animated.Value(0)).current;
|
|
|
|
const ratio = max > 0 ? Math.min(count / max, 1) : 0;
|
|
|
|
useEffect(() => {
|
|
Animated.timing(fillAnim, {
|
|
toValue: ratio,
|
|
duration: 380,
|
|
useNativeDriver: false,
|
|
}).start();
|
|
}, [ratio]);
|
|
|
|
const fillColor = atLimit ? colors.brandOrange : colors.success;
|
|
|
|
return (
|
|
<View style={{ gap: 5 }}>
|
|
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
<Text
|
|
style={{
|
|
fontSize: 12,
|
|
color: atLimit ? colors.brandOrange : colors.textMuted,
|
|
fontFamily: 'Nunito_600SemiBold',
|
|
}}
|
|
>
|
|
{atLimit
|
|
? (label ? `${label} — ${t('devices.progress_at_limit')}` : t('devices.progress_at_limit'))
|
|
: (label ?? t('devices.progress_label', { count, max }))}
|
|
</Text>
|
|
<Text
|
|
style={{
|
|
fontSize: 11,
|
|
color: colors.textMuted,
|
|
fontFamily: 'Nunito_400Regular',
|
|
}}
|
|
>
|
|
{count}/{max}
|
|
</Text>
|
|
</View>
|
|
|
|
<View
|
|
style={{
|
|
height: 6,
|
|
borderRadius: 3,
|
|
backgroundColor: colors.surfaceElevated,
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
<Animated.View
|
|
style={{
|
|
height: '100%',
|
|
borderRadius: 3,
|
|
backgroundColor: fillColor,
|
|
width: fillAnim.interpolate({
|
|
inputRange: [0, 1],
|
|
outputRange: ['0%', '100%'],
|
|
}),
|
|
}}
|
|
/>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|