mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Add compass functionality and shared preferences support to map tab
This commit is contained in:
@@ -1,9 +1,12 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
import 'package:flutter_compass/flutter_compass.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../providers/contacts_provider.dart';
|
import '../providers/contacts_provider.dart';
|
||||||
import '../providers/messages_provider.dart';
|
import '../providers/messages_provider.dart';
|
||||||
import '../providers/map_provider.dart';
|
import '../providers/map_provider.dart';
|
||||||
@@ -28,9 +31,12 @@ class _MapTabState extends State<MapTab> {
|
|||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
MapLayer _currentLayer = MapLayer.openStreetMap;
|
MapLayer _currentLayer = MapLayer.openStreetMap;
|
||||||
Position? _currentPosition;
|
Position? _currentPosition;
|
||||||
bool _showLegend = true;
|
double? _compassHeading; // Compass sensor heading
|
||||||
|
bool _rotateMarkerWithHeading = false; // Toggle for rotation
|
||||||
|
bool _showLegend = false;
|
||||||
double _gpsUpdateDistance = 3.0; // meters
|
double _gpsUpdateDistance = 3.0; // meters
|
||||||
StreamSubscription<Position>? _positionStreamSubscription;
|
StreamSubscription<Position>? _positionStreamSubscription;
|
||||||
|
StreamSubscription<CompassEvent>? _compassStreamSubscription;
|
||||||
|
|
||||||
// Default center point (will be updated based on markers)
|
// Default center point (will be updated based on markers)
|
||||||
static const LatLng _defaultCenter = LatLng(46.0569, 14.5058); // Ljubljana, Slovenia
|
static const LatLng _defaultCenter = LatLng(46.0569, 14.5058); // Ljubljana, Slovenia
|
||||||
@@ -39,8 +45,10 @@ class _MapTabState extends State<MapTab> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_loadSettings();
|
||||||
_initializeTileCache();
|
_initializeTileCache();
|
||||||
_requestLocationPermission();
|
_requestLocationPermission();
|
||||||
|
_startCompassTracking();
|
||||||
|
|
||||||
// Listen to map provider for navigation requests
|
// Listen to map provider for navigation requests
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
@@ -49,6 +57,41 @@ class _MapTabState extends State<MapTab> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _startCompassTracking() {
|
||||||
|
// Start listening to compass events
|
||||||
|
_compassStreamSubscription = FlutterCompass.events?.listen((CompassEvent event) {
|
||||||
|
if (mounted && event.heading != null) {
|
||||||
|
setState(() {
|
||||||
|
_compassHeading = event.heading;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rotate map if rotation mode is enabled and we have compass heading
|
||||||
|
if (_rotateMarkerWithHeading && event.heading != null) {
|
||||||
|
debugPrint('Rotating map to compass heading: ${event.heading}');
|
||||||
|
_mapController.rotate(-event.heading!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadSettings() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_showLegend = prefs.getBool('map_show_legend') ?? false;
|
||||||
|
_rotateMarkerWithHeading = prefs.getBool('map_rotate_with_heading') ?? false;
|
||||||
|
_gpsUpdateDistance = prefs.getDouble('map_gps_update_distance') ?? 3.0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveSettings() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setBool('map_show_legend', _showLegend);
|
||||||
|
await prefs.setBool('map_rotate_with_heading', _rotateMarkerWithHeading);
|
||||||
|
await prefs.setDouble('map_gps_update_distance', _gpsUpdateDistance);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _requestLocationPermission() async {
|
Future<void> _requestLocationPermission() async {
|
||||||
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||||
if (!serviceEnabled) {
|
if (!serviceEnabled) {
|
||||||
@@ -92,9 +135,18 @@ class _MapTabState extends State<MapTab> {
|
|||||||
),
|
),
|
||||||
).listen((Position position) {
|
).listen((Position position) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
debugPrint('Position update - Heading: ${position.heading}, Speed: ${position.speed}');
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentPosition = position;
|
_currentPosition = position;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Rotate map if rotation mode is enabled and heading is available
|
||||||
|
// Heading of -1.0 means heading is unavailable
|
||||||
|
if (_rotateMarkerWithHeading && position.heading != null && position.heading >= 0) {
|
||||||
|
debugPrint('Rotating map to heading: ${position.heading}');
|
||||||
|
_mapController.rotate(-position.heading);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -134,11 +186,25 @@ class _MapTabState extends State<MapTab> {
|
|||||||
final mapProvider = context.read<MapProvider>();
|
final mapProvider = context.read<MapProvider>();
|
||||||
mapProvider.removeListener(_handleMapNavigation);
|
mapProvider.removeListener(_handleMapNavigation);
|
||||||
_positionStreamSubscription?.cancel();
|
_positionStreamSubscription?.cancel();
|
||||||
|
_compassStreamSubscription?.cancel();
|
||||||
_mapController.dispose();
|
_mapController.dispose();
|
||||||
_tileCache.dispose();
|
_tileCache.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the current heading from compass or GPS
|
||||||
|
double? get _currentHeading {
|
||||||
|
// Prefer compass heading as it works when stationary
|
||||||
|
if (_compassHeading != null) {
|
||||||
|
return _compassHeading;
|
||||||
|
}
|
||||||
|
// Fall back to GPS heading when moving
|
||||||
|
if (_currentPosition?.heading != null && _currentPosition!.heading >= 0) {
|
||||||
|
return _currentPosition!.heading;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
LatLng _calculateCenter(List<Contact> contacts, List<SarMarker> sarMarkers) {
|
LatLng _calculateCenter(List<Contact> contacts, List<SarMarker> sarMarkers) {
|
||||||
final allPoints = <LatLng>[];
|
final allPoints = <LatLng>[];
|
||||||
|
|
||||||
@@ -272,6 +338,29 @@ class _MapTabState extends State<MapTab> {
|
|||||||
_showLegend = value;
|
_showLegend = value;
|
||||||
});
|
});
|
||||||
setModalState(() {});
|
setModalState(() {});
|
||||||
|
_saveSettings();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
// Compass rotation toggle
|
||||||
|
SwitchListTile(
|
||||||
|
secondary: const Icon(Icons.explore),
|
||||||
|
title: const Text('Rotate Map with Heading'),
|
||||||
|
subtitle: const Text('Map follows your direction when moving'),
|
||||||
|
value: _rotateMarkerWithHeading,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_rotateMarkerWithHeading = value;
|
||||||
|
// Reset map rotation when disabling
|
||||||
|
if (!_rotateMarkerWithHeading) {
|
||||||
|
_mapController.rotate(0);
|
||||||
|
} else if (_currentHeading != null) {
|
||||||
|
// Apply current heading rotation when enabling (if heading is valid)
|
||||||
|
_mapController.rotate(-_currentHeading!);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setModalState(() {});
|
||||||
|
_saveSettings();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
@@ -302,6 +391,7 @@ class _MapTabState extends State<MapTab> {
|
|||||||
});
|
});
|
||||||
// Restart location stream with new distance
|
// Restart location stream with new distance
|
||||||
_restartLocationStream();
|
_restartLocationStream();
|
||||||
|
_saveSettings();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
@@ -345,6 +435,12 @@ class _MapTabState extends State<MapTab> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_currentPosition = position;
|
_currentPosition = position;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Rotate map if rotation mode is enabled and heading is available
|
||||||
|
// Heading of -1.0 means heading is unavailable
|
||||||
|
if (_rotateMarkerWithHeading && position.heading != null && position.heading >= 0) {
|
||||||
|
_mapController.rotate(-position.heading);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -398,6 +494,7 @@ class _MapTabState extends State<MapTab> {
|
|||||||
),
|
),
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
|
rotate: false, // Don't rotate with map
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.blue.withOpacity(0.3),
|
color: Colors.blue.withOpacity(0.3),
|
||||||
@@ -416,7 +513,7 @@ class _MapTabState extends State<MapTab> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
Icons.navigation,
|
Icons.my_location,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 16,
|
size: 16,
|
||||||
),
|
),
|
||||||
@@ -440,10 +537,20 @@ class _MapTabState extends State<MapTab> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Compass widget - top right
|
||||||
|
if (_rotateMarkerWithHeading)
|
||||||
|
Positioned(
|
||||||
|
top: 16,
|
||||||
|
right: 16,
|
||||||
|
child: _CompassWidget(
|
||||||
|
heading: _currentHeading ?? 0,
|
||||||
|
hasHeading: _currentHeading != null,
|
||||||
|
),
|
||||||
|
),
|
||||||
// Map legend overlay
|
// Map legend overlay
|
||||||
if (_showLegend)
|
if (_showLegend)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 16,
|
top: _rotateMarkerWithHeading ? 80 : 16,
|
||||||
right: 16,
|
right: 16,
|
||||||
child: _MapLegend(
|
child: _MapLegend(
|
||||||
teamMemberCount: contactsWithLocation.length,
|
teamMemberCount: contactsWithLocation.length,
|
||||||
@@ -624,3 +731,107 @@ class _LegendItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _CompassWidget extends StatelessWidget {
|
||||||
|
final double heading;
|
||||||
|
final bool hasHeading;
|
||||||
|
|
||||||
|
const _CompassWidget({
|
||||||
|
required this.heading,
|
||||||
|
required this.hasHeading,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
child: Container(
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
// Compass rose background - rotates to show true north at top
|
||||||
|
Transform.rotate(
|
||||||
|
angle: heading * pi / 180,
|
||||||
|
child: CustomPaint(
|
||||||
|
size: const Size(40, 40),
|
||||||
|
painter: _CompassRosePainter(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Fixed needle pointing up (since map rotates)
|
||||||
|
Icon(
|
||||||
|
Icons.navigation,
|
||||||
|
color: hasHeading ? Colors.red : Colors.grey,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
// Heading text
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.7),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
hasHeading ? '${heading.round()}°' : '--',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CompassRosePainter extends CustomPainter {
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paint = Paint()
|
||||||
|
..color = Colors.grey.withOpacity(0.3)
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 1;
|
||||||
|
|
||||||
|
final center = Offset(size.width / 2, size.height / 2);
|
||||||
|
final radius = size.width / 2;
|
||||||
|
|
||||||
|
// Draw circle
|
||||||
|
canvas.drawCircle(center, radius, paint);
|
||||||
|
|
||||||
|
// Draw cardinal direction markers
|
||||||
|
final textPainter = TextPainter(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
);
|
||||||
|
|
||||||
|
final directions = ['N', 'E', 'S', 'W'];
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
final angle = i * pi / 2 - pi / 2; // Start from North (top)
|
||||||
|
final x = center.dx + radius * 0.7 * cos(angle);
|
||||||
|
final y = center.dy + radius * 0.7 * sin(angle);
|
||||||
|
|
||||||
|
textPainter.text = TextSpan(
|
||||||
|
text: directions[i],
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
textPainter.layout();
|
||||||
|
textPainter.paint(
|
||||||
|
canvas,
|
||||||
|
Offset(x - textPainter.width / 2, y - textPainter.height / 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import objectbox_flutter_libs
|
|||||||
import package_info_plus
|
import package_info_plus
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import share_plus
|
import share_plus
|
||||||
|
import shared_preferences_foundation
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
@@ -21,4 +22,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
66
pubspec.lock
66
pubspec.lock
@@ -206,6 +206,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.0.0"
|
version: "8.0.0"
|
||||||
|
flutter_compass:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_compass
|
||||||
|
sha256: "1b4d7e6c95a675ec8482b5c9c9ccf1ebf0ced3dbec59dce28ad609da953de850"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.1"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -648,6 +656,62 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.2"
|
version: "5.0.2"
|
||||||
|
shared_preferences:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: shared_preferences
|
||||||
|
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.3"
|
||||||
|
shared_preferences_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_android
|
||||||
|
sha256: "34266009473bf71d748912da4bf62d439185226c03e01e2d9687bc65bbfcb713"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.15"
|
||||||
|
shared_preferences_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_foundation
|
||||||
|
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.4"
|
||||||
|
shared_preferences_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_linux
|
||||||
|
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
shared_preferences_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_platform_interface
|
||||||
|
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
shared_preferences_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_web
|
||||||
|
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.3"
|
||||||
|
shared_preferences_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_windows
|
||||||
|
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -823,4 +887,4 @@ packages:
|
|||||||
version: "6.6.1"
|
version: "6.6.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.9.2 <4.0.0"
|
dart: ">=3.9.2 <4.0.0"
|
||||||
flutter: ">=3.29.0"
|
flutter: ">=3.35.0"
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ dependencies:
|
|||||||
|
|
||||||
# Location services
|
# Location services
|
||||||
geolocator: ^14.0.2
|
geolocator: ^14.0.2
|
||||||
|
flutter_compass: ^0.8.0
|
||||||
|
|
||||||
# Utilities
|
# Utilities
|
||||||
intl: ^0.20.2
|
intl: ^0.20.2
|
||||||
@@ -63,6 +64,9 @@ dependencies:
|
|||||||
share_plus: ^10.1.3
|
share_plus: ^10.1.3
|
||||||
path_provider: ^2.1.5
|
path_provider: ^2.1.5
|
||||||
|
|
||||||
|
# Persistent storage
|
||||||
|
shared_preferences: ^2.3.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||
Reference in New Issue
Block a user