import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:geolocator/geolocator.dart'; import 'package:provider/provider.dart'; import '../providers/auth_provider.dart'; import '../providers/map_provider.dart'; import '../providers/share_provider.dart'; import '../services/geo_service.dart'; import '../services/background_geo_service.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; class MapScreen extends StatefulWidget { const MapScreen({super.key}); @override State createState() => _MapScreenState(); } class _MapScreenState extends State { LatLng _center = const LatLng(40.7128, -74.0060); bool _loading = true; Timer? _geoTimer; @override void initState() { super.initState(); _initLocationAndShare(); } @override void dispose() { _geoTimer?.cancel(); super.dispose(); } Future _initLocationAndShare() async { try { if (!await Geolocator.isLocationServiceEnabled()) { setState(() => _loading = false); return; } final status = await Geolocator.checkPermission(); if (status == LocationPermission.denied) { final requested = await Geolocator.requestPermission(); if (requested == LocationPermission.denied) { return; } } if (status == LocationPermission.deniedForever) { return; } final position = await Geolocator.getCurrentPosition(); setState(() { _center = LatLng(position.latitude, position.longitude); }); if (!mounted) return; final authProvider = context.read(); final shareProvider = context.read(); if (authProvider.token.isNotEmpty) { try { await shareProvider.createShare( authProvider.token, position.longitude, position.latitude, ); if (!mounted) return; final bgGeo = context.read(); bgGeo.configure( token: authProvider.token, geoId: shareProvider.geoId, ); FlutterForegroundTask.setTaskHandler(BackgroundTaskHandler()); await bgGeo.start(); } catch (e) { if (!mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Ошибка геолокации: $e'))); } } if (!mounted) return; setState(() => _loading = false); _geoTimer = Timer.periodic( const Duration(seconds: 30), (_) => _updateGeolocation(), ); } catch (_) { setState(() => _loading = false); } } Future _updateGeolocation() async { try { final status = await Geolocator.checkPermission(); if (status == LocationPermission.denied) { final requested = await Geolocator.requestPermission(); if (requested == LocationPermission.denied) return; } final position = await Geolocator.getCurrentPosition(); setState(() { _center = LatLng(position.latitude, position.longitude); }); if (!mounted) return; final authProvider = context.read(); final shareProvider = context.read(); if (shareProvider.geoId.isEmpty) return; await GeoService().updatePosition( authProvider.token, shareProvider.geoId, position.longitude, position.latitude, ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Ошибка обновления позиции: $e'))); } } void _copyShareLink() { final shareProvider = context.read(); if (shareProvider.shareId.isNotEmpty) { Clipboard.setData(ClipboardData(text: shareProvider.shareId)); ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('Ссылка скопирована'))); } } void _openWatchMenu() { final authProvider = context.read(); final shareProvider = context.read(); showModalBottomSheet( context: context, isScrollControlled: true, builder: (context) => _WatchMenu( token: authProvider.token, onAdd: (shareId) async { await shareProvider.addTracking(shareId, authProvider.token); if (shareProvider.trackedPeople.length == 1) { shareProvider.startTrackingPolling(authProvider.token); } }, onRemove: (shareId) { shareProvider.removeTracking(shareId); if (shareProvider.trackedPeople.isEmpty) { shareProvider.stopAllTracking(); } }, ), ); } List _buildMarkers(ShareProvider shareProvider) { final markers = [ Marker( point: _center, width: 40, height: 40, child: const Icon(Icons.my_location, color: Colors.blue, size: 30), ), ]; for (final person in shareProvider.trackedPeople) { if (person.position != null) { final pos = person.position!; final lat = pos['y'] is String ? double.parse(pos['y']) : pos['y'] as double; final lng = pos['x'] is String ? double.parse(pos['x']) : pos['x'] as double; markers.add( Marker( point: LatLng(lat, lng), width: 40, height: 40, child: Icon(Icons.person, color: person.color, size: 30), ), ); } } return markers; } @override Widget build(BuildContext context) { final authProvider = context.watch(); final _ = context.watch(); final shareProvider = context.watch(); if (_loading) { return Scaffold(body: const Center(child: CircularProgressIndicator())); } return Scaffold( appBar: AppBar( title: Text( 'Family Safety Map\n${_center.latitude.toStringAsFixed(4)}, ${_center.longitude.toStringAsFixed(4)}', textAlign: TextAlign.center, ), actions: [ IconButton( icon: const Icon(Icons.refresh), onPressed: _updateGeolocation, ), IconButton(icon: const Icon(Icons.share), onPressed: _copyShareLink), IconButton( icon: const Icon(Icons.visibility), onPressed: _openWatchMenu, ), IconButton( icon: const Icon(Icons.logout), onPressed: () async { _geoTimer?.cancel(); final bgGeo = context.read(); await bgGeo.stop(); bgGeo.dispose(); authProvider.logout(); Navigator.of(context).pushReplacementNamed('/'); }, ), ], ), body: FlutterMap( options: MapOptions(initialCenter: _center, initialZoom: 12.0), children: [ TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', maxZoom: 20, userAgentPackageName: 'FamilySafety/1.0.0', ), MarkerLayer(markers: _buildMarkers(shareProvider)), ], ), ); } } class _WatchMenu extends StatefulWidget { final String token; final Future Function(String shareId) onAdd; final void Function(String shareId) onRemove; const _WatchMenu({ required this.token, required this.onAdd, required this.onRemove, }); @override State<_WatchMenu> createState() => _WatchMenuState(); } class _WatchMenuState extends State<_WatchMenu> { final TextEditingController _controller = TextEditingController(); @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final shareProvider = context.watch(); final trackedPeople = shareProvider.trackedPeople; return Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: Container( constraints: const BoxConstraints(maxHeight: 400), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: const BorderRadius.vertical( top: Radius.circular(16), ), ), child: Row( children: [ const Icon(Icons.visibility, size: 24), const SizedBox(width: 12), const Text( 'Отслеживаемые', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const Spacer(), if (trackedPeople.isNotEmpty) TextButton( onPressed: () { shareProvider.stopAllTracking(); Navigator.pop(context); }, child: const Text('Очистить'), ), IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context), ), ], ), ), if (trackedPeople.isEmpty) const Padding( padding: EdgeInsets.all(32), child: Text( 'Нет отслеживаемых людей', style: TextStyle(color: Colors.grey), ), ), if (trackedPeople.isNotEmpty) Expanded( child: ListView.builder( shrinkWrap: true, itemCount: trackedPeople.length, itemBuilder: (context, index) { final person = trackedPeople[index]; final hasPosition = person.position != null; return ListTile( leading: CircleAvatar( backgroundColor: person.color, child: const Icon( Icons.person, color: Colors.white, size: 18, ), ), title: Text(person.name), subtitle: hasPosition ? Text( '${person.position!['y'].toStringAsFixed(4)}, ${person.position!['x'].toStringAsFixed(4)}', ) : const Text('Нет данных'), trailing: IconButton( icon: const Icon(Icons.close, color: Colors.red), onPressed: () => widget.onRemove(person.shareId), ), ); }, ), ), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: const BorderRadius.vertical( bottom: Radius.circular(16), ), ), child: Row( children: [ Expanded( child: TextField( controller: _controller, decoration: const InputDecoration( hintText: 'Введите Share ID', border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), ), onSubmitted: (value) { if (value.trim().isNotEmpty) { widget.onAdd(value.trim()); _controller.clear(); } }, ), ), const SizedBox(width: 8), ElevatedButton( onPressed: () { if (_controller.text.trim().isNotEmpty) { widget.onAdd(_controller.text.trim()); _controller.clear(); } }, child: const Text('Добавить'), ), ], ), ), ], ), ), ); } }