From ce174b671e232ab6e2566fd97b7fe2eacd520ecd Mon Sep 17 00:00:00 2001 From: goon-foss Date: Tue, 6 Oct 2026 16:37:15 +0200 Subject: [PATCH] feat(mobile): ukrywanie stron w zakladce Sites 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 --- mobile/src/api.ts | 16 +++++++++ mobile/src/changelog.ts | 8 +++++ mobile/src/screens/BlacklistScreen.tsx | 30 +++++++++++++++- mobile/src/screens/SitesScreen.tsx | 49 +++++++++++++++++++++++--- mobile/src/types.ts | 2 ++ 5 files changed, 99 insertions(+), 6 deletions(-) diff --git a/mobile/src/api.ts b/mobile/src/api.ts index ba9d853..4581d54 100644 --- a/mobile/src/api.ts +++ b/mobile/src/api.ts @@ -474,6 +474,22 @@ export class GoonClient { } } + // Ukryte strony (lista w GET /blacklist, pole `sites`). + async hideSite(origin: string): Promise { + await this.request(`/blacklist/sites/${encodeURIComponent(origin)}`, { method: 'POST' }); + } + + async unhideSite(origin: string): Promise { + 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 { return this.request('/favorites/movies'); } diff --git a/mobile/src/changelog.ts b/mobile/src/changelog.ts index cc3bc35..1a91254 100644 --- a/mobile/src/changelog.ts +++ b/mobile/src/changelog.ts @@ -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', diff --git a/mobile/src/screens/BlacklistScreen.tsx b/mobile/src/screens/BlacklistScreen.tsx index f941a99..d4e0b20 100644 --- a/mobile/src/screens/BlacklistScreen.tsx +++ b/mobile/src/screens/BlacklistScreen.tsx @@ -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() { 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. Hide a tag @@ -130,6 +146,18 @@ export function BlacklistScreen() {
{(bl?.studios ?? []).map((e) => renderRow('studio', e))}
+
+ {(bl?.sites ?? []).map((origin) => ( + + + {siteName(origin)} + + unhideSite.mutate(origin)} style={styles.removeBtn}> + Unhide + + + ))} +
)}
diff --git a/mobile/src/screens/SitesScreen.tsx b/mobile/src/screens/SitesScreen.tsx index 3b298ad..3ab1dc5 100644 --- a/mobile/src/screens/SitesScreen.tsx +++ b/mobile/src/screens/SitesScreen.tsx @@ -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(() => { - 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 ( @@ -141,8 +173,15 @@ export function SitesScreen() { }} onClose={() => setSortOpen(false)} /> - {favQuery.data && pinned.size === 0 ? ( - long-press a site to pin it to the top + {favQuery.data && pinned.size === 0 && hidden.size === 0 ? ( + long-press a site to pin it or hide it + ) : null} + {hidden.size > 0 ? ( + navigation.navigate('Blacklist')} hitSlop={6}> + + {hidden.size} hidden {hidden.size === 1 ? 'site' : 'sites'}, tap to manage + + ) : null} {isLoading && } @@ -167,7 +206,7 @@ export function SitesScreen() { name: prettySiteName(item.display_name), }) } - onLongPress={() => togglePin.mutate(item.origin)} + onLongPress={() => openMenu(item)} onShowRating={() => setDetail(item)} /> )} diff --git a/mobile/src/types.ts b/mobile/src/types.ts index eb5efd9..03d6c82 100644 --- a/mobile/src/types.ts +++ b/mobile/src/types.ts @@ -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';