goon/mobile/src/components/MoviePosterCard.tsx
jtrzupek 0281e449fe build(apk): 0.2.0 — expo-font native, runtime 1.1, fonts re-enabled
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>
2026-05-31 12:51:32 +02:00

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' },
});