Statt Half-Donut (Höhen-Mismatch mit Circles): zwei volle Circles (Mobil/Computer) + darunter ein eigener animierter Balken (grün/blau-Segmente, gleiche Easing/Dauer wie die Ringe) mit Legende. Kein native-Default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
109 lines
3.2 KiB
TypeScript
109 lines
3.2 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
import { Animated, Easing, Text, View } from 'react-native';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useColors } from '../../lib/theme';
|
|
import { MOBILE_COLOR, DESKTOP_COLOR } from './DeviceSlotDonut';
|
|
|
|
/**
|
|
* Animierter Verteilungs-Balken (Gesamt) — gleiche Design-Sprache wie die
|
|
* Slot-Ringe (grün=Mobil, blau=Computer, gleiche Easing/Dauer). Sitzt unter
|
|
* den beiden Circles und zeigt die Aufteilung mobil/stationär.
|
|
*/
|
|
export function DeviceDistributionBar({
|
|
mobile,
|
|
desktop,
|
|
total,
|
|
}: {
|
|
mobile: number;
|
|
desktop: number;
|
|
total: number;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const colors = useColors();
|
|
const safeTotal = Math.max(1, total);
|
|
const used = mobile + desktop;
|
|
|
|
const anim = useRef(new Animated.Value(0)).current;
|
|
useEffect(() => {
|
|
anim.setValue(0);
|
|
Animated.timing(anim, {
|
|
toValue: 1,
|
|
duration: 1400,
|
|
easing: Easing.out(Easing.cubic),
|
|
useNativeDriver: false,
|
|
}).start();
|
|
}, [mobile, desktop, total, anim]);
|
|
|
|
const mobileW = anim.interpolate({
|
|
inputRange: [0, 1],
|
|
outputRange: ['0%', `${(mobile / safeTotal) * 100}%`],
|
|
});
|
|
const desktopW = anim.interpolate({
|
|
inputRange: [0, 1],
|
|
outputRange: ['0%', `${(desktop / safeTotal) * 100}%`],
|
|
});
|
|
|
|
return (
|
|
<View style={{ gap: 8 }}>
|
|
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
<Text
|
|
style={{
|
|
fontSize: 11,
|
|
color: colors.textMuted,
|
|
fontFamily: 'Nunito_700Bold',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 0.8,
|
|
}}
|
|
>
|
|
{t('devices.progress_total')}
|
|
</Text>
|
|
<Text style={{ fontSize: 14, color: colors.text, fontFamily: 'Nunito_900Black', letterSpacing: -0.3 }}>
|
|
{used}
|
|
<Text style={{ fontSize: 12, color: colors.textMuted, fontFamily: 'Nunito_700Bold' }}>
|
|
/{total}
|
|
</Text>
|
|
</Text>
|
|
</View>
|
|
|
|
<View
|
|
style={{
|
|
flexDirection: 'row',
|
|
height: 10,
|
|
borderRadius: 5,
|
|
backgroundColor: colors.surfaceElevated,
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
<Animated.View style={{ width: mobileW, height: '100%', backgroundColor: MOBILE_COLOR }} />
|
|
<Animated.View style={{ width: desktopW, height: '100%', backgroundColor: DESKTOP_COLOR }} />
|
|
</View>
|
|
|
|
<View style={{ flexDirection: 'row', gap: 16 }}>
|
|
<Legend color={MOBILE_COLOR} label={t('devices.progress_mobile')} count={mobile} colors={colors} />
|
|
<Legend color={DESKTOP_COLOR} label={t('devices.progress_desktop')} count={desktop} colors={colors} />
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function Legend({
|
|
color,
|
|
label,
|
|
count,
|
|
colors,
|
|
}: {
|
|
color: string;
|
|
label: string;
|
|
count: number;
|
|
colors: ReturnType<typeof useColors>;
|
|
}) {
|
|
return (
|
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
|
|
<View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: color }} />
|
|
<Text style={{ fontSize: 12, color: colors.textMuted, fontFamily: 'Nunito_600SemiBold' }}>
|
|
{label} {count}
|
|
</Text>
|
|
</View>
|
|
);
|
|
}
|