From a306682b52b2e06bbbcc68c5c9de064d5339759f Mon Sep 17 00:00:00 2001 From: Davide Scaini Date: Fri, 24 Apr 2026 21:52:25 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20add=20sync=20mode=20setting=20=E2=80=94?= =?UTF-8?q?=20summaries=20only=20vs=20full=20data=20download?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mobile/app/(tabs)/index.tsx | 7 ++++-- mobile/app/(tabs)/settings.tsx | 45 +++++++++++++++++++++++++++++++--- mobile/db/sync.ts | 45 ++++++++++++++++++++++++++++++++-- 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/mobile/app/(tabs)/index.tsx b/mobile/app/(tabs)/index.tsx index da20d17..144cb1b 100644 --- a/mobile/app/(tabs)/index.tsx +++ b/mobile/app/(tabs)/index.tsx @@ -20,10 +20,13 @@ export default function FeedScreen() { setSyncMsg(result.error); } else if (result.total === 0) { setSyncMsg('No activities on instance'); - } else if (result.synced === 0) { + } else if (result.synced === 0 && !result.fetched) { setSyncMsg(`Up to date (${result.total} activities)`); } else { - setSyncMsg(`${result.synced} of ${result.total} activities synced`); + const parts = []; + if (result.synced > 0) parts.push(`${result.synced} new`); + if (result.fetched) parts.push(`${result.fetched} full dataset${result.fetched === 1 ? '' : 's'}`); + setSyncMsg(`Synced: ${parts.join(', ')} (${result.total} total)`); } setTimeout(() => setSyncMsg(null), 3500); }, [db]); diff --git a/mobile/app/(tabs)/settings.tsx b/mobile/app/(tabs)/settings.tsx index b672dfc..b564075 100644 --- a/mobile/app/(tabs)/settings.tsx +++ b/mobile/app/(tabs)/settings.tsx @@ -9,10 +9,11 @@ import { deleteRemoteActivities, getSetting, setSetting, useSetting } from '@/db export default function SettingsScreen() { const db = useSQLiteContext(); - const storedUrl = useSetting('instance_url') ?? ''; - const storedHandle = useSetting('handle') ?? ''; - const storedPath = useSetting('auto_import_path') ?? ''; - const storedToken = useSetting('api_token'); + const storedUrl = useSetting('instance_url') ?? ''; + const storedHandle = useSetting('handle') ?? ''; + const storedPath = useSetting('auto_import_path') ?? ''; + const storedToken = useSetting('api_token'); + const storedSyncMode = (useSetting('sync_mode') ?? 'summaries') as 'summaries' | 'full'; const [instanceUrl, setInstanceUrl] = useState(storedUrl); const [handle, setHandle] = useState(storedHandle); @@ -174,6 +175,26 @@ export default function SettingsScreen() { )} +
+ + setSetting(db, 'sync_mode', 'summaries')} + /> + setSetting(db, 'sync_mode', 'full')} + /> + + + {storedSyncMode === 'full' + ? 'Downloads map route and elevation chart for every activity during sync. Uses more storage and takes longer.' + : 'Syncs activity summaries only. Map and chart are fetched on demand when you open an activity.'} + +
+
void }) { + return ( + + {label} + + ); +} + function Row({ label, value }: { label: string; value: string }) { return ( @@ -282,6 +314,11 @@ const styles = StyleSheet.create({ disconnectText: { color: '#71717a', fontSize: 14 }, msgOk: { color: '#86efac', fontSize: 13, paddingHorizontal: 12, paddingBottom: 10 }, msgErr: { color: '#fca5a5', fontSize: 13, paddingHorizontal: 12, paddingBottom: 10 }, + modeRow: { flexDirection: 'row', gap: 8, padding: 12 }, + modeButton: { flex: 1, paddingVertical: 9, borderRadius: 8, borderWidth: 1, borderColor: '#3f3f46', alignItems: 'center' }, + modeButtonActive: { backgroundColor: '#1e3a5f', borderColor: '#2563eb' }, + modeButtonText: { color: '#71717a', fontSize: 13, fontWeight: '500' }, + modeButtonTextActive: { color: '#60a5fa' }, resetButton: { margin: 12, paddingVertical: 10, alignItems: 'center', borderRadius: 8, borderWidth: 1, borderColor: '#3f3f46', diff --git a/mobile/db/sync.ts b/mobile/db/sync.ts index b5551d2..2e46e3a 100644 --- a/mobile/db/sync.ts +++ b/mobile/db/sync.ts @@ -1,7 +1,7 @@ import type { SQLiteDatabase } from 'expo-sqlite'; import { getSetting, upsertRemoteActivity } from './queries'; -export type SyncResult = { synced: number; total: number; error?: string }; +export type SyncResult = { synced: number; total: number; fetched?: number; error?: string }; export async function syncFeed(db: SQLiteDatabase): Promise { const instanceUrl = (await getSetting(db, 'instance_url'))?.replace(/\/$/, ''); @@ -30,6 +30,8 @@ export async function syncFeed(db: SQLiteDatabase): Promise { const data: { activities?: RemoteSummary[] } = await resp.json(); const activities = data.activities ?? []; + const syncMode = (await getSetting(db, 'sync_mode')) ?? 'summaries'; + let synced = 0; for (const a of activities) { const detailJson = JSON.stringify({ @@ -48,7 +50,46 @@ export async function syncFeed(db: SQLiteDatabase): Promise { if (changed) synced++; } - return { synced, total: activities.length }; + if (syncMode !== 'full') { + return { synced, total: activities.length }; + } + + // Full mode: fetch geojson + timeseries for any activity missing them + const headers = { Authorization: `Bearer ${token}` }; + let fetched = 0; + for (const a of activities) { + const row = db.getFirstSync<{ g: number; t: number }>( + 'SELECT (geojson IS NOT NULL) as g, (timeseries_json IS NOT NULL) as t FROM activities WHERE id = ?', + [a.id], + ); + if (row?.g && row?.t) continue; + + let gj: string | null = null; + let ts: string | null = null; + try { + if (!row?.g) { + const r = await fetch(`${instanceUrl}/api/activity/${a.id}/geojson`, { headers }); + if (r.ok) gj = await r.text(); + } + if (!row?.t) { + const r = await fetch(`${instanceUrl}/api/activity/${a.id}/timeseries`, { headers }); + if (r.ok) ts = await r.text(); + } + } catch {} + + if (gj !== null || ts !== null) { + await db.runAsync( + `UPDATE activities SET + geojson = COALESCE(geojson, ?), + timeseries_json = COALESCE(timeseries_json, ?) + WHERE id = ? AND origin = 'remote'`, + [gj, ts, a.id], + ); + fetched++; + } + } + + return { synced, total: activities.length, fetched }; } type RemoteSummary = {