Goon — self-hosted aggregator for adult-content scene metadata. Indexes scenes from TPDB, StashDB, and 30+ public adult tube sites. Cross-source deduplication via perceptual hash + Levenshtein distance. FastAPI backend + APScheduler worker + React Native (Expo) mobile client. FOSS, ad-free, donation-funded. See README for details.
255 lines
6.6 KiB
TypeScript
255 lines
6.6 KiB
TypeScript
import * as Clipboard from 'expo-clipboard';
|
|
import * as Haptics from 'expo-haptics';
|
|
import React, { useState } from 'react';
|
|
import {
|
|
Linking,
|
|
Pressable,
|
|
ScrollView,
|
|
StyleSheet,
|
|
Text,
|
|
View,
|
|
} from 'react-native';
|
|
import QRCode from 'react-native-qrcode-svg';
|
|
import { DONATE_COINS, DonateCoin, isPlaceholder, qrPayload } from '../lib/donate';
|
|
import { theme } from '../theme';
|
|
|
|
export function DonateScreen() {
|
|
return (
|
|
<ScrollView style={styles.root} contentContainerStyle={styles.scroll}>
|
|
<View style={styles.hero}>
|
|
<Text style={styles.heroTitle}>Support Goon</Text>
|
|
<Text style={styles.heroSubtitle}>
|
|
Goon is free, open-source, and ad-free. It stays that way only because
|
|
donations cover the server, the API keys, and the time. If the app
|
|
earned you a smile, throw a coin into the bucket.
|
|
</Text>
|
|
</View>
|
|
|
|
{DONATE_COINS.map((coin) => (
|
|
<CoinCard key={coin.id} coin={coin} />
|
|
))}
|
|
|
|
<View style={styles.footnote}>
|
|
<Text style={styles.footnoteTitle}>Why crypto?</Text>
|
|
<Text style={styles.footnoteBody}>
|
|
Mainstream payment processors (Stripe, PayPal, Patreon) refuse adult
|
|
projects — even open-source tooling like this one. Crypto is the only
|
|
channel that does not require us to identify our donors and does not
|
|
let third parties freeze the project mid-month.
|
|
</Text>
|
|
<Text style={[styles.footnoteBody, { marginTop: 10 }]}>
|
|
Monero is privacy-preserving by design — if you care about not being
|
|
linked to this app on-chain, send XMR. Bitcoin and USDT are public
|
|
ledgers; use a fresh wallet if anonymity matters to you.
|
|
</Text>
|
|
</View>
|
|
|
|
<View style={styles.footer}>
|
|
<Text style={styles.footerLink} onPress={() => Linking.openURL('https://www.getmonero.org/get-started/')}>
|
|
New to Monero? Learn how →
|
|
</Text>
|
|
<Text style={styles.footerLink} onPress={() => Linking.openURL('https://bitcoin.org/en/getting-started')}>
|
|
New to Bitcoin? Learn how →
|
|
</Text>
|
|
</View>
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
function CoinCard({ coin }: { coin: DonateCoin }) {
|
|
const [copied, setCopied] = useState(false);
|
|
const placeholder = isPlaceholder(coin.address);
|
|
|
|
async function copy() {
|
|
if (placeholder) return;
|
|
await Clipboard.setStringAsync(coin.address);
|
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 1800);
|
|
}
|
|
|
|
return (
|
|
<View style={[styles.card, { borderColor: coin.color }]}>
|
|
<View style={styles.cardHeader}>
|
|
<View style={[styles.ticker, { backgroundColor: coin.color }]}>
|
|
<Text style={styles.tickerText}>{coin.ticker}</Text>
|
|
</View>
|
|
<View style={{ flex: 1 }}>
|
|
<Text style={styles.coinName}>{coin.name}</Text>
|
|
<Text style={styles.coinBlurb}>{coin.blurb}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{placeholder ? (
|
|
<View style={styles.placeholderBox}>
|
|
<Text style={styles.placeholderText}>
|
|
Address not configured yet — see{' '}
|
|
<Text style={{ color: theme.muted, fontWeight: '700' }}>mobile/src/lib/donate.ts</Text>
|
|
</Text>
|
|
</View>
|
|
) : (
|
|
<>
|
|
<View style={styles.qrWrap}>
|
|
<View style={styles.qrInner}>
|
|
{/* white background mandated for QR scanability across cameras */}
|
|
<QRCode value={qrPayload(coin)} size={180} backgroundColor="#fff" color="#000" />
|
|
</View>
|
|
</View>
|
|
|
|
<Pressable onPress={copy} style={({ pressed }) => [styles.addressBox, pressed && styles.addressBoxPressed]}>
|
|
<Text style={styles.addressText} selectable>
|
|
{coin.address}
|
|
</Text>
|
|
<Text style={[styles.copyHint, copied && { color: theme.good }]}>
|
|
{copied ? '✓ Copied' : 'Tap to copy'}
|
|
</Text>
|
|
</Pressable>
|
|
</>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
root: { flex: 1, backgroundColor: theme.bg },
|
|
scroll: { padding: 16, paddingBottom: 40 },
|
|
|
|
hero: {
|
|
marginBottom: 18,
|
|
paddingHorizontal: 4,
|
|
},
|
|
heroTitle: {
|
|
color: theme.fg,
|
|
fontSize: 28,
|
|
fontWeight: '800',
|
|
letterSpacing: -0.5,
|
|
marginBottom: 8,
|
|
},
|
|
heroSubtitle: {
|
|
color: theme.muted,
|
|
fontSize: 14,
|
|
lineHeight: 20,
|
|
},
|
|
|
|
card: {
|
|
backgroundColor: theme.card,
|
|
borderWidth: 1.5,
|
|
borderRadius: 16,
|
|
padding: 16,
|
|
marginBottom: 14,
|
|
},
|
|
cardHeader: {
|
|
flexDirection: 'row',
|
|
alignItems: 'flex-start',
|
|
gap: 12,
|
|
marginBottom: 14,
|
|
},
|
|
ticker: {
|
|
width: 56,
|
|
height: 56,
|
|
borderRadius: 14,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
tickerText: {
|
|
color: '#fff',
|
|
fontWeight: '900',
|
|
fontSize: 14,
|
|
letterSpacing: 0.5,
|
|
},
|
|
coinName: {
|
|
color: theme.fg,
|
|
fontSize: 18,
|
|
fontWeight: '700',
|
|
marginBottom: 2,
|
|
},
|
|
coinBlurb: {
|
|
color: theme.muted,
|
|
fontSize: 12,
|
|
lineHeight: 16,
|
|
},
|
|
|
|
qrWrap: {
|
|
alignItems: 'center',
|
|
marginBottom: 14,
|
|
},
|
|
qrInner: {
|
|
padding: 10,
|
|
backgroundColor: '#fff',
|
|
borderRadius: 12,
|
|
},
|
|
|
|
addressBox: {
|
|
backgroundColor: theme.bgElevated,
|
|
borderWidth: 1,
|
|
borderColor: theme.border,
|
|
borderRadius: 10,
|
|
padding: 12,
|
|
},
|
|
addressBoxPressed: {
|
|
backgroundColor: theme.card,
|
|
borderColor: theme.borderFocus,
|
|
},
|
|
addressText: {
|
|
color: theme.fg,
|
|
fontFamily: 'monospace',
|
|
fontSize: 12,
|
|
lineHeight: 17,
|
|
},
|
|
copyHint: {
|
|
color: theme.mutedDim,
|
|
fontSize: 11,
|
|
fontWeight: '700',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 0.8,
|
|
marginTop: 8,
|
|
textAlign: 'right',
|
|
},
|
|
|
|
placeholderBox: {
|
|
backgroundColor: theme.bgElevated,
|
|
borderWidth: 1,
|
|
borderColor: theme.warn,
|
|
borderStyle: 'dashed',
|
|
borderRadius: 10,
|
|
padding: 14,
|
|
},
|
|
placeholderText: {
|
|
color: theme.warn,
|
|
fontSize: 12,
|
|
lineHeight: 17,
|
|
},
|
|
|
|
footnote: {
|
|
backgroundColor: theme.bgElevated,
|
|
borderWidth: 1,
|
|
borderColor: theme.border,
|
|
borderRadius: 14,
|
|
padding: 16,
|
|
marginTop: 6,
|
|
},
|
|
footnoteTitle: {
|
|
color: theme.fg,
|
|
fontSize: 13,
|
|
fontWeight: '700',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 0.5,
|
|
marginBottom: 8,
|
|
},
|
|
footnoteBody: {
|
|
color: theme.muted,
|
|
fontSize: 13,
|
|
lineHeight: 19,
|
|
},
|
|
|
|
footer: {
|
|
marginTop: 18,
|
|
alignItems: 'center',
|
|
gap: 10,
|
|
},
|
|
footerLink: {
|
|
color: theme.accent,
|
|
fontSize: 13,
|
|
fontWeight: '600',
|
|
},
|
|
});
|