chore: normalize line endings (CRLF -> LF)
No content changes: git diff --ignore-all-space over these files is empty. The churn came from editing on Windows against a repo checked out with LF.
This commit is contained in:
1 parent
15566a6951
commit
caf8e98378
315 files changed
+86950
-86950
No files matched your search
@@ -1,199 +1,199 @@
|
||||
export 'pending_document_model.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../models/document_model.dart';
|
||||
import '../../core/location/location_service.dart';
|
||||
import '../../core/network/api_client.dart';
|
||||
import '../../core/network/api_exception.dart';
|
||||
import '../../config/app_config.dart';
|
||||
import '../../main.dart';
|
||||
import '../camera/scan_mode_provider.dart';
|
||||
import 'pending_document_model.dart';
|
||||
import 'poll_outcome.dart';
|
||||
|
||||
class PendingDocumentsNotifier extends StateNotifier<List<PendingDocument>> {
|
||||
PendingDocumentsNotifier(this._ref) : super([]) {
|
||||
_loadPendingDocuments();
|
||||
}
|
||||
|
||||
final Ref _ref;
|
||||
|
||||
void _loadPendingDocuments() {
|
||||
final localStorage = _ref.read(localStorageProvider);
|
||||
final list = localStorage.getAllPendingDocuments()
|
||||
.map((e) => PendingDocument.fromJson(e))
|
||||
.toList();
|
||||
state = list;
|
||||
|
||||
for (var doc in list) {
|
||||
if (doc.status == PendingDocumentStatus.processing && doc.document != null) {
|
||||
_resumePolling(doc.id, doc.document!);
|
||||
} else if (doc.status == PendingDocumentStatus.uploading) {
|
||||
_uploadAndProcess(doc.id, doc.imagePath, latitude: doc.latitude, longitude: doc.longitude);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _persistPendingDocument(PendingDocument doc) {
|
||||
_ref.read(localStorageProvider).savePendingDocument(doc.toJson());
|
||||
}
|
||||
|
||||
void _removePersistedPendingDocument(String id) {
|
||||
_ref.read(localStorageProvider).removePendingDocument(id);
|
||||
}
|
||||
|
||||
void _updateItem(String id, PendingDocument Function(PendingDocument) updateFn) {
|
||||
state = [for (var item in state) if (item.id == id) updateFn(item) else item];
|
||||
_persistPendingDocument(state.firstWhere((item) => item.id == id));
|
||||
}
|
||||
|
||||
void _updateItemInMemory(String id, PendingDocument Function(PendingDocument) updateFn) {
|
||||
state = [for (var item in state) if (item.id == id) updateFn(item) else item];
|
||||
}
|
||||
|
||||
void addDocument(String imagePath, {double? latitude, double? longitude}) {
|
||||
final id = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
final scanMode = _ref.read(scanModeProvider);
|
||||
final newDoc = PendingDocument(
|
||||
id: id,
|
||||
imagePath: imagePath,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
scanMode: scanMode,
|
||||
);
|
||||
state = [newDoc, ...state];
|
||||
_persistPendingDocument(newDoc);
|
||||
_uploadAndProcess(id, imagePath, latitude: latitude, longitude: longitude);
|
||||
}
|
||||
|
||||
Future<void> _uploadAndProcess(String id, String imagePath, {double? latitude, double? longitude}) async {
|
||||
double? lat = latitude;
|
||||
double? lng = longitude;
|
||||
|
||||
if (lat == null || lng == null) {
|
||||
try {
|
||||
final position = await LocationService.determinePosition();
|
||||
if (position != null) {
|
||||
lat = position.latitude;
|
||||
lng = position.longitude;
|
||||
if (mounted) _updateItem(id, (item) => item.copyWith(latitude: lat, longitude: lng));
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
|
||||
|
||||
try {
|
||||
final scanMode = _ref.read(scanModeProvider);
|
||||
final apiClient = _ref.read(apiClientProvider);
|
||||
final formData = FormData.fromMap({
|
||||
'image': await MultipartFile.fromFile(imagePath, filename: imagePath.split(RegExp(r'[/\\]')).last),
|
||||
if (lat != null) 'latitude': lat.toString(),
|
||||
if (lng != null) 'longitude': lng.toString(),
|
||||
'scan_mode': scanMode,
|
||||
});
|
||||
|
||||
final response = await apiClient.client.post(AppConfig.uploadDocumentEndpoint, data: formData);
|
||||
final doc = DocumentModel.fromJson(response.data['data']);
|
||||
|
||||
if (!mounted) return;
|
||||
_updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.processing, document: doc));
|
||||
await _pollUntilParsed(id, doc);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
_updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.error, errorMessage: ApiException.from(e).displayMessage));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _resumePolling(String id, DocumentModel doc) async {
|
||||
try {
|
||||
await _pollUntilParsed(id, doc);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
_updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.error, errorMessage: ApiException.from(e).displayMessage));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pollUntilParsed(String id, DocumentModel doc) async {
|
||||
final apiClient = _ref.read(apiClientProvider);
|
||||
DocumentModel? resultDoc;
|
||||
String? failureMessage;
|
||||
int retries = 0;
|
||||
const maxRetries = 130;
|
||||
|
||||
while (retries < maxRetries && mounted) {
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
final getRes = await apiClient.client.get('${AppConfig.fetchDocumentsEndpoint}/${doc.id}');
|
||||
if (getRes.statusCode == 200 && getRes.data['data'] != null) {
|
||||
final fetched = DocumentModel.fromJson(Map<String, dynamic>.from(getRes.data['data']));
|
||||
final outcome = resolvePollOutcome(fetched);
|
||||
if (outcome.kind == PollOutcomeKind.done) {
|
||||
resultDoc = outcome.document;
|
||||
break;
|
||||
} else if (outcome.kind == PollOutcomeKind.failed) {
|
||||
failureMessage = outcome.message;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
retries++;
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
if (resultDoc != null) {
|
||||
_updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.success, document: resultDoc));
|
||||
} else {
|
||||
_updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.error, errorMessage: failureMessage ?? 'Gagal mengekstrak data (Timeout).'));
|
||||
}
|
||||
}
|
||||
|
||||
void removeDocument(String id) {
|
||||
state = state.where((item) => item.id != id).toList();
|
||||
_removePersistedPendingDocument(id);
|
||||
}
|
||||
|
||||
void markSyncFailed(String id, DocumentModel doc, String message) {
|
||||
_updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.syncFailed, document: doc, errorMessage: message));
|
||||
}
|
||||
|
||||
Future<void> retrySync(String id) async {
|
||||
final matches = state.where((item) => item.id == id);
|
||||
if (matches.isEmpty) return;
|
||||
final doc = matches.first.document;
|
||||
if (doc == null) return;
|
||||
|
||||
_updateItemInMemory(id, (item) => item.copyWith(status: PendingDocumentStatus.uploading, errorMessage: null));
|
||||
|
||||
try {
|
||||
final apiClient = _ref.read(apiClientProvider);
|
||||
await apiClient.client.put('${AppConfig.fetchDocumentsEndpoint}/${doc.id}', data: doc.toPutPayload());
|
||||
if (!mounted) return;
|
||||
removeDocument(id);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
_updateItem(id, (item) => item.copyWith(
|
||||
status: PendingDocumentStatus.syncFailed,
|
||||
errorMessage: 'Gagal sinkronisasi ke server: ${ApiException.from(e).displayMessage}',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void retryUpload(String id) {
|
||||
final doc = state.firstWhere((item) => item.id == id);
|
||||
_updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.uploading, errorMessage: null));
|
||||
_uploadAndProcess(id, doc.imagePath, latitude: doc.latitude, longitude: doc.longitude);
|
||||
}
|
||||
|
||||
void clearQueue() {
|
||||
state = const [];
|
||||
_ref.read(localStorageProvider).pendingDocumentsBox.clear();
|
||||
}
|
||||
}
|
||||
|
||||
final pendingDocumentsProvider = StateNotifierProvider<PendingDocumentsNotifier, List<PendingDocument>>((ref) {
|
||||
return PendingDocumentsNotifier(ref);
|
||||
});
|
||||
export 'pending_document_model.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../models/document_model.dart';
|
||||
import '../../core/location/location_service.dart';
|
||||
import '../../core/network/api_client.dart';
|
||||
import '../../core/network/api_exception.dart';
|
||||
import '../../config/app_config.dart';
|
||||
import '../../main.dart';
|
||||
import '../camera/scan_mode_provider.dart';
|
||||
import 'pending_document_model.dart';
|
||||
import 'poll_outcome.dart';
|
||||
|
||||
class PendingDocumentsNotifier extends StateNotifier<List<PendingDocument>> {
|
||||
PendingDocumentsNotifier(this._ref) : super([]) {
|
||||
_loadPendingDocuments();
|
||||
}
|
||||
|
||||
final Ref _ref;
|
||||
|
||||
void _loadPendingDocuments() {
|
||||
final localStorage = _ref.read(localStorageProvider);
|
||||
final list = localStorage.getAllPendingDocuments()
|
||||
.map((e) => PendingDocument.fromJson(e))
|
||||
.toList();
|
||||
state = list;
|
||||
|
||||
for (var doc in list) {
|
||||
if (doc.status == PendingDocumentStatus.processing && doc.document != null) {
|
||||
_resumePolling(doc.id, doc.document!);
|
||||
} else if (doc.status == PendingDocumentStatus.uploading) {
|
||||
_uploadAndProcess(doc.id, doc.imagePath, latitude: doc.latitude, longitude: doc.longitude);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _persistPendingDocument(PendingDocument doc) {
|
||||
_ref.read(localStorageProvider).savePendingDocument(doc.toJson());
|
||||
}
|
||||
|
||||
void _removePersistedPendingDocument(String id) {
|
||||
_ref.read(localStorageProvider).removePendingDocument(id);
|
||||
}
|
||||
|
||||
void _updateItem(String id, PendingDocument Function(PendingDocument) updateFn) {
|
||||
state = [for (var item in state) if (item.id == id) updateFn(item) else item];
|
||||
_persistPendingDocument(state.firstWhere((item) => item.id == id));
|
||||
}
|
||||
|
||||
void _updateItemInMemory(String id, PendingDocument Function(PendingDocument) updateFn) {
|
||||
state = [for (var item in state) if (item.id == id) updateFn(item) else item];
|
||||
}
|
||||
|
||||
void addDocument(String imagePath, {double? latitude, double? longitude}) {
|
||||
final id = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
final scanMode = _ref.read(scanModeProvider);
|
||||
final newDoc = PendingDocument(
|
||||
id: id,
|
||||
imagePath: imagePath,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
scanMode: scanMode,
|
||||
);
|
||||
state = [newDoc, ...state];
|
||||
_persistPendingDocument(newDoc);
|
||||
_uploadAndProcess(id, imagePath, latitude: latitude, longitude: longitude);
|
||||
}
|
||||
|
||||
Future<void> _uploadAndProcess(String id, String imagePath, {double? latitude, double? longitude}) async {
|
||||
double? lat = latitude;
|
||||
double? lng = longitude;
|
||||
|
||||
if (lat == null || lng == null) {
|
||||
try {
|
||||
final position = await LocationService.determinePosition();
|
||||
if (position != null) {
|
||||
lat = position.latitude;
|
||||
lng = position.longitude;
|
||||
if (mounted) _updateItem(id, (item) => item.copyWith(latitude: lat, longitude: lng));
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
|
||||
|
||||
try {
|
||||
final scanMode = _ref.read(scanModeProvider);
|
||||
final apiClient = _ref.read(apiClientProvider);
|
||||
final formData = FormData.fromMap({
|
||||
'image': await MultipartFile.fromFile(imagePath, filename: imagePath.split(RegExp(r'[/\\]')).last),
|
||||
if (lat != null) 'latitude': lat.toString(),
|
||||
if (lng != null) 'longitude': lng.toString(),
|
||||
'scan_mode': scanMode,
|
||||
});
|
||||
|
||||
final response = await apiClient.client.post(AppConfig.uploadDocumentEndpoint, data: formData);
|
||||
final doc = DocumentModel.fromJson(response.data['data']);
|
||||
|
||||
if (!mounted) return;
|
||||
_updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.processing, document: doc));
|
||||
await _pollUntilParsed(id, doc);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
_updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.error, errorMessage: ApiException.from(e).displayMessage));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _resumePolling(String id, DocumentModel doc) async {
|
||||
try {
|
||||
await _pollUntilParsed(id, doc);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
_updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.error, errorMessage: ApiException.from(e).displayMessage));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pollUntilParsed(String id, DocumentModel doc) async {
|
||||
final apiClient = _ref.read(apiClientProvider);
|
||||
DocumentModel? resultDoc;
|
||||
String? failureMessage;
|
||||
int retries = 0;
|
||||
const maxRetries = 130;
|
||||
|
||||
while (retries < maxRetries && mounted) {
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
final getRes = await apiClient.client.get('${AppConfig.fetchDocumentsEndpoint}/${doc.id}');
|
||||
if (getRes.statusCode == 200 && getRes.data['data'] != null) {
|
||||
final fetched = DocumentModel.fromJson(Map<String, dynamic>.from(getRes.data['data']));
|
||||
final outcome = resolvePollOutcome(fetched);
|
||||
if (outcome.kind == PollOutcomeKind.done) {
|
||||
resultDoc = outcome.document;
|
||||
break;
|
||||
} else if (outcome.kind == PollOutcomeKind.failed) {
|
||||
failureMessage = outcome.message;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
retries++;
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
if (resultDoc != null) {
|
||||
_updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.success, document: resultDoc));
|
||||
} else {
|
||||
_updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.error, errorMessage: failureMessage ?? 'Gagal mengekstrak data (Timeout).'));
|
||||
}
|
||||
}
|
||||
|
||||
void removeDocument(String id) {
|
||||
state = state.where((item) => item.id != id).toList();
|
||||
_removePersistedPendingDocument(id);
|
||||
}
|
||||
|
||||
void markSyncFailed(String id, DocumentModel doc, String message) {
|
||||
_updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.syncFailed, document: doc, errorMessage: message));
|
||||
}
|
||||
|
||||
Future<void> retrySync(String id) async {
|
||||
final matches = state.where((item) => item.id == id);
|
||||
if (matches.isEmpty) return;
|
||||
final doc = matches.first.document;
|
||||
if (doc == null) return;
|
||||
|
||||
_updateItemInMemory(id, (item) => item.copyWith(status: PendingDocumentStatus.uploading, errorMessage: null));
|
||||
|
||||
try {
|
||||
final apiClient = _ref.read(apiClientProvider);
|
||||
await apiClient.client.put('${AppConfig.fetchDocumentsEndpoint}/${doc.id}', data: doc.toPutPayload());
|
||||
if (!mounted) return;
|
||||
removeDocument(id);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
_updateItem(id, (item) => item.copyWith(
|
||||
status: PendingDocumentStatus.syncFailed,
|
||||
errorMessage: 'Gagal sinkronisasi ke server: ${ApiException.from(e).displayMessage}',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void retryUpload(String id) {
|
||||
final doc = state.firstWhere((item) => item.id == id);
|
||||
_updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.uploading, errorMessage: null));
|
||||
_uploadAndProcess(id, doc.imagePath, latitude: doc.latitude, longitude: doc.longitude);
|
||||
}
|
||||
|
||||
void clearQueue() {
|
||||
state = const [];
|
||||
_ref.read(localStorageProvider).pendingDocumentsBox.clear();
|
||||
}
|
||||
}
|
||||
|
||||
final pendingDocumentsProvider = StateNotifierProvider<PendingDocumentsNotifier, List<PendingDocument>>((ref) {
|
||||
return PendingDocumentsNotifier(ref);
|
||||
});
|
||||
Reference in new issue
Block a user