feat(mobile): ukrywanie stron w zakladce Sites
Some checks failed
Backend tests / test (push) Has been cancelled

Przytrzymanie strony otwiera menu Pin/Unpin, Hide this site. Ukryte znikaja
z listy, nad nia "N hidden sites, tap to manage" prowadzi do Hidden content,
gdzie nowa sekcja Hidden sites ma Unhide. Emulator: ukrycie sxyprn, sekcja
z wpisem, odkrycie i powrot na liste, wpis w hidden_sites i jego usuniecie.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
goon-foss 2026-10-06 16:37:15 +02:00
parent 6540d4a009
commit ce174b671e
5 changed files with 99 additions and 6 deletions

View file

@ -474,6 +474,22 @@ export class GoonClient {
}
}
// Ukryte strony (lista w GET /blacklist, pole `sites`).
async hideSite(origin: string): Promise<void> {
await this.request(`/blacklist/sites/${encodeURIComponent(origin)}`, { method: 'POST' });
}
async unhideSite(origin: string): Promise<void> {
const res = await fetch(`${this.baseUrl}/blacklist/sites/${encodeURIComponent(origin)}`, {
method: 'DELETE',
headers: await this._authHeaders(),
});
if (!res.ok && res.status !== 204) {
const text = await res.text().catch(() => res.statusText);
throw new ApiError(res.status, `${res.status}: ${text}`);
}
}
async listFavoriteMovies(): Promise<FavoriteMovieListOut> {
return this.request('/favorites/movies');
}

View file

@ -35,6 +35,14 @@ export type ChangelogEntry = {
};
export const CHANGELOG: ChangelogEntry[] = [
{
id: '2026-10-06b',
date: 'October 2026',
items: [
'You can hide a site: long-press it in the Sites tab and pick Hide this site. Scenes that are only on that site disappear from every list, and on a scene that is also elsewhere its button is gone. Unhide in Settings, Hidden content.',
'Pinned sites now come across with your sync code too.',
],
},
{
id: '2026-10-06a',
date: 'October 2026',

View file

@ -51,6 +51,21 @@ export function BlacklistScreen() {
client.removeBlacklist(kind, id),
onSuccess: afterChange,
});
// Ukryte strony (przytrzymanie strony w zakladce Sites). Nazwy z listy stron, ktora
// i tak jest w cache; bez niej pokazujemy sam origin bez 'tube:'.
const sources = useQuery({ queryKey: ['sources'], queryFn: () => client.listSources(), staleTime: 60_000 });
const siteName = (origin: string) => {
const s = sources.data?.items.find((x) => x.origin === origin);
const raw = s?.display_name || origin.replace(/^tube:/, '');
return raw.replace(/\.[a-z]{2,5}$/i, '') || raw;
};
const unhideSite = useMutation({
mutationFn: (origin: string) => client.unhideSite(origin),
onSuccess: () => {
afterChange();
queryClient.invalidateQueries({ queryKey: ['scene'], refetchType: 'none' });
},
});
const bl = blacklistQuery.data;
const blacklistedTagIds = new Set((bl?.tags ?? []).map((t) => t.id));
@ -79,7 +94,8 @@ export function BlacklistScreen() {
<View style={{ paddingBottom: 40 }}>
<Text style={styles.intro}>
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).
the app. Add a tag below (e.g. a category you never want to see). To hide a site,
long-press it in the Sites tab.
</Text>
<Text style={styles.sectionTitle}>Hide a tag</Text>
@ -130,6 +146,18 @@ export function BlacklistScreen() {
<Section title="Hidden studios" count={bl?.studios.length ?? 0}>
{(bl?.studios ?? []).map((e) => renderRow('studio', e))}
</Section>
<Section title="Hidden sites" count={bl?.sites?.length ?? 0}>
{(bl?.sites ?? []).map((origin) => (
<View key={`site-${origin}`} style={styles.row}>
<Text style={styles.rowName} numberOfLines={1}>
{siteName(origin)}
</Text>
<Pressable hitSlop={10} onPress={() => unhideSite.mutate(origin)} style={styles.removeBtn}>
<Text style={styles.removeText}>Unhide</Text>
</Pressable>
</View>
))}
</Section>
</>
)}
</View>

View file

@ -11,6 +11,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import React, { useMemo, useState } from 'react';
import {
ActivityIndicator,
Alert,
FlatList,
Modal,
Pressable,
@ -87,11 +88,42 @@ export function SitesScreen() {
onSuccess: () => qc.invalidateQueries({ queryKey: ['favorite-sites'] }),
});
// Ukryte strony (zgloszenie 3910b19b): znikaja z tej listy, a z list scen znikaja sceny,
// ktore maja zrodla tylko z ukrytych stron. Na serwerze, razem z reszta "Hidden content",
// odkrywanie w Settings, Hidden content.
const blQuery = useQuery({
queryKey: ['blacklist'],
queryFn: () => client.listBlacklist(),
staleTime: 60_000,
});
const hidden = useMemo(() => new Set(blQuery.data?.sites ?? []), [blQuery.data?.sites]);
const hideSite = useMutation({
mutationFn: (origin: string) => client.hideSite(origin),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['blacklist'] });
qc.invalidateQueries({ queryKey: ['scenes'], refetchType: 'none' });
qc.invalidateQueries({ queryKey: ['scene'], refetchType: 'none' });
},
onError: (e) => Alert.alert('Could not hide the site', e instanceof Error ? e.message : String(e)),
});
const openMenu = (item: SourceOut) => {
const isPinned = pinned.has(item.origin);
Alert.alert(prettySiteName(item.display_name), undefined, [
{ text: isPinned ? 'Unpin' : 'Pin to top', onPress: () => togglePin.mutate(item.origin) },
{
text: 'Hide this site',
style: 'destructive',
onPress: () => hideSite.mutate(item.origin),
},
{ text: 'Cancel', style: 'cancel' },
]);
};
// Sort + filter client-side, lista ma <50 entries, nie warto roundtripować.
// Backend zwraca pre-sorted po scene_count DESC, więc dla 'popular' kolejność
// zachowana. Dla 'recent' sortujemy po last_scraped_at DESC.
const items = useMemo<SourceOut[]>(() => {
const all = data?.items ?? [];
const all = (data?.items ?? []).filter((s) => !hidden.has(s.origin));
const filtered = debouncedQ
? all.filter(
(s) =>
@ -118,7 +150,7 @@ export function SitesScreen() {
}
if (pinned.size === 0) return sorted;
return [...sorted.filter((s) => pinned.has(s.origin)), ...sorted.filter((s) => !pinned.has(s.origin))];
}, [data?.items, debouncedQ, order, pinned]);
}, [data?.items, debouncedQ, order, pinned, hidden]);
return (
<View style={styles.container}>
@ -141,8 +173,15 @@ export function SitesScreen() {
}}
onClose={() => setSortOpen(false)}
/>
{favQuery.data && pinned.size === 0 ? (
<Text style={styles.hint}>long-press a site to pin it to the top</Text>
{favQuery.data && pinned.size === 0 && hidden.size === 0 ? (
<Text style={styles.hint}>long-press a site to pin it or hide it</Text>
) : null}
{hidden.size > 0 ? (
<Pressable onPress={() => navigation.navigate('Blacklist')} hitSlop={6}>
<Text style={styles.hint}>
{hidden.size} hidden {hidden.size === 1 ? 'site' : 'sites'}, tap to manage
</Text>
</Pressable>
) : null}
{isLoading && <ActivityIndicator color={theme.fg} style={{ marginTop: 24 }} />}
@ -167,7 +206,7 @@ export function SitesScreen() {
name: prettySiteName(item.display_name),
})
}
onLongPress={() => togglePin.mutate(item.origin)}
onLongPress={() => openMenu(item)}
onShowRating={() => setDetail(item)}
/>
)}

View file

@ -410,6 +410,8 @@ export interface BlacklistOut {
performers: BlacklistEntry[];
studios: BlacklistEntry[];
tags: BlacklistEntry[];
// Ukryte strony (originy, np. 'tube:porndoecom'); starszy serwer ich nie zwraca.
sites?: string[];
}
export type BlacklistKind = 'performer' | 'studio' | 'tag';