From e2e5a1003ca860f11723631cc515e681891a94f7 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Mon, 8 Jun 2026 00:17:05 +0200 Subject: [PATCH] =?UTF-8?q?feat(native):=20Ger=C3=A4te-Slots=20als=20Progr?= =?UTF-8?q?ess-Ringe=20+=20Status-Pill=20in=20der=20Liste?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Slots: zwei animierte volle Progress-Circles (Mobil/Computer) statt Balken, via react-native-svg (keine neue Lib) - Status-Zeile pro Gerät: Online (grün) / Cooldown · noch Xh (amber, aus releaseRequestedAt) / Ungeschützt (rot) — ersetzt Footer + StatusBadge Co-Authored-By: Claude Opus 4.8 --- apps/rebreak-native/app/devices.tsx | 69 +++++------ .../components/devices/DeviceSlotDonut.tsx | 108 ++++++++++++++++++ .../components/devices/DeviceStatusPill.tsx | 53 +++++++++ apps/rebreak-native/locales/de.json | 23 ++++ apps/rebreak-native/locales/en.json | 23 ++++ 5 files changed, 236 insertions(+), 40 deletions(-) create mode 100644 apps/rebreak-native/components/devices/DeviceSlotDonut.tsx create mode 100644 apps/rebreak-native/components/devices/DeviceStatusPill.tsx diff --git a/apps/rebreak-native/app/devices.tsx b/apps/rebreak-native/app/devices.tsx index 6bc25dc..303a1c3 100644 --- a/apps/rebreak-native/app/devices.tsx +++ b/apps/rebreak-native/app/devices.tsx @@ -32,7 +32,8 @@ import { useUserDevicesRealtime } from '../hooks/useUserDevicesRealtime'; import { useUserPlan } from '../hooks/useUserPlan'; import { AppHeader } from '../components/AppHeader'; import { MagicSheet } from '../components/devices/MagicSheet'; -import { DeviceProgressBar } from '../components/devices/DeviceProgressBar'; +import { DeviceSlotDonut } from '../components/devices/DeviceSlotDonut'; +import { DeviceStatusPill } from '../components/devices/DeviceStatusPill'; import { DeviceDetailSheet, type DeviceDetail } from '../components/devices/DeviceDetailSheet'; import { deviceImage } from '../components/devices/deviceIcon'; @@ -270,19 +271,10 @@ function MobileDeviceRow({ ) : null} - - {releaseActive && releaseAt - ? t('devices.release_countdown', { remaining: formatCountdown(releaseAt) }) - : footerText} - + @@ -399,22 +391,17 @@ function ProtectedDeviceRow({ > {device.label} - - - {device.status === 'degraded' - ? t('plan_limit.device_degraded_body') - : `${t('settings.devices_since')} ${formatSince(device.createdAt)}`} - + @@ -592,18 +579,20 @@ export default function DevicesScreen() { > {subtitle} - = mobileLimit} - label={t('devices.progress_mobile')} - /> - + + = mobileLimit} + label={t('devices.progress_mobile')} + /> + + {/* Unified devices section: Mobile zuerst, dann Desktop */} diff --git a/apps/rebreak-native/components/devices/DeviceSlotDonut.tsx b/apps/rebreak-native/components/devices/DeviceSlotDonut.tsx new file mode 100644 index 0000000..632c545 --- /dev/null +++ b/apps/rebreak-native/components/devices/DeviceSlotDonut.tsx @@ -0,0 +1,108 @@ +import { useEffect, useRef, useState } from 'react'; +import { Animated, Easing, Text, View } from 'react-native'; +import Svg, { Circle } from 'react-native-svg'; +import { useColors } from '../../lib/theme'; + +const SIZE = 104; +const STROKE = 9; +const R = (SIZE - STROKE) / 2; +const C = 2 * Math.PI * R; + +/** + * Voller Progress-Ring (kein Half-Donut) für belegte/freie Geräte-Slots. + * Mit react-native-svg (schon im Projekt) statt neuer Lib — animiert über + * strokeDashoffset. Zwei nebeneinander (Mobil + Computer). + */ +export function DeviceSlotDonut({ + count, + max, + atLimit, + label, +}: { + count: number; + max: number; + atLimit: boolean; + label: string; +}) { + const colors = useColors(); + const ratio = max > 0 ? Math.min(count / max, 1) : 0; + const fill = atLimit ? colors.brandOrange : colors.success; + + const anim = useRef(new Animated.Value(0)).current; + const [progress, setProgress] = useState(0); + + useEffect(() => { + anim.setValue(0); + const l = anim.addListener(({ value }) => setProgress(value)); + Animated.timing(anim, { + toValue: 1, + duration: 950, + easing: Easing.out(Easing.cubic), + useNativeDriver: false, + }).start(); + return () => anim.removeListener(l); + }, [ratio, anim]); + + const offset = C * (1 - ratio * progress); + + return ( + + + + + + + + + {count} + + /{max} + + + + + + + {label} + + + ); +} diff --git a/apps/rebreak-native/components/devices/DeviceStatusPill.tsx b/apps/rebreak-native/components/devices/DeviceStatusPill.tsx new file mode 100644 index 0000000..64314c8 --- /dev/null +++ b/apps/rebreak-native/components/devices/DeviceStatusPill.tsx @@ -0,0 +1,53 @@ +import { Text, View } from 'react-native'; +import { useTranslation } from 'react-i18next'; +import { useColors } from '../../lib/theme'; + +export type DeviceStatusKind = 'online' | 'cooldown' | 'unprotected' | 'pending'; + +/** + * Status-Zeile in der Geräte-Liste: farbiger Punkt + Text. + * - online → geschützt/verbunden (grün) + * - cooldown → Pause beantragt, läuft ab (amber, mit Restdauer) + * - unprotected → Schutz aus (rot, mit Dauer) + * - pending → wartet auf Aktivierung (amber) + */ +export function DeviceStatusPill({ + kind, + durationText, +}: { + kind: DeviceStatusKind; + durationText?: string; +}) { + const { t } = useTranslation(); + const colors = useColors(); + + const cfg: Record = { + online: { color: colors.success, label: t('devices.status_online') }, + cooldown: { + color: '#f59e0b', + label: durationText + ? t('devices.status_cooldown', { time: durationText }) + : t('devices.status_cooldown_short'), + }, + unprotected: { + color: colors.error, + label: durationText + ? t('devices.status_unprotected_since', { time: durationText }) + : t('devices.status_unprotected'), + }, + pending: { color: '#f59e0b', label: t('devices.status_pending') }, + }; + const c = cfg[kind]; + + return ( + + + + {c.label} + + + ); +} diff --git a/apps/rebreak-native/locales/de.json b/apps/rebreak-native/locales/de.json index 2af9be3..3caadab 100644 --- a/apps/rebreak-native/locales/de.json +++ b/apps/rebreak-native/locales/de.json @@ -664,6 +664,24 @@ "title": "Wähle deinen Alias", "body": "Das ist dein einziger Name in rebreak. Niemand sieht deine Mail oder deinen echten Namen.", "finish": "Verstanden" + }, + "protection_confirm": { + "checkbox": "Ich hab’s verstanden", + "cta": "Weiter", + "vpn_title": "VPN-Erlaubnis", + "vpn_body": "Gleich fragt Android nach VPN-Erlaubnis. Tipp im Dialog auf „Zulassen/OK“ — das ist kein echtes VPN, der Filter läuft lokal auf deinem Gerät.", + "deviceadmin_title": "Geräteschutz", + "deviceadmin_body": "Im nächsten Dialog auf „Aktivieren“ tippen. Damit ist der Schutz schon ab dem Neustart aktiv — es werden keine weiteren Rechte angefragt.", + "applock_title": "App-Sperre", + "applock_body": "Achtung im nächsten Dialog: Tipp den UNTEREN Button — nicht den blauen. Nur so wird die App-Sperre aktiv.", + "urlfilter_title": "Inhaltsfilter", + "urlfilter_body": "Gleich fragt iOS nach Erlaubnis für den Filter. Tipp auf „Erlauben“.", + "a11y_title": "Schutz-Wächter (Bedienungshilfen)", + "a11y_body": "Dieser Schritt ist etwas länger — ich führ dich durch. Über die Bedienungshilfen schützt ReBreak deine Einstellungen vor versehentlichem Abschalten.", + "a11y_step1": "Zuerst fragt Android nach „Über anderen Apps anzeigen“ — tippe Erlauben (brauchen wir, um dir den nächsten Schritt einzublenden).", + "a11y_step2": "Dann öffnet sich die Bedienungshilfen-Liste — such „ReBreak“.", + "a11y_step3": "Tippe ReBreak an und schalte den Schalter ein. Komm danach zurück zur App.", + "a11y_indicator": "Hier ReBreak antippen & einschalten" } }, "protection_onboarding": { @@ -1331,6 +1349,11 @@ "status_pending": "Bereit zum Installieren", "status_active": "Aktiv", "status_revoked": "Entfernt", + "status_online": "Online", + "status_cooldown": "Cooldown · noch %{time}", + "status_cooldown_short": "Cooldown", + "status_unprotected": "Ungeschützt", + "status_unprotected_since": "Ungeschützt · seit %{time}", "label_placeholder": "z.B. MacBook Pro", "label_default": "MacBook Pro", "label_question": "Wie soll der Mac heißen?", diff --git a/apps/rebreak-native/locales/en.json b/apps/rebreak-native/locales/en.json index 61ad35a..1ee03be 100644 --- a/apps/rebreak-native/locales/en.json +++ b/apps/rebreak-native/locales/en.json @@ -664,6 +664,24 @@ "title": "Pick your alias", "body": "This is your only name on rebreak. No one sees your email or real name.", "finish": "Got it" + }, + "protection_confirm": { + "checkbox": "I understand", + "cta": "Continue", + "vpn_title": "VPN permission", + "vpn_body": "Android will now ask for VPN permission. Tap “Allow/OK” in the dialog — it’s not a real VPN, the filter runs locally on your device.", + "deviceadmin_title": "Device protection", + "deviceadmin_body": "Tap “Activate” in the next dialog. This keeps protection on right from restart — no further rights are requested.", + "applock_title": "App lock", + "applock_body": "Heads up in the next dialog: tap the BOTTOM button — not the blue one. That’s the only way the app lock turns on.", + "urlfilter_title": "Content filter", + "urlfilter_body": "iOS will now ask permission for the filter. Tap “Allow”.", + "a11y_title": "Protection guard (Accessibility)", + "a11y_body": "This step is a little longer — I’ll guide you. Via Accessibility, ReBreak protects your settings from being switched off by accident.", + "a11y_step1": "First Android asks for “Display over other apps” — tap Allow (we need it to show you the next step on screen).", + "a11y_step2": "Then the Accessibility list opens — find “ReBreak”.", + "a11y_step3": "Tap ReBreak and turn the switch on. Then come back to the app.", + "a11y_indicator": "Tap ReBreak here & switch it on" } }, "protection_onboarding": { @@ -1331,6 +1349,11 @@ "status_pending": "Ready to install", "status_active": "Active", "status_revoked": "Removed", + "status_online": "Online", + "status_cooldown": "Cooldown · %{time} left", + "status_cooldown_short": "Cooldown", + "status_unprotected": "Unprotected", + "status_unprotected_since": "Unprotected · for %{time}", "label_placeholder": "e.g. MacBook Pro", "label_default": "MacBook Pro", "label_question": "What should this Mac be called?",