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:
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user