Initial commit: family safety frontend project setup

This commit is contained in:
dmit.b
2026-05-09 12:38:19 +03:00
commit ca90c6c3fc
147 changed files with 6350 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/auth_provider.dart';
class LoginScreen extends StatelessWidget {
const LoginScreen({super.key});
@override
Widget build(BuildContext context) {
final authProvider = context.watch<AuthProvider>();
final TextEditingController loginController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
return Scaffold(
body: Center(
child: Card(
margin: const EdgeInsets.all(24),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Family Safety',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 32),
TextField(
controller: loginController,
decoration: const InputDecoration(
labelText: 'Login',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: passwordController,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
obscureText: true,
),
const SizedBox(height: 16),
if (authProvider.error.isNotEmpty)
Text(
authProvider.error,
style: const TextStyle(color: Colors.red),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: authProvider.isLoading
? null
: () async {
try {
await authProvider.login(
loginController.text,
passwordController.text,
);
if (!context.mounted) return;
Navigator.of(context).pushReplacementNamed('/map');
} catch (e) {
// Error is handled by provider
}
},
child: const Text('Login'),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: authProvider.isLoading
? null
: () async {
try {
await authProvider.register(
loginController.text,
passwordController.text,
);
} catch (e) {
// Error is handled by provider
}
},
child: const Text('Register'),
),
),
],
),
],
),
),
),
),
);
}
}
+255
View File
@@ -0,0 +1,255 @@
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';
class MapScreen extends StatefulWidget {
const MapScreen({super.key});
@override
State<MapScreen> createState() => _MapScreenState();
}
class _MapScreenState extends State<MapScreen> {
LatLng _center = const LatLng(40.7128, -74.0060);
bool _loading = true;
Timer? _trackingTimer;
Timer? _geoTimer;
@override
void initState() {
super.initState();
_initLocationAndShare();
}
@override
void dispose() {
_trackingTimer?.cancel();
_geoTimer?.cancel();
super.dispose();
}
Future<void> _initLocationAndShare() async {
try {
if (!await Geolocator.isLocationServiceEnabled()) {
setState(() => _loading = false);
return;
}
final position = await Geolocator.getCurrentPosition();
setState(() {
_center = LatLng(position.latitude, position.longitude);
});
if (!mounted) return;
final authProvider = context.read<AuthProvider>();
final shareProvider = context.read<ShareProvider>();
if (authProvider.token.isNotEmpty) {
await shareProvider.createShare(
authProvider.token,
position.longitude,
position.latitude,
);
}
if (!mounted) return;
setState(() => _loading = false);
_geoTimer = Timer.periodic(
const Duration(seconds: 30),
(_) => _updateGeolocation(),
);
} catch (_) {
setState(() => _loading = false);
}
}
Future<void> _updateGeolocation() async {
try {
final position = await Geolocator.getCurrentPosition();
setState(() {
_center = LatLng(position.latitude, position.longitude);
});
if (!mounted) return;
final authProvider = context.read<AuthProvider>();
final shareProvider = context.read<ShareProvider>();
await GeoService().updatePosition(
authProvider.token,
shareProvider.geoId,
position.longitude,
position.latitude,
);
} catch (_) {}
}
void _copyShareLink() {
final shareProvider = context.read<ShareProvider>();
if (shareProvider.shareId.isNotEmpty) {
Clipboard.setData(ClipboardData(text: 'watch/${shareProvider.shareId}'));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Ссылка скопирована')),
);
}
}
void _startTracking() {
final authProvider = context.read<AuthProvider>();
final shareProvider = context.read<ShareProvider>();
if (shareProvider.trackingShareId.isEmpty) {
showDialog(
context: context,
builder: (context) {
final TextEditingController _controller = TextEditingController();
return AlertDialog(
title: const Text('Отслеживание'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Введите Share ID для отслеживания:'),
const SizedBox(height: 16),
TextField(
controller: _controller,
decoration: const InputDecoration(
hintText: 'Введите Share ID',
border: OutlineInputBorder(),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Отмена'),
),
TextButton(
onPressed: () async {
final shareId = _controller.text.trim();
if (shareId.isNotEmpty) {
Navigator.pop(context);
final token = authProvider.token;
await shareProvider.startTracking(shareId, token);
_trackingTimer = Timer.periodic(
const Duration(seconds: 5),
(_) => shareProvider.updateTrackedPosition(),
);
}
},
child: const Text('Начать'),
),
],
);
},
);
} else {
shareProvider.stopTracking();
_trackingTimer?.cancel();
}
}
@override
Widget build(BuildContext context) {
final authProvider = context.watch<AuthProvider>();
final _ = context.watch<MapProvider>();
final shareProvider = context.watch<ShareProvider>();
if (_loading) {
return Scaffold(
body: const Center(child: CircularProgressIndicator()),
);
}
Marker? trackedMarker;
if (shareProvider.trackedPosition != null) {
final pos = shareProvider.trackedPosition!;
final lat = pos['y'] as double;
final lng = pos['x'] as double;
trackedMarker = Marker(
point: LatLng(lat, lng),
width: 40,
height: 40,
child: const Icon(
Icons.person,
color: Colors.red,
size: 30,
),
);
}
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: Icon(
shareProvider.trackingShareId.isEmpty
? Icons.person_search
: Icons.person,
color: shareProvider.trackingShareId.isEmpty
? null
: Colors.red,
),
onPressed: _startTracking,
),
IconButton(
icon: const Icon(Icons.logout),
onPressed: () {
_geoTimer?.cancel();
authProvider.logout();
Navigator.of(context).pushReplacementNamed('/');
},
),
],
),
body: FlutterMap(
options: MapOptions(
center: _center,
zoom: 12.0,
),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
),
MarkerLayer(
markers: [
Marker(
point: _center,
width: 40,
height: 40,
child: const Icon(
Icons.my_location,
color: Colors.blue,
size: 30,)
),
if (trackedMarker != null) trackedMarker,
],
),
],
),
);
}
}
+94
View File
@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/auth_provider.dart';
import '../providers/share_provider.dart';
class ShareScreen extends StatelessWidget {
const ShareScreen({super.key});
@override
Widget build(BuildContext context) {
final authProvider = context.watch<AuthProvider>();
final shareProvider = context.watch<ShareProvider>();
return Scaffold(
appBar: AppBar(
title: const Text('Share Location'),
),
body: Center(
child: Card(
margin: const EdgeInsets.all(24),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Share Your Location',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
const Text(
'Create a share link to share your location with family',
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
if (shareProvider.shareId.isNotEmpty)
Column(
children: [
Text(
'Share ID: ${shareProvider.shareId}',
style: const TextStyle(
fontSize: 16,
color: Colors.blue,
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
// Copy to clipboard functionality
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Share ID copied to clipboard'),
),
);
},
child: const Text('Copy Share ID'),
),
],
)
else
ElevatedButton(
onPressed: shareProvider.isLoading
? null
: () async {
try {
await shareProvider.createShare(
authProvider.token,
55.7558, // Default Moscow coordinates
37.6173,
);
} catch (e) {
// Error is handled by provider
}
},
child: const Text('Create Share Link'),
),
if (shareProvider.error.isNotEmpty) ...[
const SizedBox(height: 16),
Text(
shareProvider.error,
style: const TextStyle(color: Colors.red),
),
],
],
),
),
),
),
);
}
}