diff --git a/mobile/App.tsx b/mobile/App.tsx index d2a8aa3..5d5976a 100644 --- a/mobile/App.tsx +++ b/mobile/App.tsx @@ -6,6 +6,8 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import * as Sentry from '@sentry/react-native'; import Constants from 'expo-constants'; import { registerRootComponent } from 'expo'; +import { useFonts } from 'expo-font'; +import { Text as RNText } from 'react-native'; import * as ScreenCapture from 'expo-screen-capture'; import { StatusBar } from 'expo-status-bar'; import * as Updates from 'expo-updates'; @@ -75,7 +77,30 @@ const queryClient = new QueryClient({ }, }); +// Globalny default fontu dla całego — General Sans Regular jako body. +// Komponenty które chcą display/mono nadpisują fontFamily jawnie (bo RN nie +// syntezuje weightów dla custom fontów). Bold-ale-bez-fontFamily tekst zostanie +// Regular weightem General Sans — wciąż distinctive face, akceptowalne dla +// nietkniętych ekranów; high-traffic komponenty mają jawny Semibold. +let _textDefaultApplied = false; +function applyDefaultFont() { + if (_textDefaultApplied) return; + _textDefaultApplied = true; + const T = RNText as unknown as { defaultProps?: { style?: unknown } }; + T.defaultProps = T.defaultProps || {}; + const prev = T.defaultProps.style; + T.defaultProps.style = [{ fontFamily: 'GeneralSans-Regular' }, prev].filter(Boolean); +} + export default function App() { + const [fontsLoaded] = useFonts({ + 'GeneralSans-Regular': require('./assets/fonts/GeneralSans-Regular.ttf'), + 'GeneralSans-Medium': require('./assets/fonts/GeneralSans-Medium.ttf'), + 'GeneralSans-Semibold': require('./assets/fonts/GeneralSans-Semibold.ttf'), + 'GeistMono-Regular': require('./assets/fonts/GeistMono-Regular.ttf'), + }); + if (fontsLoaded) applyDefaultFont(); + const [hydrated, setHydrated] = useState(false); const [ageAccepted, setAgeAccepted] = useState(false); const [client, setClient] = useState(null); @@ -245,7 +270,7 @@ export default function App() { return () => sub.remove(); }, []); - if (!hydrated || !lockReady) { + if (!fontsLoaded || !hydrated || !lockReady) { return ( diff --git a/mobile/app.json b/mobile/app.json index 67a3396..9210df5 100644 --- a/mobile/app.json +++ b/mobile/app.json @@ -44,11 +44,12 @@ } ], "expo-video", - "@sentry/react-native/expo" + "@sentry/react-native/expo", + "expo-font" ], "extra": { "sentryDsn": "", "sentryEnvironment": "production" } } -} \ No newline at end of file +} diff --git a/mobile/assets/fonts/GeistMono-Regular.ttf b/mobile/assets/fonts/GeistMono-Regular.ttf new file mode 100644 index 0000000..643e6ba Binary files /dev/null and b/mobile/assets/fonts/GeistMono-Regular.ttf differ diff --git a/mobile/assets/fonts/GeneralSans-Medium.ttf b/mobile/assets/fonts/GeneralSans-Medium.ttf new file mode 100644 index 0000000..e072c83 Binary files /dev/null and b/mobile/assets/fonts/GeneralSans-Medium.ttf differ diff --git a/mobile/assets/fonts/GeneralSans-Regular.ttf b/mobile/assets/fonts/GeneralSans-Regular.ttf new file mode 100644 index 0000000..d970a99 Binary files /dev/null and b/mobile/assets/fonts/GeneralSans-Regular.ttf differ diff --git a/mobile/assets/fonts/GeneralSans-Semibold.ttf b/mobile/assets/fonts/GeneralSans-Semibold.ttf new file mode 100644 index 0000000..916b809 Binary files /dev/null and b/mobile/assets/fonts/GeneralSans-Semibold.ttf differ diff --git a/mobile/package-lock.json b/mobile/package-lock.json index bd543d5..88b0278 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -16,6 +16,7 @@ "expo-asset": "~11.0.5", "expo-build-properties": "~0.13.3", "expo-clipboard": "~7.0.1", + "expo-font": "~13.0.4", "expo-haptics": "~14.0.1", "expo-image": "~2.0.7", "expo-intent-launcher": "~12.0.2", @@ -6246,6 +6247,19 @@ "integrity": "sha512-t+1F1tiDocSot8iSnrn/CjTUMvVvPV2DpafSVcticpbSzMGybEN7wcamO1t18fK7WxGXpZE9gxtd80qwv/LLqQ==", "license": "MIT" }, + "node_modules/expo-font": { + "version": "13.0.4", + "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-13.0.4.tgz", + "integrity": "sha512-eAP5hyBgC8gafFtprsz0HMaB795qZfgJWqTmU0NfbSin1wUuVySFMEPMOrTkTgmazU73v4Cb4x7p86jY1XXYUw==", + "license": "MIT", + "dependencies": { + "fontfaceobserver": "^2.1.0" + }, + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, "node_modules/expo-haptics": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/expo-haptics/-/expo-haptics-14.0.1.tgz", @@ -6500,19 +6514,6 @@ "react-native": "*" } }, - "node_modules/expo/node_modules/expo-font": { - "version": "13.0.4", - "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-13.0.4.tgz", - "integrity": "sha512-eAP5hyBgC8gafFtprsz0HMaB795qZfgJWqTmU0NfbSin1wUuVySFMEPMOrTkTgmazU73v4Cb4x7p86jY1XXYUw==", - "license": "MIT", - "dependencies": { - "fontfaceobserver": "^2.1.0" - }, - "peerDependencies": { - "expo": "*", - "react": "*" - } - }, "node_modules/expo/node_modules/expo-keep-awake": { "version": "14.0.3", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.0.3.tgz", diff --git a/mobile/package.json b/mobile/package.json index 79ee2de..3381c14 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -18,6 +18,7 @@ "expo-asset": "~11.0.5", "expo-build-properties": "~0.13.3", "expo-clipboard": "~7.0.1", + "expo-font": "~13.0.4", "expo-haptics": "~14.0.1", "expo-image": "~2.0.7", "expo-intent-launcher": "~12.0.2", diff --git a/mobile/src/components/GoonWordmark.tsx b/mobile/src/components/GoonWordmark.tsx index cc5b1d1..c846268 100644 --- a/mobile/src/components/GoonWordmark.tsx +++ b/mobile/src/components/GoonWordmark.tsx @@ -1,124 +1,76 @@ /** - * Goon wordmark — custom letterform, NOT a typed text in a font. + * Goon brand marks. * - * 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" + * Rework 2026-05-30: poprzednia wersja rysowała litery ręcznie jako SVG path + * geometry — wychodziło krzywo (o-ka jako nachodzące elipsy, zniekształcone n). + * Teraz mamy General Sans Semibold jako realny font (useFonts w App.tsx), więc + * wordmark renderuje PRAWDZIWY tekst w tej rodzinie — czysto i spójnie z resztą + * typografii. * - * Viewbox 200×56 — 4 litery × ~44px width, używamy aspect ratio scale. + * Distinctive twist: dwutonowe "g[oo]n" — środkowe "oo" w oxblood (brand accent), + * "g"+"n" w foreground. Czytelne nawet w małym headerze, wiąże logo z paletą. * - * Użycie: - * - * // dla splash + * // header + * // splash, jednolity kolor */ import React from 'react'; -import Svg, { Path } from 'react-native-svg'; +import { Text, View } from 'react-native'; +import Svg, { Circle } from 'react-native-svg'; -interface Props { - width?: number; +import { fonts, theme } from '../theme'; + +interface WordmarkProps { + /** fontSize wordmarku w px. */ + size?: number; + /** Kolor liter g+n (oo zawsze accent, chyba że `mono`). */ color?: string; + /** Jednolity kolor (bez dwutonu) — np. na splash gdzie tło = accent. */ + mono?: boolean; } -export function GoonWordmark({ width = 120, color = '#F5EDE0' }: Props) { - const height = width * (56 / 200); +export function GoonWordmark({ size = 26, color = theme.fg, mono = false }: WordmarkProps) { + const base = { + fontFamily: fonts.display, + fontSize: size, + letterSpacing: -size * 0.03, + includeFontPadding: false as const, + }; + const accent = mono ? color : theme.accent; 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. - */} - - + + g + oo + n + ); } /** - * Monogram — tylko "g" + ucho descender, jako standalone icon. - * 56×56, używany jako adaptive-icon / splash mark. + * Monogram — koncentryczny ring + wypełniona kropka (oxblood). Czyta się jako + * "o" z wordmarku, motyw soczewki/oka (watching) bez dosłowności. Czysty SVG + * (bez fontu) — używany do generowania app-icon / adaptive-icon / splash PNG, + * gdzie custom font nie jest dostępny. + * + * */ -export function GoonMark({ size = 48, color = '#F5EDE0' }: { size?: number; color?: string }) { +export function GoonMark({ + size = 48, + ringColor = theme.fg, + dotColor = theme.accent, + bg, +}: { + size?: number; + ringColor?: string; + dotColor?: string; + /** Opcjonalne tło (dla icon — np. theme.bg). Pominięte = przezroczyste. */ + bg?: string; +}) { + // viewBox 100×100. Ring: cx50 cy50 r36, stroke 11. Dot: r15 wypełniony. return ( - - + + {bg ? : null} + + ); } diff --git a/mobile/src/components/MoviePosterCard.tsx b/mobile/src/components/MoviePosterCard.tsx index 8e13370..78cf24e 100644 --- a/mobile/src/components/MoviePosterCard.tsx +++ b/mobile/src/components/MoviePosterCard.tsx @@ -3,7 +3,7 @@ import { Image } from 'expo-image'; import React from 'react'; import { Pressable, StyleSheet, Text, View } from 'react-native'; -import { theme } from '../theme'; +import { fonts, theme } from '../theme'; import type { MovieOut } from '../types'; export function MoviePosterCard({ @@ -101,7 +101,7 @@ const styles = StyleSheet.create({ paddingHorizontal: 6, paddingVertical: 2, }, - newBadgeText: { color: theme.fg, fontSize: 10, fontWeight: '800', letterSpacing: 0.5 }, + newBadgeText: { color: theme.fg, fontSize: 9, fontFamily: fonts.mono, fontWeight: '700', letterSpacing: 0.6 }, posterDimmed: { opacity: 0.45 }, watchedBadge: { position: 'absolute', @@ -124,7 +124,7 @@ const styles = StyleSheet.create({ backgroundColor: 'rgba(0,0,0,0.5)', }, progressFg: { height: 3, backgroundColor: theme.accent }, - title: { color: theme.fg, fontSize: 13, fontWeight: '600', marginTop: 6 }, + title: { color: theme.fg, fontSize: 13, fontFamily: fonts.display, marginTop: 6, letterSpacing: -0.2 }, titleDimmed: { color: theme.muted }, - meta: { color: theme.muted, fontSize: 11, marginTop: 2 }, + meta: { color: theme.muted, fontSize: 10, fontFamily: fonts.mono, marginTop: 2, letterSpacing: 0.5, textTransform: 'uppercase' }, }); diff --git a/mobile/src/components/SceneTile.tsx b/mobile/src/components/SceneTile.tsx index 31ed1d9..94749cc 100644 --- a/mobile/src/components/SceneTile.tsx +++ b/mobile/src/components/SceneTile.tsx @@ -27,7 +27,7 @@ import React, { useState } from 'react'; import { Pressable, StyleSheet, Text, View } from 'react-native'; import type { RootStackParamList } from '../navigation'; -import { theme } from '../theme'; +import { fonts, theme } from '../theme'; import type { SceneOut } from '../types'; import { Thumb } from './Thumb'; @@ -190,22 +190,23 @@ const styles = StyleSheet.create({ durText: { color: theme.fg, fontSize: 11, - fontWeight: '600', + fontFamily: fonts.mono, fontVariant: ['tabular-nums'], }, title: { color: theme.fg, fontSize: 14, - fontWeight: '600', + fontFamily: fonts.display, marginTop: 8, letterSpacing: -0.2, }, titleDim: { color: theme.muted }, meta: { color: theme.muted, - fontSize: 11, - marginTop: 2, - letterSpacing: 0.3, + fontSize: 10, + fontFamily: fonts.mono, + marginTop: 3, + letterSpacing: 0.5, textTransform: 'uppercase', }, }); diff --git a/mobile/src/navigation.tsx b/mobile/src/navigation.tsx index deb1b0a..2613853 100644 --- a/mobile/src/navigation.tsx +++ b/mobile/src/navigation.tsx @@ -24,7 +24,7 @@ import { SitesScreen } from './screens/SitesScreen'; import { StudioScenesScreen } from './screens/StudioScenesScreen'; import { TagScenesScreen } from './screens/TagScenesScreen'; import { TagsScreen } from './screens/TagsScreen'; -import { theme } from './theme'; +import { fonts, theme } from './theme'; export type RootStackParamList = { Scenes: undefined; @@ -90,8 +90,8 @@ function TopTabs({ const tabs: TopTab[] = ['Scenes', 'Movies', 'Sites']; return ( - {/* Wordmark — branding po lewej, dystans 14px do pierwszego tabu. */} - + {/* Wordmark — branding po lewej, dystans do pierwszego tabu. */} + {tabs.map((t) => { const active = t === current; @@ -101,7 +101,7 @@ function TopTabs({ style={{ color: active ? theme.accent : theme.muted, fontSize: 13, - fontWeight: active ? '700' : '500', + fontFamily: active ? fonts.display : fonts.medium, letterSpacing: 0.3, }} > @@ -145,7 +145,7 @@ export function AppNavigator({ onLogout, client, appVersion }: AppNavigatorProps {() => } diff --git a/mobile/src/screens/AgeGateScreen.tsx b/mobile/src/screens/AgeGateScreen.tsx index f03385b..0c77986 100644 --- a/mobile/src/screens/AgeGateScreen.tsx +++ b/mobile/src/screens/AgeGateScreen.tsx @@ -9,6 +9,7 @@ import { View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; +import { GoonWordmark } from '../components/GoonWordmark'; import { markAccepted } from '../lib/agegate'; import { theme } from '../theme'; @@ -65,7 +66,7 @@ export function AgeGateScreen({ onAccept }: Props) { contentContainerStyle={styles.scroll} showsVerticalScrollIndicator={false} > - goon + Adult content · Self-hosted only diff --git a/mobile/src/screens/LoginScreen.tsx b/mobile/src/screens/LoginScreen.tsx index 6f5ad43..d6c310e 100644 --- a/mobile/src/screens/LoginScreen.tsx +++ b/mobile/src/screens/LoginScreen.tsx @@ -11,6 +11,7 @@ import { View, } from 'react-native'; import { GoonClient } from '../api'; +import { GoonMark, GoonWordmark } from '../components/GoonWordmark'; import { saveCredentials } from '../storage'; import { theme } from '../theme'; @@ -57,8 +58,9 @@ export function LoginScreen({ onAuthenticated }: Props) { keyboardShouldPersistTaps="handled" > - - goon + + + self-hosted scene catalog diff --git a/mobile/src/theme.ts b/mobile/src/theme.ts index 596d0b8..4c428f7 100644 --- a/mobile/src/theme.ts +++ b/mobile/src/theme.ts @@ -41,29 +41,26 @@ export const theme = { } 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. + * Font family — General Sans (display + body, Fontshare) + Geist Mono (meta, + * Vercel OFL). Pliki .ttf w `mobile/assets/fonts/`, ładowane przez useFonts() + * w App.tsx (runtime load — działa przez OTA bo expo-font native jest w APK). * - * Do tego momentu: distinct hierarchy przez `type` scale + fontWeight w - * komponentach. System font (San Francisco / Roboto) jest OK temporary. + * WAŻNE (custom-font gotcha): RN NIE syntezuje weightów dla custom fontów — + * fontWeight:'700' na fontFamily:'GeneralSans-Regular' NIE pogrubi. Trzeba + * jawnie wskazać rodzinę per-weight. Stąd 4 osobne sloty: + * - body → Regular (400) — domyślny tekst + * - medium → Medium (500) — labels, tab inactive + * - display → Semibold (600)— headingi, tytuły, bold + * - mono → Geist Mono — meta, duration, liczby, kategorie + * + * Gdy useFonts jeszcze nie ready, App.tsx blokuje render (fonty z bundla + * ładują się <100ms), więc te stałe są zawsze valid przy pierwszym paint. */ export const fonts = { - display: undefined as string | undefined, - displayRegular: undefined as string | undefined, - mono: undefined as string | undefined, + body: 'GeneralSans-Regular', + medium: 'GeneralSans-Medium', + display: 'GeneralSans-Semibold', + mono: 'GeistMono-Regular', } as const; export function scoreColor(score: number): string {