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