import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'src/app/providers.dart'; import 'src/recording/recording_engine.dart'; /// Phase 3 entry point. /// /// The UI proper arrives in Phase 4 (T15–T21). This is deliberately a thin harness: it /// exists so the recording pipeline can be exercised on a real device before any screens /// are built, because the pipeline is the part that cannot be validated by unit tests or /// by an emulator. void main() { runApp(const ProviderScope(child: RipprApp())); } class RipprApp extends StatelessWidget { const RipprApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Rippr', debugShowCheckedModeBanner: false, theme: ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: const Color(0xFFFF6B1A), // safety orange, matching the icon brightness: Brightness.dark, ), useMaterial3: true, ), home: const _RecorderHarness(), ); } } class _RecorderHarness extends ConsumerStatefulWidget { const _RecorderHarness(); @override ConsumerState<_RecorderHarness> createState() => _RecorderHarnessState(); } class _RecorderHarnessState extends ConsumerState<_RecorderHarness> { String? _error; @override void initState() { super.initState(); // Re-attach to a ride that was in progress when the process died. Must happen before // any user interaction, and the database is the only thing that knows. WidgetsBinding.instance.addPostFrameCallback((_) async { await ref.read(recordingEngineProvider).restoreAfterProcessDeath(); }); } Future _run(Future Function() action) async { try { await action(); if (mounted) setState(() => _error = null); } catch (e) { if (mounted) setState(() => _error = '$e'); } } @override Widget build(BuildContext context) { final engine = ref.watch(recordingEngineProvider); final state = ref.watch(recorderStateProvider).valueOrNull ?? engine.state; final trip = ref.watch(activeTripProvider).valueOrNull; return Scaffold( appBar: AppBar(title: const Text('Rippr — recorder harness')), body: Padding( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text('State: ${state.name}', style: Theme.of(context).textTheme.headlineSmall), const SizedBox(height: 12), if (trip != null) ...[ Text('Trip ${trip.id} · ${trip.pointCount} points'), Text('${trip.distanceM.toStringAsFixed(1)} m'), Text('max ${trip.maxSpeedKmh.toStringAsFixed(1)} km/h'), ] else const Text('No active ride'), if (_error != null) ...[ const SizedBox(height: 12), Text(_error!, style: const TextStyle(color: Colors.redAccent)), ], const Spacer(), // 72dp targets, matching the native app's glove-sized controls. SizedBox( height: 72, child: FilledButton( onPressed: () => _run(engine.start), child: Text(state == RecorderState.paused ? 'RESUME' : 'START'), ), ), const SizedBox(height: 12), SizedBox( height: 72, child: OutlinedButton( onPressed: state == RecorderState.recording ? () => _run(engine.pause) : null, child: const Text('PAUSE'), ), ), const SizedBox(height: 12), SizedBox( height: 72, child: OutlinedButton( onPressed: state == RecorderState.idle ? null : () => _run(() async => engine.stop()), child: const Text('STOP'), ), ), ], ), ), ); } }