feat(mobile): editable activity title for local activities

Adds edits_json column (migration v3) to store user overrides separately
from detail_json so Option A server re-extraction never clobbers them.

- Tap the title in the detail screen to edit (local activities only, shown
  with a ✎ hint). Saves on keyboard dismiss via onEndEditing.
- Cards and search display user_title ?? title.
- Raw upload: user_title sent to server -> sidecar written so web UI shows
  the correct title (server re-extracts from FIT, which has Karoo's title).
- BAS upload: detail.title overridden before sending, no sidecar needed.
This commit is contained in:
Davide Scaini
2026-04-27 15:20:19 +02:00
parent 090d4bd8dc
commit 946da685e5
6 changed files with 81 additions and 18 deletions
+35 -4
View File
@@ -2,10 +2,10 @@ import { Camera, GeoJSONSource, Layer, Map } from '@maplibre/maplibre-react-nati
import * as FileSystem from 'expo-file-system';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { useEffect, useRef, useState } from 'react';
import { Alert, Modal, Platform, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { Alert, Modal, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
import Svg, { Defs, LinearGradient, Path, Stop } from 'react-native-svg';
import { useSQLiteContext } from 'expo-sqlite';
import { deleteActivity, useActivity, useSetting } from '@/db/queries';
import { deleteActivity, setActivityTitle, useActivity, useSetting } from '@/db/queries';
import { useTheme } from '@/ThemeContext';
const MAP_STYLE = 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json';
@@ -38,6 +38,8 @@ export default function ActivityScreen() {
const [timeseries, setTimeseries] = useState<Timeseries | null>(null);
const [loadingMap, setLoadingMap] = useState(false);
const [loadingChart, setLoadingChart] = useState(false);
const [editingTitle, setEditingTitle] = useState(false);
const [titleDraft, setTitleDraft] = useState('');
async function confirmDelete() {
Alert.alert(
@@ -103,6 +105,9 @@ export default function ActivityScreen() {
}
const detail = JSON.parse(row.detail_json);
const edits = row.edits_json ? JSON.parse(row.edits_json) : {};
const displayTitle = edits.title ?? detail.title;
const canEdit = row.origin === 'local';
const km = detail.distance_m != null ? (detail.distance_m / 1000).toFixed(2) : null;
const elev = detail.elevation_gain_m != null ? Math.round(detail.elevation_gain_m) : null;
const elevLoss = detail.elevation_loss_m != null ? Math.round(Math.abs(detail.elevation_loss_m)) : null;
@@ -126,7 +131,30 @@ export default function ActivityScreen() {
</View>
<Text style={styles.sport}>{detail.sport ?? 'Activity'}</Text>
<Text style={styles.title}>{detail.title}</Text>
{editingTitle ? (
<TextInput
style={styles.titleInput}
value={titleDraft}
onChangeText={setTitleDraft}
autoFocus
returnKeyType="done"
onEndEditing={(e) => {
const trimmed = e.nativeEvent.text.trim();
if (trimmed && trimmed !== displayTitle) {
setActivityTitle(db, id, trimmed);
}
setEditingTitle(false);
}}
/>
) : (
<Pressable
onPress={canEdit ? () => { setTitleDraft(displayTitle); setEditingTitle(true); } : undefined}
style={styles.titleRow}
>
<Text style={styles.title}>{displayTitle}</Text>
{canEdit && <Text style={styles.editHint}></Text>}
</Pressable>
)}
<Text style={styles.date}>{date}</Text>
{/* Map */}
@@ -468,7 +496,10 @@ const styles = StyleSheet.create({
deleteButton: { paddingHorizontal: 16 },
deleteText: { color: '#f87171', fontSize: 15 },
sport: { color: '#71717a', fontSize: 12, fontWeight: '600', letterSpacing: 0.8, paddingHorizontal: 16, marginBottom: 4 },
title: { color: '#f4f4f5', fontSize: 22, fontWeight: '700', paddingHorizontal: 16, marginBottom: 4 },
titleRow: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, marginBottom: 4 },
title: { color: '#f4f4f5', fontSize: 22, fontWeight: '700', flexShrink: 1 },
titleInput: { color: '#f4f4f5', fontSize: 22, fontWeight: '700', paddingHorizontal: 16, marginBottom: 4, borderBottomWidth: 1, borderBottomColor: '#3b82f6' },
editHint: { color: '#52525b', fontSize: 16, marginLeft: 8 },
date: { color: '#71717a', fontSize: 13, paddingHorizontal: 16, marginBottom: 16 },
mapContainer: { height: 220, marginBottom: 16, borderTopWidth: 1, borderBottomWidth: 1, borderColor: '#27272a' },
map: { flex: 1 },