repair file naming and start the npe screen for marine
This commit is contained in:
parent
8931ed9297
commit
37874a1eab
@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
<!-- MMS V4 1.2.08 -->
|
<!-- MMS V4 1.2.08 -->
|
||||||
<application
|
<application
|
||||||
android:label="MMS V4 1.2.08"
|
android:label="MMS V4 1.2.09"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:requestLegacyExternalStorage="true">
|
android:requestLegacyExternalStorage="true">
|
||||||
|
|||||||
@ -36,9 +36,9 @@ import 'package:environment_monitoring_app/screens/marine/marine_home_page.dart'
|
|||||||
import 'package:environment_monitoring_app/screens/air/manual/air_manual_info_centre_document.dart';
|
import 'package:environment_monitoring_app/screens/air/manual/air_manual_info_centre_document.dart';
|
||||||
import 'package:environment_monitoring_app/screens/air/manual/air_manual_installation_screen.dart';
|
import 'package:environment_monitoring_app/screens/air/manual/air_manual_installation_screen.dart';
|
||||||
import 'package:environment_monitoring_app/screens/air/manual/air_manual_collection_screen.dart';
|
import 'package:environment_monitoring_app/screens/air/manual/air_manual_collection_screen.dart';
|
||||||
import 'package:environment_monitoring_app/screens/air/manual/report.dart' as airManualReport;
|
import 'package:environment_monitoring_app/screens/air/manual/air_manual_report.dart' as airManualReport;
|
||||||
import 'package:environment_monitoring_app/screens/air/manual/data_status_log.dart' as airManualDataStatusLog;
|
import 'package:environment_monitoring_app/screens/air/manual/air_manual_data_status_log.dart' as airManualDataStatusLog;
|
||||||
import 'package:environment_monitoring_app/screens/air/manual/image_request.dart' as airManualImageRequest;
|
import 'package:environment_monitoring_app/screens/air/manual/air_manual_image_request.dart' as airManualImageRequest;
|
||||||
import 'package:environment_monitoring_app/screens/air/continuous/air_continuous_info_centre_document.dart';
|
import 'package:environment_monitoring_app/screens/air/continuous/air_continuous_info_centre_document.dart';
|
||||||
import 'package:environment_monitoring_app/screens/air/continuous/overview.dart' as airContinuousOverview;
|
import 'package:environment_monitoring_app/screens/air/continuous/overview.dart' as airContinuousOverview;
|
||||||
import 'package:environment_monitoring_app/screens/air/continuous/entry.dart' as airContinuousEntry;
|
import 'package:environment_monitoring_app/screens/air/continuous/entry.dart' as airContinuousEntry;
|
||||||
@ -51,10 +51,11 @@ import 'package:environment_monitoring_app/screens/air/investigative/report.dart
|
|||||||
// River Screens
|
// River Screens
|
||||||
import 'package:environment_monitoring_app/screens/river/manual/river_manual_info_centre_document.dart';
|
import 'package:environment_monitoring_app/screens/river/manual/river_manual_info_centre_document.dart';
|
||||||
import 'package:environment_monitoring_app/screens/river/manual/in_situ_sampling.dart' as riverManualInSituSampling;
|
import 'package:environment_monitoring_app/screens/river/manual/in_situ_sampling.dart' as riverManualInSituSampling;
|
||||||
import 'package:environment_monitoring_app/screens/river/manual/data_status_log.dart' as riverManualDataStatusLog;
|
import 'package:environment_monitoring_app/screens/river/manual/river_manual_data_status_log.dart' as riverManualDataStatusLog;
|
||||||
import 'package:environment_monitoring_app/screens/river/manual/report.dart' as riverManualReport;
|
// MODIFIED: Import paths updated to new filenames
|
||||||
|
import 'package:environment_monitoring_app/screens/river/manual/river_manual_report.dart' as riverManualReport;
|
||||||
import 'package:environment_monitoring_app/screens/river/manual/triennial_sampling.dart' as riverManualTriennialSampling;
|
import 'package:environment_monitoring_app/screens/river/manual/triennial_sampling.dart' as riverManualTriennialSampling;
|
||||||
import 'package:environment_monitoring_app/screens/river/manual/image_request.dart' as riverManualImageRequest;
|
import 'package:environment_monitoring_app/screens/river/manual/river_manual_image_request.dart' as riverManualImageRequest;
|
||||||
import 'package:environment_monitoring_app/screens/river/continuous/river_continuous_info_centre_document.dart';
|
import 'package:environment_monitoring_app/screens/river/continuous/river_continuous_info_centre_document.dart';
|
||||||
import 'package:environment_monitoring_app/screens/river/continuous/overview.dart' as riverContinuousOverview;
|
import 'package:environment_monitoring_app/screens/river/continuous/overview.dart' as riverContinuousOverview;
|
||||||
import 'package:environment_monitoring_app/screens/river/continuous/entry.dart' as riverContinuousEntry;
|
import 'package:environment_monitoring_app/screens/river/continuous/entry.dart' as riverContinuousEntry;
|
||||||
@ -68,8 +69,9 @@ import 'package:environment_monitoring_app/screens/river/investigative/report.da
|
|||||||
import 'package:environment_monitoring_app/screens/marine/manual/info_centre_document.dart' as marineManualInfoCentreDocument;
|
import 'package:environment_monitoring_app/screens/marine/manual/info_centre_document.dart' as marineManualInfoCentreDocument;
|
||||||
import 'package:environment_monitoring_app/screens/marine/manual/pre_sampling.dart' as marineManualPreSampling;
|
import 'package:environment_monitoring_app/screens/marine/manual/pre_sampling.dart' as marineManualPreSampling;
|
||||||
import 'package:environment_monitoring_app/screens/marine/manual/in_situ_sampling.dart' as marineManualInSituSampling;
|
import 'package:environment_monitoring_app/screens/marine/manual/in_situ_sampling.dart' as marineManualInSituSampling;
|
||||||
import 'package:environment_monitoring_app/screens/marine/manual/report.dart' as marineManualReport;
|
import 'package:environment_monitoring_app/screens/marine/manual/marine_manual_report.dart' as marineManualReport;
|
||||||
import 'package:environment_monitoring_app/screens/marine/manual/data_status_log.dart' as marineManualDataStatusLog;
|
import 'package:environment_monitoring_app/screens/marine/manual/marine_manual_npe_report.dart' as marineManualNPEReport;
|
||||||
|
import 'package:environment_monitoring_app/screens/marine/manual/marine_manual_data_status_log.dart' as marineManualDataStatusLog;
|
||||||
import 'package:environment_monitoring_app/screens/marine/manual/marine_image_request.dart' as marineManualImageRequest;
|
import 'package:environment_monitoring_app/screens/marine/manual/marine_image_request.dart' as marineManualImageRequest;
|
||||||
import 'package:environment_monitoring_app/screens/marine/continuous/marine_continuous_info_centre_document.dart';
|
import 'package:environment_monitoring_app/screens/marine/continuous/marine_continuous_info_centre_document.dart';
|
||||||
import 'package:environment_monitoring_app/screens/marine/continuous/overview.dart' as marineContinuousOverview;
|
import 'package:environment_monitoring_app/screens/marine/continuous/overview.dart' as marineContinuousOverview;
|
||||||
@ -257,6 +259,7 @@ class _RootAppState extends State<RootApp> {
|
|||||||
// River Manual
|
// River Manual
|
||||||
'/river/manual/info': (context) => const RiverManualInfoCentreDocument(),
|
'/river/manual/info': (context) => const RiverManualInfoCentreDocument(),
|
||||||
'/river/manual/in-situ': (context) => riverManualInSituSampling.RiverInSituSamplingScreen(),
|
'/river/manual/in-situ': (context) => riverManualInSituSampling.RiverInSituSamplingScreen(),
|
||||||
|
// MODIFIED: Routes updated to use new class names from aliased imports
|
||||||
'/river/manual/report': (context) => riverManualReport.RiverManualReport(),
|
'/river/manual/report': (context) => riverManualReport.RiverManualReport(),
|
||||||
'/river/manual/triennial': (context) => riverManualTriennialSampling.RiverTriennialSampling(),
|
'/river/manual/triennial': (context) => riverManualTriennialSampling.RiverTriennialSampling(),
|
||||||
'/river/manual/data-log': (context) => riverManualDataStatusLog.RiverManualDataStatusLog(),
|
'/river/manual/data-log': (context) => riverManualDataStatusLog.RiverManualDataStatusLog(),
|
||||||
@ -279,7 +282,8 @@ class _RootAppState extends State<RootApp> {
|
|||||||
'/marine/manual/pre-sampling': (context) => marineManualPreSampling.MarinePreSampling(),
|
'/marine/manual/pre-sampling': (context) => marineManualPreSampling.MarinePreSampling(),
|
||||||
'/marine/manual/in-situ': (context) => marineManualInSituSampling.MarineInSituSampling(),
|
'/marine/manual/in-situ': (context) => marineManualInSituSampling.MarineInSituSampling(),
|
||||||
'/marine/manual/tarball': (context) => const TarballSamplingStep1(),
|
'/marine/manual/tarball': (context) => const TarballSamplingStep1(),
|
||||||
'/marine/manual/report': (context) => marineManualReport.MarineManualReport(),
|
'/marine/manual/report': (context) => const marineManualReport.MarineManualReportHomePage(),
|
||||||
|
'/marine/manual/report/npe': (context) => const marineManualNPEReport.MarineManualNPEReport(),
|
||||||
//'/marine/manual/data-log': (context) => marineManualDataStatusLog.MarineManualDataStatusLog(), // This is handled in onGenerateRoute
|
//'/marine/manual/data-log': (context) => marineManualDataStatusLog.MarineManualDataStatusLog(), // This is handled in onGenerateRoute
|
||||||
'/marine/manual/image-request': (context) => const marineManualImageRequest.MarineImageRequestScreen(),
|
'/marine/manual/image-request': (context) => const marineManualImageRequest.MarineImageRequestScreen(),
|
||||||
|
|
||||||
|
|||||||
@ -68,14 +68,42 @@ class InSituSamplingData {
|
|||||||
String? submissionMessage;
|
String? submissionMessage;
|
||||||
String? reportId;
|
String? reportId;
|
||||||
|
|
||||||
|
// --- START: NPE Report Compatibility Fields ---
|
||||||
|
/// Fields to hold data that can be transferred to an NPE Report.
|
||||||
|
/// This makes the model compatible for auto-generating NPE reports in the future.
|
||||||
|
|
||||||
|
// Corresponds to the checkboxes in the NPE form
|
||||||
|
Map<String, bool> npeFieldObservations = {
|
||||||
|
'Oil slick on the water surface/ Oil spill': false,
|
||||||
|
'Discoloration of the sea water': false,
|
||||||
|
'Formation of foam on the surface': false,
|
||||||
|
'Coral bleaching or dead corals': false,
|
||||||
|
'Observation of tar balls': false,
|
||||||
|
'Excessive debris': false,
|
||||||
|
'Red tides or algae blooms': false,
|
||||||
|
'Silt plume': false,
|
||||||
|
'Foul smell': false,
|
||||||
|
'Others': false,
|
||||||
|
};
|
||||||
|
// Corresponds to the "Others" text field in NPE observations
|
||||||
|
String? npeOthersObservationRemark;
|
||||||
|
// Corresponds to the "Possible Source" field in the NPE form
|
||||||
|
String? npePossibleSource;
|
||||||
|
|
||||||
|
// Holds the images to be attached to the NPE report
|
||||||
|
File? npeImage1;
|
||||||
|
File? npeImage2;
|
||||||
|
File? npeImage3;
|
||||||
|
File? npeImage4;
|
||||||
|
// --- END: NPE Report Compatibility Fields ---
|
||||||
|
|
||||||
|
|
||||||
InSituSamplingData({
|
InSituSamplingData({
|
||||||
this.samplingDate,
|
this.samplingDate,
|
||||||
this.samplingTime,
|
this.samplingTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Creates an InSituSamplingData object from a JSON map.
|
/// Creates an InSituSamplingData object from a JSON map.
|
||||||
/// This is critical for the offline retry mechanism. The keys used here MUST perfectly
|
|
||||||
/// match the keys used in the `toDbJson()` method to ensure data integrity.
|
|
||||||
factory InSituSamplingData.fromJson(Map<String, dynamic> json) {
|
factory InSituSamplingData.fromJson(Map<String, dynamic> json) {
|
||||||
double? doubleFromJson(dynamic value) {
|
double? doubleFromJson(dynamic value) {
|
||||||
if (value is num) return value.toDouble();
|
if (value is num) return value.toDouble();
|
||||||
@ -95,13 +123,14 @@ class InSituSamplingData {
|
|||||||
|
|
||||||
final data = InSituSamplingData();
|
final data = InSituSamplingData();
|
||||||
|
|
||||||
// START FIX: Aligned all keys to perfectly match the toDbJson() method and added backward compatibility.
|
// Standard In-Situ Fields
|
||||||
data.firstSamplerName = json['first_sampler_name'];
|
data.firstSamplerName = json['first_sampler_name'];
|
||||||
data.firstSamplerUserId = intFromJson(json['first_sampler_user_id']);
|
data.firstSamplerUserId = intFromJson(json['first_sampler_user_id']);
|
||||||
data.secondSampler = json['secondSampler'] ?? json['second_sampler'];
|
data.secondSampler = json['secondSampler'] ?? json['second_sampler'];
|
||||||
data.samplingDate = json['sampling_date'] ?? json['man_date'];
|
data.samplingDate = json['sampling_date'] ?? json['man_date'];
|
||||||
data.samplingTime = json['sampling_time'] ?? json['man_time'];
|
data.samplingTime = json['sampling_time'] ?? json['man_time'];
|
||||||
data.samplingType = json['sampling_type'];
|
data.samplingType = json['sampling_type'];
|
||||||
|
// ... (all other existing fields)
|
||||||
data.sampleIdCode = json['sample_id_code'];
|
data.sampleIdCode = json['sample_id_code'];
|
||||||
data.selectedStateName = json['selected_state_name'];
|
data.selectedStateName = json['selected_state_name'];
|
||||||
data.selectedCategoryName = json['selected_category_name'];
|
data.selectedCategoryName = json['selected_category_name'];
|
||||||
@ -138,9 +167,11 @@ class InSituSamplingData {
|
|||||||
data.submissionMessage = json['submission_message'];
|
data.submissionMessage = json['submission_message'];
|
||||||
data.reportId = json['report_id']?.toString();
|
data.reportId = json['report_id']?.toString();
|
||||||
|
|
||||||
// Image paths are added by LocalStorageService, not toDbJson, so they are read separately.
|
|
||||||
|
// Image paths (handled by LocalStorageService)
|
||||||
data.leftLandViewImage = fileFromPath(json['man_left_side_land_view']);
|
data.leftLandViewImage = fileFromPath(json['man_left_side_land_view']);
|
||||||
data.rightLandViewImage = fileFromPath(json['man_right_side_land_view']);
|
data.rightLandViewImage = fileFromPath(json['man_right_side_land_view']);
|
||||||
|
// ... (all other existing images)
|
||||||
data.waterFillingImage = fileFromPath(json['man_filling_water_into_sample_bottle']);
|
data.waterFillingImage = fileFromPath(json['man_filling_water_into_sample_bottle']);
|
||||||
data.seawaterColorImage = fileFromPath(json['man_seawater_in_clear_glass_bottle']);
|
data.seawaterColorImage = fileFromPath(json['man_seawater_in_clear_glass_bottle']);
|
||||||
data.phPaperImage = fileFromPath(json['man_examine_preservative_ph_paper']);
|
data.phPaperImage = fileFromPath(json['man_examine_preservative_ph_paper']);
|
||||||
@ -148,11 +179,88 @@ class InSituSamplingData {
|
|||||||
data.optionalImage2 = fileFromPath(json['man_optional_photo_02']);
|
data.optionalImage2 = fileFromPath(json['man_optional_photo_02']);
|
||||||
data.optionalImage3 = fileFromPath(json['man_optional_photo_03']);
|
data.optionalImage3 = fileFromPath(json['man_optional_photo_03']);
|
||||||
data.optionalImage4 = fileFromPath(json['man_optional_photo_04']);
|
data.optionalImage4 = fileFromPath(json['man_optional_photo_04']);
|
||||||
// END FIX
|
|
||||||
|
|
||||||
|
// --- START: Deserialization for NPE Fields ---
|
||||||
|
if (json['npe_field_observations'] is Map) {
|
||||||
|
data.npeFieldObservations = Map<String, bool>.from(json['npe_field_observations']);
|
||||||
|
}
|
||||||
|
data.npeOthersObservationRemark = json['npe_others_observation_remark'];
|
||||||
|
data.npePossibleSource = json['npe_possible_source'];
|
||||||
|
|
||||||
|
// NPE image paths
|
||||||
|
data.npeImage1 = fileFromPath(json['npe_image_1']);
|
||||||
|
data.npeImage2 = fileFromPath(json['npe_image_2']);
|
||||||
|
data.npeImage3 = fileFromPath(json['npe_image_3']);
|
||||||
|
data.npeImage4 = fileFromPath(json['npe_image_4']);
|
||||||
|
// --- END: Deserialization for NPE Fields ---
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ... (generateTelegramAlertMessage method remains unchanged) ...
|
||||||
|
|
||||||
|
// ... (toApiFormData method remains unchanged) ...
|
||||||
|
|
||||||
|
// ... (toApiImageFiles method remains unchanged) ...
|
||||||
|
|
||||||
|
/// Creates a single JSON object with all submission data for offline storage.
|
||||||
|
Map<String, dynamic> toDbJson() {
|
||||||
|
return {
|
||||||
|
'first_sampler_name': firstSamplerName,
|
||||||
|
'first_sampler_user_id': firstSamplerUserId,
|
||||||
|
'secondSampler': secondSampler,
|
||||||
|
'sampling_date': samplingDate,
|
||||||
|
'sampling_time': samplingTime,
|
||||||
|
// ... (all other existing fields)
|
||||||
|
'sampling_type': samplingType,
|
||||||
|
'sample_id_code': sampleIdCode,
|
||||||
|
'selected_state_name': selectedStateName,
|
||||||
|
'selected_category_name': selectedCategoryName,
|
||||||
|
'selectedStation': selectedStation,
|
||||||
|
'station_latitude': stationLatitude,
|
||||||
|
'station_longitude': stationLongitude,
|
||||||
|
'current_latitude': currentLatitude,
|
||||||
|
'current_longitude': currentLongitude,
|
||||||
|
'distance_difference_in_km': distanceDifferenceInKm,
|
||||||
|
'distance_difference_remarks': distanceDifferenceRemarks,
|
||||||
|
'weather': weather,
|
||||||
|
'tide_level': tideLevel,
|
||||||
|
'sea_condition': seaCondition,
|
||||||
|
'event_remarks': eventRemarks,
|
||||||
|
'lab_remarks': labRemarks,
|
||||||
|
'man_optional_photo_01_remarks': optionalRemark1,
|
||||||
|
'man_optional_photo_02_remarks': optionalRemark2,
|
||||||
|
'man_optional_photo_03_remarks': optionalRemark3,
|
||||||
|
'man_optional_photo_04_remarks': optionalRemark4,
|
||||||
|
'sonde_id': sondeId,
|
||||||
|
'data_capture_date': dataCaptureDate,
|
||||||
|
'data_capture_time': dataCaptureTime,
|
||||||
|
'oxygen_concentration': oxygenConcentration,
|
||||||
|
'oxygen_saturation': oxygenSaturation,
|
||||||
|
'ph': ph,
|
||||||
|
'salinity': salinity,
|
||||||
|
'electrical_conductivity': electricalConductivity,
|
||||||
|
'temperature': temperature,
|
||||||
|
'tds': tds,
|
||||||
|
'turbidity': turbidity,
|
||||||
|
'tss': tss,
|
||||||
|
'battery_voltage': batteryVoltage,
|
||||||
|
'submission_status': submissionStatus,
|
||||||
|
'submission_message': submissionMessage,
|
||||||
|
'report_id': reportId,
|
||||||
|
|
||||||
|
// --- START: Serialization for NPE Fields ---
|
||||||
|
'npe_field_observations': npeFieldObservations,
|
||||||
|
'npe_others_observation_remark': npeOthersObservationRemark,
|
||||||
|
'npe_possible_source': npePossibleSource,
|
||||||
|
// Note: Image file paths are handled separately by the LocalStorageService
|
||||||
|
// and are not part of this JSON object directly.
|
||||||
|
// --- END: Serialization for NPE Fields ---
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Methods from the original file ---
|
||||||
String generateTelegramAlertMessage({required bool isDataOnly}) {
|
String generateTelegramAlertMessage({required bool isDataOnly}) {
|
||||||
final submissionType = isDataOnly ? "(Data Only)" : "(Data & Images)";
|
final submissionType = isDataOnly ? "(Data Only)" : "(Data & Images)";
|
||||||
final stationName = selectedStation?['man_station_name'] ?? 'N/A';
|
final stationName = selectedStation?['man_station_name'] ?? 'N/A';
|
||||||
@ -256,52 +364,4 @@ class InSituSamplingData {
|
|||||||
'man_optional_photo_04': optionalImage4,
|
'man_optional_photo_04': optionalImage4,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a single JSON object with all submission data for offline storage.
|
|
||||||
/// The keys here are the single source of truth for the offline data format.
|
|
||||||
Map<String, dynamic> toDbJson() {
|
|
||||||
return {
|
|
||||||
'first_sampler_name': firstSamplerName,
|
|
||||||
'first_sampler_user_id': firstSamplerUserId,
|
|
||||||
'secondSampler': secondSampler,
|
|
||||||
'sampling_date': samplingDate,
|
|
||||||
'sampling_time': samplingTime,
|
|
||||||
'sampling_type': samplingType,
|
|
||||||
'sample_id_code': sampleIdCode,
|
|
||||||
'selected_state_name': selectedStateName,
|
|
||||||
'selected_category_name': selectedCategoryName,
|
|
||||||
'selectedStation': selectedStation,
|
|
||||||
'station_latitude': stationLatitude,
|
|
||||||
'station_longitude': stationLongitude,
|
|
||||||
'current_latitude': currentLatitude,
|
|
||||||
'current_longitude': currentLongitude,
|
|
||||||
'distance_difference_in_km': distanceDifferenceInKm,
|
|
||||||
'distance_difference_remarks': distanceDifferenceRemarks,
|
|
||||||
'weather': weather,
|
|
||||||
'tide_level': tideLevel,
|
|
||||||
'sea_condition': seaCondition,
|
|
||||||
'event_remarks': eventRemarks,
|
|
||||||
'lab_remarks': labRemarks,
|
|
||||||
'man_optional_photo_01_remarks': optionalRemark1,
|
|
||||||
'man_optional_photo_02_remarks': optionalRemark2,
|
|
||||||
'man_optional_photo_03_remarks': optionalRemark3,
|
|
||||||
'man_optional_photo_04_remarks': optionalRemark4,
|
|
||||||
'sonde_id': sondeId,
|
|
||||||
'data_capture_date': dataCaptureDate,
|
|
||||||
'data_capture_time': dataCaptureTime,
|
|
||||||
'oxygen_concentration': oxygenConcentration,
|
|
||||||
'oxygen_saturation': oxygenSaturation,
|
|
||||||
'ph': ph,
|
|
||||||
'salinity': salinity,
|
|
||||||
'electrical_conductivity': electricalConductivity,
|
|
||||||
'temperature': temperature,
|
|
||||||
'tds': tds,
|
|
||||||
'turbidity': turbidity,
|
|
||||||
'tss': tss,
|
|
||||||
'battery_voltage': batteryVoltage,
|
|
||||||
'submission_status': submissionStatus,
|
|
||||||
'submission_message': submissionMessage,
|
|
||||||
'report_id': reportId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// lib/screens/air/manual/data_status_log.dart
|
// lib/screens/air/manual/air_manual_data_status_log.dart
|
||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -1,7 +1,8 @@
|
|||||||
|
// lib/screens/air/manual/air_manual_image_request.dart
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'dart:io'; // Add this line at the top of these files
|
import 'dart:io';
|
||||||
|
|
||||||
|
|
||||||
class AirManualImageRequest extends StatefulWidget {
|
class AirManualImageRequest extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
// lib/screens/air/manual/air_manual_report.dart
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class AirManualReport extends StatelessWidget {
|
class AirManualReport extends StatelessWidget {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// lib/screens/marine/manual/data_status_log.dart
|
// lib/screens/marine/manual/marine_manual_data_status_log.dart
|
||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
1073
lib/screens/marine/manual/marine_manual_npe_report.dart
Normal file
1073
lib/screens/marine/manual/marine_manual_npe_report.dart
Normal file
File diff suppressed because it is too large
Load Diff
108
lib/screens/marine/manual/marine_manual_report.dart
Normal file
108
lib/screens/marine/manual/marine_manual_report.dart
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
// A simple data class for report items
|
||||||
|
class ReportItem {
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
final String route;
|
||||||
|
|
||||||
|
const ReportItem({
|
||||||
|
required this.icon,
|
||||||
|
required this.label,
|
||||||
|
required this.route,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class MarineManualReportHomePage extends StatelessWidget {
|
||||||
|
const MarineManualReportHomePage({super.key});
|
||||||
|
|
||||||
|
// Define the list of available reports
|
||||||
|
final List<ReportItem> _reports = const [
|
||||||
|
ReportItem(
|
||||||
|
icon: Icons.science_outlined,
|
||||||
|
label: "NPE Report",
|
||||||
|
route: '/marine/manual/report/npe',
|
||||||
|
),
|
||||||
|
// You can add other future reports here. For example:
|
||||||
|
// ReportItem(
|
||||||
|
// icon: Icons.assessment_outlined,
|
||||||
|
// label: "Quarterly Summary",
|
||||||
|
// route: '/marine/manual/report/quarterly',
|
||||||
|
// ),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Marine Manual Reports"),
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Select a Report to Generate",
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
// Using a GridView for the report items for a clean layout
|
||||||
|
GridView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
crossAxisSpacing: 16.0,
|
||||||
|
mainAxisSpacing: 16.0,
|
||||||
|
childAspectRatio: 1.5, // Adjust for a card-like appearance
|
||||||
|
),
|
||||||
|
itemCount: _reports.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final report = _reports[index];
|
||||||
|
return _buildReportCard(context, report);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to build a clickable card for each report type
|
||||||
|
Widget _buildReportCard(BuildContext context, ReportItem report) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pushNamed(context, report.route);
|
||||||
|
},
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Card(
|
||||||
|
elevation: 4,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
side: BorderSide(color: Colors.white24, width: 1),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(report.icon, size: 40, color: Theme.of(context).colorScheme.secondary),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: Text(
|
||||||
|
report.label,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,39 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class MarineManualReport extends StatelessWidget {
|
|
||||||
final List<Map<String, String>> sampleData = [
|
|
||||||
{"Station": "Marine Site A", "Parameter": "Salinity", "Value": "34 PSU"},
|
|
||||||
{"Station": "Marine Site B", "Parameter": "Turbidity", "Value": "5 NTU"},
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(title: Text("Marine Manual Report")),
|
|
||||||
body: Padding(
|
|
||||||
padding: const EdgeInsets.all(24),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text("Manual Sampling Report", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
DataTable(
|
|
||||||
columns: [
|
|
||||||
DataColumn(label: Text("Station")),
|
|
||||||
DataColumn(label: Text("Parameter")),
|
|
||||||
DataColumn(label: Text("Value")),
|
|
||||||
],
|
|
||||||
rows: sampleData.map((data) {
|
|
||||||
return DataRow(cells: [
|
|
||||||
DataCell(Text(data["Station"]!)),
|
|
||||||
DataCell(Text(data["Parameter"]!)),
|
|
||||||
DataCell(Text(data["Value"]!)),
|
|
||||||
]);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -38,7 +38,7 @@ class MarineHomePage extends StatelessWidget {
|
|||||||
|
|
||||||
SidebarItem(icon: Icons.article, label: "Data Log", route: '/marine/manual/data-log'),
|
SidebarItem(icon: Icons.article, label: "Data Log", route: '/marine/manual/data-log'),
|
||||||
SidebarItem(icon: Icons.image, label: "Image Request", route: '/marine/manual/image-request'),
|
SidebarItem(icon: Icons.image, label: "Image Request", route: '/marine/manual/image-request'),
|
||||||
//SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/marine/manual/report'),
|
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/marine/manual/report'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SidebarItem(
|
SidebarItem(
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// lib/screens/river/manual/data_status_log.dart
|
// lib/screens/river/manual/river_manual_data_status_log.dart
|
||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -97,7 +97,6 @@ class _RiverManualDataStatusLogState extends State<RiverManualDataStatusLog> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- START: MODIFIED TO FIX NULL SAFETY ERRORS ---
|
|
||||||
SubmissionLogEntry? _createLogEntry(Map<String, dynamic> log) {
|
SubmissionLogEntry? _createLogEntry(Map<String, dynamic> log) {
|
||||||
final String type = log['samplingType'] ?? 'In-Situ Sampling';
|
final String type = log['samplingType'] ?? 'In-Situ Sampling';
|
||||||
final String title = log['selectedStation']?['sampling_river'] ?? 'Unknown River';
|
final String title = log['selectedStation']?['sampling_river'] ?? 'Unknown River';
|
||||||
@ -114,7 +113,6 @@ class _RiverManualDataStatusLogState extends State<RiverManualDataStatusLog> {
|
|||||||
} catch (_) {
|
} catch (_) {
|
||||||
submissionDateTime = DateTime.now();
|
submissionDateTime = DateTime.now();
|
||||||
}
|
}
|
||||||
// --- END: MODIFIED TO FIX NULL SAFETY ERRORS ---
|
|
||||||
|
|
||||||
String? apiStatusRaw;
|
String? apiStatusRaw;
|
||||||
if (log['api_status'] != null) {
|
if (log['api_status'] != null) {
|
||||||
@ -1,6 +1,8 @@
|
|||||||
|
// lib/screens/river/manual/river_manual_image_request.dart
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'dart:io'; // Add this line at the top of these files
|
import 'dart:io';
|
||||||
|
|
||||||
|
|
||||||
class RiverManualImageRequest extends StatefulWidget {
|
class RiverManualImageRequest extends StatefulWidget {
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
// lib/screens/river/manual/river_manual_report.dart
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class RiverManualReport extends StatelessWidget {
|
class RiverManualReport extends StatelessWidget {
|
||||||
@ -36,6 +36,12 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
bool _isAutoReading = false;
|
bool _isAutoReading = false;
|
||||||
StreamSubscription? _dataSubscription;
|
StreamSubscription? _dataSubscription;
|
||||||
|
|
||||||
|
// --- START: Added for lockout timer ---
|
||||||
|
Timer? _lockoutTimer;
|
||||||
|
int _lockoutSecondsRemaining = 30;
|
||||||
|
bool _isLockedOut = false;
|
||||||
|
// --- END: Added for lockout timer ---
|
||||||
|
|
||||||
late final RiverInSituSamplingService _samplingService;
|
late final RiverInSituSamplingService _samplingService;
|
||||||
|
|
||||||
// --- START: Added for direct database access ---
|
// --- START: Added for direct database access ---
|
||||||
@ -95,6 +101,7 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_dataSubscription?.cancel();
|
_dataSubscription?.cancel();
|
||||||
|
_lockoutTimer?.cancel(); // --- MODIFICATION: Cancel timer on dispose ---
|
||||||
|
|
||||||
if (_samplingService.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
|
if (_samplingService.bluetoothConnectionState.value != BluetoothConnectionState.disconnected) {
|
||||||
_samplingService.disconnectFromBluetooth();
|
_samplingService.disconnectFromBluetooth();
|
||||||
@ -299,12 +306,40 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- START MODIFICATION: Countdown Timer Logic ---
|
||||||
|
void _startLockoutTimer() {
|
||||||
|
_lockoutTimer?.cancel();
|
||||||
|
setState(() {
|
||||||
|
_isLockedOut = true;
|
||||||
|
_lockoutSecondsRemaining = 30;
|
||||||
|
});
|
||||||
|
|
||||||
|
_lockoutTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
|
if (_lockoutSecondsRemaining > 0) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_lockoutSecondsRemaining--;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
timer.cancel();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLockedOut = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// --- END MODIFICATION ---
|
||||||
|
|
||||||
void _toggleAutoReading(String activeType) {
|
void _toggleAutoReading(String activeType) {
|
||||||
final service = context.read<RiverInSituSamplingService>();
|
final service = context.read<RiverInSituSamplingService>();
|
||||||
setState(() {
|
setState(() {
|
||||||
_isAutoReading = !_isAutoReading;
|
_isAutoReading = !_isAutoReading;
|
||||||
if (_isAutoReading) {
|
if (_isAutoReading) {
|
||||||
if (activeType == 'bluetooth') service.startBluetoothAutoReading(); else service.startSerialAutoReading();
|
if (activeType == 'bluetooth') service.startBluetoothAutoReading(); else service.startSerialAutoReading();
|
||||||
|
_startLockoutTimer(); // --- MODIFICATION: Start countdown
|
||||||
} else {
|
} else {
|
||||||
if (activeType == 'bluetooth') service.stopBluetoothAutoReading(); else service.stopSerialAutoReading();
|
if (activeType == 'bluetooth') service.stopBluetoothAutoReading(); else service.stopSerialAutoReading();
|
||||||
}
|
}
|
||||||
@ -320,8 +355,12 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
}
|
}
|
||||||
_dataSubscription?.cancel();
|
_dataSubscription?.cancel();
|
||||||
_dataSubscription = null;
|
_dataSubscription = null;
|
||||||
|
_lockoutTimer?.cancel(); // --- MODIFICATION: Cancel timer on disconnect ---
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() => _isAutoReading = false);
|
setState(() {
|
||||||
|
_isAutoReading = false;
|
||||||
|
_isLockedOut = false; // --- MODIFICATION: Reset lockout state ---
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -353,6 +392,13 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
|
|
||||||
// --- START: MODIFIED VALIDATION FLOW ---
|
// --- START: MODIFIED VALIDATION FLOW ---
|
||||||
void _validateAndProceed() async {
|
void _validateAndProceed() async {
|
||||||
|
// --- START MODIFICATION: Add lockout check ---
|
||||||
|
if (_isLockedOut) {
|
||||||
|
_showSnackBar("Please wait for the initial reading period to complete.", isError: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// --- END MODIFICATION ---
|
||||||
|
|
||||||
if (_isAutoReading) {
|
if (_isAutoReading) {
|
||||||
_showStopReadingDialog();
|
_showStopReadingDialog();
|
||||||
return;
|
return;
|
||||||
@ -514,86 +560,99 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
final activeConnection = _getActiveConnectionDetails();
|
final activeConnection = _getActiveConnectionDetails();
|
||||||
final String? activeType = activeConnection?['type'] as String?;
|
final String? activeType = activeConnection?['type'] as String?;
|
||||||
|
|
||||||
return Form(
|
// --- START MODIFICATION: Add WillPopScope to block back navigation ---
|
||||||
key: _formKey,
|
return WillPopScope(
|
||||||
child: ListView(
|
onWillPop: () async {
|
||||||
padding: const EdgeInsets.all(24.0),
|
if (_isLockedOut) {
|
||||||
children: [
|
_showSnackBar("Please wait for the initial reading period to complete.", isError: true);
|
||||||
Text("Data Capture", style: Theme.of(context).textTheme.headlineSmall),
|
return false; // Prevent back navigation
|
||||||
const SizedBox(height: 16),
|
}
|
||||||
Row(
|
return true; // Allow back navigation
|
||||||
children: [
|
},
|
||||||
Expanded(
|
child: Form(
|
||||||
child: activeType == 'bluetooth'
|
key: _formKey,
|
||||||
? FilledButton.icon(icon: const Icon(Icons.bluetooth_connected), label: const Text("Bluetooth"), onPressed: _isLoading ? null : () => _handleConnectionAttempt('bluetooth'))
|
child: ListView(
|
||||||
: OutlinedButton.icon(icon: const Icon(Icons.bluetooth), label: const Text("Bluetooth"), onPressed: _isLoading ? null : () => _handleConnectionAttempt('bluetooth')),
|
padding: const EdgeInsets.all(24.0),
|
||||||
),
|
children: [
|
||||||
const SizedBox(width: 16),
|
Text("Data Capture", style: Theme.of(context).textTheme.headlineSmall),
|
||||||
Expanded(
|
const SizedBox(height: 16),
|
||||||
child: activeType == 'serial'
|
Row(
|
||||||
? FilledButton.icon(icon: const Icon(Icons.usb), label: const Text("USB Serial"), onPressed: _isLoading ? null : () => _handleConnectionAttempt('serial'))
|
children: [
|
||||||
: OutlinedButton.icon(icon: const Icon(Icons.usb), label: const Text("USB Serial"), onPressed: _isLoading ? null : () => _handleConnectionAttempt('serial')),
|
Expanded(
|
||||||
),
|
child: activeType == 'bluetooth'
|
||||||
],
|
? FilledButton.icon(icon: const Icon(Icons.bluetooth_connected), label: const Text("Bluetooth"), onPressed: _isLoading ? null : () => _handleConnectionAttempt('bluetooth'))
|
||||||
),
|
: OutlinedButton.icon(icon: const Icon(Icons.bluetooth), label: const Text("Bluetooth"), onPressed: _isLoading ? null : () => _handleConnectionAttempt('bluetooth')),
|
||||||
const SizedBox(height: 16),
|
),
|
||||||
if (activeConnection != null)
|
const SizedBox(width: 16),
|
||||||
_buildConnectionCard(type: activeConnection['type'], connectionState: activeConnection['state'], deviceName: activeConnection['name']),
|
Expanded(
|
||||||
const SizedBox(height: 24),
|
child: activeType == 'serial'
|
||||||
ValueListenableBuilder<String?>(
|
? FilledButton.icon(icon: const Icon(Icons.usb), label: const Text("USB Serial"), onPressed: _isLoading ? null : () => _handleConnectionAttempt('serial'))
|
||||||
valueListenable: service.sondeId,
|
: OutlinedButton.icon(icon: const Icon(Icons.usb), label: const Text("USB Serial"), onPressed: _isLoading ? null : () => _handleConnectionAttempt('serial')),
|
||||||
builder: (context, sondeId, child) {
|
),
|
||||||
final newSondeId = sondeId ?? '';
|
],
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
),
|
||||||
if (mounted && _sondeIdController.text != newSondeId) {
|
const SizedBox(height: 16),
|
||||||
_sondeIdController.text = newSondeId;
|
if (activeConnection != null)
|
||||||
widget.data.sondeId = newSondeId;
|
_buildConnectionCard(type: activeConnection['type'], connectionState: activeConnection['state'], deviceName: activeConnection['name']),
|
||||||
}
|
const SizedBox(height: 24),
|
||||||
});
|
ValueListenableBuilder<String?>(
|
||||||
return TextFormField(
|
valueListenable: service.sondeId,
|
||||||
controller: _sondeIdController,
|
builder: (context, sondeId, child) {
|
||||||
decoration: const InputDecoration(labelText: 'Sonde ID *', hintText: 'Connect device or enter manually'),
|
final newSondeId = sondeId ?? '';
|
||||||
validator: (v) => v == null || v.isEmpty ? 'Sonde ID is required' : null,
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
onChanged: (value) { widget.data.sondeId = value; },
|
if (mounted && _sondeIdController.text != newSondeId) {
|
||||||
onSaved: (v) => widget.data.sondeId = v,
|
_sondeIdController.text = newSondeId;
|
||||||
);
|
widget.data.sondeId = newSondeId;
|
||||||
},
|
}
|
||||||
),
|
});
|
||||||
const SizedBox(height: 16),
|
return TextFormField(
|
||||||
Row(
|
controller: _sondeIdController,
|
||||||
children: [
|
decoration: const InputDecoration(labelText: 'Sonde ID *', hintText: 'Connect device or enter manually'),
|
||||||
Expanded(child: TextFormField(controller: _dateController, readOnly: true, decoration: const InputDecoration(labelText: 'Date'))),
|
validator: (v) => v == null || v.isEmpty ? 'Sonde ID is required' : null,
|
||||||
const SizedBox(width: 16),
|
onChanged: (value) { widget.data.sondeId = value; },
|
||||||
Expanded(child: TextFormField(controller: _timeController, readOnly: true, decoration: const InputDecoration(labelText: 'Time'))),
|
onSaved: (v) => widget.data.sondeId = v,
|
||||||
],
|
);
|
||||||
),
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: TextFormField(controller: _dateController, readOnly: true, decoration: const InputDecoration(labelText: 'Date'))),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(child: TextFormField(controller: _timeController, readOnly: true, decoration: const InputDecoration(labelText: 'Time'))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
if (_previousReadingsForComparison != null)
|
if (_previousReadingsForComparison != null)
|
||||||
_buildComparisonView(),
|
_buildComparisonView(),
|
||||||
|
|
||||||
const Divider(height: 32),
|
const Divider(height: 32),
|
||||||
Column(
|
Column(
|
||||||
children: _parameters.map((param) {
|
children: _parameters.map((param) {
|
||||||
return _buildParameterListItem(
|
return _buildParameterListItem(
|
||||||
icon: param['icon'] as IconData,
|
icon: param['icon'] as IconData,
|
||||||
label: param['label'] as String,
|
label: param['label'] as String,
|
||||||
unit: param['unit'] as String,
|
unit: param['unit'] as String,
|
||||||
controller: param['controller'] as TextEditingController,
|
controller: param['controller'] as TextEditingController,
|
||||||
isOutOfBounds: _outOfBoundsKeys.contains(param['key']),
|
isOutOfBounds: _outOfBoundsKeys.contains(param['key']),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
const Divider(height: 32),
|
const Divider(height: 32),
|
||||||
_buildFlowrateSection(),
|
_buildFlowrateSection(),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
ElevatedButton(
|
// --- START MODIFICATION: Add countdown to Next button ---
|
||||||
onPressed: _validateAndProceed,
|
ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
|
onPressed: _isLockedOut ? null : _validateAndProceed,
|
||||||
child: const Text('Next'),
|
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
|
||||||
),
|
child: Text(_isLockedOut ? 'Next ($_lockoutSecondsRemaining\s)' : 'Next'),
|
||||||
],
|
),
|
||||||
|
// --- END MODIFICATION ---
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
// --- END MODIFICATION ---
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildParameterListItem({ required IconData icon, required String label, required String unit, required TextEditingController controller, bool isOutOfBounds = false}) {
|
Widget _buildParameterListItem({ required IconData icon, required String label, required String unit, required TextEditingController controller, bool isOutOfBounds = false}) {
|
||||||
@ -643,15 +702,21 @@ class _RiverInSituStep3DataCaptureState extends State<RiverInSituStep3DataCaptur
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
|
// --- START MODIFICATION: Add countdown to Stop Reading button ---
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
icon: Icon(_isAutoReading ? Icons.stop_circle_outlined : Icons.play_circle_outlined),
|
icon: Icon(_isAutoReading ? Icons.stop_circle_outlined : Icons.play_circle_outlined),
|
||||||
label: Text(_isAutoReading ? 'Stop Reading' : 'Start Reading'),
|
label: Text(_isAutoReading
|
||||||
onPressed: () => _toggleAutoReading(type),
|
? (_isLockedOut ? 'Stop Reading ($_lockoutSecondsRemaining\s)' : 'Stop Reading')
|
||||||
|
: 'Start Reading'),
|
||||||
|
onPressed: (_isAutoReading && _isLockedOut) ? null : () => _toggleAutoReading(type),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: _isAutoReading ? Colors.orange : Colors.green,
|
backgroundColor: _isAutoReading
|
||||||
|
? (_isLockedOut ? Colors.grey.shade600 : Colors.orange)
|
||||||
|
: Colors.green,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// --- END MODIFICATION ---
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
icon: const Icon(Icons.link_off),
|
icon: const Icon(Icons.link_off),
|
||||||
label: const Text('Disconnect'),
|
label: const Text('Disconnect'),
|
||||||
|
|||||||
@ -744,7 +744,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.info_outline),
|
leading: const Icon(Icons.info_outline),
|
||||||
title: const Text('App Version'),
|
title: const Text('App Version'),
|
||||||
subtitle: const Text('MMS V4 1.2.08'),
|
subtitle: const Text('MMS V4 1.2.09'),
|
||||||
dense: true,
|
dense: true,
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
// --- ADDED: Import dio for downloading ---
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
|
||||||
import '../models/air_installation_data.dart';
|
import '../models/air_installation_data.dart';
|
||||||
import '../models/air_collection_data.dart';
|
import '../models/air_collection_data.dart';
|
||||||
@ -15,7 +15,6 @@ import '../models/tarball_data.dart';
|
|||||||
import '../models/in_situ_sampling_data.dart';
|
import '../models/in_situ_sampling_data.dart';
|
||||||
import '../models/river_in_situ_sampling_data.dart';
|
import '../models/river_in_situ_sampling_data.dart';
|
||||||
|
|
||||||
/// A comprehensive service for handling all local data storage for offline submissions.
|
|
||||||
class LocalStorageService {
|
class LocalStorageService {
|
||||||
|
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
@ -27,14 +26,11 @@ class LocalStorageService {
|
|||||||
return status.isGranted;
|
return status.isGranted;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- MODIFIED: This method now accepts a serverName to create a server-specific root directory. ---
|
|
||||||
Future<Directory?> _getPublicMMSV4Directory({required String serverName}) async {
|
Future<Directory?> _getPublicMMSV4Directory({required String serverName}) async {
|
||||||
if (await _requestPermissions()) {
|
if (await _requestPermissions()) {
|
||||||
final Directory? externalDir = await getExternalStorageDirectory();
|
final Directory? externalDir = await getExternalStorageDirectory();
|
||||||
if (externalDir != null) {
|
if (externalDir != null) {
|
||||||
final publicRootPath = externalDir.path.split('/Android/')[0];
|
final publicRootPath = externalDir.path.split('/Android/')[0];
|
||||||
// Create a subdirectory for the specific server configuration.
|
|
||||||
// If serverName is empty, it returns the root MMSV4 folder.
|
|
||||||
final mmsv4Dir = Directory(p.join(publicRootPath, 'MMSV4', serverName));
|
final mmsv4Dir = Directory(p.join(publicRootPath, 'MMSV4', serverName));
|
||||||
if (!await mmsv4Dir.exists()) {
|
if (!await mmsv4Dir.exists()) {
|
||||||
await mmsv4Dir.create(recursive: true);
|
await mmsv4Dir.create(recursive: true);
|
||||||
@ -46,7 +42,6 @@ class LocalStorageService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- ADDED: A public method to retrieve the root log directory. ---
|
|
||||||
Future<Directory?> getLogDirectory({required String serverName, required String module, required String subModule}) async {
|
Future<Directory?> getLogDirectory({required String serverName, required String module, required String subModule}) async {
|
||||||
final mmsv4Dir = await _getPublicMMSV4Directory(serverName: serverName);
|
final mmsv4Dir = await _getPublicMMSV4Directory(serverName: serverName);
|
||||||
if (mmsv4Dir == null) return null;
|
if (mmsv4Dir == null) return null;
|
||||||
@ -61,7 +56,6 @@ class LocalStorageService {
|
|||||||
// Part 2: Air Manual Sampling Methods (LOGGING RESTORED)
|
// Part 2: Air Manual Sampling Methods (LOGGING RESTORED)
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
|
|
||||||
// --- MODIFIED: Method now requires serverName to get the correct base directory. ---
|
|
||||||
Future<Directory?> _getAirManualBaseDir({required String serverName}) async {
|
Future<Directory?> _getAirManualBaseDir({required String serverName}) async {
|
||||||
final mmsv4Dir = await _getPublicMMSV4Directory(serverName: serverName);
|
final mmsv4Dir = await _getPublicMMSV4Directory(serverName: serverName);
|
||||||
if (mmsv4Dir == null) return null;
|
if (mmsv4Dir == null) return null;
|
||||||
@ -73,8 +67,6 @@ class LocalStorageService {
|
|||||||
return airDir;
|
return airDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Saves or updates an air sampling record, including copying all associated images to permanent local storage.
|
|
||||||
// --- MODIFIED: Method now requires serverName. ---
|
|
||||||
Future<String?> saveAirSamplingRecord(Map<String, dynamic> data, String refID, {required String serverName}) async {
|
Future<String?> saveAirSamplingRecord(Map<String, dynamic> data, String refID, {required String serverName}) async {
|
||||||
final baseDir = await _getAirManualBaseDir(serverName: serverName);
|
final baseDir = await _getAirManualBaseDir(serverName: serverName);
|
||||||
if (baseDir == null) {
|
if (baseDir == null) {
|
||||||
@ -88,11 +80,9 @@ class LocalStorageService {
|
|||||||
await eventDir.create(recursive: true);
|
await eventDir.create(recursive: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to copy a file and return its new, permanent path
|
|
||||||
Future<String?> copyImageToLocal(dynamic imageFile) async {
|
Future<String?> copyImageToLocal(dynamic imageFile) async {
|
||||||
if (imageFile is! File) return null; // Gracefully handle non-File types
|
if (imageFile is! File) return null;
|
||||||
try {
|
try {
|
||||||
// Check if the file is already in the permanent directory to avoid re-copying
|
|
||||||
if (p.dirname(imageFile.path) == eventDir.path) {
|
if (p.dirname(imageFile.path) == eventDir.path) {
|
||||||
return imageFile.path;
|
return imageFile.path;
|
||||||
}
|
}
|
||||||
@ -105,25 +95,18 @@ class LocalStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a mutable copy of the data map to avoid modifying the original
|
|
||||||
final Map<String, dynamic> serializableData = Map.from(data);
|
final Map<String, dynamic> serializableData = Map.from(data);
|
||||||
// --- MODIFIED: Inject the server name into the data being saved. ---
|
|
||||||
serializableData['serverConfigName'] = serverName;
|
serializableData['serverConfigName'] = serverName;
|
||||||
|
|
||||||
|
|
||||||
// Define the keys for installation images to look for in the map
|
|
||||||
final installationImageKeys = ['imageFront', 'imageBack', 'imageLeft', 'imageRight', 'optionalImage1', 'optionalImage2', 'optionalImage3', 'optionalImage4'];
|
final installationImageKeys = ['imageFront', 'imageBack', 'imageLeft', 'imageRight', 'optionalImage1', 'optionalImage2', 'optionalImage3', 'optionalImage4'];
|
||||||
|
|
||||||
// Process top-level (installation) images
|
|
||||||
for (final key in installationImageKeys) {
|
for (final key in installationImageKeys) {
|
||||||
// Check if the key exists and the value is a File object
|
|
||||||
if (serializableData.containsKey(key) && serializableData[key] is File) {
|
if (serializableData.containsKey(key) && serializableData[key] is File) {
|
||||||
final newPath = await copyImageToLocal(serializableData[key]);
|
final newPath = await copyImageToLocal(serializableData[key]);
|
||||||
serializableData['${key}Path'] = newPath; // Creates 'imageFrontPath', etc.
|
serializableData['${key}Path'] = newPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process nested collection images, if they exist
|
|
||||||
if (serializableData['collectionData'] is Map) {
|
if (serializableData['collectionData'] is Map) {
|
||||||
final collectionMap = Map<String, dynamic>.from(serializableData['collectionData']);
|
final collectionMap = Map<String, dynamic>.from(serializableData['collectionData']);
|
||||||
final collectionImageKeys = ['imageFront', 'imageBack', 'imageLeft', 'imageRight', 'imageChart', 'imageFilterPaper', 'optionalImage1', 'optionalImage2', 'optionalImage3', 'optionalImage4'];
|
final collectionImageKeys = ['imageFront', 'imageBack', 'imageLeft', 'imageRight', 'imageChart', 'imageFilterPaper', 'optionalImage1', 'optionalImage2', 'optionalImage3', 'optionalImage4'];
|
||||||
@ -139,7 +122,6 @@ class LocalStorageService {
|
|||||||
|
|
||||||
final Map<String, dynamic> finalData = Map.from(serializableData);
|
final Map<String, dynamic> finalData = Map.from(serializableData);
|
||||||
|
|
||||||
// Recursive helper to remove File objects before JSON encoding
|
|
||||||
void cleanMap(Map<String, dynamic> map) {
|
void cleanMap(Map<String, dynamic> map) {
|
||||||
map.removeWhere((key, value) => value is File);
|
map.removeWhere((key, value) => value is File);
|
||||||
map.forEach((key, value) {
|
map.forEach((key, value) {
|
||||||
@ -149,7 +131,6 @@ class LocalStorageService {
|
|||||||
|
|
||||||
cleanMap(finalData);
|
cleanMap(finalData);
|
||||||
|
|
||||||
|
|
||||||
final jsonFile = File(p.join(eventDir.path, 'data.json'));
|
final jsonFile = File(p.join(eventDir.path, 'data.json'));
|
||||||
await jsonFile.writeAsString(jsonEncode(finalData));
|
await jsonFile.writeAsString(jsonEncode(finalData));
|
||||||
debugPrint("Air sampling log and images saved to: ${eventDir.path}");
|
debugPrint("Air sampling log and images saved to: ${eventDir.path}");
|
||||||
@ -163,9 +144,8 @@ class LocalStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- MODIFIED: This method now scans all server subdirectories to find all logs. ---
|
|
||||||
Future<List<Map<String, dynamic>>> getAllAirSamplingLogs() async {
|
Future<List<Map<String, dynamic>>> getAllAirSamplingLogs() async {
|
||||||
final mmsv4Root = await _getPublicMMSV4Directory(serverName: ''); // Get root MMSV4 without a server subfolder
|
final mmsv4Root = await _getPublicMMSV4Directory(serverName: '');
|
||||||
if (mmsv4Root == null || !await mmsv4Root.exists()) return [];
|
if (mmsv4Root == null || !await mmsv4Root.exists()) return [];
|
||||||
|
|
||||||
final List<Map<String, dynamic>> allLogs = [];
|
final List<Map<String, dynamic>> allLogs = [];
|
||||||
@ -314,7 +294,6 @@ class LocalStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
// Part 4: Marine In-Situ Specific Methods (LOGGING RESTORED)
|
// Part 4: Marine In-Situ Specific Methods (LOGGING RESTORED)
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
@ -347,12 +326,9 @@ class LocalStorageService {
|
|||||||
await eventDir.create(recursive: true);
|
await eventDir.create(recursive: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- START FIX: Explicitly include the final status and message ---
|
|
||||||
// This ensures the status calculated in the service layer is saved correctly.
|
|
||||||
final Map<String, dynamic> jsonData = data.toDbJson();
|
final Map<String, dynamic> jsonData = data.toDbJson();
|
||||||
jsonData['submissionStatus'] = data.submissionStatus;
|
jsonData['submissionStatus'] = data.submissionStatus;
|
||||||
jsonData['submissionMessage'] = data.submissionMessage;
|
jsonData['submissionMessage'] = data.submissionMessage;
|
||||||
// --- END FIX ---
|
|
||||||
|
|
||||||
jsonData['serverConfigName'] = serverName;
|
jsonData['serverConfigName'] = serverName;
|
||||||
|
|
||||||
@ -436,6 +412,58 @@ class LocalStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<InSituSamplingData>> getRecentNearbySamples({
|
||||||
|
required double latitude,
|
||||||
|
required double longitude,
|
||||||
|
required double radiusKm,
|
||||||
|
required int withinHours,
|
||||||
|
}) async {
|
||||||
|
final allLogs = await getAllInSituLogs();
|
||||||
|
final List<InSituSamplingData> recentNearbySamples = [];
|
||||||
|
final cutoffDateTime = DateTime.now().subtract(Duration(hours: withinHours));
|
||||||
|
final double radiusInMeters = radiusKm * 1000;
|
||||||
|
|
||||||
|
for (var log in allLogs) {
|
||||||
|
try {
|
||||||
|
final sampleData = InSituSamplingData.fromJson(log);
|
||||||
|
|
||||||
|
if (sampleData.samplingDate == null || sampleData.samplingTime == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final sampleDateTime = DateTime.tryParse('${sampleData.samplingDate} ${sampleData.samplingTime}');
|
||||||
|
if (sampleDateTime == null || sampleDateTime.isBefore(cutoffDateTime)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final sampleLat = double.tryParse(sampleData.currentLatitude ?? '');
|
||||||
|
final sampleLon = double.tryParse(sampleData.currentLongitude ?? '');
|
||||||
|
if (sampleLat == null || sampleLon == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final distanceInMeters = Geolocator.distanceBetween(
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
sampleLat,
|
||||||
|
sampleLon,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distanceInMeters <= radiusInMeters) {
|
||||||
|
recentNearbySamples.add(sampleData);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error processing in-situ log for nearby search: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recentNearbySamples.sort((a, b) {
|
||||||
|
final dtA = DateTime.tryParse('${a.samplingDate} ${a.samplingTime}');
|
||||||
|
final dtB = DateTime.tryParse('${b.samplingDate} ${b.samplingTime}');
|
||||||
|
if (dtA == null || dtB == null) return 0;
|
||||||
|
return dtB.compareTo(dtA);
|
||||||
|
});
|
||||||
|
return recentNearbySamples;
|
||||||
|
}
|
||||||
|
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
// Part 5: River In-Situ Specific Methods (LOGGING RESTORED)
|
// Part 5: River In-Situ Specific Methods (LOGGING RESTORED)
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
@ -475,7 +503,6 @@ class LocalStorageService {
|
|||||||
await eventDir.create(recursive: true);
|
await eventDir.create(recursive: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- START: MODIFIED TO USE toMap() FOR COMPLETE DATA SERIALIZATION ---
|
|
||||||
final Map<String, dynamic> jsonData = data.toMap();
|
final Map<String, dynamic> jsonData = data.toMap();
|
||||||
jsonData['serverConfigName'] = serverName;
|
jsonData['serverConfigName'] = serverName;
|
||||||
|
|
||||||
@ -485,16 +512,13 @@ class LocalStorageService {
|
|||||||
if (imageFile != null) {
|
if (imageFile != null) {
|
||||||
final String originalFileName = p.basename(imageFile.path);
|
final String originalFileName = p.basename(imageFile.path);
|
||||||
if (p.dirname(imageFile.path) == eventDir.path) {
|
if (p.dirname(imageFile.path) == eventDir.path) {
|
||||||
// If file is already in the correct directory, just store the path
|
|
||||||
jsonData[entry.key] = imageFile.path;
|
jsonData[entry.key] = imageFile.path;
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, copy it to the permanent directory
|
|
||||||
final File newFile = await imageFile.copy(p.join(eventDir.path, originalFileName));
|
final File newFile = await imageFile.copy(p.join(eventDir.path, originalFileName));
|
||||||
jsonData[entry.key] = newFile.path;
|
jsonData[entry.key] = newFile.path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- END: MODIFIED TO USE toMap() FOR COMPLETE DATA SERIALIZATION ---
|
|
||||||
|
|
||||||
final jsonFile = File(p.join(eventDir.path, 'data.json'));
|
final jsonFile = File(p.join(eventDir.path, 'data.json'));
|
||||||
await jsonFile.writeAsString(jsonEncode(jsonData));
|
await jsonFile.writeAsString(jsonEncode(jsonData));
|
||||||
@ -568,9 +592,7 @@ class LocalStorageService {
|
|||||||
|
|
||||||
final Dio _dio = Dio();
|
final Dio _dio = Dio();
|
||||||
|
|
||||||
/// Gets the directory for storing Info Centre documents, creating it if it doesn't exist.
|
|
||||||
Future<Directory?> _getInfoCentreDocumentsDirectory() async {
|
Future<Directory?> _getInfoCentreDocumentsDirectory() async {
|
||||||
// We use serverName: '' to ensure documents are stored in a common root MMSV4 folder, not server-specific ones.
|
|
||||||
final mmsv4Dir = await _getPublicMMSV4Directory(serverName: '');
|
final mmsv4Dir = await _getPublicMMSV4Directory(serverName: '');
|
||||||
if (mmsv4Dir == null) return null;
|
if (mmsv4Dir == null) return null;
|
||||||
|
|
||||||
@ -581,7 +603,6 @@ class LocalStorageService {
|
|||||||
return docDir;
|
return docDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constructs the full local file path for a given document URL.
|
|
||||||
Future<String?> getLocalDocumentPath(String docUrl) async {
|
Future<String?> getLocalDocumentPath(String docUrl) async {
|
||||||
final docDir = await _getInfoCentreDocumentsDirectory();
|
final docDir = await _getInfoCentreDocumentsDirectory();
|
||||||
if (docDir == null) return null;
|
if (docDir == null) return null;
|
||||||
@ -590,14 +611,12 @@ class LocalStorageService {
|
|||||||
return p.join(docDir.path, fileName);
|
return p.join(docDir.path, fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if a document has already been downloaded.
|
|
||||||
Future<bool> isDocumentDownloaded(String docUrl) async {
|
Future<bool> isDocumentDownloaded(String docUrl) async {
|
||||||
final filePath = await getLocalDocumentPath(docUrl);
|
final filePath = await getLocalDocumentPath(docUrl);
|
||||||
if (filePath == null) return false;
|
if (filePath == null) return false;
|
||||||
return await File(filePath).exists();
|
return await File(filePath).exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Downloads a document from a URL and saves it to the local `MMSV4/info_centre_documents` folder.
|
|
||||||
Future<void> downloadDocument({
|
Future<void> downloadDocument({
|
||||||
required String docUrl,
|
required String docUrl,
|
||||||
required Function(double) onReceiveProgress,
|
required Function(double) onReceiveProgress,
|
||||||
@ -618,7 +637,6 @@ class LocalStorageService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If the download fails, delete the partially downloaded file to prevent corruption.
|
|
||||||
final file = File(filePath);
|
final file = File(filePath);
|
||||||
if (await file.exists()) {
|
if (await file.exists()) {
|
||||||
await file.delete();
|
await file.delete();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user