feat(tags): scal duplikaty liczby mnogiej + prewencja + skrocone liczniki
Some checks failed
Backend tests / test (push) Has been cancelled
Some checks failed
Backend tests / test (push) Has been cancelled
1) Duplikaty tagow wrocily, ale INNYM wzorcem niz w lipcu: alnum-klucz z natury nie
lapie liczby mnogiej ('blowjob' != 'blowjobs'). Audyt: 1944 pary, m.in.
Blowjob/Blowjobs (390728 vs 791), Teen/Teens (53096 vs 41294), Cumshot/Cumshots.
Scalone oba kierunki (kanoniczny = wiekszy scene_count): 1938 + 2063 tagow,
przeniesione 251251 + 26142 przypisan scen i 118522 + 392 filmow. Zostaje 581 par
(lancuchy i remisy) - celowo pominiete, wymagaja recznej decyzji.
2) Prewencja w _resolve_by_altkey: gdy alnum-klucz nie trafi, probujemy obu form
(+s / -s). Tylko proste 's' - form nieregularnych (panty/panties) NIE ruszamy, bo
zbyt latwo o falszywy zlew (glass/glasses, bra/bras). Zweryfikowane na prodzie:
Blowjobs->Blowjob, Teens->Teen, Cumshots->Cumshot, Lesbians->Lesbian.
3) UI: formatCount() skraca duze liczniki (181764 -> 181k, 1200 -> 1,2k) w tagach,
performerach, ulubionych i filtrach filmow - user-request, chipy przestaja puchnac.
This commit is contained in:
parent
21d51ca691
commit
4417ae0ff0
7 changed files with 62 additions and 13 deletions
|
|
@ -31,15 +31,32 @@ def _resolve_by_altkey(session: Session, name: str) -> Tag | None:
|
|||
key = _altkey(name)
|
||||
if not key:
|
||||
return None
|
||||
return (
|
||||
session.execute(
|
||||
select(Tag)
|
||||
.where(func.regexp_replace(func.lower(func.btrim(Tag.name)), "[^a-z0-9]", "", "g") == key)
|
||||
.order_by(Tag.scene_count.desc())
|
||||
)
|
||||
expr = func.regexp_replace(func.lower(func.btrim(Tag.name)), "[^a-z0-9]", "", "g")
|
||||
hit = (
|
||||
session.execute(select(Tag).where(expr == key).order_by(Tag.scene_count.desc()))
|
||||
.scalars()
|
||||
.first()
|
||||
)
|
||||
if hit is not None:
|
||||
return hit
|
||||
# Liczba mnoga. Alnum-klucz z natury jej NIE łapie ("blowjob" != "blowjobs"), więc
|
||||
# duplikaty odrastały mimo tamtej prewencji (audyt 2026-08-02: 1944 pary, m.in.
|
||||
# Blowjob/Blowjobs, Teen/Teens, Cumshot/Cumshots). Próbujemy obu kierunków: nowe
|
||||
# "Blowjobs" trafia w istniejące "Blowjob", a nowe "Blowjob" w istniejące "Blowjobs".
|
||||
# Tylko proste `+s` — form nieregularnych (panty/panties) świadomie nie ruszamy, bo
|
||||
# zbyt łatwo o fałszywy zlew (glass/glasses, bra/bras).
|
||||
alts = [key + "s"] if not key.endswith("s") else [key[:-1]]
|
||||
for alt in alts:
|
||||
if len(alt) < 3:
|
||||
continue
|
||||
hit = (
|
||||
session.execute(select(Tag).where(expr == alt).order_by(Tag.scene_count.desc()))
|
||||
.scalars()
|
||||
.first()
|
||||
)
|
||||
if hit is not None:
|
||||
return hit
|
||||
return None
|
||||
|
||||
|
||||
def _canonical_dup2_slug(session: Session, slug: str) -> str:
|
||||
|
|
|
|||
|
|
@ -16,6 +16,14 @@ export type ChangelogEntry = {
|
|||
};
|
||||
|
||||
export const CHANGELOG: ChangelogEntry[] = [
|
||||
{
|
||||
id: '2026-08-02',
|
||||
date: 'August 2026',
|
||||
items: [
|
||||
'Tag and performer counts are now shortened (181k instead of 181 764), so the chips stay readable.',
|
||||
'Merged duplicate tags that differed only by a plural, so "Blowjob" and "Blowjobs" are one tag again.',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2026-07-28b',
|
||||
date: 'July 2026',
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
* przez chipy; backend wspiera comma-separated.
|
||||
*/
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { formatCount } from '../lib/formatCount';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
|
|
@ -257,7 +258,7 @@ export function MovieFiltersSheet({ visible, value, onChange, onClose }: Props)
|
|||
>
|
||||
<Text style={[styles.chipText, active && styles.chipTextActive]}>
|
||||
{t.name}
|
||||
{t.scene_count > 0 ? ` · ${t.scene_count}` : ''}
|
||||
{t.scene_count > 0 ? ` · ${formatCount(t.scene_count)}` : ''}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
|
@ -284,7 +285,7 @@ export function MovieFiltersSheet({ visible, value, onChange, onClose }: Props)
|
|||
>
|
||||
<Text style={[styles.chipText, active && styles.chipTextActive]}>
|
||||
{s.name}
|
||||
{s.scene_count > 0 ? ` · ${s.scene_count}` : ''}
|
||||
{s.scene_count > 0 ? ` · ${formatCount(s.scene_count)}` : ''}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
|
@ -336,7 +337,7 @@ export function MovieFiltersSheet({ visible, value, onChange, onClose }: Props)
|
|||
>
|
||||
<Text style={styles.chipText}>
|
||||
{p.canonical_name}
|
||||
{p.scene_count > 0 ? ` · ${p.scene_count}` : ''}
|
||||
{p.scene_count > 0 ? ` · ${formatCount(p.scene_count)}` : ''}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
|
|
|
|||
20
mobile/src/lib/formatCount.ts
Normal file
20
mobile/src/lib/formatCount.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* Skracanie dużych liczników (user-request 2026-08-02: "jeśli jest 181 764, to napisać
|
||||
* 181k"). Popularne tagi mają setki tysięcy scen, a pełna liczba rozpycha chipy i nic
|
||||
* nie wnosi — przy tej skali liczy się rząd wielkości, nie dokładność.
|
||||
*
|
||||
* Do 999 pokazujemy dokładnie. Powyżej: 1,2k / 12k / 181k / 1,2M. Jedno miejsce po
|
||||
* przecinku tylko gdy liczba jest jednocyfrowa w swojej skali (1,2k ale już 12k),
|
||||
* żeby chip nie puchł. Separator dziesiętny przecinkiem, bo apka jest angielska tylko
|
||||
* z nazwy, a i tak czytają to ludzie z całego świata — kropka myliłaby się z tysiącami.
|
||||
*/
|
||||
export function formatCount(n: number | null | undefined): string {
|
||||
const v = typeof n === 'number' && isFinite(n) ? Math.max(0, Math.trunc(n)) : 0;
|
||||
if (v < 1000) return String(v);
|
||||
if (v < 1_000_000) {
|
||||
const k = v / 1000;
|
||||
return (k < 10 ? k.toFixed(1).replace(/\.0$/, '').replace('.', ',') : String(Math.round(k))) + 'k';
|
||||
}
|
||||
const m = v / 1_000_000;
|
||||
return (m < 10 ? m.toFixed(1).replace(/\.0$/, '').replace('.', ',') : String(Math.round(m))) + 'M';
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
// żeby PerformerScenesScreen / StudioScenesScreen mógł pokazać NEW badge na świeżych
|
||||
// scenach.
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { formatCount } from '../lib/formatCount';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
|
|
@ -411,7 +412,7 @@ function PerformerRow({
|
|||
{fav.canonical_name}
|
||||
</Text>
|
||||
<Text style={styles.rowMuted}>
|
||||
{fav.scene_count} {fav.scene_count === 1 ? 'scene' : 'scenes'}
|
||||
{formatCount(fav.scene_count)} {fav.scene_count === 1 ? 'scene' : 'scenes'}
|
||||
</Text>
|
||||
</View>
|
||||
{fav.new_count > 0 ? (
|
||||
|
|
@ -486,7 +487,7 @@ function StudioRow({
|
|||
{fav.name}
|
||||
</Text>
|
||||
<Text style={styles.rowMuted}>
|
||||
{fav.scene_count} {fav.scene_count === 1 ? 'scene' : 'scenes'}
|
||||
{formatCount(fav.scene_count)} {fav.scene_count === 1 ? 'scene' : 'scenes'}
|
||||
{fav.network ? ` · ${fav.network}` : ''}
|
||||
</Text>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Lista performerów + tap → PerformerScenes.
|
||||
// Domyślny order: scene_count desc (najpierw popularni); search po name_normalized.
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { formatCount } from '../lib/formatCount';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import React, { useState } from 'react';
|
||||
|
|
@ -153,7 +154,7 @@ function PerformerRow({
|
|||
</Text>
|
||||
<View style={styles.rowMetaRow}>
|
||||
<Text style={styles.rowCount}>
|
||||
{performer.scene_count} {performer.scene_count === 1 ? 'scene' : 'scenes'}
|
||||
{formatCount(performer.scene_count)} {performer.scene_count === 1 ? 'scene' : 'scenes'}
|
||||
</Text>
|
||||
{performer.gender ? (
|
||||
<View style={styles.genderPill}>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Lista tagów + tap → TagScenes. Domyślny order: popular (po liczbie scen desc).
|
||||
// Layout: chip-grid, bo nazwy tagów są krótkie i upacking ich w listę marnuje miejsce.
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { formatCount } from '../lib/formatCount';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import React, { useState } from 'react';
|
||||
|
|
@ -131,7 +132,7 @@ function TagChip({ tag, onPress }: { tag: TagCount; onPress: () => void }) {
|
|||
{tag.name}
|
||||
</Text>
|
||||
<View style={styles.chipCountWrap}>
|
||||
<Text style={styles.chipCount}>{tag.scene_count}</Text>
|
||||
<Text style={styles.chipCount}>{formatCount(tag.scene_count)}</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue