diff --git a/mobile/src/components/GoonWordmark.tsx b/mobile/src/components/GoonWordmark.tsx new file mode 100644 index 0000000..cc5b1d1 --- /dev/null +++ b/mobile/src/components/GoonWordmark.tsx @@ -0,0 +1,124 @@ +/** + * Goon wordmark — custom letterform, NOT a typed text in a font. + * + * 4 litery jako path geometry. Detale: + * - "g" otwarte u dołu (descender curve cut), ucho descender przedłużone + * - "o" — okrąg z lekkim spłaszczeniem w pionie (1.0×0.95 aspect), counter + * z 60% wysokości (cięższe niż typical grotesk → daje "weight") + * - "n" — minimal grotesk, stem łączy się z arc bez serif joint + * - Kerning naturalny, ale "oo" lekko ciaśniejsze niż "go"/"on" + * + * Viewbox 200×56 — 4 litery × ~44px width, używamy aspect ratio scale. + * + * Użycie: + * + * // dla splash + */ +import React from 'react'; +import Svg, { Path } from 'react-native-svg'; + +interface Props { + width?: number; + color?: string; +} + +export function GoonWordmark({ width = 120, color = '#F5EDE0' }: Props) { + const height = width * (56 / 200); + return ( + + {/* + g — descender extends below baseline. Counter = 12 radius hole. + Path: outer ellipse, inner counter (evenodd), descender hook from + bottom-right going down-left. + */} + + {/* + o — slightly flat ellipse, heavy counter. + */} + + {/* o #2 — tight pair z poprzednim o (kerning -2px) */} + + {/* + n — grotesk z arc bez serif. Lewa stem prosty, prawa wychodzi z arc. + */} + + + ); +} + +/** + * Monogram — tylko "g" + ucho descender, jako standalone icon. + * 56×56, używany jako adaptive-icon / splash mark. + */ +export function GoonMark({ size = 48, color = '#F5EDE0' }: { size?: number; color?: string }) { + return ( + + + + ); +} diff --git a/mobile/src/navigation.tsx b/mobile/src/navigation.tsx index cbee240..deb1b0a 100644 --- a/mobile/src/navigation.tsx +++ b/mobile/src/navigation.tsx @@ -4,6 +4,7 @@ import { useNavigationContainerRef, } from '@react-navigation/native'; import { BugReportFAB } from './components/BugReportFAB'; +import { GoonWordmark } from './components/GoonWordmark'; import { GoonClient } from './api'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import React from 'react'; @@ -88,7 +89,10 @@ function TopTabs({ }) { const tabs: TopTab[] = ['Scenes', 'Movies', 'Sites']; return ( - + + {/* Wordmark — branding po lewej, dystans 14px do pierwszego tabu. */} + + {tabs.map((t) => { const active = t === current; return ( @@ -96,8 +100,9 @@ function TopTabs({ {t} diff --git a/mobile/src/screens/ScenesScreen.tsx b/mobile/src/screens/ScenesScreen.tsx index 62fde60..1182bfe 100644 --- a/mobile/src/screens/ScenesScreen.tsx +++ b/mobile/src/screens/ScenesScreen.tsx @@ -118,7 +118,9 @@ export function ScenesScreen() { s.id} - renderItem={({ item }) => } + numColumns={2} + renderItem={({ item }) => } + columnWrapperStyle={styles.gridRow} ListHeaderComponent={!debouncedQ && activeCount === 0 ? : null} refreshing={isRefetching} onRefresh={refetch} @@ -221,6 +223,80 @@ function FavoritesButton() { ); } +/** + * SceneTile — 2-col 16:9 grid item, minimal text (video portal). + * + * Per impeccable.style/slop + Jan feedback "większe miniaturki, mniej tekstu": + * - Thumb wypełnia szerokość kolumny (16:9 aspect ratio) + * - Title 1 linijka, system font, weight 600 + * - Overlay: studio (mono small) + duration → tylko na thumb, NIE pod + * - Wyrzucone: performers, release_date, sources count, "✓ watched" string + * (zastąpione check badge na thumb) + * - Long-press → animated preview (zachowane) + */ +function SceneTile({ scene }: { scene: SceneOut }) { + const navigation = + useNavigation>(); + const [isPreviewing, setIsPreviewing] = useState(false); + + const animatedUrl = scene.playback_sources.find((s) => s.animated_thumbnail_url) + ?.animated_thumbnail_url; + const staticUrl = scene.playback_sources.find((s) => s.thumbnail_url)?.thumbnail_url; + const displayUrl = isPreviewing && animatedUrl ? animatedUrl : staticUrl ?? animatedUrl; + + const startPreview = () => { + if (!animatedUrl) return; + setIsPreviewing(true); + Haptics.selectionAsync().catch(() => {}); + }; + + const dim = scene.finished === true; + const dur = scene.duration_sec; + const durLabel = + dur && dur > 0 + ? dur >= 3600 + ? `${Math.floor(dur / 3600)}h${String(Math.floor((dur % 3600) / 60)).padStart(2, '0')}` + : `${Math.floor(dur / 60)}m` + : null; + + return ( + navigation.navigate('SceneDetail', { id: scene.id })} + onLongPress={startPreview} + onPressOut={() => setIsPreviewing(false)} + delayLongPress={180} + > + + + {scene.is_favorite ? ( + + + + ) : null} + {durLabel ? ( + + {durLabel} + + ) : null} + {dim ? ( + + + + ) : null} + + + {scene.title} + + {scene.studio?.name ? ( + + {scene.studio.name} + + ) : null} + + ); +} + function SceneRow({ scene }: { scene: SceneOut }) { const navigation = useNavigation>(); @@ -429,4 +505,72 @@ const styles = StyleSheet.create({ marginTop: 6, lineHeight: 15, }, + + // 2-col grid (SceneTile) — wprowadzone 2026-05-29 (UI overhaul, Jan feedback + // "większe miniaturki, mniej tekstu"). Wcześniej był full-width SceneRow z 6 + // liniami tekstu — sloppy density dla video portalu. + gridRow: { gap: 10, marginBottom: 14 }, + tile: { flex: 1 }, + tileThumbWrap: { + width: '100%', + aspectRatio: 16 / 9, + borderRadius: 6, + overflow: 'hidden', + position: 'relative', + backgroundColor: theme.bgElevated, + }, + tileThumb: { width: '100%', height: '100%' }, + tileThumbDim: { opacity: 0.45 }, + tileFavBadge: { + position: 'absolute', + top: 6, + left: 6, + backgroundColor: 'rgba(0,0,0,0.7)', + paddingHorizontal: 5, + paddingVertical: 1, + borderRadius: 6, + }, + tileFavBadgeText: { color: theme.accent, fontSize: 11, fontWeight: '700' }, + tileDurBadge: { + position: 'absolute', + bottom: 6, + right: 6, + backgroundColor: 'rgba(0,0,0,0.78)', + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: 4, + }, + tileDurText: { + color: theme.fg, + fontSize: 11, + fontWeight: '600', + fontVariant: ['tabular-nums'], + }, + tileWatchedBadge: { + position: 'absolute', + top: 6, + right: 6, + backgroundColor: 'rgba(0,0,0,0.78)', + width: 20, + height: 20, + borderRadius: 999, + alignItems: 'center', + justifyContent: 'center', + }, + tileWatchedText: { color: theme.fg, fontSize: 11, fontWeight: '700' }, + tileTitle: { + color: theme.fg, + fontSize: 14, + fontWeight: '600', + marginTop: 8, + letterSpacing: -0.2, + }, + tileTitleDim: { color: theme.muted }, + tileStudio: { + color: theme.muted, + fontSize: 11, + marginTop: 2, + letterSpacing: 0.3, + textTransform: 'uppercase', + }, }); diff --git a/mobile/src/theme.ts b/mobile/src/theme.ts index df39539..596d0b8 100644 --- a/mobile/src/theme.ts +++ b/mobile/src/theme.ts @@ -1,26 +1,100 @@ +/** + * Goon theme — warm dark + oxblood, NOT generic navy/purple AI-default. + * + * Audit 2026-05-29 (impeccable.style/slop): + * - Wcześniej: bg #08090F (deep navy), accent #8B5CF6 (purple) + glow #A78BFA + * → literal example "AI default palette" z impeccable. + * - Teraz: bg #15110D (warm charcoal — orange undertone), accent #B23A48 + * (oxblood/rust), brak glowów, brak gradientów neon. + * + * Typography: General Sans (display, Fontshare) + Geist Mono (meta, Vercel + * fonts). Font files w `mobile/assets/fonts/` — `useFonts()` w App.tsx + * graceful-fallback do system gdy brak (development). + */ + export const theme = { - bg: '#08090F', - bgElevated: '#11131C', - card: '#1A1D2A', - border: '#262A3D', - borderFocus: '#8B5CF6', + // Warm dark — charcoal z orange undertone (NIE navy). + // Filmowy, premium feel — jak Letterboxd/Mubi/A24. + bg: '#15110D', + bgElevated: '#1E1A14', + card: '#26201A', + border: '#3A3128', + borderFocus: '#B23A48', - fg: '#F4F4F8', - muted: '#9CA0B5', - mutedDim: '#6B6F85', + // Foreground — warm off-white (nie pure white żeby nie biło na warm dark). + fg: '#F5EDE0', + muted: '#A89B85', + mutedDim: '#6F6555', - accent: '#8B5CF6', - accentGlow: '#A78BFA', - accentDeep: '#5B21B6', - accentSecondary: '#3B82F6', + // Accent — oxblood/rust. Distinctive, niekonwencjonalne dla "media app". + // Brak glow/neon. Deep wariant dla pressed states. + accent: '#B23A48', + accentGlow: '#B23A48', // alias — back-compat, ale bez prawdziwego glow + accentDeep: '#7A1F2A', + accentSecondary: '#D89B4A', // muted amber dla secondary CTAs - good: '#10B981', - bad: '#EF4444', - warn: '#F59E0B', -}; + // Status — tonowane do palety (nie generic green/red). + good: '#5E8C5A', // muted olive-green + bad: '#C44545', + warn: '#D89B4A', // amber + +} as const; + +/** + * Font family — pusty string = system default na iOS/Android (uniknięcie + * "font not found" warningów). Po zainstalowaniu custom fontów (TODO niżej): + * 1. `npx expo install expo-font expo-splash-screen` + * 2. Pobierz fonty: + * - General Sans (Fontshare, free): https://www.fontshare.com/fonts/general-sans + * - Geist Mono (Vercel, free OFL): https://github.com/vercel/geist-font + * 3. Skopiuj .ttf do `mobile/assets/fonts/` + * 4. W App.tsx: + * const [loaded] = useFonts({ + * 'GeneralSans-Semibold': require('./assets/fonts/GeneralSans-Semibold.ttf'), + * 'GeneralSans-Regular': require('./assets/fonts/GeneralSans-Regular.ttf'), + * 'GeistMono-Regular': require('./assets/fonts/GeistMono-Regular.ttf'), + * }); + * if (!loaded) return null; // lub SplashScreen.preventAutoHideAsync() + * 5. Zmień stałe poniżej na konkretne fontFamily strings. + * + * Do tego momentu: distinct hierarchy przez `type` scale + fontWeight w + * komponentach. System font (San Francisco / Roboto) jest OK temporary. + */ +export const fonts = { + display: undefined as string | undefined, + displayRegular: undefined as string | undefined, + mono: undefined as string | undefined, +} as const; export function scoreColor(score: number): string { if (score >= 0.92) return theme.good; if (score >= 0.75) return theme.warn; return theme.bad; } + +/** + * Type-scale (impeccable: "at least 1.25 ratio between steps"). + * Base 14 → 17 → 22 → 28 → 36. Użyj zamiast hardcodowanego fontSize. + */ +export const type = { + micro: 11, + meta: 13, + body: 14, + bodyLarge: 17, + title: 22, + display: 28, + hero: 36, +} as const; + +/** + * Spacing scale — "tight groupings, generous between sections" (impeccable). + * Pomijamy 12/20 — preferuj 8/16/24/32 dla widocznej hierarchii sekcji. + */ +export const space = { + xs: 4, + sm: 8, + md: 16, + lg: 24, + xl: 32, + xxl: 48, +} as const;