// Dart imports: import 'dart:async'; import 'dart:io' as file; import 'dart:io'; // Flutter imports: import 'package:built_collection/built_collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; // Package imports: import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_styled_toast/flutter_styled_toast.dart'; // Project imports: import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/main_app.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/redux/auth/auth_actions.dart'; import 'package:invoiceninja_flutter/redux/client/client_actions.dart'; import 'package:invoiceninja_flutter/redux/credit/credit_actions.dart'; import 'package:invoiceninja_flutter/redux/expense/expense_actions.dart'; import 'package:invoiceninja_flutter/redux/group/group_actions.dart'; import 'package:invoiceninja_flutter/redux/invoice/invoice_actions.dart'; import 'package:invoiceninja_flutter/redux/product/product_actions.dart'; import 'package:invoiceninja_flutter/redux/project/project_actions.dart'; import 'package:invoiceninja_flutter/redux/purchase_order/purchase_order_actions.dart'; import 'package:invoiceninja_flutter/redux/quote/quote_actions.dart'; import 'package:invoiceninja_flutter/redux/recurring_expense/recurring_expense_actions.dart'; import 'package:invoiceninja_flutter/redux/recurring_invoice/recurring_invoice_actions.dart'; import 'package:invoiceninja_flutter/redux/task/task_actions.dart'; import 'package:invoiceninja_flutter/redux/vendor/vendor_actions.dart'; import 'package:invoiceninja_flutter/ui/app/entities/entity_actions_dialog.dart'; import 'package:invoiceninja_flutter/utils/completers.dart'; import 'package:invoiceninja_flutter/utils/dialogs.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/platforms.dart'; import 'package:path_provider/path_provider.dart'; import 'package:pinch_zoom/pinch_zoom.dart'; import 'package:printing/printing.dart'; import 'package:invoiceninja_flutter/utils/web_stub.dart' if (dart.library.html) 'package:invoiceninja_flutter/utils/web.dart'; import 'package:share_plus/share_plus.dart'; class ViewDocumentList implements PersistUI { ViewDocumentList({ this.force = false, this.page = 0, }); final bool force; final int page; } class ViewDocument implements PersistUI { ViewDocument({this.documentId, this.force}); final String? documentId; final bool? force; } class EditDocument implements PersistUI { EditDocument({ this.document, this.completer, }); final DocumentEntity? document; final Completer? completer; } class UpdateDocument implements PersistUI { UpdateDocument(this.document); final DocumentEntity document; } class LoadDocument { LoadDocument({this.completer, this.documentId}); final Completer? completer; final String? documentId; } class LoadDocumentData { LoadDocumentData({this.completer, this.documentId}); final Completer? completer; final String? documentId; } class LoadDocumentActivity { LoadDocumentActivity({this.completer, this.documentId}); final Completer? completer; final String? documentId; } class LoadDocuments { LoadDocuments({this.completer}); final Completer? completer; } class LoadDocumentRequest implements StartLoading {} class LoadDocumentFailure implements StopLoading { LoadDocumentFailure(this.error); final dynamic error; @override String toString() { return 'LoadDocumentFailure{error: $error}'; } } class LoadDocumentSuccess implements StopLoading, PersistData { LoadDocumentSuccess(this.document); final DocumentEntity document; @override String toString() { return 'LoadDocumentSuccess{document: $document}'; } } class LoadDocumentDataRequest implements StartLoading {} class LoadDocumentsRequest implements StartLoading {} class LoadDocumentsFailure implements StopLoading { LoadDocumentsFailure(this.error); final dynamic error; @override String toString() { return 'LoadDocumentsFailure{error: $error}'; } } class LoadDocumentsSuccess implements StopLoading { LoadDocumentsSuccess(this.documents); final List documents; @override String toString() { return 'LoadDocumentsSuccess{documents: $documents}'; } } class SaveDocumentRequest implements StartSaving { SaveDocumentRequest({ required this.completer, required this.document, }); final Completer completer; final DocumentEntity? document; } class SaveDocumentSuccess implements StopSaving, PersistData, PersistUI { SaveDocumentSuccess(this.document); final DocumentEntity document; } class AddDocumentSuccess implements StopSaving, PersistData, PersistUI { AddDocumentSuccess(this.documents, this.parentType, this.parentId); final BuiltList documents; final EntityType parentType; final String parentId; } class SaveDocumentFailure implements StopSaving { SaveDocumentFailure(this.error); final Object error; } class DownloadDocumentsRequest implements StartSaving { DownloadDocumentsRequest({this.completer, this.documentIds}); final Completer? completer; final List? documentIds; } class DownloadDocumentsSuccess implements StopSaving {} class DownloadDocumentsFailure implements StopSaving { DownloadDocumentsFailure(this.error); final Object error; } class ArchiveDocumentRequest implements StartSaving { ArchiveDocumentRequest(this.completer, this.documentIds); final Completer completer; final List documentIds; } class ArchiveDocumentSuccess implements StopSaving, PersistData { ArchiveDocumentSuccess(this.documents); final List documents; } class ArchiveDocumentFailure implements StopSaving { ArchiveDocumentFailure(this.documents); final List documents; } class DeleteDocumentRequest implements StartSaving { DeleteDocumentRequest({ required this.completer, required this.documentIds, required this.password, required this.idToken, }); final Completer completer; final List documentIds; final String? password; final String? idToken; } class DeleteDocumentSuccess implements StopSaving, PersistData, UserVerifiedPassword { DeleteDocumentSuccess({this.documentId}); final String? documentId; //DeleteDocumentSuccess(this.documents); //final List documents; } class DeleteDocumentFailure implements StopSaving { //DeleteDocumentFailure(this.documents); //final List documents; } class RestoreDocumentRequest implements StartSaving { RestoreDocumentRequest(this.completer, this.documentIds); final Completer completer; final List documentIds; } class RestoreDocumentSuccess implements StopSaving, PersistData { RestoreDocumentSuccess(this.documents); final List documents; } class RestoreDocumentFailure implements StopSaving { RestoreDocumentFailure(this.documents); final List documents; } class FilterDocuments implements PersistUI { FilterDocuments(this.filter); final String? filter; } class FilterDocumentsByStatus implements PersistUI { FilterDocumentsByStatus(this.status); final EntityStatus status; } class SortDocuments implements PersistUI, PersistPrefs { SortDocuments(this.field); final String field; } class FilterDocumentsByState implements PersistUI { FilterDocumentsByState(this.state); final EntityState state; } class FilterDocumentsByCustom1 implements PersistUI { FilterDocumentsByCustom1(this.value); final String value; } class FilterDocumentsByCustom2 implements PersistUI { FilterDocumentsByCustom2(this.value); final String value; } class FilterDocumentsByCustom3 implements PersistUI { FilterDocumentsByCustom3(this.value); final String value; } class FilterDocumentsByCustom4 implements PersistUI { FilterDocumentsByCustom4(this.value); final String value; } void handleDocumentAction( BuildContext? context, List documents, EntityAction? action) { if (documents.isEmpty) { return; } final store = StoreProvider.of(context!); final localization = AppLocalization.of(context); final documentIds = documents.map((document) => document.id).toList(); final document = store.state.documentState.map[documentIds.first]!; switch (action) { case EntityAction.edit: editEntity(entity: document); break; case EntityAction.restore: final message = documentIds.length > 1 ? localization!.restoredDocuments .replaceFirst(':value', ':count') .replaceFirst(':count', documentIds.length.toString()) : localization!.restoredDocument; store.dispatch(RestoreDocumentRequest( snackBarCompleter(message), documentIds)); break; case EntityAction.archive: final message = documentIds.length > 1 ? localization!.archivedDocuments .replaceFirst(':value', ':count') .replaceFirst(':count', documentIds.length.toString()) : localization!.archivedDocument; store.dispatch(ArchiveDocumentRequest( snackBarCompleter(message), documentIds)); break; /* case EntityAction.delete: final message = documentIds.length > 1 ? localization.deletedDocuments .replaceFirst(':value', ':count').replaceFirst(':count', documentIds.length.toString()) : localization.deletedDocument; store.dispatch(DeleteDocumentRequest( completer: snackBarCompleter( message), documentIds: documentIds, )); break; */ case EntityAction.toggleMultiselect: if (!store.state.documentListState.isInMultiselect()) { store.dispatch(StartDocumentMultiselect()); } if (documents.isEmpty) { break; } for (final document in documents) { if (!store.state.documentListState.isSelected(document.id)) { store.dispatch(AddToDocumentMultiselect(entity: document)); } else { store.dispatch(RemoveFromDocumentMultiselect(entity: document)); } } break; case EntityAction.more: showEntityActionsDialog( entities: [document], ); break; case EntityAction.bulkDownload: store.dispatch( DownloadDocumentsRequest( documentIds: documentIds, completer: snackBarCompleter( localization!.exportedData, ), ), ); break; case EntityAction.viewDocument: void showDocument() { showDialog( context: navigatorKey.currentContext!, builder: (context) { final DocumentEntity document = store.state.documentState.map[documentIds.first]!; return AlertDialog( actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text(localization!.close.toUpperCase())), ], content: document.isImage ? PinchZoom( child: Image.memory(document.data!), ) : SizedBox( width: 600, child: PdfPreview( build: (format) => document.data!, canChangeOrientation: false, canChangePageFormat: false, allowPrinting: false, allowSharing: false, canDebug: false, ), ), ); }); } if (document.data == null) { store.dispatch(LoadDocumentData( documentId: document.id, completer: Completer() ..future.then((value) => showDocument()))); } else { showDocument(); } break; case EntityAction.download: void downloadDocument() async { final DocumentEntity? document = store.state.documentState.map[documentIds.first]; if (kIsWeb) { if (document?.data != null) { WebUtils.downloadBinaryFile(document!.name, document.data!); } } else { final directory = await (isDesktopOS() ? getDownloadsDirectory() as FutureOr : getApplicationDocumentsDirectory()); String filePath = '${directory.path}${file.Platform.pathSeparator}${document!.name}'; if (file.File(filePath).existsSync()) { final extension = document.name.split('.').last; final timestamp = DateTime.now().millisecondsSinceEpoch; filePath = filePath.replaceFirst('.$extension', '_$timestamp.$extension'); } await File(filePath).writeAsBytes(document.data!); if (isDesktopOS()) { showToast(localization!.fileSavedInPath .replaceFirst(':path', directory.path)); } else { await Share.shareXFiles([XFile(filePath)]); } } } if (document.data == null) { store.dispatch(LoadDocumentData( documentId: document.id, completer: Completer() ..future.then((value) => downloadDocument()))); } else { downloadDocument(); } break; case EntityAction.delete: confirmCallback( context: context, callback: (_) { passwordCallback( context: context, callback: (password, idToken) { final completer = snackBarCompleter( AppLocalization.of(context)!.deletedDocument); switch (document.parentType) { case EntityType.client: completer.future.then((_) => store .dispatch(LoadClient(clientId: document.parentId))); break; case EntityType.credit: completer.future.then((_) => store .dispatch(LoadCredit(creditId: document.parentId))); break; case EntityType.expense: completer.future.then((_) => store .dispatch(LoadExpense(expenseId: document.parentId))); break; case EntityType.group: completer.future.then((_) => store .dispatch(LoadGroup(groupId: document.parentId))); break; case EntityType.invoice: completer.future.then((_) => store .dispatch(LoadInvoice(invoiceId: document.parentId))); break; case EntityType.product: completer.future.then((_) => store .dispatch(LoadProduct(productId: document.parentId))); break; case EntityType.project: completer.future.then((_) => store .dispatch(LoadProject(projectId: document.parentId))); break; case EntityType.purchaseOrder: completer.future.then((_) => store.dispatch( LoadPurchaseOrder( purchaseOrderId: document.parentId))); break; case EntityType.quote: completer.future.then((_) => store .dispatch(LoadQuote(quoteId: document.parentId))); break; case EntityType.recurringExpense: completer.future.then((_) => store.dispatch( LoadRecurringExpense( recurringExpenseId: document.parentId))); break; case EntityType.recurringInvoice: completer.future.then((_) => store.dispatch( LoadRecurringInvoice( recurringInvoiceId: document.parentId))); break; case EntityType.task: completer.future.then((_) => store.dispatch(LoadTask(taskId: document.parentId))); break; case EntityType.vendor: completer.future.then((_) => store .dispatch(LoadVendor(vendorId: document.parentId))); break; default: completer.future .then((_) => store.dispatch(RefreshData())); } completer.future .then((_) => store.dispatch(RefreshData())); store.dispatch(DeleteDocumentRequest( completer: completer, documentIds: [document.id], password: password, idToken: idToken, )); }); }); break; default: print('## ERROR: unhandled action $action in document_actions'); break; } } class StartDocumentMultiselect {} class AddToDocumentMultiselect { AddToDocumentMultiselect({required this.entity}); final BaseEntity? entity; } class RemoveFromDocumentMultiselect { RemoveFromDocumentMultiselect({required this.entity}); final BaseEntity? entity; } class ClearDocumentMultiselect {}