From 15ae6a75fc9d52e85cd1cf290ef1eafe2628e376 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Tue, 22 Apr 2025 18:19:30 +0530 Subject: [PATCH] feat: Enhance attendance functionality with image capture and location tracking --- android/app/src/debug/AndroidManifest.xml | 3 + android/app/src/main/AndroidManifest.xml | 3 + .../attendance_screen_controller.dart | 90 ++++- lib/helpers/services/api_service.dart | 125 ++++++- lib/model/AttendanceLogModel.dart | 25 ++ lib/routes.dart | 4 +- lib/view/dashboard/attendanceScreen.dart | 331 ++++++++++++------ macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 122 ++++++- pubspec.yaml | 2 + 10 files changed, 572 insertions(+), 135 deletions(-) create mode 100644 lib/model/AttendanceLogModel.dart diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 399f698..db3ff6b 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -4,4 +4,7 @@ to allow setting breakpoints, to provide hot reload, etc. --> + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index cb21973..c45876e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,7 @@ + + + attendances = []; @@ -17,26 +20,28 @@ class AttendanceController extends GetxController { } // Fetch projects from API -Future fetchProjects() async { - var response = await ApiService.getProjects(); // Call the project API + Future fetchProjects() async { + var response = await ApiService.getProjects(); // Call the project API - if (response != null) { - projects = response - .map((json) => ProjectModel.fromJson(json)) - .toList(); + if (response != null) { + projects = response + .map((json) => ProjectModel.fromJson(json)) + .toList(); - // Set default to the first project if available - if (projects.isNotEmpty) { - selectedProjectId = projects.first.id.toString(); - await fetchEmployeesByProject(selectedProjectId); // Fetch employees for the first project + // Set default to the first project if available + if (projects.isNotEmpty) { + selectedProjectId = projects.first.id.toString(); + await fetchEmployeesByProject( + selectedProjectId); // Fetch employees for the first project + } + + update([ + 'attendance_dashboard_controller' + ]); // Notify GetBuilder with your tag + } else { + print("No projects data found or failed to fetch data."); } - - update(['attendance_dashboard_controller']); // Notify GetBuilder with your tag - } else { - print("No projects data found or failed to fetch data."); } -} - // Fetch employees by project ID Future fetchEmployeesByProject(String? projectId) async { @@ -53,4 +58,53 @@ Future fetchProjects() async { print("Failed to fetch employees for project $projectId."); } } + + Future captureAndUploadAttendance(int employeeId, int projectId, + {String comment = "Marked via mobile app"}) async { + try { + final XFile? image = await ImagePicker().pickImage( + source: ImageSource.camera, + imageQuality: 80, + ); + + if (image == null) return false; + + final position = await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.high); + + String imageName = ApiService.generateImageName( + employeeId, + employees.length + 1, + ); + + return await ApiService.uploadAttendanceImage( + employeeId, + image, + position.latitude, + position.longitude, + imageName: imageName, + projectId: projectId, + comment: comment, + ); + } catch (e) { + print("Error capturing or uploading: $e"); + return false; + } + } + + List attendanceLogs = []; + Future fetchAttendanceLogs(String? projectId) async { + if (projectId == null) return; + + var response = await ApiService.getAttendanceLogs(int.parse(projectId)); + + if (response != null) { + attendanceLogs = response + .map((json) => AttendanceLogModel.fromJson(json)) + .toList(); + update(); + } else { + print("Failed to fetch logs for project $projectId."); + } + } } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index cf130cc..3d27902 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -1,9 +1,11 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:image_picker/image_picker.dart'; import 'package:marco/helpers/services/storage/local_storage.dart'; +import 'package:intl/intl.dart'; class ApiService { - static const String baseUrl = "https://api.marcoaiot.com/api"; + static const String baseUrl = "https://api.marcoaiot.com/api"; // Fetch the list of projects static Future?> getProjects() async { @@ -50,7 +52,8 @@ class ApiService { } final response = await http.get( - Uri.parse("$baseUrl/attendance/project/team?projectId=$projectId"), // Ensure correct endpoint + Uri.parse( + "$baseUrl/attendance/project/team?projectId=$projectId"), // Ensure correct endpoint headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $jwtToken', @@ -74,4 +77,122 @@ class ApiService { } return null; } + + static String generateImageName(int employeeId, int count) { + final now = DateTime.now(); + final formattedDate = "${now.year.toString().padLeft(4, '0')}" + "${now.month.toString().padLeft(2, '0')}" + "${now.day.toString().padLeft(2, '0')}_" + "${now.hour.toString().padLeft(2, '0')}" + "${now.minute.toString().padLeft(2, '0')}" + "${now.second.toString().padLeft(2, '0')}"; + final imageNumber = count.toString().padLeft(3, '0'); + return "${employeeId}_${formattedDate}_$imageNumber.jpg"; + } + + static Future uploadAttendanceImage( + int employeeId, XFile imageFile, double latitude, double longitude, + {required String imageName, + required int projectId, + String comment = "", + int action = 0}) async { + try { + String? jwtToken = LocalStorage.getJwtToken(); + if (jwtToken == null) { + print("No JWT token found. Please log in."); + return false; + } + + final bytes = await imageFile.readAsBytes(); + final base64Image = base64Encode(bytes); + final fileSize = await imageFile.length(); + final contentType = "image/${imageFile.path.split('.').last}"; + + final imageObject = { + "FileName": imageName, + "Base64Data": base64Image, + "ContentType": contentType, + "FileSize": fileSize, + "Description": "Employee attendance photo" + }; + + final now = DateTime.now(); + + // You can now include the attendance record directly in the main body + final response = await http.post( + Uri.parse("$baseUrl/attendance/record"), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $jwtToken', + }, + body: jsonEncode({ + "ID": null, + "employeeId": employeeId, + "projectId": projectId, + "markTime": DateFormat('hh:mm a').format(now), + "comment": comment, + "action": action, + "date": DateFormat('yyyy-MM-dd').format(now), + "latitude": latitude, + "longitude": longitude, + "image": [imageObject], // Directly included in the body + }), + ); + print('body: ${jsonEncode({ + "employeeId": employeeId, + "projectId": projectId, + "markTime": DateFormat('hh:mm a').format(now), + "comment": comment, + "action": action, + "date": DateFormat('yyyy-MM-dd').format(now), + "latitude": latitude, + "longitude": longitude, + "image": [imageObject], + })}'); + print('uploadAttendanceImage: $baseUrl/attendance/record'); + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + return json['success'] == true; + } else { + print("Error uploading image: ${response.statusCode}"); + print("Response: ${response.body}"); + } + } catch (e) { + print("Exception during image upload: $e"); + } + return false; + } + + static Future?> getAttendanceLogs(int projectId) async { + try { + String? jwtToken = LocalStorage.getJwtToken(); + if (jwtToken == null) { + print("No JWT token found. Please log in."); + return null; + } + + final response = await http.get( + Uri.parse( + "$baseUrl/attendance/project/team?projectId=$projectId"), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $jwtToken', + }, + ); + + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + if (json['success'] == true) { + return json['data']; + } else { + print("Error: ${json['message']}"); + } + } else { + print("Error fetching logs: ${response.statusCode}"); + } + } catch (e) { + print("Exception while fetching logs: $e"); + } + return null; + } } diff --git a/lib/model/AttendanceLogModel.dart b/lib/model/AttendanceLogModel.dart new file mode 100644 index 0000000..bf54b2d --- /dev/null +++ b/lib/model/AttendanceLogModel.dart @@ -0,0 +1,25 @@ +class AttendanceLogModel { + final String name; + final String role; + final DateTime? checkIn; + final DateTime? checkOut; + final int activity; + + AttendanceLogModel({ + required this.name, + required this.role, + this.checkIn, + this.checkOut, + required this.activity, + }); + + factory AttendanceLogModel.fromJson(Map json) { + return AttendanceLogModel( + name: "${json['firstName'] ?? ''} ${json['lastName'] ?? ''}".trim(), + role: json['jobRoleName'] ?? '', + checkIn: json['checkInTime'] != null ? DateTime.tryParse(json['checkInTime']) : null, + checkOut: json['checkOutTime'] != null ? DateTime.tryParse(json['checkOutTime']) : null, + activity: json['activity'] ?? 0, + ); + } +} diff --git a/lib/routes.dart b/lib/routes.dart index 9fb7aec..147f97f 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -5,7 +5,7 @@ import 'package:marco/view/auth/forgot_password_screen.dart'; import 'package:marco/view/auth/login_screen.dart'; import 'package:marco/view/auth/register_account_screen.dart'; import 'package:marco/view/auth/reset_password_screen.dart'; -import 'package:marco/view/dashboard/ecommerce_screen.dart'; +// import 'package:marco/view/dashboard/ecommerce_screen.dart'; import 'package:marco/view/error_pages/coming_soon_screen.dart'; import 'package:marco/view/error_pages/error_404_screen.dart'; import 'package:marco/view/error_pages/error_500_screen.dart'; @@ -20,7 +20,7 @@ class AuthMiddleware extends GetMiddleware { getPageRoute() { var routes = [ - GetPage(name: '/', page: () => const EcommerceScreen(), middlewares: [AuthMiddleware()]), + GetPage(name: '/', page: () => const AttendanceScreen(), middlewares: [AuthMiddleware()]), // Dashboard GetPage(name: '/dashboard/attendance', page: () => AttendanceScreen(), middlewares: [AuthMiddleware()]), diff --git a/lib/view/dashboard/attendanceScreen.dart b/lib/view/dashboard/attendanceScreen.dart index e2f70cf..8cc8815 100644 --- a/lib/view/dashboard/attendanceScreen.dart +++ b/lib/view/dashboard/attendanceScreen.dart @@ -16,6 +16,7 @@ import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/view/layouts/layout.dart'; import 'package:marco/controller/dashboard/attendance_screen_controller.dart'; +import 'package:intl/intl.dart'; class AttendanceScreen extends StatefulWidget { const AttendanceScreen({super.key}); @@ -62,7 +63,45 @@ class _AttendanceScreenState extends State with UIMixin { MySpacing.height(flexSpacing), MyFlex( children: [ - MyFlexItem(child: attendanceTableCard()), + MyFlexItem( + child: DefaultTabController( + length: 2, + child: MyCard.bordered( + borderRadiusAll: 4, + border: + Border.all(color: Colors.grey.withAlpha(50)), + shadow: MyShadow( + elevation: 1, + position: MyShadowPosition.bottom), + paddingAll: 10, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TabBar( + labelColor: theme.colorScheme.primary, + unselectedLabelColor: theme + .colorScheme.onSurface + .withAlpha(150), + tabs: const [ + Tab(text: 'Employee List'), + Tab(text: 'Logs'), + ], + ), + MySpacing.height(16), + SizedBox( + height: 500, + child: TabBarView( + children: [ + employeeListTab(), + reportsTab(), + ], + ), + ), + ], + ), + ), + ), + ), ], ), ], @@ -75,125 +114,193 @@ class _AttendanceScreenState extends State with UIMixin { ); } - Widget attendanceTableCard() { - return MyCard.bordered( - borderRadiusAll: 4, - border: Border.all(color: Colors.grey.withAlpha(50)), - shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom), - paddingAll: 24, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: MyContainer.bordered( - padding: MySpacing.xy(8, 4), - child: PopupMenuButton( - onSelected: (value) { - setState(() { - attendanceController.selectedProjectId = value; - attendanceController.fetchEmployeesByProject(value); - }); - }, - itemBuilder: (BuildContext context) { - return attendanceController.projects.map((project) { - return PopupMenuItem( - value: project.id.toString(), - height: 32, - child: MyText.bodySmall( - project.name, - color: theme.colorScheme.onSurface, - fontWeight: 600, - ), - ); - }).toList(); - }, - color: theme.cardTheme.color, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - MyText.labelSmall( - attendanceController.selectedProjectId != null - ? attendanceController.projects - .firstWhereOrNull((proj) => - proj.id.toString() == - attendanceController - .selectedProjectId) - ?.name ?? - 'Select a Project' - : 'Select a Project', + Widget employeeListTab() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: MyContainer.bordered( + padding: MySpacing.xy(4, 8), + child: PopupMenuButton( + onSelected: (value) { + setState(() { + attendanceController.selectedProjectId = value; + attendanceController.fetchEmployeesByProject(value); + attendanceController.fetchAttendanceLogs(value); + }); + }, + itemBuilder: (BuildContext context) { + return attendanceController.projects.map((project) { + return PopupMenuItem( + value: project.id.toString(), + height: 32, + child: MyText.bodySmall( + project.name, color: theme.colorScheme.onSurface, + fontWeight: 600, ), - Icon(LucideIcons.chevron_down, - size: 16, color: theme.colorScheme.onSurface), - ], - ), + ); + }).toList(); + }, + color: theme.cardTheme.color, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MyText.labelSmall( + attendanceController.selectedProjectId != null + ? attendanceController.projects + .firstWhereOrNull((proj) => + proj.id.toString() == + attendanceController.selectedProjectId) + ?.name ?? + 'Select a Project' + : 'Select a Project', + color: theme.colorScheme.onSurface, + ), + Icon(LucideIcons.chevron_down, + size: 16, color: theme.colorScheme.onSurface), + ], ), ), ), - ], - ), - MySpacing.height(24), - attendanceController.employees.isEmpty - ? const Center(child: CircularProgressIndicator()) - : SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: DataTable( - sortAscending: true, - columnSpacing: 88, - onSelectAll: (_) => {}, - headingRowColor: WidgetStatePropertyAll( - contentTheme.primary.withAlpha(40)), - dataRowMaxHeight: 60, - showBottomBorder: true, - clipBehavior: Clip.antiAliasWithSaveLayer, - border: TableBorder.all( - borderRadius: BorderRadius.circular(4), - style: BorderStyle.solid, - width: 0.4, - color: Colors.grey, - ), - columns: [ - DataColumn( - label: MyText.labelLarge('ID', - color: contentTheme.primary)), - DataColumn( - label: MyText.labelLarge('Name', - color: contentTheme.primary)), - DataColumn( - label: MyText.labelLarge('Designation', - color: contentTheme.primary)), - DataColumn( - label: MyText.labelLarge('Check In', - color: contentTheme.primary)), - DataColumn( - label: MyText.labelLarge('Check Out', - color: contentTheme.primary)), - DataColumn( - label: MyText.labelLarge('Actions', - color: contentTheme.primary)), - ], - rows: attendanceController.employees - .mapIndexed((index, employee) => DataRow(cells: [ - DataCell(MyText.bodyMedium(employee.id.toString(), - fontWeight: 600)), - DataCell(MyText.bodyMedium(employee.name, - fontWeight: 600)), - DataCell(MyText.bodyMedium(employee.designation, - fontWeight: 600)), - DataCell(MyText.bodyMedium(employee.checkIn, - fontWeight: 600)), - DataCell(MyText.bodyMedium(employee.checkOut, - fontWeight: 600)), - DataCell(MyText.bodyMedium(employee.actions.toString(), - fontWeight: 600)), - ])) - .toList(), + ), + ], + ), + MySpacing.height(24), + attendanceController.employees.isEmpty + ? const Center(child: CircularProgressIndicator()) + : SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: DataTable( + sortAscending: true, + columnSpacing: 15, + onSelectAll: (_) => {}, + headingRowColor: WidgetStatePropertyAll( + contentTheme.primary.withAlpha(40)), + dataRowMaxHeight: 60, + showBottomBorder: true, + clipBehavior: Clip.antiAliasWithSaveLayer, + border: TableBorder.all( + borderRadius: BorderRadius.circular(4), + style: BorderStyle.solid, + width: 0.4, + color: Colors.grey, ), + columns: [ + DataColumn( + label: MyText.labelLarge('Name', + color: contentTheme.primary)), + DataColumn( + label: MyText.labelLarge('Designation', + color: contentTheme.primary)), + DataColumn( + label: MyText.labelLarge('Actions', + color: contentTheme.primary)), + ], + rows: attendanceController.employees + .mapIndexed((index, employee) => DataRow(cells: [ + DataCell(MyText.bodyMedium(employee.name, + fontWeight: 600)), + DataCell(MyText.bodyMedium(employee.designation, + fontWeight: 600)), + DataCell( + ElevatedButton( + onPressed: () async { + final success = await attendanceController + .captureAndUploadAttendance( + employee.id, + int.parse(attendanceController + .selectedProjectId ?? + "0"), + comment: "Checked in via app", + ); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + success + ? 'Image uploaded successfully!' + : 'Image upload failed.', + ), + ), + ); + }, + child: const Text('Check In'), + ), + ), + ])) + .toList(), ), + ), + ], + ); + } + + Widget reportsTab() { + if (attendanceController.attendanceLogs.isEmpty) { + attendanceController + .fetchAttendanceLogs(attendanceController.selectedProjectId); + return const Center(child: CircularProgressIndicator()); + } + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: DataTable( + sortAscending: true, + columnSpacing: 15, + headingRowColor: + WidgetStatePropertyAll(contentTheme.primary.withAlpha(40)), + dataRowMaxHeight: 60, + showBottomBorder: true, + clipBehavior: Clip.antiAliasWithSaveLayer, + border: TableBorder.all( + borderRadius: BorderRadius.circular(4), + style: BorderStyle.solid, + width: 0.4, + color: Colors.grey, + ), + columns: [ + DataColumn( + label: MyText.labelLarge('Name', color: contentTheme.primary)), + DataColumn( + label: MyText.labelLarge('Role', color: contentTheme.primary)), + DataColumn( + label: + MyText.labelLarge('Check-In', color: contentTheme.primary)), + DataColumn( + label: + MyText.labelLarge('Check-Out', color: contentTheme.primary)), + DataColumn( + label: MyText.labelLarge('Action', color: contentTheme.primary)), ], + rows: attendanceController.attendanceLogs + .mapIndexed((index, log) => DataRow(cells: [ + DataCell(MyText.bodyMedium(log.name, fontWeight: 600)), + DataCell(MyText.bodyMedium(log.role, fontWeight: 600)), + DataCell(MyText.bodyMedium( + log.checkIn != null + ? DateFormat('dd MMM yyyy hh:mm a').format(log.checkIn!) + : '-', + fontWeight: 600, + )), + DataCell(MyText.bodyMedium( + log.checkOut != null + ? DateFormat('dd MMM yyyy hh:mm a') + .format(log.checkOut!) + : '-', + fontWeight: 600, + )), + DataCell(IconButton( + icon: Icon(Icons.info_outline, color: contentTheme.primary), + onPressed: () { + // Action logic here + }, + )), + ])) + .toList(), ), ); } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 0795ff4..9f5aa6e 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import file_picker +import file_selector_macos import geolocator_apple import path_provider_foundation import quill_native_bridge_macos @@ -14,6 +15,7 @@ import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) QuillNativeBridgePlugin.register(with: registry.registrar(forPlugin: "QuillNativeBridgePlugin")) diff --git a/pubspec.lock b/pubspec.lock index ae4dd33..99be552 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.2" + archive: + dependency: transitive + description: + name: archive + sha256: a7f37ff061d7abc2fcf213554b9dcaca713c5853afa5c065c44888bc9ccaf813 + url: "https://pub.dev" + source: hosted + version: "4.0.6" args: dependency: transitive description: @@ -169,6 +177,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3+2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" + url: "https://pub.dev" + source: hosted + version: "0.9.4+2" file_selector_platform_interface: dependency: transitive description: @@ -437,6 +453,78 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image: + dependency: "direct main" + description: + name: image + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + url: "https://pub.dev" + source: hosted + version: "4.5.4" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "317a5d961cec5b34e777b9252393f2afbd23084aa6e60fcf601dcf6341b9ebeb" + url: "https://pub.dev" + source: hosted + version: "0.8.12+23" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" + url: "https://pub.dev" + source: hosted + version: "0.8.12+2" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9" + url: "https://pub.dev" + source: hosted + version: "0.2.1+2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" + url: "https://pub.dev" + source: hosted + version: "0.2.1+2" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" + url: "https://pub.dev" + source: hosted + version: "2.10.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" intl: dependency: "direct main" description: @@ -517,6 +605,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" nested: dependency: transitive description: @@ -629,6 +725,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" + url: "https://pub.dev" + source: hosted + version: "6.1.0" platform: dependency: transitive description: @@ -645,6 +749,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + posix: + dependency: transitive + description: + name: posix + sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62 + url: "https://pub.dev" + source: hosted + version: "6.0.2" provider: dependency: "direct main" description: @@ -1058,6 +1170,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" sdks: dart: ">=3.7.0-0 <4.0.0" - flutter: ">=3.24.0" + flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index 12a850f..d3badc8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,6 +61,8 @@ dependencies: http: ^1.2.2 geolocator: ^9.0.1 permission_handler: ^11.3.0 + image: ^4.0.17 + image_picker: ^1.0.7 dev_dependencies: flutter_test: -- 2.43.0