Save measured point to file

This commit is contained in:
torok.istvan 2025-12-04 14:31:07 +01:00
parent 7e035f1414
commit 061e64fe98
9 changed files with 664 additions and 33 deletions

View File

@ -0,0 +1,414 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
typedef MarkerCreationCallback = Marker Function(
LatLng point, Map<String, dynamic> properties);
typedef CircleMarkerCreationCallback = CircleMarker Function(
LatLng point, Map<String, dynamic> properties);
typedef PolylineCreationCallback = Polyline Function(
List<LatLng> points, Map<String, dynamic> properties);
typedef PolygonCreationCallback = Polygon Function(List<LatLng> points,
List<List<LatLng>>? holePointsList, Map<String, dynamic> properties);
typedef FilterFunction = bool Function(Map<String, dynamic> properties);
/// GeoJsonParser parses the GeoJson and fills three lists of parsed objects
/// which are defined in flutter_map package
/// - list of [Marker]s
/// - list of [CircleMarker]s
/// - list of [Polyline]s
/// - list of [Polygon]s
///
/// One should pass these lists when creating adequate layers in flutter_map.
/// For details see example.
///
/// Currently GeoJson parser supports only FeatureCollection and not GeometryCollection.
/// See the GeoJson Format specification at: https://www.rfc-editor.org/rfc/rfc7946
///
/// For creation of [Marker], [Polyline], [CircleMarker] and [Polygon] objects the default callback functions
/// are provided which are used in case when no user-defined callback function is provided.
/// To fully customize the [Marker], [Polyline], [CircleMarker] and [Polygon] creation one has to write his own
/// callback functions. As a template the default callback functions can be used.
///
class GeoJsonParser {
/// list of [Marker] objects created as result of parsing
final List<Marker> markers = [];
/// list of [Polyline] objects created as result of parsing
final List<Polyline> polylines = [];
/// list of [Polygon] objects created as result of parsing
final List<Polygon> polygons = [];
/// list of [CircleMarker] objects created as result of parsing
final List<CircleMarker> circles = [];
/// user defined callback function that creates a [Marker] object
MarkerCreationCallback? markerCreationCallback;
/// user defined callback function that creates a [Polyline] object
PolylineCreationCallback? polyLineCreationCallback;
/// user defined callback function that creates a [Polygon] object
PolygonCreationCallback? polygonCreationCallback;
/// user defined callback function that creates a [Polygon] object
CircleMarkerCreationCallback? circleMarkerCreationCallback;
/// default [Marker] color
Color? defaultMarkerColor;
/// default [Marker] icon
IconData? defaultMarkerIcon;
/// default [Polyline] color
Color? defaultPolylineColor;
/// default [Polyline] stroke
double? defaultPolylineStroke;
/// default [Polygon] border color
Color? defaultPolygonBorderColor;
/// default [Polygon] fill color
Color? defaultPolygonFillColor;
/// default [Polygon] border stroke
double? defaultPolygonBorderStroke;
/// default flag if [Polygon] is filled (default is true)
bool? defaultPolygonIsFilled;
/// default [CircleMarker] border color
Color? defaultCircleMarkerColor;
/// default [CircleMarker] border stroke
Color? defaultCircleMarkerBorderColor;
/// default flag if [CircleMarker] is filled (default is true)
bool? defaultCircleMarkerIsFilled;
/// user defined callback function called when the [Marker] is tapped
void Function(Map<String, dynamic>)? onMarkerTapCallback;
/// user defined callback function called when the [CircleMarker] is tapped
void Function(Map<String, dynamic>)? onCircleMarkerTapCallback;
/// user defined callback function called during parse for filtering
FilterFunction? filterFunction;
/// default constructor - all parameters are optional and can be set later with setters
GeoJsonParser({
this.markerCreationCallback,
this.polyLineCreationCallback,
this.polygonCreationCallback,
this.circleMarkerCreationCallback,
this.filterFunction,
this.defaultMarkerColor,
this.defaultMarkerIcon,
this.onMarkerTapCallback,
this.defaultPolylineColor,
this.defaultPolylineStroke,
this.defaultPolygonBorderColor,
this.defaultPolygonFillColor,
this.defaultPolygonBorderStroke,
this.defaultPolygonIsFilled,
this.defaultCircleMarkerColor,
this.defaultCircleMarkerBorderColor,
this.defaultCircleMarkerIsFilled,
this.onCircleMarkerTapCallback,
});
/// parse GeJson in [String] format
void parseGeoJsonAsString(String g) {
return parseGeoJson(jsonDecode(g) as Map<String, dynamic>);
}
/// set default [Marker] color
set setDefaultMarkerColor(Color color) {
defaultMarkerColor = color;
}
/// set default [Marker] icon
set setDefaultMarkerIcon(IconData ic) {
defaultMarkerIcon = ic;
}
/// set default [Marker] tap callback function
void setDefaultMarkerTapCallback(
Function(Map<String, dynamic> f) onTapFunction) {
onMarkerTapCallback = onTapFunction;
}
/// set default [CircleMarker] color
set setDefaultCircleMarkerColor(Color color) {
defaultCircleMarkerColor = color;
}
/// set default [CircleMarker] tap callback function
void setDefaultCircleMarkerTapCallback(
Function(Map<String, dynamic> f) onTapFunction) {
onCircleMarkerTapCallback = onTapFunction;
}
/// set default [Polyline] color
set setDefaultPolylineColor(Color color) {
defaultPolylineColor = color;
}
/// set default [Polyline] stroke
set setDefaultPolylineStroke(double stroke) {
defaultPolylineStroke = stroke;
}
/// set default [Polygon] fill color
set setDefaultPolygonFillColor(Color color) {
defaultPolygonFillColor = color;
}
/// set default [Polygon] border stroke
set setDefaultPolygonBorderStroke(double stroke) {
defaultPolygonBorderStroke = stroke;
}
/// set default [Polygon] border color
set setDefaultPolygonBorderColorStroke(Color color) {
defaultPolygonBorderColor = color;
}
/// set default [Polygon] setting whether polygon is filled
set setDefaultPolygonIsFilled(bool filled) {
defaultPolygonIsFilled = filled;
}
/// main GeoJson parsing function
void parseGeoJson(Map<String, dynamic> g) {
// set default values if they are not specified by constructor
markerCreationCallback ??= createDefaultMarker;
circleMarkerCreationCallback ??= createDefaultCircleMarker;
polyLineCreationCallback ??= createDefaultPolyline;
polygonCreationCallback ??= createDefaultPolygon;
filterFunction ??= defaultFilterFunction;
defaultMarkerColor ??= Colors.red.withOpacity(0.8);
defaultMarkerIcon ??= Icons.location_pin;
defaultPolylineColor ??= Colors.blue.withOpacity(0.8);
defaultPolylineStroke ??= 3.0;
defaultPolygonBorderColor ??= Colors.black.withOpacity(0.8);
defaultPolygonFillColor ??= Colors.black.withOpacity(0.1);
defaultPolygonIsFilled ??= true;
defaultPolygonBorderStroke ??= 1.0;
defaultCircleMarkerColor ??= Colors.blue.withOpacity(0.25);
defaultCircleMarkerBorderColor ??= Colors.black.withOpacity(0.8);
defaultCircleMarkerIsFilled ??= true;
// loop through the GeoJson Map and parse it
for (Map f in g['features'] as List) {
String geometryType = f['geometry']['type'].toString();
// check if this spatial object passes the filter function
if (!filterFunction!(f['properties'] as Map<String, dynamic>)) {
continue;
}
switch (geometryType) {
case 'Point':
{
markers.add(
markerCreationCallback!(
LatLng(f['geometry']['coordinates'][1] as double,
f['geometry']['coordinates'][0] as double),
f['properties'] as Map<String, dynamic>),
);
}
break;
case 'Circle':
{
circles.add(
circleMarkerCreationCallback!(
LatLng(f['geometry']['coordinates'][1] as double,
f['geometry']['coordinates'][0] as double),
f['properties'] as Map<String, dynamic>),
);
}
break;
case 'MultiPoint':
{
for (final point in f['geometry']['coordinates'] as List) {
markers.add(
markerCreationCallback!(
LatLng(point[1] as double, point[0] as double),
f['properties'] as Map<String, dynamic>),
);
}
}
break;
case 'LineString':
{
final List<LatLng> lineString = [];
for (final coords in f['geometry']['coordinates'] as List) {
lineString.add(LatLng(coords[1] as double, coords[0] as double));
}
polylines.add(polyLineCreationCallback!(
lineString, f['properties'] as Map<String, dynamic>));
}
break;
case 'MultiLineString':
{
for (final line in f['geometry']['coordinates'] as List) {
final List<LatLng> lineString = [];
for (final coords in line as List) {
lineString
.add(LatLng(coords[1] as double, coords[0] as double));
}
polylines.add(polyLineCreationCallback!(
lineString, f['properties'] as Map<String, dynamic>));
}
}
break;
case 'Polygon':
{
final List<LatLng> outerRing = [];
final List<List<LatLng>> holesList = [];
int pathIndex = 0;
for (final path in f['geometry']['coordinates'] as List) {
final List<LatLng> hole = [];
for (final coords in path as List<dynamic>) {
if (pathIndex == 0) {
// add to polygon's outer ring
outerRing
.add(LatLng(coords[1] as double, coords[0] as double));
} else {
// add it to current hole
hole.add(LatLng(coords[1] as double, coords[0] as double));
}
}
if (pathIndex > 0) {
// add hole to the polygon's list of holes
holesList.add(hole);
}
pathIndex++;
}
polygons.add(polygonCreationCallback!(
outerRing, holesList, f['properties'] as Map<String, dynamic>));
}
break;
case 'MultiPolygon':
{
for (final polygon in f['geometry']['coordinates'] as List) {
final List<LatLng> outerRing = [];
final List<List<LatLng>> holesList = [];
int pathIndex = 0;
for (final path in polygon as List) {
List<LatLng> hole = [];
for (final coords in path as List<dynamic>) {
if (pathIndex == 0) {
// add to polygon's outer ring
outerRing
.add(LatLng(coords[1] as double, coords[0] as double));
} else {
// add it to a hole
hole.add(LatLng(coords[1] as double, coords[0] as double));
}
}
if (pathIndex > 0) {
// add to polygon's list of holes
holesList.add(hole);
}
pathIndex++;
}
polygons.add(polygonCreationCallback!(outerRing, holesList,
f['properties'] as Map<String, dynamic>));
}
}
break;
}
}
return;
}
/// default function for creating tappable [Marker]
Widget defaultTappableMarker(Map<String, dynamic> properties,
void Function(Map<String, dynamic>) onMarkerTap) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
onMarkerTap(properties);
},
child: Icon(defaultMarkerIcon, color: defaultMarkerColor),
),
);
}
/// default callback function for creating [Marker]
Marker createDefaultMarker(LatLng point, Map<String, dynamic> properties) {
return Marker(
point: point,
child: defaultTappableMarker(properties, markerTapped),
);
}
// /// default callback function for creating [Marker]
// Marker createDefaultMarker(LatLng point, Map<String, dynamic> properties) {
// return Marker(
// point: point,
// child: MouseRegion(
// cursor: SystemMouseCursors.click,
// child: GestureDetector(
// onTap: () {
// markerTapped(properties);
// },
// child: Icon(defaultMarkerIcon, color: defaultMarkerColor),
// ),
// ),
// width: 60,
// height: 60,
// );
// }
/// default callback function for creating [Polygon]
CircleMarker createDefaultCircleMarker(
LatLng point, Map<String, dynamic> properties) {
return CircleMarker(
point: point,
radius: properties["radius"].toDouble(),
useRadiusInMeter: true,
color: defaultCircleMarkerColor!,
borderColor: defaultCircleMarkerBorderColor!,
);
}
/// default callback function for creating [Polyline]
Polyline createDefaultPolyline(
List<LatLng> points, Map<String, dynamic> properties) {
return Polyline(
points: points,
color: defaultPolylineColor!,
strokeWidth: defaultPolylineStroke!);
}
/// default callback function for creating [Polygon]
Polygon createDefaultPolygon(List<LatLng> outerRing,
List<List<LatLng>>? holesList, Map<String, dynamic> properties) {
return Polygon(
points: outerRing,
holePointsList: holesList,
borderColor: defaultPolygonBorderColor!,
color: defaultPolygonFillColor!,
// isFilled: defaultPolygonIsFilled!,
borderStrokeWidth: defaultPolygonBorderStroke!,
);
}
/// the default filter function returns always true - therefore no filtering
bool defaultFilterFunction(Map<String, dynamic> properties) {
return true;
}
/// default callback function called when tappable [Marker] is tapped
void markerTapped(Map<String, dynamic> map) {
if (onMarkerTapCallback != null) {
onMarkerTapCallback!(map);
}
}
}

View File

@ -65,11 +65,16 @@ class HomeView extends GetView<HomeViewController> {
Get.toNamed("/field_trip");
},
child: Text('Field trip')),
// TextButton(
// onPressed: () async {
// Get.toNamed("/tracking");
// },
// child: Text('Tracking')),
TextButton(
onPressed: () async {
Get.toNamed("/tracking");
Get.toNamed("/map");
},
child: Text('Tracking'))
child: Text('Kitűzés'))
])),
Padding(
padding: const EdgeInsets.only(top: 10.0),

View File

@ -29,6 +29,7 @@ import 'package:terepi_seged/models/point_to_measure.dart';
import 'package:terepi_seged/models/point_with_description_model.dart';
import 'package:proj4dart/proj4dart.dart' as proj4;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:terepi_seged/pages/map/presentation/views/measured_points_table_dialog.dart';
class MapViewController extends GetxController {
// String gpsAddress = "E8:31:CD:14:8B:B2";
@ -59,6 +60,8 @@ class MapViewController extends GetxController {
DateTime.now().add(const Duration(seconds: -30));
NumberFormat formatEov = NumberFormat("##0,000.0", "hu-HU");
NumberFormat formatEovZ = NumberFormat("###0.0", "hu-HU");
NumberFormat formatAltitudeError = NumberFormat("####0.000", "hu-HU");
NumberFormat formatEovForFile = NumberFormat("#####0.0", "hu-HU");
NumberFormat formatWgs84Sec = NumberFormat('00.000', 'hu-HU');
@ -128,6 +131,8 @@ class MapViewController extends GetxController {
// late StateMachineController riveGpsIconController;
late SharedPreferences prefs;
Rx<bool> isShowPassword = false.obs;
final passwordFieldFocusNode = FocusNode();
late AuthResponse authResponse;
late Session? session;
@ -1062,4 +1067,63 @@ class MapViewController extends GetxController {
}
void updatePointStatus(int pointId) {}
void toggleShowPassword() {
isShowPassword.value = !isShowPassword.value;
if (passwordFieldFocusNode.hasPrimaryFocus) return;
passwordFieldFocusNode.canRequestFocus = false;
}
void showMeasuredPointsTableDialog() {
Get.to(() => MeasuredPointsTableDialog(), transition: Transition.fadeIn);
}
Future<List> readMeasuredPoints() async {
var response = await Supabase.instance.client
.from('TerepiSeged_MeasuredPoints')
.select()
.eq('projectId', 2)
.order('created_at');
print(response);
return response;
}
void SaveMeasuredPointsToFile() async {
var pointsDirectory = await getExternalStorageDirectory();
print(directory!.path);
// String newPath = '';
// List<String> folders = directory!.path.split("/");
// for (int i = 1; i < folders.length; i++) {
// String folder = folders[i];
// if (folder != "Android") {
// newPath += "/" + folder;
// } else {
// break;
// }
// }
// newPath = newPath + "/TerepisSegedApp";
// directory = Directory(newPath);
if (!await pointsDirectory!.exists()) {
await pointsDirectory.create(recursive: true);
}
var measuredPointsFile = File("${directory!.path}/measuredsPoints.csv");
if (await pointsDirectory.exists()) {
if (!await measuredPointsFile.exists()) {
measuredPointsFile.writeAsString(
"Id;DateTime;Description;EovX;EovY;Latitude;Longitude;Altitude;Hor.Err;Vert.Err\r\n");
}
}
var data = await readMeasuredPoints();
data.forEach((d) {
print("Data EovX: ${d['EovX']}");
print("Data EovY: $d[EovY]");
});
print('Number of data: ${data.length}');
}
}

View File

@ -458,14 +458,7 @@ class MapView extends GetView<MapViewController> {
FloatingActionButton(
onPressed: () {
// controller.isMapMoveToCenter();
// controller.addMeasuredPoint();
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text(
"Fejlesztlés alatt",
style: TextStyle(fontWeight: FontWeight.bold),
),
backgroundColor: Colors.black54,
));
controller.showMeasuredPointsTableDialog();
},
heroTag: 'Database test',
tooltip: 'Pont bemérése',

View File

@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:terepi_seged/pages/map/presentation/controllers/map_controller.dart';
class MeasuredPointsTableDialog extends StatelessWidget {
final controller = Get.find<MapViewController>();
MeasuredPointsTableDialog({super.key});
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.only(top: 20.0),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(children: [
IconButton(
onPressed: () {
Get.back();
},
icon: const Icon(Icons.close)),
SizedBox(
width: 10,
),
IconButton(
onPressed: () {
controller.SaveMeasuredPointsToFile();
},
icon: const Icon(Icons.save)),
]),
TextButton(
style: ButtonStyle(
overlayColor:
MaterialStateProperty.all(Colors.transparent)),
onPressed: () {
Get.back();
},
child: const Text(
'Bezár',
style: TextStyle(color: Colors.blue, fontSize: 14.0),
))
],
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 40.0),
child: Text(
'Bemért pontok',
style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 5),
Expanded(
child: FutureBuilder<List<dynamic>>(
future: controller.readMeasuredPoints(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: Text(
snapshot.error.toString(),
),
);
}
if (!snapshot.hasData) {
return const Center(
child: Text("No Data available.\n Create new Data"));
}
// print(snapshot.data);
// return const Center(child: Text("Data available."));
return ListView.builder(
itemCount: snapshot.data!.length,
shrinkWrap: true,
itemBuilder: (context, int index) {
var data = snapshot.data![index];
print("snapshot data:");
print(data);
return ListTile(
leading: CircleAvatar(
backgroundColor: const Color(0xff764abc),
child: Text((index + 1).toString())),
title: Text(data['pointNumber'].toString(),
style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(data['description'],
style: TextStyle(
fontStyle: FontStyle.italic,
color: Colors.grey.shade700)),
Text(
"EovX: ${controller.formatEov.format(data['eovY'])} - EovY: ${controller.formatEov.format(data['eovX'])}",
style: TextStyle(
fontStyle: FontStyle.italic,
color: Colors.grey.shade400)),
Text(
"EovZ: ${controller.formatEovZ.format(data['altitude'] - data['poleHeight'])} (m)",
style: TextStyle(
fontStyle: FontStyle.italic,
color: Colors.grey.shade400)),
Text(
"H.hiba: ${controller.formatAltitudeError.format(data['horizontalError'])} (m) - V.hiba: ${controller.formatAltitudeError.format(data['verticalError'])} (m)",
style: TextStyle(
fontStyle: FontStyle.italic,
color: Colors.grey.shade400)),
],
));
});
},
),
)
],
),
),
);
}
}

View File

@ -330,26 +330,54 @@ class SettingsDialog extends StatelessWidget {
height: 40,
child: TextField(
controller: controller.ntripUsernameController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
enableSuggestions: false,
autocorrect: false,
decoration: InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.never,
isDense: true,
filled: true,
fillColor: Colors.grey.shade300,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(12)),
labelText: 'Felhasználónév',
icon: Icon(Icons.account_circle_rounded)),
prefixIcon: Icon(Icons.account_circle_rounded)),
),
),
const SizedBox(height: 10),
SizedBox(
height: 40,
child: TextField(
obscureText: true,
enableSuggestions: false,
autocorrect: false,
controller: controller.ntripPasswordController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Jelszó',
icon: Icon(
Icons.lock,
)),
Obx(
() => SizedBox(
height: 40,
child: TextField(
keyboardType: TextInputType.visiblePassword,
obscureText: !controller.isShowPassword.value,
focusNode: controller.passwordFieldFocusNode,
enableSuggestions: false,
autocorrect: false,
controller: controller.ntripPasswordController,
decoration: InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.never,
isDense: true,
filled: true,
fillColor: Colors.grey.shade300,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(12)),
labelText: 'Jelszó',
prefixIcon: Icon(
Icons.lock_rounded,
size: 24,
),
suffixIcon: Padding(
padding: const EdgeInsets.fromLTRB(0, 0, 4, 0),
child: GestureDetector(
onTap: controller.toggleShowPassword,
child: Icon(
controller.isShowPassword.value
? Icons.visibility_rounded
: Icons.visibility_off_rounded,
size: 24)))),
),
),
),
const SizedBox(height: 20)

View File

@ -22,6 +22,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart'
as permission_handler;
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:terepi_seged/controls/geojson_parser.dart';
import 'package:terepi_seged/eov/convert_coordinate.dart';
import 'package:terepi_seged/eov/eov.dart';
import 'package:terepi_seged/gnss_sentences/gngga.dart';
@ -116,7 +117,7 @@ class NavigationViewController extends GetxController {
late proj4.Projection eovProj, wgsProj;
RxBool mapIsInitialized = false.obs;
// GeoJsonParser parser = GeoJsonParser(defaultMarkerColor: Colors.yellow);
GeoJsonParser parser = GeoJsonParser(defaultMarkerColor: Colors.yellow);
final CollectionReference _vibratorTracker =
FirebaseFirestore.instance.collection('vibratorTracker');
@ -1028,10 +1029,10 @@ class NavigationViewController extends GetxController {
}
if (await file!.exists()) {
String data = await file.readAsString();
// parser.defaultPolylineColor = Colors.orangeAccent;
// parser.defaultPolylineStroke = 5.0;
// parser.parseGeoJsonAsString(data);
// pathLayer = parser.polylines;
parser.defaultPolylineColor = Colors.orangeAccent;
parser.defaultPolylineStroke = 5.0;
parser.parseGeoJsonAsString(data);
pathLayer = parser.polylines;
}
}

View File

@ -256,8 +256,8 @@ class NavigationView extends GetView<NavigationViewController> {
PolylineLayer(polylines: controller.pathLayer),
MarkerLayer(
markers: controller.pointsToMeasureMarker),
// PolylineLayer(
// polylines: controller.parser.polylines),
PolylineLayer(
polylines: controller.parser.polylines),
PolyWidgetLayer(
polyWidgets: controller.pointsToMeasureLabel),
],

View File

@ -31,6 +31,7 @@ dependencies:
flutter_map: ^8.2.2
flutter_map_polygon_editor: ^0.1.2
flutter_bluetooth_serial: ^0.4.0
flutter_map_geojson2: ^1.0.2
get: ^4.7.2
latlong2: ^0.9.1
nmea: ^3.3.2
@ -66,6 +67,7 @@ dependencies:
widget_zoom: ^0.0.4
supabase_flutter: ^2.10.2
appwrite: ^20.0.0
# share_plus: ^12.0.1
flutter:
sdk: flutter