goon/mobile/src/screens/SubScoreBars.tsx
goon-foss ad0284585b Initial commit
Goon — self-hosted aggregator for adult-content scene metadata.

Indexes scenes from TPDB, StashDB, and 30+ public adult tube sites.
Cross-source deduplication via perceptual hash + Levenshtein distance.
FastAPI backend + APScheduler worker + React Native (Expo) mobile client.

FOSS, ad-free, donation-funded. See README for details.
2026-05-20 10:10:22 +02:00

136 lines
4.5 KiB
TypeScript

// Renderuje sub-scores merge candidate jako horizontal bars zamiast JSON dumpa.
// Wagi: fp 0.20 / title 0.15 / performers 0.35 / duration 0.25 / date 0.05 (aggregator)
// Wartości w `reasons` to top-level keys (fp, title, performers, duration, date) jako float [0..1].
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { theme } from '../theme';
const SUBSCORES: { key: string; label: string }[] = [
{ key: 'fp', label: 'fingerprint' },
{ key: 'title', label: 'title' },
{ key: 'performers', label: 'performers' },
{ key: 'duration', label: 'duration' },
{ key: 'date', label: 'date' },
];
function barColor(v: number): string {
if (v >= 0.85) return theme.good;
if (v >= 0.5) return theme.warn;
if (v > 0) return theme.bad;
return theme.border;
}
export function SubScoreBars({ reasons }: { reasons: Record<string, unknown> }) {
// reasons.path = "composite" / "cross_source" / "fp_phash" / etc.
// reasons.{fp,title,performers,duration,date} are floats or null.
// reasons.reasons.aggregator_weights or reasons.reasons.weights — for context label.
const path = (reasons.path as string) ?? 'unknown';
const studioMatch = reasons.studio_match as boolean | null | undefined;
const inner = (reasons.reasons as Record<string, unknown>) ?? {};
const flags: string[] = [];
if (inner.aggregator_low_performer_cap) flags.push('cap: low perf');
if (inner.duration_perf_strong_match_bump) flags.push('bumped (dur+perf)');
if (inner.studio_ignored_aggregator) flags.push('studio ignored');
if (inner.studio_mismatch_overridden_by_fp) flags.push('fp overrode studio');
return (
<View style={styles.container}>
<View style={styles.metaRow}>
<Text style={styles.metaLabel}>path</Text>
<Text style={styles.metaValue}>{path}</Text>
</View>
{studioMatch !== null && studioMatch !== undefined ? (
<View style={styles.metaRow}>
<Text style={styles.metaLabel}>studio match</Text>
<Text style={[styles.metaValue, { color: studioMatch ? theme.good : theme.bad }]}>
{studioMatch ? 'yes' : 'no'}
</Text>
</View>
) : null}
{SUBSCORES.map(({ key, label }) => {
const v = reasons[key] as number | null | undefined;
const present = typeof v === 'number';
return (
<View key={key} style={styles.barRow}>
<Text style={styles.barLabel}>{label}</Text>
<View style={styles.barTrack}>
{present ? (
<View
style={[
styles.barFill,
{
width: `${Math.max(2, Math.round((v as number) * 100))}%`,
backgroundColor: barColor(v as number),
},
]}
/>
) : null}
</View>
<Text style={[styles.barValue, !present && styles.barValueMuted]}>
{present ? (v as number).toFixed(2) : '—'}
</Text>
</View>
);
})}
{flags.length > 0 ? (
<View style={styles.flagsRow}>
{flags.map((f) => (
<View key={f} style={styles.flagPill}>
<Text style={styles.flagText}>{f}</Text>
</View>
))}
</View>
) : null}
</View>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: theme.card,
borderColor: theme.border,
borderWidth: 1,
borderRadius: 8,
padding: 12,
marginBottom: 12,
gap: 6,
},
metaRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
metaLabel: { color: theme.muted, fontSize: 12, textTransform: 'uppercase' },
metaValue: { color: theme.fg, fontSize: 13, fontWeight: '600' },
barRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
barLabel: { color: theme.muted, fontSize: 12, width: 80 },
barTrack: {
flex: 1,
height: 8,
backgroundColor: theme.bg,
borderRadius: 4,
overflow: 'hidden',
},
barFill: {
height: '100%',
borderRadius: 4,
},
barValue: { color: theme.fg, fontSize: 12, fontFamily: 'Courier', width: 40, textAlign: 'right' },
barValueMuted: { color: theme.muted },
flagsRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 4, marginTop: 4 },
flagPill: {
backgroundColor: theme.bg,
borderColor: theme.warn,
borderWidth: 1,
borderRadius: 10,
paddingHorizontal: 8,
paddingVertical: 2,
},
flagText: { color: theme.warn, fontSize: 11 },
});