// 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 },
});