feat: Enhance attendance functionality with image capture and location tracking
This commit is contained in:
parent
753bfcad8a
commit
15ae6a75fc
@ -4,4 +4,7 @@
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
|
||||
</manifest>
|
||||
|
@ -1,4 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
|
||||
<application
|
||||
android:label="marco"
|
||||
android:name="${applicationName}"
|
||||
|
@ -1,8 +1,11 @@
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/model/attendance_model.dart';
|
||||
import 'package:marco/model/project_model.dart'; // Assuming you have a ProjectModel for the projects.
|
||||
import 'package:marco/model/employee_model.dart'; // Assuming you have an EmployeeModel for the employees.
|
||||
import 'package:marco/model/AttendanceLogModel.dart';
|
||||
|
||||
class AttendanceController extends GetxController {
|
||||
List<AttendanceModel> attendances = [];
|
||||
@ -17,7 +20,7 @@ class AttendanceController extends GetxController {
|
||||
}
|
||||
|
||||
// Fetch projects from API
|
||||
Future<void> fetchProjects() async {
|
||||
Future<void> fetchProjects() async {
|
||||
var response = await ApiService.getProjects(); // Call the project API
|
||||
|
||||
if (response != null) {
|
||||
@ -28,15 +31,17 @@ Future<void> fetchProjects() async {
|
||||
// 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
|
||||
await fetchEmployeesByProject(
|
||||
selectedProjectId); // Fetch employees for the first project
|
||||
}
|
||||
|
||||
update(['attendance_dashboard_controller']); // Notify GetBuilder with your tag
|
||||
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<void> fetchEmployeesByProject(String? projectId) async {
|
||||
@ -53,4 +58,53 @@ Future<void> fetchProjects() async {
|
||||
print("Failed to fetch employees for project $projectId.");
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> 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<AttendanceLogModel> attendanceLogs = [];
|
||||
Future<void> fetchAttendanceLogs(String? projectId) async {
|
||||
if (projectId == null) return;
|
||||
|
||||
var response = await ApiService.getAttendanceLogs(int.parse(projectId));
|
||||
|
||||
if (response != null) {
|
||||
attendanceLogs = response
|
||||
.map<AttendanceLogModel>((json) => AttendanceLogModel.fromJson(json))
|
||||
.toList();
|
||||
update();
|
||||
} else {
|
||||
print("Failed to fetch logs for project $projectId.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
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";
|
||||
@ -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<bool> 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<List<dynamic>?> 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;
|
||||
}
|
||||
}
|
||||
|
25
lib/model/AttendanceLogModel.dart
Normal file
25
lib/model/AttendanceLogModel.dart
Normal file
@ -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<String, dynamic> 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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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()]),
|
||||
|
@ -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<AttendanceScreen> 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,13 +114,8 @@ class _AttendanceScreenState extends State<AttendanceScreen> 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(
|
||||
Widget employeeListTab() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
@ -89,12 +123,13 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyContainer.bordered(
|
||||
padding: MySpacing.xy(8, 4),
|
||||
padding: MySpacing.xy(4, 8),
|
||||
child: PopupMenuButton<String>(
|
||||
onSelected: (value) {
|
||||
setState(() {
|
||||
attendanceController.selectedProjectId = value;
|
||||
attendanceController.fetchEmployeesByProject(value);
|
||||
attendanceController.fetchAttendanceLogs(value);
|
||||
});
|
||||
},
|
||||
itemBuilder: (BuildContext context) {
|
||||
@ -119,8 +154,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
? attendanceController.projects
|
||||
.firstWhereOrNull((proj) =>
|
||||
proj.id.toString() ==
|
||||
attendanceController
|
||||
.selectedProjectId)
|
||||
attendanceController.selectedProjectId)
|
||||
?.name ??
|
||||
'Select a Project'
|
||||
: 'Select a Project',
|
||||
@ -142,7 +176,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: DataTable(
|
||||
sortAscending: true,
|
||||
columnSpacing: 88,
|
||||
columnSpacing: 15,
|
||||
onSelectAll: (_) => {},
|
||||
headingRowColor: WidgetStatePropertyAll(
|
||||
contentTheme.primary.withAlpha(40)),
|
||||
@ -156,9 +190,6 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
color: Colors.grey,
|
||||
),
|
||||
columns: [
|
||||
DataColumn(
|
||||
label: MyText.labelLarge('ID',
|
||||
color: contentTheme.primary)),
|
||||
DataColumn(
|
||||
label: MyText.labelLarge('Name',
|
||||
color: contentTheme.primary)),
|
||||
@ -166,34 +197,110 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
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)),
|
||||
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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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"))
|
||||
|
122
pubspec.lock
122
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"
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user