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:
parent
007f84a236
commit
017927793a
16 changed files with 57 additions and 37 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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:',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export function PerformersScreen() {
|
|||
<SegButton
|
||||
active={order === 'name'}
|
||||
onPress={() => setOrder('name')}
|
||||
label="A–Z"
|
||||
label="A-Z"
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export function TagsScreen() {
|
|||
<SegButton
|
||||
active={order === 'name'}
|
||||
onPress={() => setOrder('name')}
|
||||
label="A–Z"
|
||||
label="A-Z"
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue