feat(mobile): ukrywanie stron w zakladce Sites
Some checks failed
Backend tests / test (push) Has been cancelled
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:
parent
6540d4a009
commit
ce174b671e
5 changed files with 99 additions and 6 deletions
|
|
@ -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> {
|
async listFavoriteMovies(): Promise<FavoriteMovieListOut> {
|
||||||
return this.request('/favorites/movies');
|
return this.request('/favorites/movies');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,14 @@ export type ChangelogEntry = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CHANGELOG: 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',
|
id: '2026-10-06a',
|
||||||
date: 'October 2026',
|
date: 'October 2026',
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,21 @@ export function BlacklistScreen() {
|
||||||
client.removeBlacklist(kind, id),
|
client.removeBlacklist(kind, id),
|
||||||
onSuccess: afterChange,
|
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 bl = blacklistQuery.data;
|
||||||
const blacklistedTagIds = new Set((bl?.tags ?? []).map((t) => t.id));
|
const blacklistedTagIds = new Set((bl?.tags ?? []).map((t) => t.id));
|
||||||
|
|
@ -79,7 +94,8 @@ export function BlacklistScreen() {
|
||||||
<View style={{ paddingBottom: 40 }}>
|
<View style={{ paddingBottom: 40 }}>
|
||||||
<Text style={styles.intro}>
|
<Text style={styles.intro}>
|
||||||
Scenes tagged with, or from, anything you hide here stop showing up anywhere in
|
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>
|
||||||
|
|
||||||
<Text style={styles.sectionTitle}>Hide a tag</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}>
|
<Section title="Hidden studios" count={bl?.studios.length ?? 0}>
|
||||||
{(bl?.studios ?? []).map((e) => renderRow('studio', e))}
|
{(bl?.studios ?? []).map((e) => renderRow('studio', e))}
|
||||||
</Section>
|
</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>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
|
Alert,
|
||||||
FlatList,
|
FlatList,
|
||||||
Modal,
|
Modal,
|
||||||
Pressable,
|
Pressable,
|
||||||
|
|
@ -87,11 +88,42 @@ export function SitesScreen() {
|
||||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['favorite-sites'] }),
|
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ć.
|
// Sort + filter client-side, lista ma <50 entries, nie warto roundtripować.
|
||||||
// Backend zwraca pre-sorted po scene_count DESC, więc dla 'popular' kolejność
|
// Backend zwraca pre-sorted po scene_count DESC, więc dla 'popular' kolejność
|
||||||
// zachowana. Dla 'recent' sortujemy po last_scraped_at DESC.
|
// zachowana. Dla 'recent' sortujemy po last_scraped_at DESC.
|
||||||
const items = useMemo<SourceOut[]>(() => {
|
const items = useMemo<SourceOut[]>(() => {
|
||||||
const all = data?.items ?? [];
|
const all = (data?.items ?? []).filter((s) => !hidden.has(s.origin));
|
||||||
const filtered = debouncedQ
|
const filtered = debouncedQ
|
||||||
? all.filter(
|
? all.filter(
|
||||||
(s) =>
|
(s) =>
|
||||||
|
|
@ -118,7 +150,7 @@ export function SitesScreen() {
|
||||||
}
|
}
|
||||||
if (pinned.size === 0) return sorted;
|
if (pinned.size === 0) return sorted;
|
||||||
return [...sorted.filter((s) => pinned.has(s.origin)), ...sorted.filter((s) => !pinned.has(s.origin))];
|
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 (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
|
|
@ -141,8 +173,15 @@ export function SitesScreen() {
|
||||||
}}
|
}}
|
||||||
onClose={() => setSortOpen(false)}
|
onClose={() => setSortOpen(false)}
|
||||||
/>
|
/>
|
||||||
{favQuery.data && pinned.size === 0 ? (
|
{favQuery.data && pinned.size === 0 && hidden.size === 0 ? (
|
||||||
<Text style={styles.hint}>long-press a site to pin it to the top</Text>
|
<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}
|
) : null}
|
||||||
|
|
||||||
{isLoading && <ActivityIndicator color={theme.fg} style={{ marginTop: 24 }} />}
|
{isLoading && <ActivityIndicator color={theme.fg} style={{ marginTop: 24 }} />}
|
||||||
|
|
@ -167,7 +206,7 @@ export function SitesScreen() {
|
||||||
name: prettySiteName(item.display_name),
|
name: prettySiteName(item.display_name),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
onLongPress={() => togglePin.mutate(item.origin)}
|
onLongPress={() => openMenu(item)}
|
||||||
onShowRating={() => setDetail(item)}
|
onShowRating={() => setDetail(item)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -410,6 +410,8 @@ export interface BlacklistOut {
|
||||||
performers: BlacklistEntry[];
|
performers: BlacklistEntry[];
|
||||||
studios: BlacklistEntry[];
|
studios: BlacklistEntry[];
|
||||||
tags: BlacklistEntry[];
|
tags: BlacklistEntry[];
|
||||||
|
// Ukryte strony (originy, np. 'tube:porndoecom'); starszy serwer ich nie zwraca.
|
||||||
|
sites?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BlacklistKind = 'performer' | 'studio' | 'tag';
|
export type BlacklistKind = 'performer' | 'studio' | 'tag';
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue