feat: add sync mode setting — summaries only vs full data download
This commit is contained in:
@@ -20,10 +20,13 @@ export default function FeedScreen() {
|
|||||||
setSyncMsg(result.error);
|
setSyncMsg(result.error);
|
||||||
} else if (result.total === 0) {
|
} else if (result.total === 0) {
|
||||||
setSyncMsg('No activities on instance');
|
setSyncMsg('No activities on instance');
|
||||||
} else if (result.synced === 0) {
|
} else if (result.synced === 0 && !result.fetched) {
|
||||||
setSyncMsg(`Up to date (${result.total} activities)`);
|
setSyncMsg(`Up to date (${result.total} activities)`);
|
||||||
} else {
|
} 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);
|
setTimeout(() => setSyncMsg(null), 3500);
|
||||||
}, [db]);
|
}, [db]);
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ import { deleteRemoteActivities, getSetting, setSetting, useSetting } from '@/db
|
|||||||
export default function SettingsScreen() {
|
export default function SettingsScreen() {
|
||||||
const db = useSQLiteContext();
|
const db = useSQLiteContext();
|
||||||
|
|
||||||
const storedUrl = useSetting('instance_url') ?? '';
|
const storedUrl = useSetting('instance_url') ?? '';
|
||||||
const storedHandle = useSetting('handle') ?? '';
|
const storedHandle = useSetting('handle') ?? '';
|
||||||
const storedPath = useSetting('auto_import_path') ?? '';
|
const storedPath = useSetting('auto_import_path') ?? '';
|
||||||
const storedToken = useSetting('api_token');
|
const storedToken = useSetting('api_token');
|
||||||
|
const storedSyncMode = (useSetting('sync_mode') ?? 'summaries') as 'summaries' | 'full';
|
||||||
|
|
||||||
const [instanceUrl, setInstanceUrl] = useState(storedUrl);
|
const [instanceUrl, setInstanceUrl] = useState(storedUrl);
|
||||||
const [handle, setHandle] = useState(storedHandle);
|
const [handle, setHandle] = useState(storedHandle);
|
||||||
@@ -174,6 +175,26 @@ export default function SettingsScreen() {
|
|||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Section title="Sync">
|
||||||
|
<View style={styles.modeRow}>
|
||||||
|
<ModeButton
|
||||||
|
label="Summaries only"
|
||||||
|
active={storedSyncMode === 'summaries'}
|
||||||
|
onPress={() => setSetting(db, 'sync_mode', 'summaries')}
|
||||||
|
/>
|
||||||
|
<ModeButton
|
||||||
|
label="Full data"
|
||||||
|
active={storedSyncMode === 'full'}
|
||||||
|
onPress={() => setSetting(db, 'sync_mode', 'full')}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.hint}>
|
||||||
|
{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.'}
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
|
||||||
<Section title="Data">
|
<Section title="Data">
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[styles.resetButton, resetArmed && styles.resetButtonArmed]}
|
style={[styles.resetButton, resetArmed && styles.resetButtonArmed]}
|
||||||
@@ -231,6 +252,17 @@ function Field({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ModeButton({ label, active, onPress }: { label: string; active: boolean; onPress: () => void }) {
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
style={[styles.modeButton, active && styles.modeButtonActive]}
|
||||||
|
onPress={onPress}
|
||||||
|
>
|
||||||
|
<Text style={[styles.modeButtonText, active && styles.modeButtonTextActive]}>{label}</Text>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function Row({ label, value }: { label: string; value: string }) {
|
function Row({ label, value }: { label: string; value: string }) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.row}>
|
<View style={styles.row}>
|
||||||
@@ -282,6 +314,11 @@ const styles = StyleSheet.create({
|
|||||||
disconnectText: { color: '#71717a', fontSize: 14 },
|
disconnectText: { color: '#71717a', fontSize: 14 },
|
||||||
msgOk: { color: '#86efac', fontSize: 13, paddingHorizontal: 12, paddingBottom: 10 },
|
msgOk: { color: '#86efac', fontSize: 13, paddingHorizontal: 12, paddingBottom: 10 },
|
||||||
msgErr: { color: '#fca5a5', 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: {
|
resetButton: {
|
||||||
margin: 12, paddingVertical: 10, alignItems: 'center',
|
margin: 12, paddingVertical: 10, alignItems: 'center',
|
||||||
borderRadius: 8, borderWidth: 1, borderColor: '#3f3f46',
|
borderRadius: 8, borderWidth: 1, borderColor: '#3f3f46',
|
||||||
|
|||||||
+43
-2
@@ -1,7 +1,7 @@
|
|||||||
import type { SQLiteDatabase } from 'expo-sqlite';
|
import type { SQLiteDatabase } from 'expo-sqlite';
|
||||||
import { getSetting, upsertRemoteActivity } from './queries';
|
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<SyncResult> {
|
export async function syncFeed(db: SQLiteDatabase): Promise<SyncResult> {
|
||||||
const instanceUrl = (await getSetting(db, 'instance_url'))?.replace(/\/$/, '');
|
const instanceUrl = (await getSetting(db, 'instance_url'))?.replace(/\/$/, '');
|
||||||
@@ -30,6 +30,8 @@ export async function syncFeed(db: SQLiteDatabase): Promise<SyncResult> {
|
|||||||
const data: { activities?: RemoteSummary[] } = await resp.json();
|
const data: { activities?: RemoteSummary[] } = await resp.json();
|
||||||
const activities = data.activities ?? [];
|
const activities = data.activities ?? [];
|
||||||
|
|
||||||
|
const syncMode = (await getSetting(db, 'sync_mode')) ?? 'summaries';
|
||||||
|
|
||||||
let synced = 0;
|
let synced = 0;
|
||||||
for (const a of activities) {
|
for (const a of activities) {
|
||||||
const detailJson = JSON.stringify({
|
const detailJson = JSON.stringify({
|
||||||
@@ -48,7 +50,46 @@ export async function syncFeed(db: SQLiteDatabase): Promise<SyncResult> {
|
|||||||
if (changed) synced++;
|
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 = {
|
type RemoteSummary = {
|
||||||
|
|||||||
Reference in New Issue
Block a user