// Zarządzanie blacklistą treści — ukryte tagi / performerzy / studia. Sceny z // blacklisted entity wypadają z każdego /scenes (backend auto-apply, app/api/blacklist.py). // // Powód (user-report 86a9ec72: "remove all gay scenes from randomly popping up"): nie było // żadnego UI do (a) ukrycia tagu, (b) podejrzenia/cofnięcia blacklisty — mimo że Alert przy // "Hide performer" obiecywał "undo from Settings → Blacklist". Ten ekran domyka oba. import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import React, { useState } from 'react'; import { ActivityIndicator, FlatList, Pressable, StyleSheet, Text, TextInput, View, } from 'react-native'; import { useClient } from '../ClientContext'; import { theme } from '../theme'; import type { BlacklistEntry, BlacklistKind, TagCount } from '../types'; export function BlacklistScreen() { const client = useClient(); const queryClient = useQueryClient(); const [tagQ, setTagQ] = useState(''); const blacklistQuery = useQuery({ queryKey: ['blacklist'], queryFn: () => client.listBlacklist(), }); const tagSearch = useQuery({ queryKey: ['blacklist-tag-search', tagQ], queryFn: () => client.listTags({ q: tagQ || undefined, per_page: 40 }), enabled: tagQ.trim().length > 0, }); const afterChange = () => { queryClient.invalidateQueries({ queryKey: ['blacklist'] }); // Listy scen filtrują po blackliście — odśwież leniwie (bez wymuszania refetcha). queryClient.invalidateQueries({ queryKey: ['scenes'], refetchType: 'none' }); }; const addTag = useMutation({ mutationFn: (id: string) => client.addBlacklist('tag', id), onSuccess: afterChange, }); const remove = useMutation({ mutationFn: ({ kind, id }: { kind: BlacklistKind; id: string }) => client.removeBlacklist(kind, id), onSuccess: afterChange, }); const bl = blacklistQuery.data; const blacklistedTagIds = new Set((bl?.tags ?? []).map((t) => t.id)); const renderRow = (kind: BlacklistKind, e: BlacklistEntry) => ( {e.name} remove.mutate({ kind, id: e.id })} style={styles.removeBtn} > Unhide ); return ( 'x'} renderItem={() => ( Scenes tagged with, or from, anything you hide here stop showing up anywhere in the app. Add a tag below (e.g. a category you never want to see). Hide a tag {tagQ.trim().length > 0 ? ( tagSearch.isLoading ? ( ) : ( {(tagSearch.data?.items ?? []).map((tag: TagCount) => { const already = blacklistedTagIds.has(tag.id); return ( addTag.mutate(tag.id)} > {already ? '✓ ' : '+ '} {tag.name} ); })} ) ) : null} {blacklistQuery.isLoading ? ( ) : ( <>
{(bl?.tags ?? []).map((e) => renderRow('tag', e))}
{(bl?.performers ?? []).map((e) => renderRow('performer', e))}
{(bl?.studios ?? []).map((e) => renderRow('studio', e))}
)}
)} /> ); } function Section({ title, count, children, }: { title: string; count: number; children: React.ReactNode; }) { return ( {title} {count > 0 ? `(${count})` : ''} {count === 0 ? nothing hidden : children} ); } const styles = StyleSheet.create({ root: { flex: 1, backgroundColor: theme.bg }, intro: { color: theme.muted, fontSize: 13, lineHeight: 19, paddingHorizontal: 16, paddingTop: 16, paddingBottom: 4, }, section: { paddingVertical: 12, borderTopColor: theme.border, borderTopWidth: 1, marginTop: 12, }, sectionTitle: { color: theme.muted, fontSize: 12, textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 10, paddingHorizontal: 16, }, search: { backgroundColor: theme.card, borderColor: theme.border, borderWidth: 1, borderRadius: 8, color: theme.fg, padding: 10, marginHorizontal: 16, }, chips: { flexDirection: 'row', flexWrap: 'wrap', gap: 6, padding: 16 }, chip: { backgroundColor: theme.card, borderColor: theme.accent, borderWidth: 1, borderRadius: 14, paddingHorizontal: 10, paddingVertical: 5, }, chipDisabled: { borderColor: theme.border, opacity: 0.6 }, chipText: { color: theme.accent, fontSize: 13 }, chipTextDisabled: { color: theme.muted }, row: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 8, paddingHorizontal: 16, }, rowName: { color: theme.fg, fontSize: 15, flex: 1, marginRight: 12 }, removeBtn: { borderColor: theme.border, borderWidth: 1, borderRadius: 8, paddingHorizontal: 12, paddingVertical: 6, }, removeText: { color: theme.accent, fontSize: 13, fontWeight: '600' }, empty: { color: theme.muted, fontSize: 13, paddingHorizontal: 16 }, });