import * as FileSystem from 'expo-file-system'; import { useSQLiteContext } from 'expo-sqlite'; import { useRouter } from 'expo-router'; import { useCallback, useState } from 'react'; import { Alert, FlatList, Pressable, RefreshControl, StyleSheet, Text, View } from 'react-native'; import { deleteActivities, useActivities, type ActivitySummary } from '@/db/queries'; import { syncFeed } from '@/db/sync'; import { useTheme } from '@/ThemeContext'; export default function FeedScreen() { const db = useSQLiteContext(); const theme = useTheme(); const activities = useActivities(); const [syncing, setSyncing] = useState(false); const [syncMsg, setSyncMsg] = useState(null); const [selected, setSelected] = useState>(new Set()); const selecting = selected.size > 0; const doSync = useCallback(async () => { setSyncing(true); setSyncMsg(null); const result = await syncFeed(db); setSyncing(false); if (result.error) { setSyncMsg(result.error); } else if (result.total === 0) { setSyncMsg('No activities on instance'); } else if (result.synced === 0 && !result.fetched && !result.uploaded) { setSyncMsg(`Up to date (${result.total} activities)`); } else { const parts = []; if (result.synced > 0) parts.push(`${result.synced} new`); if (result.fetched) parts.push(`${result.fetched} full dataset${result.fetched === 1 ? '' : 's'}`); if (result.uploaded) parts.push(`${result.uploaded} uploaded`); setSyncMsg(`Synced: ${parts.join(', ')} (${result.total} total)`); } setTimeout(() => setSyncMsg(null), 3500); }, [db]); function toggleSelect(id: string) { setSelected(prev => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); } function cancelSelect() { setSelected(new Set()); } function confirmDeleteSelected() { const count = selected.size; Alert.alert( `Delete ${count} activit${count === 1 ? 'y' : 'ies'}`, 'These activities will be permanently removed from your device.', [ { text: 'Cancel', style: 'cancel' }, { text: 'Delete', style: 'destructive', onPress: async () => { const ids = Array.from(selected); const paths = await deleteActivities(db, ids); setSelected(new Set()); for (const p of paths) { if (p) { try { await FileSystem.deleteAsync(p, { idempotent: true }); } catch {} } } }, }, ], ); } return ( {selecting ? ( <> {selected.size} selected Cancel ) : ( <> Feed {syncing ? 'Syncing…' : '↓ Sync'} )} {syncMsg && ( {syncMsg} )} {activities.length === 0 && !syncing ? ( 🚴 No activities yet Import a file or tap Sync to pull from your instance. ) : ( a.id} renderItem={({ item }) => ( toggleSelect(item.id)} onLongPress={() => toggleSelect(item.id)} /> )} contentContainerStyle={styles.list} refreshControl={ } /> )} {selecting && ( Delete {selected.size} )} ); } function ActivityCard({ activity, selecting, checked, onToggleSelect, onLongPress, }: { activity: ActivitySummary; selecting: boolean; checked: boolean; onToggleSelect: () => void; onLongPress: () => void; }) { const router = useRouter(); const theme = useTheme(); const km = activity.distance_m != null ? (activity.distance_m / 1000).toFixed(1) : null; const elev = activity.elevation_gain_m != null ? Math.round(activity.elevation_gain_m) : null; const date = new Date(activity.started_at).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric', }); function handlePress() { if (selecting) { onToggleSelect(); } else { router.push(`/activity/${activity.id}`); } } return ( {selecting && ( {checked && βœ“} )} {sportIcon(activity.sport)} {date} {activity.origin === 'remote' ? cloud : !activity.synced_at && local } {activity.title} {km && } {elev != null && } ); } function Stat({ label, value }: { label: string; value: string }) { return ( {value} {label} ); } function sportIcon(sport: string): string { const icons: Record = { cycling: '🚴', running: 'πŸƒ', hiking: 'πŸ₯Ύ', swimming: '🏊', walking: '🚢', }; return icons[sport] ?? 'πŸ…'; } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#09090b' }, headerRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, paddingTop: 60, paddingBottom: 12, }, header: { color: '#fff', fontSize: 22, fontWeight: '700' }, syncButton: { borderRadius: 8, paddingHorizontal: 14, paddingVertical: 7, }, syncButtonDisabled: { opacity: 0.5 }, syncText: { fontSize: 13, fontWeight: '600' }, cancelButton: { backgroundColor: '#27272a', borderRadius: 8, paddingHorizontal: 14, paddingVertical: 7, }, cancelText: { color: '#a1a1aa', fontSize: 13, fontWeight: '600' }, syncMsg: { color: '#a1a1aa', fontSize: 12, textAlign: 'center', paddingHorizontal: 16, paddingBottom: 8, }, list: { padding: 16, gap: 12, paddingBottom: 80 }, card: { backgroundColor: '#18181b', borderRadius: 12, padding: 16, borderWidth: 1, borderColor: '#27272a', }, cardTop: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 6 }, cardLeft: { flexDirection: 'row', alignItems: 'center', gap: 10 }, sportIcon: { fontSize: 20 }, cardMeta: { flexDirection: 'row', alignItems: 'center', gap: 8 }, cardDate: { color: '#71717a', fontSize: 12 }, remoteBadge: { fontSize: 10, borderWidth: 1, borderRadius: 4, paddingHorizontal: 4, }, localBadge: { color: '#a1a1aa', fontSize: 10, borderWidth: 1, borderColor: '#3f3f46', borderRadius: 4, paddingHorizontal: 4, }, cardTitle: { color: '#f4f4f5', fontSize: 15, fontWeight: '600', marginBottom: 10 }, cardStats: { flexDirection: 'row', gap: 16 }, stat: { flexDirection: 'row', alignItems: 'baseline', gap: 3 }, statValue: { color: '#f4f4f5', fontSize: 16, fontWeight: '600' }, statLabel: { color: '#71717a', fontSize: 12 }, checkbox: { width: 20, height: 20, borderRadius: 4, borderWidth: 1.5, borderColor: '#52525b', alignItems: 'center', justifyContent: 'center', }, checkmark: { color: '#fff', fontSize: 12, fontWeight: '700' }, empty: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 32, }, emptyIcon: { fontSize: 48, marginBottom: 16 }, emptyTitle: { color: '#f4f4f5', fontSize: 18, fontWeight: '600', marginBottom: 8 }, emptyBody: { color: '#71717a', fontSize: 14, textAlign: 'center', lineHeight: 20 }, actionBar: { position: 'absolute', bottom: 0, left: 0, right: 0, backgroundColor: '#18181b', borderTopWidth: 1, borderTopColor: '#27272a', paddingHorizontal: 16, paddingVertical: 12, paddingBottom: 28, }, deleteBarButton: { backgroundColor: '#7f1d1d', borderRadius: 10, paddingVertical: 14, alignItems: 'center', }, deleteBarText: { color: '#fca5a5', fontSize: 15, fontWeight: '700' }, });