Option B (rebuild APK) — odblokowuje custom fonty na stałe + sprawia że przyszłe font-OTA nie crashują. - runtime 1.0 → 1.1 (app.json + AndroidManifest EXPO_RUNTIME_VERSION): nowy APK ma native ExpoFontLoader, więc MUSI mieć inny runtime niż stare instalacje 1.0 (inaczej font-OTA crashnęłoby stare). 1.0 channel zostaje na d5b87e5c (font-stripped) dla starych, 1.1 = nowy APK z fontami. - version 0.2.0 / versionCode 10 (build.gradle) — in-app updater (/version=0.2.0) zaoferuje install starym 0.1.9. - Fonty przywrócone (useFonts, theme.fonts realne, SceneTile/MoviePosterCard/ navigation/GoonWordmark fontFamily) — działają bo native jest w APK. - Build: gradlew assembleRelease (autolinking expo-font, BEZ prebuild — zachowane custom native AntiTamper/ApkInstaller), Sentry source-map upload wyłączony (SENTRY_DISABLE_AUTO_UPLOAD, brak org/auth — krok poboczny). - app/main.py /version 0.1.9 → 0.2.0. ZWERYFIKOWANE na emulatorze: podpis SHA-256 == ALLOWED_APP_SIG_HASH (anti-tamper OK), ExpoFontLoader w classes3.dex, `ReactNativeJS: Running "main"` bez crasha. APK live: /static/app-release.apk + goon-v0.2.0.apk + landing webroot. UWAGA: launcher-icon (native mipmaps) NIE zmienione w tym buildzie — nadal stara ikona. Nowy oo-icon wymaga regeneracji res/mipmap-* + rebuild (follow-up). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
130 lines
4.2 KiB
TypeScript
130 lines
4.2 KiB
TypeScript
// Movie poster card — 2:3 aspect poster + title + year/studio meta.
|
|
// Współdzielona przez MoviesScreen (grid) i PerformerScenes (Movies tab).
|
|
import { Image } from 'expo-image';
|
|
import React from 'react';
|
|
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
|
import { fonts, theme } from '../theme';
|
|
import type { MovieOut } from '../types';
|
|
|
|
export function MoviePosterCard({
|
|
movie,
|
|
isNew = false,
|
|
onPress,
|
|
}: {
|
|
movie: MovieOut;
|
|
isNew?: boolean;
|
|
onPress: () => void;
|
|
}) {
|
|
const studio = movie.studio?.name;
|
|
// Watched indicator (parytet ze ScenesScreen): finished=true → dim posteru +
|
|
// ✓ badge w prawym górnym. Pośredni progress bez dim'a (user może wrócić).
|
|
// bug-report b207ff17 2026-05-26.
|
|
const dim = movie.finished === true;
|
|
const progressPct =
|
|
!dim && movie.position_sec && movie.duration_sec && movie.duration_sec > 0
|
|
? Math.min(100, Math.round((movie.position_sec / movie.duration_sec) * 100))
|
|
: 0;
|
|
return (
|
|
<Pressable style={styles.card} onPress={onPress}>
|
|
<View style={styles.posterWrap}>
|
|
{movie.poster_url ? (
|
|
<Image
|
|
source={{ uri: movie.poster_url }}
|
|
style={[styles.poster, dim && styles.posterDimmed]}
|
|
contentFit="cover"
|
|
/>
|
|
) : (
|
|
<View style={[styles.poster, styles.posterPlaceholder, dim && styles.posterDimmed]}>
|
|
<Text style={styles.posterPlaceholderText}>{movie.title}</Text>
|
|
</View>
|
|
)}
|
|
{movie.rating != null ? (
|
|
<View style={styles.ratingBadge}>
|
|
<Text style={styles.ratingText}>{movie.rating.toFixed(1)}</Text>
|
|
</View>
|
|
) : null}
|
|
{isNew ? (
|
|
<View style={styles.newBadge}>
|
|
<Text style={styles.newBadgeText}>NEW</Text>
|
|
</View>
|
|
) : null}
|
|
{dim ? (
|
|
<View style={styles.watchedBadge}>
|
|
<Text style={styles.watchedBadgeText}>✓</Text>
|
|
</View>
|
|
) : null}
|
|
{progressPct > 0 ? (
|
|
<View style={styles.progressBg}>
|
|
<View style={[styles.progressFg, { width: `${progressPct}%` }]} />
|
|
</View>
|
|
) : null}
|
|
</View>
|
|
<Text style={[styles.title, dim && styles.titleDimmed]} numberOfLines={2}>
|
|
{movie.title}
|
|
</Text>
|
|
<Text style={styles.meta} numberOfLines={1}>
|
|
{movie.release_year ?? '—'}
|
|
{studio ? ` · ${studio}` : ''}
|
|
</Text>
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
card: { flex: 1, marginBottom: 14 },
|
|
posterWrap: {
|
|
aspectRatio: 2 / 3,
|
|
backgroundColor: theme.card,
|
|
borderRadius: 10,
|
|
overflow: 'hidden',
|
|
position: 'relative',
|
|
},
|
|
poster: { width: '100%', height: '100%' },
|
|
posterPlaceholder: { alignItems: 'center', justifyContent: 'center', padding: 12 },
|
|
posterPlaceholderText: { color: theme.muted, fontSize: 12, textAlign: 'center' },
|
|
ratingBadge: {
|
|
position: 'absolute',
|
|
top: 8,
|
|
right: 8,
|
|
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
borderRadius: 6,
|
|
paddingHorizontal: 6,
|
|
paddingVertical: 2,
|
|
},
|
|
ratingText: { color: theme.fg, fontSize: 11, fontWeight: '700' },
|
|
newBadge: {
|
|
position: 'absolute',
|
|
top: 8,
|
|
left: 8,
|
|
backgroundColor: theme.accent,
|
|
borderRadius: 4,
|
|
paddingHorizontal: 6,
|
|
paddingVertical: 2,
|
|
},
|
|
newBadgeText: { color: theme.fg, fontSize: 9, fontFamily: fonts.mono, fontWeight: '700', letterSpacing: 0.6 },
|
|
posterDimmed: { opacity: 0.45 },
|
|
watchedBadge: {
|
|
position: 'absolute',
|
|
bottom: 8,
|
|
right: 8,
|
|
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
borderRadius: 999,
|
|
width: 22,
|
|
height: 22,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
watchedBadgeText: { color: theme.fg, fontSize: 12, fontWeight: '700' },
|
|
progressBg: {
|
|
position: 'absolute',
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
height: 3,
|
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
},
|
|
progressFg: { height: 3, backgroundColor: theme.accent },
|
|
title: { color: theme.fg, fontSize: 13, fontFamily: fonts.display, marginTop: 6, letterSpacing: -0.2 },
|
|
titleDimmed: { color: theme.muted },
|
|
meta: { color: theme.muted, fontSize: 10, fontFamily: fonts.mono, marginTop: 2, letterSpacing: 0.5, textTransform: 'uppercase' },
|
|
});
|