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: