Initial commit: family safety frontend project setup
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
class ApiConfig {
|
||||
static final String baseUrl = Uri.base.origin;
|
||||
|
||||
// Auth endpoints
|
||||
static final String loginUrl = '$baseUrl/login';
|
||||
static final String regUrl = '$baseUrl/reg';
|
||||
|
||||
// Geo endpoints
|
||||
static final String geoUrl = '$baseUrl/geo';
|
||||
|
||||
// Share endpoints
|
||||
static final String shareUrl = '$baseUrl/share';
|
||||
static final String watchUrl = '$baseUrl/watch';
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'providers/auth_provider.dart';
|
||||
import 'providers/map_provider.dart';
|
||||
import 'providers/share_provider.dart';
|
||||
import 'screens/login_screen.dart';
|
||||
import 'screens/map_screen.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (_) => AuthProvider()),
|
||||
ChangeNotifierProvider(create: (_) => MapProvider()),
|
||||
ChangeNotifierProvider(create: (_) => ShareProvider()),
|
||||
],
|
||||
child: MaterialApp(
|
||||
title: 'Family Safety',
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
),
|
||||
supportedLocales: const [
|
||||
Locale('en', ''),
|
||||
],
|
||||
initialRoute: '/',
|
||||
routes: {
|
||||
'/': (_) => const LoginScreen(),
|
||||
'/map': (_) => const MapScreen(),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/auth_service.dart';
|
||||
|
||||
class AuthProvider with ChangeNotifier {
|
||||
final AuthService _authService;
|
||||
|
||||
String _token = '';
|
||||
bool _isLoading = false;
|
||||
String _error = '';
|
||||
|
||||
AuthProvider({AuthService? authService})
|
||||
: _authService = authService ?? AuthService();
|
||||
|
||||
String get token => _token;
|
||||
bool get isLoggedIn => _token.isNotEmpty;
|
||||
bool get isLoading => _isLoading;
|
||||
String get error => _error;
|
||||
|
||||
Future<void> login(String login, String password) async {
|
||||
_isLoading = true;
|
||||
_error = '';
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
var response = await _authService.login(login, password);
|
||||
var data = jsonDecode(response);
|
||||
_token = data['token'];
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
rethrow;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> register(String login, String password) async {
|
||||
_isLoading = true;
|
||||
_error = '';
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await _authService.register(login, password);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
rethrow;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void logout() {
|
||||
_token = '';
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MapProvider with ChangeNotifier {
|
||||
bool _isInitialized = false;
|
||||
String _error = '';
|
||||
|
||||
bool get isInitialized => _isInitialized;
|
||||
String get error => _error;
|
||||
|
||||
void initialize() {
|
||||
_isInitialized = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void handleError(String error) {
|
||||
_error = error;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
_error = '';
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/share_service.dart';
|
||||
|
||||
class ShareProvider with ChangeNotifier {
|
||||
final ShareService _shareService;
|
||||
|
||||
String _shareId = '';
|
||||
int _geoId = 0;
|
||||
bool _isLoading = false;
|
||||
String _error = '';
|
||||
|
||||
Map<String, dynamic>? _position;
|
||||
|
||||
String _trackingShareId = '';
|
||||
String _trackingToken = '';
|
||||
Map<String, dynamic>? _trackedPosition;
|
||||
|
||||
ShareProvider({ShareService? shareService})
|
||||
: _shareService = shareService ?? ShareService();
|
||||
|
||||
String get shareId => _shareId;
|
||||
int get geoId => _geoId;
|
||||
bool get isLoading => _isLoading;
|
||||
String get error => _error;
|
||||
Map<String, dynamic>? get position => _position;
|
||||
|
||||
String get trackingShareId => _trackingShareId;
|
||||
Map<String, dynamic>? get trackedPosition => _trackedPosition;
|
||||
|
||||
Future<void> createShare(String token, double x, double y) async {
|
||||
_isLoading = true;
|
||||
_error = '';
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final result = await _shareService.createShare(token, x, y);
|
||||
_geoId = result['geo_id'];
|
||||
_shareId = result['share_id'];
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
rethrow;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getPosition(String token, String shareID) async {
|
||||
_isLoading = true;
|
||||
_error = '';
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
_position = await _shareService.getPosition(token, shareID);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
rethrow;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void clearShare() {
|
||||
_shareId = '';
|
||||
_geoId = 0;
|
||||
_position = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> startTracking(String shareId, String token) async {
|
||||
_trackingShareId = shareId;
|
||||
_trackingToken = token;
|
||||
_error = '';
|
||||
notifyListeners();
|
||||
|
||||
await updateTrackedPosition();
|
||||
}
|
||||
|
||||
Future<void> updateTrackedPosition() async {
|
||||
if (_trackingShareId.isEmpty) return;
|
||||
|
||||
try {
|
||||
_trackedPosition = await _shareService.getPositionByShareId(_trackingToken, _trackingShareId);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void stopTracking() {
|
||||
_trackingShareId = '';
|
||||
_trackingToken = '';
|
||||
_trackedPosition = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../config/api.dart';
|
||||
|
||||
class AuthService {
|
||||
final http.Client _client;
|
||||
|
||||
AuthService({http.Client? client}) : _client = client ?? http.Client();
|
||||
|
||||
Future<String> login(String login, String password) async {
|
||||
final response = await _client.post(
|
||||
Uri.parse(ApiConfig.loginUrl),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({'login': login, 'password': password}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return response.body;
|
||||
} else {
|
||||
throw Exception('Invalid credentials');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> register(String login, String password) async {
|
||||
final response = await _client.post(
|
||||
Uri.parse(ApiConfig.regUrl),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({'login': login, 'password': password}),
|
||||
);
|
||||
|
||||
if (response.statusCode != 201) {
|
||||
throw Exception('Registration failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../config/api.dart';
|
||||
|
||||
class GeoService {
|
||||
final http.Client _client;
|
||||
|
||||
GeoService({http.Client? client}) : _client = client ?? http.Client();
|
||||
|
||||
Future<void> updatePosition(String token, int id, double x, double y) async {
|
||||
final response = await _client.put(
|
||||
Uri.parse('${ApiConfig.geoUrl}?id=$id'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer $token',
|
||||
},
|
||||
body: jsonEncode({'x': x, 'y': y}),
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Failed to update position');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../config/api.dart';
|
||||
|
||||
class ShareService {
|
||||
final http.Client _client;
|
||||
|
||||
ShareService({http.Client? client}) : _client = client ?? http.Client();
|
||||
|
||||
Future<Map<String, dynamic>> createShare(String token, double x, double y) async {
|
||||
final response = await _client.post(
|
||||
Uri.parse(ApiConfig.shareUrl),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer $token',
|
||||
},
|
||||
body: jsonEncode({'x': x, 'y': y}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 201) {
|
||||
final data = jsonDecode(response.body);
|
||||
return {
|
||||
'geo_id': data['geo_id'],
|
||||
'share_id': data['share_id'],
|
||||
};
|
||||
} else {
|
||||
throw Exception('Failed to create share link');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getPosition(String token, String uniqueId) async {
|
||||
final response = await _client.get(
|
||||
Uri.parse('${ApiConfig.watchUrl}?unique_id=$uniqueId'),
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
return {
|
||||
'id': data['id'],
|
||||
'x': data['x'],
|
||||
'y': data['y'],
|
||||
'created_at': data['created_at'],
|
||||
'expires_at': data['expires_at'],
|
||||
};
|
||||
} else {
|
||||
throw Exception('Share link not found or no position available');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getPositionByShareId(String token, String shareId) async {
|
||||
final response = await _client.get(
|
||||
Uri.parse('${ApiConfig.watchUrl}?share_id=$shareId'),
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
return {
|
||||
'x': data['x'],
|
||||
'y': data['y'],
|
||||
'created_at': data['created_at'],
|
||||
'expires_at': data['expires_at'],
|
||||
};
|
||||
} else {
|
||||
throw Exception('Share link not found or no position available');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
class AppConstants {
|
||||
static const String appName = 'Family Safety';
|
||||
static const String defaultMapsApiKey = 'YOUR_MAPS_API_KEY';
|
||||
|
||||
// Default coordinates (Moscow)
|
||||
static const double defaultLatitude = 55.7558;
|
||||
static const double defaultLongitude = 37.6173;
|
||||
|
||||
// Share link expiration (5 minutes in seconds)
|
||||
static const int shareExpirationSeconds = 300;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
class StringUtil {
|
||||
static bool isValidEmail(String email) {
|
||||
final regExp = RegExp(r'^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)*\.(\w+)$');
|
||||
return regExp.hasMatch(email);
|
||||
}
|
||||
|
||||
static String truncate(String text, int maxLength) {
|
||||
if (text.length <= maxLength) return text;
|
||||
return '${text.substring(0, maxLength)}...';
|
||||
}
|
||||
|
||||
static String formatDateTime(String dateTime) {
|
||||
try {
|
||||
final date = DateTime.parse(dateTime);
|
||||
return '${date.day}/${date.month}/${date.year} '
|
||||
'${date.hour}:${date.minute.toString().padLeft(2, '0')}';
|
||||
} catch (e) {
|
||||
return dateTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ErrorDisplay extends StatelessWidget {
|
||||
final String message;
|
||||
final VoidCallback? onRetry;
|
||||
|
||||
const ErrorDisplay({
|
||||
super.key,
|
||||
required this.message,
|
||||
this.onRetry,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: Colors.red,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
message,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (onRetry != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: onRetry,
|
||||
child: const Text('Retry'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LoadingIndicator extends StatelessWidget {
|
||||
final String message;
|
||||
|
||||
const LoadingIndicator({
|
||||
super.key,
|
||||
this.message = 'Loading...',
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
message,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user