diff --git a/src/screens/PostRecordingScreen.tsx b/src/screens/PostRecordingScreen.tsx index 2657556..f1de6f1 100644 --- a/src/screens/PostRecordingScreen.tsx +++ b/src/screens/PostRecordingScreen.tsx @@ -7,17 +7,32 @@ import { insertRecording } from '../services/db'; import { SavedRecording } from '../types'; import { randomUUID } from 'expo-crypto'; import { colors } from '../theme'; +import { useTheme } from '../ThemeContext'; +import { + ALL_SPORTS, SPORT_ICONS, SPORT_LABELS, SPORT_SUBTYPES, SUB_SPORT_LABELS, + type Sport, type SubSport, +} from '../sports'; export function PostRecordingScreen() { const nav = useNavigation(); + const { accent, accentDim } = useTheme(); const { trackPoints, reset, getStats } = useRecordingStore(); const stats = getStats(); const [title, setTitle] = useState(''); + const [sport, setSport] = useState('cycling'); + const [subSport, setSubSport] = useState(null); const [saving, setSaving] = useState(false); + const subtypes = SPORT_SUBTYPES[sport] ?? []; + + function handleSportSelect(s: Sport) { + setSport(s); + setSubSport(null); // reset subtype when sport changes + } + const formatDuration = (secs: number) => { const h = Math.floor(secs / 3600).toString().padStart(2, '0'); - const m = Math.floor((secs % 3600) / 60).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}`; }; @@ -34,6 +49,8 @@ export function PostRecordingScreen() { durationSeconds: stats.elapsedSeconds, distanceMeters: stats.distanceMeters, filePath, + sport, + subSport, }; await insertRecording(rec); reset(); @@ -57,12 +74,51 @@ export function PostRecordingScreen() { Recording complete - - + + + {/* Sport selector */} + Activity type + + {ALL_SPORTS.map((s) => { + const active = sport === s; + return ( + handleSportSelect(s)} + > + {SPORT_ICONS[s]} + {SPORT_LABELS[s]} + + ); + })} + + + {/* Subtype selector — only shown when sport has subtypes */} + {subtypes.length > 0 && ( + <> + Subtype + + {subtypes.map((sub) => { + const active = subSport === sub; + return ( + setSubSport(active ? null : sub)} + > + {SUB_SPORT_LABELS[sub]} + + ); + })} + + + )} + ( - {item.title} + + {SPORT_ICONS[item.sport as Sport] ?? '⚡'} + {item.title} + - {new Date(item.date).toLocaleDateString()} · {formatDuration(item.durationSeconds)} · {(item.distanceMeters / 1000).toFixed(2)} km + {SPORT_LABELS[item.sport as Sport] ?? item.sport} + {item.subSport ? ` · ${SUB_SPORT_LABELS[item.subSport as SubSport] ?? item.subSport}` : ''} + {' · '}{new Date(item.date).toLocaleDateString()} + {' · '}{formatDuration(item.durationSeconds)} + {' · '}{(item.distanceMeters / 1000).toFixed(2)} km @@ -98,7 +106,9 @@ const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: colors.bg }, center: { flex: 1, backgroundColor: colors.bg, alignItems: 'center', justifyContent: 'center' }, list: { padding: 16, gap: 10 }, - card: { backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border, borderRadius: 12, padding: 16 }, + card: { backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border, borderRadius: 12, padding: 16 }, + cardTitleRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 4 }, + cardIcon: { fontSize: 18 }, cardMeta: { marginBottom: 12 }, cardTitle: { color: colors.text, fontSize: 15, fontWeight: '600' }, cardSub: { color: colors.textMuted, fontSize: 12, marginTop: 4 }, diff --git a/src/services/db.ts b/src/services/db.ts index c2b4571..89fb341 100644 --- a/src/services/db.ts +++ b/src/services/db.ts @@ -13,9 +13,18 @@ async function getDb(): Promise { date TEXT NOT NULL, duration_seconds INTEGER NOT NULL, distance_meters REAL NOT NULL, - file_path TEXT NOT NULL + file_path TEXT NOT NULL, + sport TEXT NOT NULL DEFAULT 'other', + sub_sport TEXT ); `); + // Migrate existing installs that pre-date the sport columns + for (const stmt of [ + "ALTER TABLE recordings ADD COLUMN sport TEXT NOT NULL DEFAULT 'other'", + 'ALTER TABLE recordings ADD COLUMN sub_sport TEXT', + ]) { + try { await db.execAsync(stmt); } catch { /* column already exists */ } + } } return db; } @@ -23,8 +32,8 @@ async function getDb(): Promise { export async function insertRecording(rec: SavedRecording): Promise { const d = await getDb(); await d.runAsync( - 'INSERT INTO recordings (id, title, date, duration_seconds, distance_meters, file_path) VALUES (?, ?, ?, ?, ?, ?)', - [rec.id, rec.title, rec.date, rec.durationSeconds, rec.distanceMeters, rec.filePath], + 'INSERT INTO recordings (id, title, date, duration_seconds, distance_meters, file_path, sport, sub_sport) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', + [rec.id, rec.title, rec.date, rec.durationSeconds, rec.distanceMeters, rec.filePath, rec.sport, rec.subSport ?? null], ); } @@ -38,6 +47,8 @@ export async function listRecordings(): Promise { durationSeconds: r.duration_seconds, distanceMeters: r.distance_meters, filePath: r.file_path, + sport: r.sport ?? 'other', + subSport: r.sub_sport ?? null, })); } diff --git a/src/sports.ts b/src/sports.ts new file mode 100644 index 0000000..782352a --- /dev/null +++ b/src/sports.ts @@ -0,0 +1,56 @@ +// Mirror of bincio_activity sport definitions — keep in sync with +// bincio_activity/site/src/lib/types.ts and bincio/extract/sport.py + +export type Sport = + | 'cycling' | 'running' | 'hiking' + | 'walking' | 'swimming' | 'skiing' | 'other'; + +export type SubSport = + | 'road' | 'mountain' | 'gravel' | 'indoor' // cycling + | 'trail' | 'track' // running (+ indoor shared) + | 'open_water' | 'pool' // swimming + | 'nordic' | 'alpine'; // skiing + +export const SPORT_ICONS: Record = { + cycling: '🚴', + running: '🏃', + hiking: '🥾', + walking: '🚶', + swimming: '🏊', + skiing: '⛷️', + other: '⚡', +}; + +export const SPORT_LABELS: Record = { + cycling: 'Cycling', + running: 'Running', + hiking: 'Hiking', + walking: 'Walking', + swimming: 'Swimming', + skiing: 'Skiing', + other: 'Other', +}; + +export const SUB_SPORT_LABELS: Record = { + road: 'Road', + mountain: 'MTB', + gravel: 'Gravel', + indoor: 'Indoor', + trail: 'Trail', + track: 'Track', + nordic: 'Nordic', + alpine: 'Alpine', + open_water: 'Open Water', + pool: 'Pool', +}; + +export const SPORT_SUBTYPES: Partial> = { + cycling: ['road', 'mountain', 'gravel', 'indoor'], + running: ['trail', 'track', 'indoor'], + swimming: ['open_water', 'pool'], + skiing: ['nordic', 'alpine'], +}; + +export const ALL_SPORTS: Sport[] = [ + 'cycling', 'running', 'hiking', 'walking', 'swimming', 'skiing', 'other', +]; diff --git a/src/types/index.ts b/src/types/index.ts index 599c200..b4affbd 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -35,6 +35,8 @@ export interface SavedRecording { durationSeconds: number; distanceMeters: number; filePath: string; + sport: string; + subSport: string | null; } export type RootStackParamList = {