import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../dji_service.dart'; import '../flight_model.dart'; import '../pb_auth.dart'; import '../theme.dart'; import 'flight_logs_page.dart'; import 'pv_icons.dart'; import 'routes_page.dart'; /// Profile — mirrors the v2 "Profile" mockup. Shows the PilotVault identity and /// (optionally) the linked DJI account, headline stats, and library shortcuts. class ProfilePage extends StatefulWidget { const ProfilePage({ super.key, required this.model, required this.dji, required this.onAppSettings, required this.onSignIn, }); final FlightModel model; final DjiService dji; final VoidCallback onAppSettings; final VoidCallback onSignIn; @override State createState() => _ProfilePageState(); } class _ProfilePageState extends State { StreamSubscription? _authSub; DjiService get _dji => widget.dji; FlightModel get _m => widget.model; @override void initState() { super.initState(); _authSub = auth.status.listen((_) { if (mounted) setState(() {}); }); widget.dji.refreshDjiAccountState().catchError((_) {}); } @override void dispose() { _authSub?.cancel(); super.dispose(); } void _snack(String msg) { ScaffoldMessenger.of(context) ..clearSnackBars() ..showSnackBar(SnackBar(content: Text(msg))); } Future _djiLogin() async { try { await _dji.djiLogin(); _snack('DJI account linked'); } catch (e) { _snack('DJI login: ${e is PlatformException ? (e.message ?? e.code) : e}'); } } Future _djiLogout() async { try { await _dji.djiLogout(); } catch (_) {} } bool get _djiLinked => _m.djiAccountState == 'AUTHORIZED'; @override Widget build(BuildContext context) { final PVScheme s = PVScheme.of(context); final bool signedIn = auth.isAuthed; return Scaffold( backgroundColor: s.bgApp, body: SafeArea( child: AnimatedBuilder( animation: _m, builder: (BuildContext context, _) => ListView( children: [ _headerBar(s), _identity(s, signedIn), _stats(s), _djiCard(s), const SizedBox(height: 6), ..._rows(s), ], ), ), ), ); } Widget _headerBar(PVScheme s) => Padding( padding: const EdgeInsets.fromLTRB(12, 6, 12, 0), child: Row(children: [ IconButton(onPressed: () => Navigator.of(context).maybePop(), icon: PVIcon('chevronLeft', size: 24, color: s.textSecondary)), const Spacer(), IconButton(onPressed: widget.onAppSettings, icon: PVIcon('settings', size: 20, color: s.textSecondary)), ]), ); Widget _identity(PVScheme s, bool signedIn) { return Padding( padding: const EdgeInsets.fromLTRB(20, 6, 20, 16), child: Row(children: [ Container( width: 60, height: 60, decoration: BoxDecoration(color: s.accentSoft, shape: BoxShape.circle), alignment: Alignment.center, child: PVIcon('user', size: 30, color: s.accent), ), const SizedBox(width: 14), Expanded( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(signedIn ? (auth.userEmail.isEmpty ? 'PilotVault pilot' : auth.userEmail) : 'Guest pilot', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontFamily: PV.fontSans, fontSize: 18, fontWeight: FontWeight.w700, letterSpacing: -0.2, color: s.textPrimary)), const SizedBox(height: 2), Text(signedIn ? 'Verified pilot' : 'Not signed in', style: TextStyle(fontFamily: PV.fontMono, fontSize: 12, color: s.textSecondary)), ]), ), if (!signedIn) FilledButton(onPressed: widget.onSignIn, child: const Text('Sign in')), ]), ); } Widget _stats(PVScheme s) { const List<(String, String)> stats = <(String, String)>[('Flights', '—'), ('Distance', '—'), ('Flight time', '—')]; return Container( margin: const EdgeInsets.fromLTRB(20, 0, 20, 18), decoration: BoxDecoration(color: s.surface, borderRadius: BorderRadius.circular(16), border: Border.all(color: s.border), boxShadow: s.shadowXs), child: Row(children: [ for (int i = 0; i < stats.length; i++) Expanded( child: Container( decoration: BoxDecoration(border: Border(left: i == 0 ? BorderSide.none : BorderSide(color: s.border))), padding: const EdgeInsets.symmetric(vertical: 14), child: Column(children: [ Text(stats[i].$2, style: TextStyle(fontFamily: PV.fontMono, fontSize: 19, fontWeight: FontWeight.w700, color: s.textPrimary)), const SizedBox(height: 2), Text(stats[i].$1, style: TextStyle(fontFamily: PV.fontSans, fontSize: 11.5, color: s.textSecondary)), ]), ), ), ]), ); } Widget _djiCard(PVScheme s) { return Container( margin: const EdgeInsets.fromLTRB(20, 0, 20, 8), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration(color: s.surface, borderRadius: BorderRadius.circular(14), border: Border.all(color: s.border)), child: Row(children: [ Container( width: 34, height: 34, decoration: BoxDecoration(color: s.surfaceInset, borderRadius: BorderRadius.circular(9)), alignment: Alignment.center, child: PVIcon('drone', size: 18, color: s.textSecondary), ), const SizedBox(width: 12), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('DJI account', style: TextStyle(fontFamily: PV.fontSans, fontSize: 14, fontWeight: FontWeight.w600, color: s.textPrimary)), Text(_djiLinked ? (_m.djiAccountUser ?? 'Linked') : 'Optional · unlocks NFZ & sync', style: TextStyle(fontFamily: PV.fontMono, fontSize: 11, color: s.textSecondary)), ])), _djiLinked ? TextButton(onPressed: _djiLogout, child: const Text('Unlink')) : FilledButton(onPressed: _djiLogin, child: const Text('Link')), ]), ); } List _rows(PVScheme s) { final List<(String, String, VoidCallback)> rows = <(String, String, VoidCallback)>[ ('gauge', 'Flight records', () => _push(const FlightLogsPage())), ('route', 'My routes', () => _push(const RoutesPage())), ('download', 'Downloads', () => _snack('Downloaded media is saved to the app files folder')), ('shield', 'Find my drone', _findDrone), ('settings', 'App settings', widget.onAppSettings), ]; return [ for (int i = 0; i < rows.length; i++) Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: InkWell( onTap: rows[i].$3, child: Container( height: 52, decoration: BoxDecoration(border: Border(bottom: i < rows.length - 1 ? BorderSide(color: s.border) : BorderSide.none)), child: Row(children: [ Container( width: 34, height: 34, decoration: BoxDecoration(color: s.surfaceInset, borderRadius: BorderRadius.circular(9)), alignment: Alignment.center, child: PVIcon(rows[i].$1, size: 17, color: s.textSecondary), ), const SizedBox(width: 14), Expanded(child: Text(rows[i].$2, style: TextStyle(fontFamily: PV.fontSans, fontSize: 14.5, fontWeight: FontWeight.w500, color: s.textPrimary))), PVIcon('chevronRight', size: 18, color: s.textTertiary), ]), ), ), ), ]; } void _push(Widget page) => Navigator.of(context).push(MaterialPageRoute(builder: (_) => page)); void _findDrone() { if (_m.latitude != null && _m.longitude != null) { _snack('Aircraft at ${_m.latitude!.toStringAsFixed(5)}, ${_m.longitude!.toStringAsFixed(5)}'); } else { _snack('No aircraft GPS fix available'); } } }