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:
parent
77ce5e5a80
commit
c3478f4743
@ -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>
|
||||
|
||||
|
||||
@ -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}
|
||||
{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 }}>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user