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.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>
|
||||||
|
|
|
||||||
|
|
@ -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.',
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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.',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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:',
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 && (
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 && (
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue