feat(native): replace device text-counter with animated progress bar
- DeviceProgressBar component: 6px pill-bar, Animated.timing (380ms) on count change, brandOrange at limit / success otherwise - devices.tsx: swaps counterText block for <DeviceProgressBar> (Legend-only gating preserved) - locales (de/en/fr): counter_some/counter_limit → progress_label + progress_at_limit Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
701e32c36e
commit
a9fb9273b8
@ -21,6 +21,7 @@ import { useUserPlan } from '../hooks/useUserPlan';
|
|||||||
import { AppHeader } from '../components/AppHeader';
|
import { AppHeader } from '../components/AppHeader';
|
||||||
import { AddMacSheet } from '../components/devices/AddMacSheet';
|
import { AddMacSheet } from '../components/devices/AddMacSheet';
|
||||||
import { AddWindowsSheet } from '../components/devices/AddWindowsSheet';
|
import { AddWindowsSheet } from '../components/devices/AddWindowsSheet';
|
||||||
|
import { DeviceProgressBar } from '../components/devices/DeviceProgressBar';
|
||||||
|
|
||||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -411,16 +412,6 @@ export default function DevicesScreen() {
|
|||||||
const currentDevice = mobileDevices.find((d) => d.isCurrent);
|
const currentDevice = mobileDevices.find((d) => d.isCurrent);
|
||||||
const subtitle = isLegend ? t('devices.subtitle_legend') : t('devices.subtitle_free');
|
const subtitle = isLegend ? t('devices.subtitle_legend') : t('devices.subtitle_free');
|
||||||
|
|
||||||
const counterText = isLegend
|
|
||||||
? atDeviceLimit
|
|
||||||
? t('devices.counter_limit', { max: TOTAL_DEVICE_SLOTS })
|
|
||||||
: t('devices.counter_some', {
|
|
||||||
count: totalRegistered,
|
|
||||||
max: TOTAL_DEVICE_SLOTS,
|
|
||||||
remaining: TOTAL_DEVICE_SLOTS - totalRegistered,
|
|
||||||
})
|
|
||||||
: null;
|
|
||||||
|
|
||||||
async function handleRemoveProtected(id: string) {
|
async function handleRemoveProtected(id: string) {
|
||||||
try {
|
try {
|
||||||
const { manualRemovalRequired } = await removeProtected(id);
|
const { manualRemovalRequired } = await removeProtected(id);
|
||||||
@ -446,8 +437,8 @@ export default function DevicesScreen() {
|
|||||||
}}
|
}}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
{/* Subtitle + counter */}
|
{/* Subtitle + progress */}
|
||||||
<View style={{ gap: 4, marginBottom: -12 }}>
|
<View style={{ gap: 8, marginBottom: -12 }}>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
@ -458,16 +449,12 @@ export default function DevicesScreen() {
|
|||||||
>
|
>
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</Text>
|
</Text>
|
||||||
{counterText ? (
|
{isLegend ? (
|
||||||
<Text
|
<DeviceProgressBar
|
||||||
style={{
|
count={totalRegistered}
|
||||||
fontSize: 12,
|
max={TOTAL_DEVICE_SLOTS}
|
||||||
color: atDeviceLimit ? colors.brandOrange : colors.textMuted,
|
atLimit={atDeviceLimit}
|
||||||
fontFamily: 'Nunito_600SemiBold',
|
/>
|
||||||
}}
|
|
||||||
>
|
|
||||||
{counterText}
|
|
||||||
</Text>
|
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
76
apps/rebreak-native/components/devices/DeviceProgressBar.tsx
Normal file
76
apps/rebreak-native/components/devices/DeviceProgressBar.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeviceProgressBar({ count, max, atLimit }: 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
|
||||||
|
? t('devices.progress_at_limit')
|
||||||
|
: 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -890,8 +890,8 @@
|
|||||||
"remove_warning_title": "Profile manuell entfernen",
|
"remove_warning_title": "Profile manuell entfernen",
|
||||||
"remove_warning_body": "Wir können das Profile nicht aus der Ferne löschen. Auf dem Mac: Systemeinstellungen → Profile → ReBreak → Entfernen (Admin-Passwort nötig).",
|
"remove_warning_body": "Wir können das Profile nicht aus der Ferne löschen. Auf dem Mac: Systemeinstellungen → Profile → ReBreak → Entfernen (Admin-Passwort nötig).",
|
||||||
"add_device": "Neues Gerät hinzufügen",
|
"add_device": "Neues Gerät hinzufügen",
|
||||||
"counter_some": "%{count} von %{max} Geräten · noch %{remaining} frei",
|
"progress_label": "%{count} von %{max} Geräten",
|
||||||
"counter_limit": "Maximum erreicht — %{max} von %{max} Geräten",
|
"progress_at_limit": "Maximum erreicht",
|
||||||
"add_windows_enabled": "Windows-PC hinzufügen",
|
"add_windows_enabled": "Windows-PC hinzufügen",
|
||||||
"windows_label_question": "Wie soll der Windows-PC heißen?",
|
"windows_label_question": "Wie soll der Windows-PC heißen?",
|
||||||
"windows_label_default": "Windows-PC",
|
"windows_label_default": "Windows-PC",
|
||||||
|
|||||||
@ -890,8 +890,8 @@
|
|||||||
"remove_warning_title": "Remove profile manually",
|
"remove_warning_title": "Remove profile manually",
|
||||||
"remove_warning_body": "We can't delete the profile remotely. On the Mac: System Settings → Profiles → ReBreak → Remove (admin password required).",
|
"remove_warning_body": "We can't delete the profile remotely. On the Mac: System Settings → Profiles → ReBreak → Remove (admin password required).",
|
||||||
"add_device": "Add new device",
|
"add_device": "Add new device",
|
||||||
"counter_some": "%{count} of %{max} devices · %{remaining} more available",
|
"progress_label": "%{count} of %{max} devices",
|
||||||
"counter_limit": "Maximum reached — %{max} of %{max} devices",
|
"progress_at_limit": "Maximum reached",
|
||||||
"add_windows_enabled": "Add Windows PC",
|
"add_windows_enabled": "Add Windows PC",
|
||||||
"windows_label_question": "What should this Windows PC be called?",
|
"windows_label_question": "What should this Windows PC be called?",
|
||||||
"windows_label_default": "Windows PC",
|
"windows_label_default": "Windows PC",
|
||||||
|
|||||||
@ -887,8 +887,8 @@
|
|||||||
"remove_warning_title": "Supprimer le profil manuellement",
|
"remove_warning_title": "Supprimer le profil manuellement",
|
||||||
"remove_warning_body": "Nous ne pouvons pas supprimer le profil à distance. Sur le Mac : Réglages système → Profils → ReBreak → Supprimer (mot de passe administrateur requis).",
|
"remove_warning_body": "Nous ne pouvons pas supprimer le profil à distance. Sur le Mac : Réglages système → Profils → ReBreak → Supprimer (mot de passe administrateur requis).",
|
||||||
"add_device": "Ajouter un appareil",
|
"add_device": "Ajouter un appareil",
|
||||||
"counter_some": "%{count} sur %{max} appareils · encore %{remaining} disponible",
|
"progress_label": "%{count} sur %{max} appareils",
|
||||||
"counter_limit": "Maximum atteint — %{max} sur %{max} appareils",
|
"progress_at_limit": "Maximum atteint",
|
||||||
"add_windows_enabled": "Ajouter un PC Windows",
|
"add_windows_enabled": "Ajouter un PC Windows",
|
||||||
"windows_label_question": "Comment appeler ce PC Windows ?",
|
"windows_label_question": "Comment appeler ce PC Windows ?",
|
||||||
"windows_label_default": "PC Windows",
|
"windows_label_default": "PC Windows",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user