fix(native): Gesamt-Verteilung als Half-Donut, 2 Circles + 1 Donut (gleiche Größe)

DeviceSlotDonut bekommt half-Modus. Reihenfolge: Mobil-Circle, Computer-Circle,
Gesamt-Half-Donut (Mobil/Computer-Anteil als zwei Bögen). Alle SIZE×SIZE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-06-08 00:30:08 +02:00
parent 77ce5e5a80
commit c3478f4743
2 changed files with 35 additions and 27 deletions

View File

@ -577,6 +577,13 @@ export default function DevicesScreen() {
label={t('devices.progress_mobile')}
/>
<DeviceSlotDonut
segments={[{ value: desktopCount, color: DESKTOP_COLOR }]}
total={desktopLimit}
atLimit={atDesktopLimit}
label={t('devices.progress_desktop')}
/>
<DeviceSlotDonut
half
segments={[
{ value: mobileCount, color: MOBILE_COLOR },
{ value: desktopCount, color: DESKTOP_COLOR },
@ -585,12 +592,6 @@ export default function DevicesScreen() {
atLimit={mobileCount + desktopCount >= mobileLimit + desktopLimit}
label={t('devices.progress_total')}
/>
<DeviceSlotDonut
segments={[{ value: desktopCount, color: DESKTOP_COLOR }]}
total={desktopLimit}
atLimit={atDesktopLimit}
label={t('devices.progress_desktop')}
/>
</View>
</View>

View File

@ -7,7 +7,7 @@ const SIZE = 88;
const STROKE = 11;
const R = (SIZE - STROKE) / 2;
/** Geräte-Kategorie-Farben — auch im Gesamt-Ring (Verteilung) verwendet. */
/** Geräte-Kategorie-Farben — auch im Gesamt-Half-Donut (Verteilung). */
export const MOBILE_COLOR = '#22c55e';
export const DESKTOP_COLOR = '#2563eb';
@ -26,29 +26,35 @@ function arcPath(cx: number, cy: number, r: number, startDeg: number, endDeg: nu
}
/**
* Voller Progress-Ring mit Mehr-Segment-Support (react-native-svg, keine Lib).
* - Einzel-Kategorie: ein Segment (Mobil/Computer).
* - Gesamt: zwei Segmente (Mobil-/Computer-Anteil farblich getrennt).
* Animiert über sweep der Bögen.
* Slot-Anzeige (react-native-svg, keine Lib), gleiche Größe in beiden Modi:
* - `half=false` (Default): voller Progress-Ring eine Kategorie (Mobil/Computer).
* - `half=true`: Half-Donut für die Gesamt-Verteilung (Mobil-/Computer-Anteil
* als zwei farbige Bögen).
*/
export function DeviceSlotDonut({
segments,
total,
label,
atLimit,
half = false,
}: {
segments: SlotSegment[];
total: number;
label: string;
atLimit: boolean;
half?: boolean;
}) {
const colors = useColors();
const cx = SIZE / 2;
const cy = SIZE / 2;
// Half-Donut tiefer setzen, damit der Bogen mittig im SIZE×SIZE-Feld sitzt.
const cy = half ? SIZE / 2 + R / 2 : SIZE / 2;
const r = R;
const safeTotal = Math.max(1, total);
const used = segments.reduce((s, x) => s + x.value, 0);
const startAngle = half ? 180 : -90;
const sweep = half ? 180 : 360;
const anim = useRef(new Animated.Value(0)).current;
const [progress, setProgress] = useState(0);
@ -64,10 +70,9 @@ export function DeviceSlotDonut({
return () => anim.removeListener(l);
}, [used, total, anim]);
// Konsekutive Bögen, Start oben (-90°).
let cum = -90;
let cum = startAngle;
const arcs = segments.map((seg) => {
const span = 360 * (seg.value / safeTotal);
const span = sweep * (seg.value / safeTotal);
const start = cum;
const end = cum + span;
cum = end;
@ -78,19 +83,21 @@ export function DeviceSlotDonut({
<View style={{ alignItems: 'center', flex: 1 }}>
<View style={{ width: SIZE, height: SIZE }}>
<Svg width={SIZE} height={SIZE}>
<Circle
cx={cx}
cy={cy}
r={r}
stroke={colors.surfaceElevated}
strokeWidth={STROKE}
fill="none"
/>
{half ? (
<Path
d={arcPath(cx, cy, r, 180, 360)}
stroke={colors.surfaceElevated}
strokeWidth={STROKE}
fill="none"
strokeLinecap="round"
/>
) : (
<Circle cx={cx} cy={cy} r={r} stroke={colors.surfaceElevated} strokeWidth={STROKE} fill="none" />
)}
{arcs.map((a, i) => {
const animEnd = a.start + (a.end - a.start) * progress;
if (animEnd <= a.start + 0.5) return null;
// Voll-Kreis vermeiden (start==end wäre degeneriert).
const drawEnd = Math.min(animEnd, a.start + 359.99);
const drawEnd = Math.min(animEnd, a.start + (half ? 179.99 : 359.99));
return (
<Path
key={i}
@ -107,12 +114,12 @@ export function DeviceSlotDonut({
pointerEvents="none"
style={{
position: 'absolute',
top: 0,
top: half ? SIZE / 2 - 4 : 0,
left: 0,
right: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center',
justifyContent: half ? 'flex-start' : 'center',
}}
>
<Text style={{ fontSize: 22, fontFamily: 'Nunito_900Black', color: colors.text, letterSpacing: -0.5 }}>