feat: activity detail screen with map, stats, and inline editing
Tap any saved recording to open ActivityDetailScreen: - Full-screen MapLibre map with track fitted to bounds (LngLatBounds padding) - Stats panel: duration, distance, avg speed, elevation gain (computed from track points), avg HR/power/cadence, point count - 'Edit' button in header opens a pageSheet modal with title TextInput, sport grid, and subtype pills — same controls as PostRecordingScreen - updateRecording() added to db.ts; edits update header title and sport summary without navigating away SavedRecordingsScreen: tapping a card in normal mode navigates to detail; tapping in selection mode still toggles the checkbox.
This commit is contained in:
@@ -9,6 +9,7 @@ import { PostRecordingScreen } from '../screens/PostRecordingScreen';
|
|||||||
import { SensorPairingScreen } from '../screens/SensorPairingScreen';
|
import { SensorPairingScreen } from '../screens/SensorPairingScreen';
|
||||||
import { SavedRecordingsScreen } from '../screens/SavedRecordingsScreen';
|
import { SavedRecordingsScreen } from '../screens/SavedRecordingsScreen';
|
||||||
import { SettingsScreen } from '../screens/SettingsScreen';
|
import { SettingsScreen } from '../screens/SettingsScreen';
|
||||||
|
import { ActivityDetailScreen } from '../screens/ActivityDetailScreen';
|
||||||
import { RootStackParamList, TabParamList } from '../types';
|
import { RootStackParamList, TabParamList } from '../types';
|
||||||
import { colors } from '../theme';
|
import { colors } from '../theme';
|
||||||
import { useTheme } from '../ThemeContext';
|
import { useTheme } from '../ThemeContext';
|
||||||
@@ -57,6 +58,7 @@ export function AppNavigator() {
|
|||||||
<Stack.Screen name="Tabs" component={Tabs} options={{ headerShown: false }} />
|
<Stack.Screen name="Tabs" component={Tabs} options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="PostRecording" component={PostRecordingScreen} options={{ title: 'Save Recording', presentation: 'modal' }} />
|
<Stack.Screen name="PostRecording" component={PostRecordingScreen} options={{ title: 'Save Recording', presentation: 'modal' }} />
|
||||||
<Stack.Screen name="SensorPairing" component={SensorPairingScreen} options={{ title: 'Sensors', presentation: 'modal' }} />
|
<Stack.Screen name="SensorPairing" component={SensorPairingScreen} options={{ title: 'Sensors', presentation: 'modal' }} />
|
||||||
|
<Stack.Screen name="ActivityDetail" component={ActivityDetailScreen} options={{ title: '' }} />
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,306 @@
|
|||||||
|
import React, { useEffect, useLayoutEffect, useMemo, useState } from 'react';
|
||||||
|
import {
|
||||||
|
View, Text, StyleSheet, ActivityIndicator, ScrollView,
|
||||||
|
Modal, TextInput, TouchableOpacity, Pressable, Alert,
|
||||||
|
} from 'react-native';
|
||||||
|
import { useRoute, RouteProp, useNavigation } from '@react-navigation/native';
|
||||||
|
import { Map, Camera, GeoJSONSource, Layer } from '@maplibre/maplibre-react-native';
|
||||||
|
import type { LineLayerStyle } from '@maplibre/maplibre-react-native';
|
||||||
|
import { parseGpxFile } from '../services/gpx';
|
||||||
|
import { updateRecording } from '../services/db';
|
||||||
|
import { TrackPoint, RootStackParamList, SavedRecording } from '../types';
|
||||||
|
import {
|
||||||
|
ALL_SPORTS, SPORT_ICONS, SPORT_LABELS, SPORT_SUBTYPES, SUB_SPORT_LABELS,
|
||||||
|
type Sport, type SubSport,
|
||||||
|
} from '../sports';
|
||||||
|
import { colors } from '../theme';
|
||||||
|
import { useTheme } from '../ThemeContext';
|
||||||
|
|
||||||
|
const MAP_STYLE = 'https://tiles.openfreemap.org/styles/liberty';
|
||||||
|
|
||||||
|
type Route = RouteProp<RootStackParamList, 'ActivityDetail'>;
|
||||||
|
|
||||||
|
// ── Stats helpers ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface Stats {
|
||||||
|
elevationGainM: number;
|
||||||
|
avgHr: number | null;
|
||||||
|
avgPower: number | null;
|
||||||
|
avgCadence: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeStats(points: TrackPoint[]): Stats {
|
||||||
|
let elevationGainM = 0;
|
||||||
|
const hrVals: number[] = [], powerVals: number[] = [], cadVals: number[] = [];
|
||||||
|
for (let i = 1; i < points.length; i++) {
|
||||||
|
const gain = points[i].ele - points[i - 1].ele;
|
||||||
|
if (gain > 0) elevationGainM += gain;
|
||||||
|
}
|
||||||
|
for (const p of points) {
|
||||||
|
if (p.hr != null) hrVals.push(p.hr);
|
||||||
|
if (p.power != null) powerVals.push(p.power);
|
||||||
|
if (p.cad != null) cadVals.push(p.cad);
|
||||||
|
}
|
||||||
|
const avg = (a: number[]) => a.length ? Math.round(a.reduce((s, v) => s + v, 0) / a.length) : null;
|
||||||
|
return { elevationGainM, avgHr: avg(hrVals), avgPower: avg(powerVals), avgCadence: avg(cadVals) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDuration(secs: number) {
|
||||||
|
const h = Math.floor(secs / 3600).toString().padStart(2, '0');
|
||||||
|
const m = Math.floor((secs % 3600) / 60).toString().padStart(2, '00');
|
||||||
|
const s = (secs % 60).toString().padStart(2, '0');
|
||||||
|
return `${h}:${m}:${s}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Screen ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function ActivityDetailScreen() {
|
||||||
|
const nav = useNavigation();
|
||||||
|
const { accent, accentDim } = useTheme();
|
||||||
|
const route = useRoute<Route>();
|
||||||
|
|
||||||
|
const [rec, setRec] = useState<SavedRecording>(route.params.recording);
|
||||||
|
const [points, setPoints] = useState<TrackPoint[] | null>(null);
|
||||||
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Edit modal state
|
||||||
|
const [editVisible, setEditVisible] = useState(false);
|
||||||
|
const [editTitle, setEditTitle] = useState(rec.title);
|
||||||
|
const [editSport, setEditSport] = useState<Sport>(rec.sport as Sport);
|
||||||
|
const [editSubSport, setEditSubSport] = useState<SubSport | null>(rec.subSport as SubSport | null);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
// Header "Edit" button
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
nav.setOptions({
|
||||||
|
headerTitle: rec.title,
|
||||||
|
headerRight: () => (
|
||||||
|
<TouchableOpacity onPress={() => setEditVisible(true)} style={{ marginRight: 16 }}>
|
||||||
|
<Text style={{ color: accent, fontSize: 15, fontWeight: '600' }}>Edit</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}, [nav, rec.title, accent]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
parseGpxFile(rec.filePath)
|
||||||
|
.then(setPoints)
|
||||||
|
.catch((e) => setLoadError(e?.message ?? 'Failed to load track'));
|
||||||
|
}, [rec.filePath]);
|
||||||
|
|
||||||
|
const trackLineStyle = useMemo<LineLayerStyle>(() => ({
|
||||||
|
lineColor: accent, lineWidth: 3, lineJoin: 'round', lineCap: 'round',
|
||||||
|
}), [accent]);
|
||||||
|
|
||||||
|
const trackGeoJSON = useMemo<GeoJSON.Feature<GeoJSON.LineString> | null>(() => {
|
||||||
|
if (!points || points.length < 2) return null;
|
||||||
|
return {
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: { type: 'LineString', coordinates: points.map((p) => [p.lon, p.lat]) },
|
||||||
|
properties: {},
|
||||||
|
};
|
||||||
|
}, [points]);
|
||||||
|
|
||||||
|
const bounds = useMemo(() => {
|
||||||
|
if (!points || points.length === 0) return null;
|
||||||
|
const pad = 0.001;
|
||||||
|
return [
|
||||||
|
Math.min(...points.map((p) => p.lon)) - pad,
|
||||||
|
Math.min(...points.map((p) => p.lat)) - pad,
|
||||||
|
Math.max(...points.map((p) => p.lon)) + pad,
|
||||||
|
Math.max(...points.map((p) => p.lat)) + pad,
|
||||||
|
] as [number, number, number, number];
|
||||||
|
}, [points]);
|
||||||
|
|
||||||
|
const stats = useMemo(() => points ? computeStats(points) : null, [points]);
|
||||||
|
|
||||||
|
const avgSpeedKph = rec.durationSeconds > 0
|
||||||
|
? (rec.distanceMeters / 1000) / (rec.durationSeconds / 3600) : 0;
|
||||||
|
|
||||||
|
const sportSummary = [
|
||||||
|
SPORT_ICONS[rec.sport as Sport] ?? '⚡',
|
||||||
|
SPORT_LABELS[rec.sport as Sport] ?? rec.sport,
|
||||||
|
rec.subSport ? (SUB_SPORT_LABELS[rec.subSport as SubSport] ?? rec.subSport) : null,
|
||||||
|
].filter(Boolean).join(' · ');
|
||||||
|
|
||||||
|
// ── Edit handlers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function handleEditSport(s: Sport) { setEditSport(s); setEditSubSport(null); }
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
if (!editTitle.trim()) { Alert.alert('Title required'); return; }
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
await updateRecording(rec.id, { title: editTitle.trim(), sport: editSport, subSport: editSubSport });
|
||||||
|
setRec((prev) => ({ ...prev, title: editTitle.trim(), sport: editSport, subSport: editSubSport }));
|
||||||
|
setEditVisible(false);
|
||||||
|
} catch (e: any) {
|
||||||
|
Alert.alert('Save failed', e?.message ?? 'Unknown error');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancelEdit() {
|
||||||
|
// Reset edit fields to current rec values
|
||||||
|
setEditTitle(rec.title);
|
||||||
|
setEditSport(rec.sport as Sport);
|
||||||
|
setEditSubSport(rec.subSport as SubSport | null);
|
||||||
|
setEditVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const editSubtypes = SPORT_SUBTYPES[editSport] ?? [];
|
||||||
|
|
||||||
|
// ── Render ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if (loadError) {
|
||||||
|
return <View style={styles.center}><Text style={styles.errorText}>{loadError}</Text></View>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{/* Map */}
|
||||||
|
<View style={styles.mapArea}>
|
||||||
|
{points === null
|
||||||
|
? <View style={styles.center}><ActivityIndicator color={accent} /></View>
|
||||||
|
: (
|
||||||
|
<Map mapStyle={MAP_STYLE} style={StyleSheet.absoluteFill} logo={false} attribution={false}>
|
||||||
|
{bounds && (
|
||||||
|
<Camera
|
||||||
|
initialViewState={{ bounds, padding: { top: 32, bottom: 32, left: 32, right: 32 } }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{trackGeoJSON && (
|
||||||
|
<GeoJSONSource id="track" data={trackGeoJSON}>
|
||||||
|
<Layer id="track-line" type="line" style={trackLineStyle} />
|
||||||
|
</GeoJSONSource>
|
||||||
|
)}
|
||||||
|
</Map>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<ScrollView style={styles.statsScroll} contentContainerStyle={styles.statsContent}>
|
||||||
|
<Text style={styles.sportSummary}>{sportSummary}</Text>
|
||||||
|
<Text style={styles.date}>{new Date(rec.date).toLocaleString()}</Text>
|
||||||
|
|
||||||
|
<View style={styles.grid}>
|
||||||
|
<StatBox label="Duration" value={formatDuration(rec.durationSeconds)} />
|
||||||
|
<StatBox label="Distance" value={`${(rec.distanceMeters / 1000).toFixed(2)} km`} />
|
||||||
|
<StatBox label="Avg Speed" value={`${avgSpeedKph.toFixed(1)} km/h`} />
|
||||||
|
<StatBox label="Elevation" value={stats ? `+${stats.elevationGainM.toFixed(0)} m` : '…'} />
|
||||||
|
<StatBox label="Avg HR" value={stats?.avgHr != null ? `${stats.avgHr} bpm` : '—'} />
|
||||||
|
<StatBox label="Avg Power" value={stats?.avgPower != null ? `${stats.avgPower} W` : '—'} />
|
||||||
|
<StatBox label="Avg Cadence" value={stats?.avgCadence != null ? `${stats.avgCadence} rpm` : '—'} />
|
||||||
|
<StatBox label="Points" value={points != null ? `${points.length}` : '…'} />
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* ── Edit modal ── */}
|
||||||
|
<Modal visible={editVisible} animationType="slide" presentationStyle="pageSheet" onRequestClose={handleCancelEdit}>
|
||||||
|
<View style={styles.editContainer}>
|
||||||
|
<View style={styles.editHeader}>
|
||||||
|
<TouchableOpacity onPress={handleCancelEdit}>
|
||||||
|
<Text style={styles.editCancel}>Cancel</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.editHeading}>Edit activity</Text>
|
||||||
|
<TouchableOpacity onPress={handleSave} disabled={saving}>
|
||||||
|
<Text style={[styles.editSave, { color: accent }, saving && { opacity: 0.5 }]}>
|
||||||
|
{saving ? 'Saving…' : 'Save'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ScrollView contentContainerStyle={styles.editContent}>
|
||||||
|
<Text style={styles.editSectionLabel}>Title</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.editInput}
|
||||||
|
value={editTitle}
|
||||||
|
onChangeText={setEditTitle}
|
||||||
|
placeholder="Activity title"
|
||||||
|
placeholderTextColor={colors.placeholder}
|
||||||
|
autoCorrect={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text style={styles.editSectionLabel}>Activity type</Text>
|
||||||
|
<View style={styles.sportGrid}>
|
||||||
|
{ALL_SPORTS.map((s) => {
|
||||||
|
const active = editSport === s;
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={s}
|
||||||
|
style={[styles.sportPill, active && { borderColor: accent, backgroundColor: accentDim }]}
|
||||||
|
onPress={() => handleEditSport(s)}
|
||||||
|
>
|
||||||
|
<Text style={styles.sportIcon}>{SPORT_ICONS[s]}</Text>
|
||||||
|
<Text style={[styles.sportLabel, active && { color: accent }]}>{SPORT_LABELS[s]}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{editSubtypes.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Text style={styles.editSectionLabel}>Subtype</Text>
|
||||||
|
<View style={styles.subRow}>
|
||||||
|
{editSubtypes.map((sub) => {
|
||||||
|
const active = editSubSport === sub;
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={sub}
|
||||||
|
style={[styles.subPill, active && { borderColor: accent, backgroundColor: accentDim }]}
|
||||||
|
onPress={() => setEditSubSport(active ? null : sub)}
|
||||||
|
>
|
||||||
|
<Text style={[styles.subLabel, active && { color: accent }]}>{SUB_SPORT_LABELS[sub]}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatBox({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<View style={styles.statBox}>
|
||||||
|
<Text style={styles.statLabel}>{label}</Text>
|
||||||
|
<Text style={styles.statValue}>{value}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: { flex: 1, backgroundColor: colors.bg },
|
||||||
|
center: { flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: colors.bg },
|
||||||
|
errorText: { color: colors.error, fontSize: 14 },
|
||||||
|
mapArea: { flex: 1 },
|
||||||
|
statsScroll: { maxHeight: 260, borderTopWidth: 1, borderTopColor: colors.border },
|
||||||
|
statsContent: { padding: 16, gap: 10 },
|
||||||
|
sportSummary: { color: colors.text, fontSize: 15, fontWeight: '600' },
|
||||||
|
date: { color: colors.textMuted, fontSize: 12 },
|
||||||
|
grid: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
|
||||||
|
statBox: { width: '47%', backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border, borderRadius: 10, padding: 12 },
|
||||||
|
statLabel: { color: colors.textMuted, fontSize: 10, textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||||
|
statValue: { color: colors.text, fontSize: 18, fontWeight: '600', marginTop: 4 },
|
||||||
|
// Edit modal
|
||||||
|
editContainer: { flex: 1, backgroundColor: colors.bg },
|
||||||
|
editHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', padding: 16, borderBottomWidth: 1, borderBottomColor: colors.border },
|
||||||
|
editHeading: { color: colors.text, fontSize: 16, fontWeight: '700' },
|
||||||
|
editCancel: { color: colors.textSub, fontSize: 15 },
|
||||||
|
editSave: { fontSize: 15, fontWeight: '700' },
|
||||||
|
editContent: { padding: 16, gap: 12 },
|
||||||
|
editSectionLabel: { color: colors.textMuted, fontSize: 11, textTransform: 'uppercase', letterSpacing: 0.8 },
|
||||||
|
editInput: { backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border, color: colors.text, borderRadius: 10, padding: 14, fontSize: 16 },
|
||||||
|
sportGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
|
||||||
|
sportPill: { flexDirection: 'row', alignItems: 'center', gap: 6, paddingVertical: 9, paddingHorizontal: 14, borderRadius: 20, borderWidth: 1, borderColor: colors.borderStrong, backgroundColor: colors.surface },
|
||||||
|
sportIcon: { fontSize: 16 },
|
||||||
|
sportLabel: { color: colors.textSub, fontSize: 13, fontWeight: '500' },
|
||||||
|
subRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
|
||||||
|
subPill: { paddingVertical: 7, paddingHorizontal: 14, borderRadius: 20, borderWidth: 1, borderColor: colors.borderStrong, backgroundColor: colors.surface },
|
||||||
|
subLabel: { color: colors.textSub, fontSize: 13, fontWeight: '500' },
|
||||||
|
});
|
||||||
@@ -4,6 +4,8 @@ import {
|
|||||||
Alert, ActivityIndicator, Modal, TextInput, Pressable,
|
Alert, ActivityIndicator, Modal, TextInput, Pressable,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useFocusEffect, useNavigation } from '@react-navigation/native';
|
import { useFocusEffect, useNavigation } from '@react-navigation/native';
|
||||||
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||||
|
import { RootStackParamList } from '../types';
|
||||||
import * as Sharing from 'expo-sharing';
|
import * as Sharing from 'expo-sharing';
|
||||||
import { listRecordings, deleteRecording, insertRecording } from '../services/db';
|
import { listRecordings, deleteRecording, insertRecording } from '../services/db';
|
||||||
import { uploadGpx } from '../services/upload';
|
import { uploadGpx } from '../services/upload';
|
||||||
@@ -16,7 +18,7 @@ import { useTheme } from '../ThemeContext';
|
|||||||
import { SPORT_ICONS, SPORT_LABELS, SUB_SPORT_LABELS, type Sport, type SubSport } from '../sports';
|
import { SPORT_ICONS, SPORT_LABELS, SUB_SPORT_LABELS, type Sport, type SubSport } from '../sports';
|
||||||
|
|
||||||
export function SavedRecordingsScreen() {
|
export function SavedRecordingsScreen() {
|
||||||
const nav = useNavigation();
|
const nav = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||||
const { accent, accentDim } = useTheme();
|
const { accent, accentDim } = useTheme();
|
||||||
const [recordings, setRecordings] = useState<SavedRecording[]>([]);
|
const [recordings, setRecordings] = useState<SavedRecording[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -200,8 +202,8 @@ export function SavedRecordingsScreen() {
|
|||||||
const isSelected = selectedIds.has(item.id);
|
const isSelected = selectedIds.has(item.id);
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
activeOpacity={selectionMode ? 0.7 : 1}
|
activeOpacity={0.7}
|
||||||
onPress={() => selectionMode && toggleId(item.id)}
|
onPress={() => selectionMode ? toggleId(item.id) : nav.navigate('ActivityDetail', { recording: item })}
|
||||||
style={[styles.card, isSelected && { borderColor: accent, backgroundColor: accentDim }]}
|
style={[styles.card, isSelected && { borderColor: accent, backgroundColor: accentDim }]}
|
||||||
>
|
>
|
||||||
<View style={styles.cardInner}>
|
<View style={styles.cardInner}>
|
||||||
|
|||||||
@@ -52,6 +52,14 @@ export async function listRecordings(): Promise<SavedRecording[]> {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateRecording(id: string, fields: { title: string; sport: string; subSport: string | null }): Promise<void> {
|
||||||
|
const d = await getDb();
|
||||||
|
await d.runAsync(
|
||||||
|
'UPDATE recordings SET title = ?, sport = ?, sub_sport = ? WHERE id = ?',
|
||||||
|
[fields.title, fields.sport, fields.subSport ?? null, id],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteRecording(id: string): Promise<void> {
|
export async function deleteRecording(id: string): Promise<void> {
|
||||||
const d = await getDb();
|
const d = await getDb();
|
||||||
await d.runAsync('DELETE FROM recordings WHERE id = ?', [id]);
|
await d.runAsync('DELETE FROM recordings WHERE id = ?', [id]);
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export type RootStackParamList = {
|
|||||||
Tabs: undefined;
|
Tabs: undefined;
|
||||||
PostRecording: undefined;
|
PostRecording: undefined;
|
||||||
SensorPairing: undefined;
|
SensorPairing: undefined;
|
||||||
|
ActivityDetail: { recording: SavedRecording };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TabParamList = {
|
export type TabParamList = {
|
||||||
|
|||||||
Reference in New Issue
Block a user