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.bannerInner}>
<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>
<Pressable onPress={cancelDuplicate} style={styles.bannerCancel} hitSlop={8}>
<Text style={styles.bannerCancelText}>Cancel</Text>

View file

@ -227,7 +227,7 @@ export const CHANGELOG: ChangelogEntry[] = [
id: '2026-06-26',
date: 'June 2026',
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',
date: 'June 2026',
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',
date: 'June 2026',
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).',
'latestpornvideo now keeps up with its newest videos.',
],
@ -295,7 +295,7 @@ export const CHANGELOG: ChangelogEntry[] = [
id: '2026-06-19',
date: 'June 2026',
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.',
],
},
@ -304,7 +304,7 @@ export const CHANGELOG: ChangelogEntry[] = [
date: 'June 2026',
items: [
'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.',
'Privacy: the app is hidden in Recents and blocks screenshots again.',
'Smoother startup when an update is downloading (no more double PIN prompt).',
@ -314,16 +314,16 @@ export const CHANGELOG: ChangelogEntry[] = [
id: '2026-06-13',
date: 'June 2026',
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.',
'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',
date: 'June 2026',
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".',
'More reliable playback and lower data use on several sites.',
'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 * as Clipboard from 'expo-clipboard';
import { MIGRATION_NOTICE } from '../migrationNotice';
import { MIGRATION_NOTICE, needsManualMigration } from '../migrationNotice';
import {
dismissMigrationNotice,
getDeviceId,
@ -31,7 +31,9 @@ export function MigrationBanner() {
const [copied, setCopied] = React.useState(false);
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;
(async () => {
try {

View file

@ -32,7 +32,7 @@ const SLIDES: Slide[] = [
icon: '👋',
title: 'Welcome to Goon',
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.',
],
},
@ -40,9 +40,9 @@ const SLIDES: Slide[] = [
icon: '🗂️',
title: 'Three tabs up top',
lines: [
'Scenes — individual clips.',
'Movies — full-length, often multi-part.',
'Sites — browse by source, each rated 0–5★.',
'Scenes: individual clips.',
'Movies: full-length, often multi-part.',
'Sites: browse by source, each rated 0-5★.',
],
},
{
@ -70,23 +70,23 @@ const SLIDES: Slide[] = [
'Tap once → show or hide the controls.',
'Double-tap left / right → skip back / forward 15s.',
'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: '⭐',
title: 'Make it yours',
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.',
'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: '✅',
title: "You're set",
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.',
],
},

View file

@ -43,7 +43,7 @@ export const DONATE_COINS: DonateCoin[] = [
id: 'btc',
ticker: 'BTC',
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',
color: '#F7931A',
qrScheme: 'bitcoin:',

View file

@ -10,14 +10,32 @@
*
* 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,
* więc nigdy tego banera nie zobaczy — nie trzeba go potem wyłączać zdalnie.
* Baner ma trafiać WYŁĄCZNIE do instalacji sprzed 0.2.2, czyli tych na runtime 1.1.
* 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 = {
/** 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,
/** Wersja do zainstalowania. Pokazywana w banerze. */
version: '0.2.2',
/** Strona pobierania. Celowo strona, nie plik: link przeżyje zmianę nazwy APK. */
downloadUrl: 'https://goon-foss.org/',
} 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 }}>
<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>
<Text style={styles.linkValue}>{REPO_LABEL} ›</Text>
</Pressable>

View file

@ -78,7 +78,7 @@ export function BlacklistScreen() {
renderItem={() => (
<View style={{ paddingBottom: 40 }}>
<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).
</Text>

View file

@ -75,7 +75,7 @@ export function LoginScreen({ onAuthenticated }: Props) {
<Text style={styles.cardHint}>
{advanced
? '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>
{advanced && (

View file

@ -219,7 +219,7 @@ function WatchChip({
playbackId: pb.id,
entityKind: 'movie',
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',
headers: pIsDirect && p.headers ? p.headers : undefined,
fallbackProxyUrl: pIsDirect ? p.stream_url || undefined : undefined,

View file

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

View file

@ -1031,7 +1031,7 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
</Text>
<Text style={styles.errorBody}>
{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.')}
</Text>
<View style={styles.errorBtnRow}>
@ -1582,7 +1582,7 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) {
Alert.alert(
'Source not loading',
'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' }],
);
}, 25000);
@ -1835,7 +1835,7 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) {
<View style={[styles.coverOverlay, styles.overlay]}>
<Text style={styles.errorTitle}>Source no longer available</Text>
<Text style={styles.errorBody}>
The host returned {httpDead} — this video was removed.
The host returned {httpDead}, this video was removed.
</Text>
<View style={styles.errorBtnRow}>
{canMark && (

View file

@ -724,7 +724,7 @@ function PlaybackButton({
const embedLinks = unique.filter((l) => !l.stream_url && !!l.embed_url);
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);
return;
}

View file

@ -205,7 +205,7 @@ export function ScenesFilterModal({
style={styles.search}
value={draft.origin}
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}
autoCapitalize="none"
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.
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>;
const full = '★'.repeat(value);
const empty = '☆'.repeat(5 - value);
@ -266,7 +266,7 @@ function RatingModal({ source, onClose }: { source: SourceOut | null; onClose: (
<ScrollView>
<Text style={styles.modalTitle}>{source ? prettySiteName(source.display_name) : ''}</Text>
{!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}>
@ -288,7 +288,7 @@ function RatingModal({ source, onClose }: { source: SourceOut | null; onClose: (
<Text style={styles.modalMuted}>
{basis === 'telemetry'
? `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>
</>
)}

View file

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