Improved error handling, added multithreading for fromJson calls, made receive sharing intent more robust

This commit is contained in:
Anton Stubenbord
2022-11-13 14:41:42 +01:00
parent afbd4bddb4
commit 1cafd5d246
43 changed files with 644 additions and 746 deletions

View File

@@ -1,47 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
///
/// Class for handling generic errors which usually only require to inform the user via a Snackbar
/// or similar that an error has occurred.
///
@singleton
class GlobalErrorCubit extends Cubit<GlobalErrorState> {
static const _waitBeforeNextErrorDuration = Duration(seconds: 5);
GlobalErrorCubit() : super(GlobalErrorState.initial);
///
/// Adds a new error to this bloc. If the new error is equal to the current error, the new error
/// will not be published unless the previous error occured over 5 seconds ago.
///
void add(ErrorMessage error) {
final now = DateTime.now();
if (error != state.error || (error == state.error && _canEmitNewError())) {
emit(GlobalErrorState(error: error, errorTimestamp: now));
}
}
bool _canEmitNewError() {
if (state.errorTimestamp != null) {
return DateTime.now().difference(state.errorTimestamp!) >=
_waitBeforeNextErrorDuration;
}
return true;
}
void reset() {
emit(GlobalErrorState.initial);
}
}
class GlobalErrorState {
static const GlobalErrorState initial = GlobalErrorState();
final ErrorMessage? error;
final DateTime? errorTimestamp;
const GlobalErrorState({this.error, this.errorTimestamp});
bool get hasError => error != null;
}

View File

@@ -1,75 +1,42 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/bloc/global_error_cubit.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/features/labels/model/label.model.dart';
import 'package:paperless_mobile/features/labels/repository/label_repository.dart';
abstract class LabelCubit<T extends Label> extends Cubit<Map<int, T>> {
final LabelRepository labelRepository;
final GlobalErrorCubit errorCubit;
LabelCubit(this.labelRepository, this.errorCubit) : super({});
LabelCubit(this.labelRepository) : super({});
@protected
void loadFrom(Iterable<T> items) =>
emit(Map.fromIterable(items, key: (e) => (e as T).id!));
Future<T> add(
T item, {
bool propagateEventOnError = true,
}) async {
Future<T> add(T item) async {
assert(item.id == null);
try {
final addedItem = await save(item);
final newState = {...state};
newState.putIfAbsent(addedItem.id!, () => addedItem);
emit(newState);
return addedItem;
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
}
return Future.error(error);
}
}
Future<T> replace(
T item, {
bool propagateEventOnError = true,
}) async {
Future<T> replace(T item) async {
assert(item.id != null);
try {
final updatedItem = await update(item);
final newState = {...state};
newState[item.id!] = updatedItem;
emit(newState);
return updatedItem;
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
}
return Future.error(error);
}
}
Future<void> remove(
T item, {
bool propagateEventOnError = true,
}) async {
Future<void> remove(T item) async {
assert(item.id != null);
if (state.containsKey(item.id)) {
try {
final deletedId = await delete(item);
final newState = {...state};
newState.remove(deletedId);
emit(newState);
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
}
return Future.error(error);
}
}
}

View File

@@ -0,0 +1 @@
const supportedFileExtensions = ['pdf', 'png', 'tiff', 'gif', 'jpg', 'jpeg'];

View File

@@ -66,5 +66,7 @@ String translateError(BuildContext context, ErrorCode code) {
return S.of(context).errorMessageRequestTimedOut;
case ErrorCode.unsupportedFileFormat:
return S.of(context).errorMessageUnsupportedFileFormat;
case ErrorCode.missingClientCertificate:
return S.of(context).errorMessageMissingClientCertificate;
}
}

View File

@@ -48,5 +48,6 @@ enum ErrorCode {
createSavedViewError,
deleteSavedViewError,
requestTimedOut,
unsupportedFileFormat;
unsupportedFileFormat,
missingClientCertificate;
}

View File

@@ -1,6 +1,7 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:paperless_mobile/core/bloc/document_status_cubit.dart';
import 'package:paperless_mobile/core/model/document_processing_status.dart';
import 'package:paperless_mobile/di_initializer.dart';
@@ -89,8 +90,11 @@ class LongPollingStatusService implements StatusService {
Uri.parse(
'$httpUrl/api/documents/?query=$documentFileName added:${formatDate(today)}'),
);
final data = PagedSearchResult.fromJson(
jsonDecode(response.body), DocumentModel.fromJson);
final data = await compute(
PagedSearchResult.fromJson,
PagedSearchResultJsonSerializer(
jsonDecode(response.body), DocumentModel.fromJson),
);
if (data.count > 0) {
consumptionFinished = true;
final docId = data.results[0].id;

View File

@@ -1,13 +1,9 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:paperless_mobile/core/logic/timeout_client.dart';
import 'package:flutter/foundation.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:http/http.dart';
import 'package:path_provider/path_provider.dart';
const requestTimeout = Duration(seconds: 5);
@@ -23,7 +19,10 @@ Future<T> getSingleResult<T>(
headers: {'accept': 'application/json; version=$minRequiredApiVersion'},
);
if (response.statusCode == 200) {
return fromJson(jsonDecode(utf8.decode(response.bodyBytes)) as JSON);
return compute(
fromJson,
jsonDecode(utf8.decode(response.bodyBytes)) as JSON,
);
}
return Future.error(errorCode);
}
@@ -45,12 +44,25 @@ Future<List<T>> getCollection<T>(
if (body['count'] == 0) {
return <T>[];
} else {
return body['results']
.cast<JSON>()
.map<T>((result) => fromJson(result))
.toList();
return compute(
_collectionFromJson,
_CollectionFromJsonSerializationParams(
fromJson, (body['results'] as List).cast<JSON>()),
);
}
}
}
return Future.error(errorCode);
}
List<T> _collectionFromJson<T>(
_CollectionFromJsonSerializationParams<T> params) {
return params.list.map<T>((result) => params.fromJson(result)).toList();
}
class _CollectionFromJsonSerializationParams<T> {
final T Function(JSON) fromJson;
final List<JSON> list;
_CollectionFromJsonSerializationParams(this.fromJson, this.list);
}

View File

@@ -1,7 +1,6 @@
import 'dart:typed_data';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/bloc/global_error_cubit.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/model/document.model.dart';
@@ -13,10 +12,8 @@ import 'package:injectable/injectable.dart';
@singleton
class DocumentsCubit extends Cubit<DocumentsState> {
final DocumentRepository documentRepository;
final GlobalErrorCubit errorCubit;
DocumentsCubit(this.documentRepository, this.errorCubit)
: super(DocumentsState.initial);
DocumentsCubit(this.documentRepository) : super(DocumentsState.initial);
Future<void> addDocument(
Uint8List bytes,
@@ -27,9 +24,7 @@ class DocumentsCubit extends Cubit<DocumentsState> {
int? correspondent,
List<int>? tags,
DateTime? createdAt,
bool propagateEventOnError = true,
}) async {
try {
await documentRepository.create(
bytes,
fileName,
@@ -39,197 +34,89 @@ class DocumentsCubit extends Cubit<DocumentsState> {
tags: tags,
createdAt: createdAt,
);
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
} else {
rethrow;
}
}
// documentRepository
// .waitForConsumptionFinished(fileName, title)
// .then((value) => onConsumptionFinished(value));
}
Future<void> removeDocument(
DocumentModel document, {
bool propagateEventOnError = true,
}) async {
try {
Future<void> removeDocument(DocumentModel document) async {
await documentRepository.delete(document);
return await reloadDocuments();
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
} else {
rethrow;
}
}
}
Future<void> bulkRemoveDocuments(List<DocumentModel> documents,
{bool propagateEventOnError = true}) async {
try {
Future<void> bulkRemoveDocuments(List<DocumentModel> documents) async {
await documentRepository.bulkDelete(documents);
return await reloadDocuments();
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
} else {
rethrow;
}
}
}
Future<void> updateDocument(
DocumentModel document, {
bool propagateEventOnError = true,
}) async {
try {
Future<void> updateDocument(DocumentModel document) async {
await documentRepository.update(document);
await reloadDocuments();
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
} else {
rethrow;
}
}
}
Future<void> loadDocuments({
bool propagateEventOnError = true,
}) async {
try {
Future<void> loadDocuments() async {
final result = await documentRepository.find(state.filter);
emit(DocumentsState(
isLoaded: true,
value: [...state.value, result],
filter: state.filter,
));
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
} else {
rethrow;
}
}
}
Future<void> reloadDocuments({
bool propagateEventOnError = true,
}) async {
Future<void> reloadDocuments() async {
if (state.currentPageNumber >= 5) {
return _bulkReloadDocuments();
}
var newPages = <PagedSearchResult>[];
try {
for (final page in state.value) {
final result = await documentRepository
.find(state.filter.copyWith(page: page.pageKey));
newPages.add(result);
}
emit(DocumentsState(
isLoaded: true, value: newPages, filter: state.filter));
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
} else {
rethrow;
}
}
emit(DocumentsState(isLoaded: true, value: newPages, filter: state.filter));
}
Future<void> _bulkReloadDocuments({
bool propagateEventOnError = true,
}) async {
try {
final result = await documentRepository.find(
state.filter.copyWith(page: 1, pageSize: state.documents.length));
emit(DocumentsState(
isLoaded: true, value: [result], filter: state.filter));
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
} else {
rethrow;
}
}
Future<void> _bulkReloadDocuments() async {
final result = await documentRepository
.find(state.filter.copyWith(page: 1, pageSize: state.documents.length));
emit(DocumentsState(isLoaded: true, value: [result], filter: state.filter));
}
Future<void> loadMore({
bool propagateEventOnError = true,
}) async {
Future<void> loadMore() async {
if (state.isLastPageLoaded) {
return;
}
final newFilter = state.filter.copyWith(page: state.filter.page + 1);
try {
final result = await documentRepository.find(newFilter);
emit(DocumentsState(
isLoaded: true, value: [...state.value, result], filter: newFilter));
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
} else {
rethrow;
}
}
}
Future<void> assignAsn(
DocumentModel document, {
bool propagateEventOnError = true,
}) async {
try {
Future<void> assignAsn(DocumentModel document) async {
if (document.archiveSerialNumber == null) {
final int asn = await documentRepository.findNextAsn();
updateDocument(document.copyWith(archiveSerialNumber: asn));
}
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
} else {
rethrow;
}
}
}
///
/// Update filter state and automatically reload documents. Always resets page to 1.
/// Use [DocumentsCubit.loadMore] to load more data.
Future<void> updateFilter(
{final DocumentFilter filter = DocumentFilter.initial,
bool propagateEventOnError = true}) async {
try {
Future<void> updateFilter({
final DocumentFilter filter = DocumentFilter.initial,
}) async {
final result = await documentRepository.find(filter.copyWith(page: 1));
emit(DocumentsState(filter: filter, value: [result], isLoaded: true));
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
} else {
rethrow;
}
}
}
///
/// Convenience method which allows to directly use [DocumentFilter.copyWith] on the current filter.
///
Future<void> updateCurrentFilter(
final DocumentFilter Function(DocumentFilter) transformFn, {
bool propagateEventOnError = true,
}) async {
try {
return updateFilter(filter: transformFn(state.filter));
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
return errorCubit.add(error);
} else {
rethrow;
}
}
}
final DocumentFilter Function(DocumentFilter) transformFn,
) async =>
updateFilter(filter: transformFn(state.filter));
void toggleDocumentSelection(DocumentModel model) {
if (state.selection.contains(model)) {

View File

@@ -1,5 +1,4 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/bloc/global_error_cubit.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/documents/bloc/saved_view_state.dart';
@@ -9,25 +8,13 @@ import 'package:injectable/injectable.dart';
@singleton
class SavedViewCubit extends Cubit<SavedViewState> {
final GlobalErrorCubit errorCubit;
SavedViewCubit(this.errorCubit) : super(SavedViewState(value: {}));
SavedViewCubit() : super(SavedViewState(value: {}));
void selectView(SavedView? view, {bool propagateEventOnError = true}) {
try {
void selectView(SavedView? view) {
emit(SavedViewState(value: state.value, selectedSavedViewId: view?.id));
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
}
rethrow;
}
}
Future<SavedView> add(
SavedView view, {
bool propagateEventOnError = true,
}) async {
try {
Future<SavedView> add(SavedView view) async {
final savedView = await getIt<SavedViewsRepository>().save(view);
emit(
SavedViewState(
@@ -36,19 +23,9 @@ class SavedViewCubit extends Cubit<SavedViewState> {
),
);
return savedView;
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
}
rethrow;
}
}
Future<int> remove(
SavedView view, {
bool propagateEventOnError = true,
}) async {
try {
Future<int> remove(SavedView view) async {
final id = await getIt<SavedViewsRepository>().delete(view);
final newValue = {...state.value};
newValue.removeWhere((key, value) => key == id);
@@ -61,27 +38,12 @@ class SavedViewCubit extends Cubit<SavedViewState> {
),
);
return id;
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
}
rethrow;
}
}
Future<void> initialize({
bool propagateEventOnError = true,
}) async {
try {
Future<void> initialize() async {
final views = await getIt<SavedViewsRepository>().getAll();
final values = {for (var element in views) element.id!: element};
emit(SavedViewState(value: values));
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
}
rethrow;
}
}
void resetSelection() {

View File

@@ -4,6 +4,13 @@ import 'package:paperless_mobile/features/documents/model/document.model.dart';
const pageRegex = r".*page=(\d+).*";
class PagedSearchResultJsonSerializer<T> {
final JSON json;
final T Function(JSON) fromJson;
PagedSearchResultJsonSerializer(this.json, this.fromJson);
}
class PagedSearchResult<T> extends Equatable {
/// Total number of available items
final int count;
@@ -46,12 +53,14 @@ class PagedSearchResult<T> extends Equatable {
});
factory PagedSearchResult.fromJson(
Map<dynamic, dynamic> json, T Function(JSON) fromJson) {
PagedSearchResultJsonSerializer<T> serializer) {
return PagedSearchResult(
count: json['count'],
next: json['next'],
previous: json['previous'],
results: List<JSON>.from(json['results']).map<T>(fromJson).toList(),
count: serializer.json['count'],
next: serializer.json['next'],
previous: serializer.json['previous'],
results: List<JSON>.from(serializer.json['results'])
.map<T>(serializer.fromJson)
.toList(),
);
}

View File

@@ -138,8 +138,10 @@ class DocumentRepositoryImpl implements DocumentRepository {
body: json.encode(doc.toJson()),
headers: {"Content-Type": "application/json"}).timeout(requestTimeout);
if (response.statusCode == 200) {
return DocumentModel.fromJson(
jsonDecode(utf8.decode(response.bodyBytes)));
return compute(
DocumentModel.fromJson,
jsonDecode(utf8.decode(response.bodyBytes)) as JSON,
);
} else {
throw const ErrorMessage(ErrorCode.documentUpdateFailed);
}
@@ -152,11 +154,13 @@ class DocumentRepositoryImpl implements DocumentRepository {
Uri.parse("/api/documents/?$filterParams"),
);
if (response.statusCode == 200) {
final searchResult = PagedSearchResult.fromJson(
return compute(
PagedSearchResult.fromJson,
PagedSearchResultJsonSerializer<DocumentModel>(
jsonDecode(utf8.decode(response.bodyBytes)),
DocumentModel.fromJson,
),
);
return searchResult;
} else {
throw const ErrorMessage(ErrorCode.documentLoadFailed);
}
@@ -261,8 +265,10 @@ class DocumentRepositoryImpl implements DocumentRepository {
Future<DocumentMetaData> getMetaData(DocumentModel document) async {
final response = await httpClient
.get(Uri.parse("/api/documents/${document.id}/metadata/"));
return DocumentMetaData.fromJson(
jsonDecode(utf8.decode(response.bodyBytes)));
return compute(
DocumentMetaData.fromJson,
jsonDecode(utf8.decode(response.bodyBytes)) as JSON,
);
}
@override
@@ -280,10 +286,14 @@ class DocumentRepositoryImpl implements DocumentRepository {
final response = await httpClient
.get(Uri.parse("/api/documents/?more_like=$docId&pageSize=10"));
if (response.statusCode == 200) {
return PagedSearchResult<SimilarDocumentModel>.fromJson(
return (await compute(
PagedSearchResult<SimilarDocumentModel>.fromJson,
PagedSearchResultJsonSerializer(
jsonDecode(utf8.decode(response.bodyBytes)),
SimilarDocumentModel.fromJson,
).results;
),
))
.results;
}
throw const ErrorMessage(ErrorCode.similarQueryError);
}

View File

@@ -198,10 +198,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
child: Text(S
.of(context)
.documentDetailsPageAssignAsnButtonLabel),
onPressed: widget.allowEdit
? () => BlocProvider.of<DocumentsCubit>(context)
.assignAsn(document)
: null,
onPressed:
widget.allowEdit ? () => _assignAsn(document) : null,
),
),
_separator(),
@@ -233,6 +231,14 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
);
}
Future<void> _assignAsn(DocumentModel document) async {
try {
await BlocProvider.of<DocumentsCubit>(context).assignAsn(document);
} on ErrorMessage catch (error) {
showError(context, error);
}
}
Widget _buildDocumentContentView(DocumentModel document, String? match) {
return SingleChildScrollView(
child: _DetailsItem(
@@ -392,21 +398,23 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
);
}
Future<void> _onDelete(DocumentModel document) async {
showDialog(
void _onDelete(DocumentModel document) async {
final delete = await showDialog(
context: context,
builder: (context) =>
DeleteDocumentConfirmationDialog(document: document))
.then((delete) {
if (delete ?? false) {
BlocProvider.of<DocumentsCubit>(context)
.removeDocument(document)
.then((value) {
Navigator.pop(context);
DeleteDocumentConfirmationDialog(document: document),
) ??
false;
if (delete) {
try {
await BlocProvider.of<DocumentsCubit>(context).removeDocument(document);
showSnackBar(context, S.of(context).documentDeleteSuccessMessage);
});
} on ErrorMessage catch (error) {
showError(context, error);
} finally {
Navigator.pop(context);
}
}
});
}
Future<void> _onOpen(DocumentModel document) async {

View File

@@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
@@ -76,10 +77,14 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
setState(() {
_isSubmitLoading = true;
});
try {
await getIt<DocumentsCubit>().updateDocument(updatedDocument);
showSnackBar(context, S.of(context).documentUpdateErrorMessage);
} on ErrorMessage catch (error) {
showError(context, error);
} finally {
Navigator.pop(context);
showSnackBar(
context, "Document successfully updated."); //TODO: INTL
}
}
},
icon: const Icon(Icons.save),

View File

@@ -44,13 +44,20 @@ class _DocumentsPageState extends State<DocumentsPage> {
@override
void initState() {
super.initState();
final documentsCubit = BlocProvider.of<DocumentsCubit>(context);
if (!documentsCubit.state.isLoaded) {
documentsCubit.loadDocuments();
if (!BlocProvider.of<DocumentsCubit>(context).state.isLoaded) {
_initDocuments();
}
_pagingController.addPageRequestListener(_loadNewPage);
}
Future<void> _initDocuments() async {
try {
BlocProvider.of<DocumentsCubit>(context).loadDocuments();
} on ErrorMessage catch (error) {
showError(context, error);
}
}
@override
void dispose() {
_pagingController.dispose();
@@ -64,17 +71,25 @@ class _DocumentsPageState extends State<DocumentsPage> {
if (pageCount <= pageKey + 1) {
_pagingController.nextPageKey = null;
}
documentsCubit.loadMore();
try {
await documentsCubit.loadMore();
} on ErrorMessage catch (error) {
showError(context, error);
}
}
void _onSelected(DocumentModel model) {
BlocProvider.of<DocumentsCubit>(context).toggleDocumentSelection(model);
}
Future<void> _onRefresh() {
final documentsCubit = BlocProvider.of<DocumentsCubit>(context);
return documentsCubit.updateFilter(
filter: documentsCubit.state.filter.copyWith(page: 1));
Future<void> _onRefresh() async {
try {
await BlocProvider.of<DocumentsCubit>(context).updateCurrentFilter(
(filter) => filter.copyWith(page: 1),
);
} on ErrorMessage catch (error) {
showError(context, error);
}
}
@override
@@ -86,9 +101,9 @@ class _DocumentsPageState extends State<DocumentsPage> {
_panelController.close();
return false;
}
final docBloc = BlocProvider.of<DocumentsCubit>(context);
if (docBloc.state.selection.isNotEmpty) {
docBloc.resetSelection();
final documentsCubit = BlocProvider.of<DocumentsCubit>(context);
if (documentsCubit.state.selection.isNotEmpty) {
documentsCubit.resetSelection();
return false;
}
return true;

View File

@@ -513,7 +513,7 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
);
}
void _onApplyFilter() {
void _onApplyFilter() async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
final v = _formKey.currentState!.value;
final docCubit = BlocProvider.of<DocumentsCubit>(context);
@@ -530,13 +530,15 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
addedDateAfter: (v[fkAddedAt] as DateTimeRange?)?.start,
queryType: v[QueryTypeFormField.fkQueryType] as QueryType,
);
BlocProvider.of<DocumentsCubit>(context)
.updateFilter(filter: newFilter)
.then((value) {
try {
await BlocProvider.of<DocumentsCubit>(context)
.updateFilter(filter: newFilter);
BlocProvider.of<SavedViewCubit>(context).resetSelection();
FocusScope.of(context).unfocus();
widget.panelController.close();
});
} on ErrorMessage catch (error) {
showError(context, error);
}
}
}

View File

@@ -75,13 +75,21 @@ class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
void _onDelete(BuildContext context, DocumentsState documentsState) async {
final shouldDelete = await showDialog<bool>(
context: context,
builder: (context) => BulkDeleteConfirmationDialog(state: documentsState),
builder: (context) =>
BulkDeleteConfirmationDialog(state: documentsState),
) ??
false;
if (shouldDelete) {
try {
await BlocProvider.of<DocumentsCubit>(context)
.bulkRemoveDocuments(documentsState.selection);
showSnackBar(
context,
S.of(context).documentsPageBulkDeleteSuccessfulText,
);
if (shouldDelete ?? false) {
BlocProvider.of<DocumentsCubit>(context)
.bulkRemoveDocuments(documentsState.selection)
.then((_) => showSnackBar(
context, S.of(context).documentsPageBulkDeleteSuccessfulText));
} on ErrorMessage catch (error) {
showError(context, error);
}
}
}

View File

@@ -78,15 +78,22 @@ class SavedViewSelectionWidget extends StatelessWidget {
final newView = await Navigator.of(context).push<SavedView?>(
MaterialPageRoute(
builder: (context) => AddSavedViewPage(
currentFilter: getIt<DocumentsCubit>().state.filter),
currentFilter: getIt<DocumentsCubit>().state.filter,
),
),
);
if (newView != null) {
BlocProvider.of<SavedViewCubit>(context).add(newView);
try {
await BlocProvider.of<SavedViewCubit>(context).add(newView);
} on ErrorMessage catch (error) {
showError(context, error);
}
}
}
void _onSelected(bool isSelected, BuildContext context, SavedView view) {
void _onSelected(
bool isSelected, BuildContext context, SavedView view) async {
try {
if (isSelected) {
BlocProvider.of<DocumentsCubit>(context)
.updateFilter(filter: view.toDocumentFilter());
@@ -95,6 +102,9 @@ class SavedViewSelectionWidget extends StatelessWidget {
BlocProvider.of<DocumentsCubit>(context).updateFilter();
BlocProvider.of<SavedViewCubit>(context).selectView(null);
}
} on ErrorMessage catch (error) {
showError(context, error);
}
}
void _onDelete(BuildContext context, SavedView view) async {
@@ -105,7 +115,11 @@ class SavedViewSelectionWidget extends StatelessWidget {
) ??
false;
if (delete) {
try {
BlocProvider.of<SavedViewCubit>(context).remove(view);
} on ErrorMessage catch (error) {
showError(context, error);
}
}
}
}

View File

@@ -43,11 +43,18 @@ class _SortDocumentsButtonState extends State<SortDocumentsButton> {
),
onPressed: () async {
setState(() => _isLoading = true);
BlocProvider.of<DocumentsCubit>(context)
.updateFilter(
filter: state.filter
.copyWith(sortOrder: state.filter.sortOrder.toggle()))
.whenComplete(() => setState(() => _isLoading = false));
try {
await BlocProvider.of<DocumentsCubit>(context)
.updateCurrentFilter(
(filter) => filter.copyWith(
sortOrder: state.filter.sortOrder.toggle(),
),
);
} on ErrorMessage catch (error) {
showError(context, error);
} finally {
setState(() => _isLoading = false);
}
},
);
}

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/bloc/global_error_cubit.dart';
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/core/widgets/offline_banner.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
@@ -74,10 +74,14 @@ class _HomePageState extends State<HomePage> {
}
initializeLabelData(BuildContext context) {
try {
BlocProvider.of<DocumentTypeCubit>(context).initialize();
BlocProvider.of<CorrespondentCubit>(context).initialize();
BlocProvider.of<TagCubit>(context).initialize();
BlocProvider.of<StoragePathCubit>(context).initialize();
BlocProvider.of<SavedViewCubit>(context).initialize();
} on ErrorMessage catch (error) {
showError(context, error);
}
}
}

View File

@@ -1,8 +1,6 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/bloc/global_error_cubit.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
@@ -129,14 +127,16 @@ class InfoDrawer extends StatelessWidget {
leading: const Icon(Icons.logout),
title: Text(S.of(context).appDrawerLogoutLabel),
onTap: () {
// Clear all bloc data
try {
BlocProvider.of<AuthenticationCubit>(context).logout();
getIt<DocumentsCubit>().reset();
getIt<CorrespondentCubit>().reset();
getIt<DocumentTypeCubit>().reset();
getIt<TagCubit>().reset();
getIt<DocumentScannerCubit>().reset();
getIt<GlobalErrorCubit>().reset();
} on ErrorMessage catch (error) {
showError(context, error);
}
},
),
const Divider(),

View File

@@ -4,7 +4,7 @@ import 'package:injectable/injectable.dart';
@singleton
class CorrespondentCubit extends LabelCubit<Correspondent> {
CorrespondentCubit(super.metaDataService, super.errorCubit);
CorrespondentCubit(super.metaDataService);
@override
Future<void> initialize() async {

View File

@@ -18,21 +18,28 @@ class EditCorrespondentPage extends StatelessWidget {
return EditLabelPage<Correspondent>(
label: correspondent,
onSubmit: BlocProvider.of<CorrespondentCubit>(context).replace,
onDelete: (correspondent) => _onDelete(correspondent, context),
onDelete: (correspondent) => _onDelete(context, correspondent),
fromJson: Correspondent.fromJson,
);
}
Future<void> _onDelete(
Correspondent correspondent, BuildContext context) async {
BuildContext context,
Correspondent correspondent,
) async {
try {
await BlocProvider.of<CorrespondentCubit>(context).remove(correspondent);
final cubit = BlocProvider.of<DocumentsCubit>(context);
if (cubit.state.filter.correspondent.id == correspondent.id) {
cubit.updateFilter(
filter: cubit.state.filter
.copyWith(correspondent: const CorrespondentQuery.unset()),
await cubit.updateCurrentFilter(
(filter) => filter.copyWith(
correspondent: const CorrespondentQuery.unset(),
),
);
}
Navigator.pop(context);
} on ErrorMessage catch (error) {
showError(context, error);
}
}
}

View File

@@ -1,10 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/correspondent_query.dart';
import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
import 'package:paperless_mobile/features/labels/correspondent/model/correspondent.model.dart';
import 'package:paperless_mobile/util.dart';
class CorrespondentWidget extends StatelessWidget {
final int? correspondentId;
@@ -44,6 +46,7 @@ class CorrespondentWidget extends StatelessWidget {
void _addCorrespondentToFilter(BuildContext context) {
final cubit = BlocProvider.of<DocumentsCubit>(context);
try {
if (cubit.state.filter.correspondent.id == correspondentId) {
cubit.updateCurrentFilter(
(filter) =>
@@ -56,5 +59,8 @@ class CorrespondentWidget extends StatelessWidget {
);
}
afterSelected?.call();
} on ErrorMessage catch (error) {
showError(context, error);
}
}
}

View File

@@ -5,7 +5,7 @@ import 'package:injectable/injectable.dart';
@singleton
class DocumentTypeCubit extends LabelCubit<DocumentType> {
DocumentTypeCubit(super.metaDataService, super.errorCubit);
DocumentTypeCubit(super.metaDataService);
@override
Future<void> initialize() async {

View File

@@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/document_type_query.dart';
import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
import 'package:paperless_mobile/features/labels/document_type/model/document_type.model.dart';
import 'package:paperless_mobile/util.dart';
class DocumentTypeWidget extends StatelessWidget {
final int? documentTypeId;
@@ -39,6 +41,7 @@ class DocumentTypeWidget extends StatelessWidget {
void _addDocumentTypeToFilter(BuildContext context) {
final cubit = BlocProvider.of<DocumentsCubit>(context);
try {
if (cubit.state.filter.documentType.id == documentTypeId) {
cubit.updateCurrentFilter(
(filter) =>
@@ -51,5 +54,8 @@ class DocumentTypeWidget extends StatelessWidget {
);
}
afterSelected?.call();
} on ErrorMessage catch (error) {
showError(context, error);
}
}
}

View File

@@ -4,7 +4,7 @@ import 'package:paperless_mobile/features/labels/storage_path/model/storage_path
@singleton
class StoragePathCubit extends LabelCubit<StoragePath> {
StoragePathCubit(super.metaDataService, super.errorCubit);
StoragePathCubit(super.metaDataService);
@override
Future<void> initialize() async {

View File

@@ -32,13 +32,19 @@ class EditStoragePathPage extends StatelessWidget {
}
Future<void> _onDelete(StoragePath path, BuildContext context) async {
try {
await BlocProvider.of<StoragePathCubit>(context).remove(path);
final cubit = BlocProvider.of<DocumentsCubit>(context);
if (cubit.state.filter.storagePath.id == path.id) {
cubit.updateFilter(
filter: cubit.state.filter
.copyWith(storagePath: const StoragePathQuery.unset()));
cubit.updateCurrentFilter(
(filter) => filter.copyWith(
storagePath: const StoragePathQuery.unset(),
),
);
}
Navigator.pop(context);
} on ErrorMessage catch (error) {
showError(context, error);
}
}
}

View File

@@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/storage_path_query.dart';
import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart';
import 'package:paperless_mobile/features/labels/storage_path/model/storage_path.model.dart';
import 'package:paperless_mobile/util.dart';
class StoragePathWidget extends StatelessWidget {
final int? pathId;
@@ -43,6 +45,7 @@ class StoragePathWidget extends StatelessWidget {
void _addStoragePathToFilter(BuildContext context) {
final cubit = BlocProvider.of<DocumentsCubit>(context);
try {
if (cubit.state.filter.correspondent.id == pathId) {
cubit.updateCurrentFilter(
(filter) =>
@@ -55,5 +58,8 @@ class StoragePathWidget extends StatelessWidget {
);
}
afterSelected?.call();
} on ErrorMessage catch (error) {
showError(context, error);
}
}
}

View File

@@ -4,7 +4,7 @@ import 'package:injectable/injectable.dart';
@singleton
class TagCubit extends LabelCubit<Tag> {
TagCubit(super.metaDataService, super.errorCubit);
TagCubit(super.metaDataService);
@override
Future<void> initialize() async {

View File

@@ -43,6 +43,7 @@ class EditTagPage extends StatelessWidget {
}
Future<void> _onDelete(Tag tag, BuildContext context) async {
try {
await BlocProvider.of<TagCubit>(context).remove(tag);
final cubit = BlocProvider.of<DocumentsCubit>(context);
final currentFilter = cubit.state.filter;
@@ -56,5 +57,8 @@ class EditTagPage extends StatelessWidget {
}
cubit.updateFilter(filter: updatedFilter);
Navigator.pop(context);
} on ErrorMessage catch (error) {
showError(context, error);
}
}
}

View File

@@ -38,11 +38,13 @@ class TagWidget extends StatelessWidget {
void _addTagToFilter(BuildContext context) {
final cubit = BlocProvider.of<DocumentsCubit>(context);
try {
if (cubit.state.filter.tags.ids.contains(tag.id)) {
cubit.updateCurrentFilter(
(filter) => filter.copyWith(
tags: TagsQuery.fromIds(
cubit.state.filter.tags.ids.where((id) => id != tag.id).toList()),
tags: TagsQuery.fromIds(cubit.state.filter.tags.ids
.where((id) => id != tag.id)
.toList()),
),
);
} else {
@@ -55,5 +57,8 @@ class TagWidget extends StatelessWidget {
if (afterTagTapped != null) {
afterTagTapped!();
}
} on ErrorMessage catch (error) {
showError(context, error);
}
}
}

View File

@@ -3,11 +3,13 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/labels/document_type/model/matching_algorithm.dart';
import 'package:paperless_mobile/features/labels/model/label.model.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
class EditLabelPage<T extends Label> extends StatefulWidget {
final T label;
@@ -144,6 +146,8 @@ class _EditLabelPageState<T extends Label> extends State<EditLabelPage<T>> {
Navigator.pop(context);
} on PaperlessValidationErrors catch (errorMessages) {
setState(() => _errors = errorMessages);
} on ErrorMessage catch (error) {
showError(context, error);
}
}
}

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/bloc/global_error_cubit.dart';
import 'package:paperless_mobile/core/bloc/label_bloc_provider.dart';
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
@@ -53,9 +52,8 @@ class LabelItem<T extends Label> extends StatelessWidget {
MaterialPageRoute(
builder: (context) => LabelBlocProvider(
child: BlocProvider(
create: (context) => DocumentsCubit(
getIt<DocumentRepository>(),
getIt<GlobalErrorCubit>())
create: (context) =>
DocumentsCubit(getIt<DocumentRepository>())
..updateFilter(filter: filter),
child: LinkedDocumentsPreview(filter: filter),
),

View File

@@ -1,7 +1,7 @@
import 'dart:developer';
import 'dart:io';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/bloc/global_error_cubit.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/core/store/local_vault.dart';
import 'package:paperless_mobile/di_initializer.dart';
@@ -17,13 +17,11 @@ const authenticationKey = "authentication";
@singleton
class AuthenticationCubit extends Cubit<AuthenticationState> {
final LocalVault localStore;
final GlobalErrorCubit errorCubit;
final AuthenticationService authenticationService;
AuthenticationCubit(
this.localStore,
this.authenticationService,
this.errorCubit,
) : super(AuthenticationState.initial);
Future<void> initialize() {
@@ -34,7 +32,6 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
required UserCredentials credentials,
required String serverUrl,
ClientCertificate? clientCertificate,
bool propagateEventOnError = true,
}) async {
assert(credentials.username != null && credentials.password != null);
try {
@@ -75,40 +72,23 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
} on TlsException catch (_) {
const error =
ErrorMessage(ErrorCode.invalidClientCertificateConfiguration);
if (propagateEventOnError) {
errorCubit.add(error);
}
throw error;
} on SocketException catch (err) {
late ErrorMessage error;
if (err.message.contains("connection timed out")) {
error = const ErrorMessage(ErrorCode.requestTimedOut);
throw const ErrorMessage(ErrorCode.requestTimedOut);
} else {
error = ErrorMessage.unknown();
throw ErrorMessage.unknown();
}
if (propagateEventOnError) {
errorCubit.add(error);
}
rethrow;
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
}
rethrow;
}
}
Future<void> restoreSessionState({
bool propagateEventOnError = true,
}) async {
try {
Future<void> restoreSessionState() async {
final storedAuth = await localStore.loadAuthenticationInformation();
final appSettings = await localStore.loadApplicationSettings() ??
ApplicationSettingsState.defaultSettings;
if (storedAuth == null || !storedAuth.isValid) {
emit(
AuthenticationState(isAuthenticated: false, wasLoginStored: false));
emit(AuthenticationState(isAuthenticated: false, wasLoginStored: false));
} else {
if (!appSettings.isLocalAuthenticationEnabled ||
await authenticationService
@@ -122,13 +102,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
),
);
} else {
emit(AuthenticationState(
isAuthenticated: false, wasLoginStored: true));
}
}
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
emit(AuthenticationState(isAuthenticated: false, wasLoginStored: true));
}
}
}

View File

@@ -26,10 +26,19 @@ class AuthenticationService {
required String password,
required String serverUrl,
}) async {
final response = await httpClient.post(
late Response response;
try {
response = await httpClient.post(
Uri.parse("/api/token/"),
body: {"username": username, "password": password},
);
} on FormatException catch (e) {
final source = e.source;
if (source is String &&
source.contains("400 No required SSL certificate was sent")) {
throw const ErrorMessage(ErrorCode.missingClientCertificate);
}
}
if (response.statusCode == 200) {
final data = jsonDecode(utf8.decode(response.bodyBytes));
return data['token'];

View File

@@ -1,12 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
import 'package:paperless_mobile/features/login/view/widgets/client_certificate_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/server_address_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/user_credentials_form_field.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
class LoginPage extends StatefulWidget {
const LoginPage({Key? key}) : super(key: key);
@@ -72,7 +74,8 @@ class _LoginPageState extends State<LoginPage> {
return ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStatePropertyAll(
Theme.of(context).colorScheme.primaryContainer),
Theme.of(context).colorScheme.primaryContainer,
),
elevation: const MaterialStatePropertyAll(0),
),
onPressed: _login,
@@ -82,19 +85,25 @@ class _LoginPageState extends State<LoginPage> {
);
}
void _login() {
void _login() async {
FocusScope.of(context).unfocus();
if (_formKey.currentState?.saveAndValidate() ?? false) {
setState(() => _isLoginLoading = true);
final form = _formKey.currentState?.value;
BlocProvider.of<AuthenticationCubit>(context)
.login(
credentials: form?[UserCredentialsFormField.fkCredentials],
serverUrl: form?[ServerAddressFormField.fkServerAddress],
final form = _formKey.currentState!.value;
try {
await BlocProvider.of<AuthenticationCubit>(context).login(
credentials: form[UserCredentialsFormField.fkCredentials],
serverUrl: form[ServerAddressFormField.fkServerAddress],
clientCertificate:
form?[ClientCertificateFormField.fkClientCertificate],
)
.whenComplete(() => setState(() => _isLoginLoading = false));
form[ClientCertificateFormField.fkClientCertificate],
);
} on ErrorMessage catch (error) {
showError(context, error);
} catch (unknownError) {
showSnackBar(context, unknownError.toString());
} finally {
setState(() => _isLoginLoading = false);
}
}
}
}

View File

@@ -22,19 +22,22 @@ class DocumentScannerCubit extends Cubit<List<File>> {
scans.removeAt(fileIndex);
emit(scans);
} catch (_) {
addError(const ErrorMessage(ErrorCode.scanRemoveFailed));
throw const ErrorMessage(ErrorCode.scanRemoveFailed);
}
}
void reset() {
try {
for (final doc in state) {
doc.deleteSync();
if (kDebugMode) {
log('[ScannerCubit]: Removed ${doc.path}');
}
}
imageCache.clear();
emit(initialState);
} catch (_) {
throw const ErrorMessage(ErrorCode.scanRemoveFailed);
}
}
}

View File

@@ -185,51 +185,32 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
}
void _onSubmit() async {
_formKey.currentState?.save();
if (_formKey.currentState?.validate() ?? false) {
if (_formKey.currentState?.saveAndValidate() ?? false) {
try {
setState(() {
_isUploadLoading = true;
});
setState(() => _isUploadLoading = true);
final fv = _formKey.currentState!.value;
final createdAt = fv[DocumentModel.createdKey] as DateTime?;
final title = fv[DocumentModel.titleKey] as String;
final docType = fv[DocumentModel.documentTypeKey] as IdQueryParameter;
final tags = fv[DocumentModel.tagsKey] as TagsQuery;
final correspondent =
fv[DocumentModel.correspondentKey] as IdQueryParameter;
await BlocProvider.of<DocumentsCubit>(context).addDocument(
widget.fileBytes,
_formKey.currentState?.value[fkFileName],
onConsumptionFinished: (document) {
ScaffoldMessenger.of(rootScaffoldKey.currentContext!).showSnackBar(
SnackBar(
action: SnackBarAction(
onPressed: () {
getIt<DocumentsCubit>().reloadDocuments();
},
label: S
.of(context)
.documentUploadProcessingSuccessfulReloadActionText,
),
content:
Text(S.of(context).documentUploadProcessingSuccessfulText),
),
onConsumptionFinished: _onConsumptionFinished,
title: title,
documentType: docType.id,
correspondent: correspondent.id,
tags: tags.ids,
createdAt: createdAt,
);
},
title: _formKey.currentState?.value[DocumentModel.titleKey],
documentType: (_formKey.currentState
?.value[DocumentModel.documentTypeKey] as IdQueryParameter)
.id,
correspondent: (_formKey.currentState
?.value[DocumentModel.correspondentKey] as IdQueryParameter)
.id,
tags:
(_formKey.currentState?.value[DocumentModel.tagsKey] as TagsQuery)
.ids,
createdAt: (_formKey.currentState?.value[DocumentModel.createdKey]
as DateTime?),
propagateEventOnError: false,
);
setState(() {
_isUploadLoading = false;
});
getIt<DocumentScannerCubit>().reset();
Navigator.pop(context);
getIt<DocumentScannerCubit>().reset(); //TODO: Access via provider
showSnackBar(context, S.of(context).documentUploadSuccessText);
Navigator.pop(context);
widget.afterUpload?.call();
} on ErrorMessage catch (error) {
showError(context, error);
@@ -239,9 +220,28 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
showSnackBar(context, other.toString());
} finally {
setState(() {
_isUploadLoading = true;
_isUploadLoading = false;
});
}
}
}
void _onConsumptionFinished(document) {
ScaffoldMessenger.of(rootScaffoldKey.currentContext!).showSnackBar(
SnackBar(
action: SnackBarAction(
onPressed: () async {
try {
getIt<DocumentsCubit>().reloadDocuments();
} on ErrorMessage catch (error) {
showError(context, error);
}
},
label:
S.of(context).documentUploadProcessingSuccessfulReloadActionText,
),
content: Text(S.of(context).documentUploadProcessingSuccessfulText),
),
);
}
}

View File

@@ -8,8 +8,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:mime/mime.dart';
import 'package:paperless_mobile/core/bloc/global_error_cubit.dart';
import 'package:paperless_mobile/core/bloc/label_bloc_provider.dart';
import 'package:paperless_mobile/core/global/constants.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/di_initializer.dart';
@@ -19,6 +19,7 @@ import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart'
import 'package:paperless_mobile/features/scan/view/document_upload_page.dart';
import 'package:paperless_mobile/features/scan/view/widgets/grid_image_item_widget.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:permission_handler/permission_handler.dart';
@@ -32,15 +33,6 @@ class ScannerPage extends StatefulWidget {
class _ScannerPageState extends State<ScannerPage>
with SingleTickerProviderStateMixin {
static const _supportedExtensions = [
'pdf',
'png',
'tiff',
'gif',
'jpg',
'jpeg'
];
late final AnimationController _fabPulsingController;
late final Animation _animation;
@@ -205,8 +197,14 @@ class _ScannerPageState extends State<ScannerPage>
itemBuilder: (context, index) {
return GridImageItemWidget(
file: scans[index],
onDelete: () => BlocProvider.of<DocumentScannerCubit>(context)
.removeScan(index),
onDelete: () async {
try {
BlocProvider.of<DocumentScannerCubit>(context)
.removeScan(index);
} on ErrorMessage catch (error) {
showError(context, error);
}
},
index: index,
totalNumberOfFiles: scans.length,
);
@@ -214,7 +212,11 @@ class _ScannerPageState extends State<ScannerPage>
}
void _reset(BuildContext context) {
try {
BlocProvider.of<DocumentScannerCubit>(context).reset();
} on ErrorMessage catch (error) {
showError(context, error);
}
}
Future<void> _requestCameraPermissions() async {
@@ -227,15 +229,14 @@ class _ScannerPageState extends State<ScannerPage>
void _onUploadFromFilesystem() async {
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: _supportedExtensions,
allowedExtensions: supportedFileExtensions,
withData: true,
);
if (result?.files.single.path != null) {
File file = File(result!.files.single.path!);
if (!_supportedExtensions.contains(file.path.split('.').last)) {
return getIt<GlobalErrorCubit>().add(
const ErrorMessage(ErrorCode.unsupportedFileFormat),
);
if (!supportedFileExtensions.contains(file.path.split('.').last)) {
//TODO: Show error message;
return;
}
final mimeType = lookupMimeType(file.path) ?? '';
late Uint8List fileBytes;

View File

@@ -187,5 +187,7 @@
"editLabelPageConfirmDeletionDialogTitle": "Löschen bestätigen",
"editLabelPageDeletionDialogText": "Dieser Kennzeichner wird von Dokumenten referenziert. Durch das Löschen dieses Kennzeichners werden alle Referenzen entfernt. Fortfahren?",
"settingsPageStorageSettingsLabel": "Storage",
"settingsPageStorageSettingsDescriptionText": "Manage files and storage space"
"settingsPageStorageSettingsDescriptionText": "Manage files and storage space",
"documentUpdateErrorMessage": "Document successfully updated.",
"errorMessageMissingClientCertificate": "Ein Client Zerfitikat wurde erwartet, aber nicht gesendet. Bitte konfiguriere ein gültiges Zertifikat."
}

View File

@@ -188,5 +188,7 @@
"editLabelPageConfirmDeletionDialogTitle": "Confirm deletion",
"editLabelPageDeletionDialogText": "This label contains references to other documents. By deleting this label, all references will be removed. Continue?",
"settingsPageStorageSettingsLabel": "Storage",
"settingsPageStorageSettingsDescriptionText": "Manage files and storage space"
"settingsPageStorageSettingsDescriptionText": "Manage files and storage space",
"documentUpdateErrorMessage": "Document successfully updated.",
"errorMessageMissingClientCertificate": "A client certificate was expected but not sent. Please provide a valid client certificate."
}

View File

@@ -9,11 +9,12 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/bloc/global_error_cubit.dart';
import 'package:paperless_mobile/core/bloc/label_bloc_provider.dart';
import 'package:paperless_mobile/core/global/asset_images.dart';
import 'package:paperless_mobile/core/global/constants.dart';
import 'package:paperless_mobile/core/global/http_self_signed_certificate_override.dart';
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart';
import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/core/util.dart';
import 'package:paperless_mobile/di_initializer.dart';
@@ -148,51 +149,46 @@ class AuthenticationWrapper extends StatefulWidget {
}
class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
bool isFileTypeSupported(SharedMediaFile file) {
return supportedFileExtensions.contains(file.path.split('.').last);
}
void handleReceivedFiles(List<SharedMediaFile> files) async {
if (files.isEmpty) {
return;
}
if (!isFileTypeSupported(files.first)) {
showError(context, const ErrorMessage(ErrorCode.unsupportedFileFormat));
await Future.delayed(
const Duration(seconds: 2),
() => SystemNavigator.pop(),
);
}
final bytes = File(files.first.path).readAsBytesSync();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BlocProvider.value(
value: getIt<DocumentsCubit>(),
child: LabelBlocProvider(
child: DocumentUploadPage(
fileBytes: bytes,
afterUpload: () => SystemNavigator.pop(),
),
),
),
),
);
}
@override
void initState() {
super.initState();
// For sharing files coming from outside the app while the app is still opened
ReceiveSharingIntent.getMediaStream().listen((List<SharedMediaFile> value) {
final bytes = File(value.first.path).readAsBytesSync();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BlocProvider.value(
value: getIt<DocumentsCubit>(),
child: LabelBlocProvider(
child: DocumentUploadPage(
fileBytes: bytes,
afterUpload: () => SystemNavigator.pop(),
),
),
),
),
);
}, onError: (err) {
log(err);
});
ReceiveSharingIntent.getMediaStream().listen(handleReceivedFiles);
// For sharing files coming from outside the app while the app is closed
ReceiveSharingIntent.getInitialMedia().then((List<SharedMediaFile> value) {
if (value.isEmpty) {
return;
}
final bytes = File(value.first.path).readAsBytesSync();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BlocProvider.value(
value: getIt<DocumentsCubit>(),
child: LabelBlocProvider(
child: DocumentUploadPage(
fileBytes: bytes,
afterUpload: () => SystemNavigator.pop(),
),
),
),
),
);
});
ReceiveSharingIntent.getInitialMedia().then(handleReceivedFiles);
}
@override
@@ -206,15 +202,7 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: getIt<GlobalErrorCubit>(),
child: BlocListener<GlobalErrorCubit, GlobalErrorState>(
listener: (context, state) {
if (state.hasError) {
showSnackBar(context, translateError(context, state.error!.code));
}
},
child: SafeArea(
return SafeArea(
top: true,
left: false,
right: false,
@@ -246,8 +234,6 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
}
},
),
),
),
);
}
}

View File

@@ -1,5 +1,4 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:paperless_mobile/core/bloc/global_error_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/model/document.model.dart';
@@ -15,32 +14,32 @@ import 'package:mockito/mockito.dart';
import '../../utils.dart';
@GenerateNiceMocks([MockSpec<DocumentRepository>()])
@GenerateNiceMocks([MockSpec<GlobalErrorCubit>()])
import 'document_cubit_test.mocks.dart';
void main() async {
TestWidgetsFlutterBinding.ensureInitialized();
final List<DocumentModel> documents = List.unmodifiable(
await loadCollection("test/fixtures/documents/documents.json", DocumentModel.fromJson),
await loadCollection(
"test/fixtures/documents/documents.json", DocumentModel.fromJson),
);
final List<Tag> tags = List.unmodifiable(
await loadCollection("test/fixtures/tags/tags.json", Tag.fromJson),
);
final List<Correspondent> correspondents = List.unmodifiable(
await loadCollection(
"test/fixtures/correspondents/correspondents.json", Correspondent.fromJson),
await loadCollection("test/fixtures/correspondents/correspondents.json",
Correspondent.fromJson),
);
final List<DocumentType> documentTypes = List.unmodifiable(
await loadCollection("test/fixtures/document_types/document_types.json", DocumentType.fromJson),
await loadCollection("test/fixtures/document_types/document_types.json",
DocumentType.fromJson),
);
final MockDocumentRepository documentRepository = MockDocumentRepository();
final MockGlobalErrorCubit globalErrorCubit = MockGlobalErrorCubit();
group("Test DocumentsCubit reloadDocuments", () {
test("Assert correct initial state", () {
expect(DocumentsCubit(documentRepository, globalErrorCubit).state, DocumentsState.initial);
expect(DocumentsCubit(documentRepository).state, DocumentsState.initial);
});
blocTest<DocumentsCubit, DocumentsState>(
@@ -53,7 +52,7 @@ void main() async {
results: documents,
),
),
build: () => DocumentsCubit(documentRepository, globalErrorCubit),
build: () => DocumentsCubit(documentRepository),
seed: () => DocumentsState.initial,
act: (bloc) => bloc.loadDocuments(),
expect: () => [
@@ -82,7 +81,7 @@ void main() async {
results: documents,
),
),
build: () => DocumentsCubit(documentRepository, globalErrorCubit),
build: () => DocumentsCubit(documentRepository),
seed: () => DocumentsState.initial,
act: (bloc) => bloc.loadDocuments(),
expect: () => [