import { useRef, useEffect, useState, useMemo } from 'react'; import { View, Text, TouchableOpacity, Animated, ActivityIndicator } from 'react-native'; import { Image } from 'expo-image'; import { Ionicons } from '@expo/vector-icons'; import { useTranslation } from 'react-i18next'; import { useColors, type ColorScheme } from '../../lib/theme'; import { RemoveDomainSheet } from './RemoveDomainSheet'; import type { CustomDomain } from '../../hooks/useCustomDomains'; // ─── Meine Filter (unified web + mail_domain) ───────────────────────────────── type MyFiltersProps = { domains: CustomDomain[]; totalCount: number; totalLimit: number; globalBlocklistCount: number; onAddPress: () => void; onRemoveDomain: (id: string) => Promise; colors: ColorScheme; }; /** * "Meine Filter" — unified Sektion für web + mail_domain Einträge. * * Ein Slot-Pool: totalLimit = Legend 20 / Pro 10 (web+mail zusammen). * Kacheln zeigen Typ-Badge (Web / Mail). Entfernen via RemoveDomainSheet. */ export function MyFiltersList({ domains, totalCount, totalLimit, globalBlocklistCount, onAddPress, onRemoveDomain, colors, }: MyFiltersProps) { const { t } = useTranslation(); const visibleDomains = useMemo( () => domains.filter((d) => d.status !== 'approved'), [domains], ); const atLimit = totalCount >= totalLimit; const fillAnim = useRef(new Animated.Value(0)).current; const ratio = totalLimit > 0 ? Math.min(totalCount / totalLimit, 1) : 0; useEffect(() => { Animated.timing(fillAnim, { toValue: ratio, duration: 380, useNativeDriver: false }).start(); }, [ratio]); const pct = ratio * 100; const barColor = pct >= 90 ? colors.error : pct >= 60 ? colors.warning : colors.brandOrange; const badgeBg = atLimit ? '#fee2e2' : colors.surfaceElevated; const badgeFg = atLimit ? colors.error : colors.textMuted; return ( {t('blocker.my_filters_title')} {t('blocker.count_label', { count: totalCount, max: totalLimit })} {visibleDomains.length === 0 ? ( ) : ( )} {t('blocker.vip_global_hint', { count: globalBlocklistCount })} ); } function MyFiltersEmptyState({ onAddPress, colors }: { onAddPress: () => void; colors: ColorScheme }) { const { t } = useTranslation(); return ( {t('blocker.my_filters_empty')} ); } function FilterTilesGrid({ domains, onRemoveDomain, }: { domains: CustomDomain[]; onRemoveDomain: (id: string) => Promise; }) { return ( {domains.map((d) => ( ))} ); } function FilterTile({ domain, onRemove, }: { domain: CustomDomain; onRemove: (id: string) => Promise; }) { const { t } = useTranslation(); const colors = useColors(); const [imgError, setImgError] = useState(false); const [removeSheetOpen, setRemoveSheetOpen] = useState(false); const [removing, setRemoving] = useState(false); const isMail = domain.type === 'mail_domain'; const stripped = domain.domain.replace(/^www\./, ''); const statusColor: string = (() => { switch (domain.status) { case 'submitted': return colors.warning; case 'rejected': return colors.error; default: return colors.brandOrange; } })(); const badgeLabel: string = (() => { switch (domain.status) { case 'submitted': return t('blocker.domain_badge_pruefung'); case 'rejected': return t('blocker.domain_badge_rejected'); default: return t('blocker.domain_badge_active'); } })(); async function handleConfirmRemove() { setRemoving(true); try { await onRemove(domain.id); } finally { setRemoving(false); } } return ( <> {/* Type + Status badge row */} {isMail ? t('blocker.type_mail') : t('blocker.type_web')} {badgeLabel} {/* Icon + label */} {isMail ? ( ) : !imgError ? ( setImgError(true)} /> ) : ( {stripped.slice(0, 2).toUpperCase()} )} {stripped} {/* Remove button */} setRemoveSheetOpen(true)} disabled={removing} activeOpacity={0.65} style={{ height: 26, borderRadius: 6, borderWidth: 1, borderColor: colors.border, alignItems: 'center', justifyContent: 'center', }} > {removing ? ( ) : ( )} setRemoveSheetOpen(false)} onConfirmRemove={handleConfirmRemove} /> ); } // ─── VIP-Liste (collapsible, Zweitschutz) ──────────────────────────────────── type VipListProps = { domains: CustomDomain[]; globalBlocklistCount: number; open: boolean; onToggle: () => void; colors: ColorScheme; }; /** * "VIP-Liste" — Zweitschutz-Sektion. Collapsible. * * Zeigt die zusammengesetzte VIP-Layer-2-Liste: * - Eigene Web-Domains des Users (für Family-Controls / webContent-Sync) * - Hinweis auf den globalen kuratierten Teil (nicht editierbar) * * Diese Liste greift als Zweitschutz, falls Layer 1 (VPN/URL-Filter) * ein technisches Problem hat. */ export function VipDomainList({ domains, globalBlocklistCount, open, onToggle, colors }: VipListProps) { const { t } = useTranslation(); const webDomains = useMemo( () => domains.filter((d) => (d.type === 'web' || !d.type) && d.status !== 'approved'), [domains], ); return ( {t('blocker.vip_layer2_title')} {open && ( {t('blocker.vip_layer2_desc')} {webDomains.length > 0 && ( {webDomains.map((d) => ( ))} )} {t('blocker.vip_layer2_global_hint', { count: globalBlocklistCount })} )} ); } function VipReadonlyChip({ domain, colors }: { domain: CustomDomain; colors: ColorScheme }) { const stripped = domain.domain.replace(/^www\./, ''); return ( {stripped} ); }