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/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.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/model/label.model.dart';
import 'package:paperless_mobile/features/labels/repository/label_repository.dart'; import 'package:paperless_mobile/features/labels/repository/label_repository.dart';
abstract class LabelCubit<T extends Label> extends Cubit<Map<int, T>> { abstract class LabelCubit<T extends Label> extends Cubit<Map<int, T>> {
final LabelRepository labelRepository; final LabelRepository labelRepository;
final GlobalErrorCubit errorCubit;
LabelCubit(this.labelRepository, this.errorCubit) : super({}); LabelCubit(this.labelRepository) : super({});
@protected @protected
void loadFrom(Iterable<T> items) => void loadFrom(Iterable<T> items) =>
emit(Map.fromIterable(items, key: (e) => (e as T).id!)); emit(Map.fromIterable(items, key: (e) => (e as T).id!));
Future<T> add( Future<T> add(T item) async {
T item, {
bool propagateEventOnError = true,
}) async {
assert(item.id == null); assert(item.id == null);
try { final addedItem = await save(item);
final addedItem = await save(item); final newState = {...state};
final newState = {...state}; newState.putIfAbsent(addedItem.id!, () => addedItem);
newState.putIfAbsent(addedItem.id!, () => addedItem); emit(newState);
emit(newState); return addedItem;
return addedItem;
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
}
return Future.error(error);
}
} }
Future<T> replace( Future<T> replace(T item) async {
T item, {
bool propagateEventOnError = true,
}) async {
assert(item.id != null); assert(item.id != null);
try { final updatedItem = await update(item);
final updatedItem = await update(item); final newState = {...state};
final newState = {...state}; newState[item.id!] = updatedItem;
newState[item.id!] = updatedItem; emit(newState);
emit(newState); return updatedItem;
return updatedItem;
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
}
return Future.error(error);
}
} }
Future<void> remove( Future<void> remove(T item) async {
T item, {
bool propagateEventOnError = true,
}) async {
assert(item.id != null); assert(item.id != null);
if (state.containsKey(item.id)) { if (state.containsKey(item.id)) {
try { final deletedId = await delete(item);
final deletedId = await delete(item); final newState = {...state};
final newState = {...state}; newState.remove(deletedId);
newState.remove(deletedId); emit(newState);
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; return S.of(context).errorMessageRequestTimedOut;
case ErrorCode.unsupportedFileFormat: case ErrorCode.unsupportedFileFormat:
return S.of(context).errorMessageUnsupportedFileFormat; return S.of(context).errorMessageUnsupportedFileFormat;
case ErrorCode.missingClientCertificate:
return S.of(context).errorMessageMissingClientCertificate;
} }
} }

View File

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

View File

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

View File

@@ -1,13 +1,9 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'package:flutter/foundation.dart';
import 'dart:typed_data';
import 'package:paperless_mobile/core/logic/timeout_client.dart';
import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/core/type/types.dart'; import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/di_initializer.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:path_provider/path_provider.dart';
const requestTimeout = Duration(seconds: 5); const requestTimeout = Duration(seconds: 5);
@@ -23,7 +19,10 @@ Future<T> getSingleResult<T>(
headers: {'accept': 'application/json; version=$minRequiredApiVersion'}, headers: {'accept': 'application/json; version=$minRequiredApiVersion'},
); );
if (response.statusCode == 200) { 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); return Future.error(errorCode);
} }
@@ -45,12 +44,25 @@ Future<List<T>> getCollection<T>(
if (body['count'] == 0) { if (body['count'] == 0) {
return <T>[]; return <T>[];
} else { } else {
return body['results'] return compute(
.cast<JSON>() _collectionFromJson,
.map<T>((result) => fromJson(result)) _CollectionFromJsonSerializationParams(
.toList(); fromJson, (body['results'] as List).cast<JSON>()),
);
} }
} }
} }
return Future.error(errorCode); 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 'dart:typed_data';
import 'package:flutter_bloc/flutter_bloc.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/core/model/error_message.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/model/document.model.dart'; import 'package:paperless_mobile/features/documents/model/document.model.dart';
@@ -13,10 +12,8 @@ import 'package:injectable/injectable.dart';
@singleton @singleton
class DocumentsCubit extends Cubit<DocumentsState> { class DocumentsCubit extends Cubit<DocumentsState> {
final DocumentRepository documentRepository; final DocumentRepository documentRepository;
final GlobalErrorCubit errorCubit;
DocumentsCubit(this.documentRepository, this.errorCubit) DocumentsCubit(this.documentRepository) : super(DocumentsState.initial);
: super(DocumentsState.initial);
Future<void> addDocument( Future<void> addDocument(
Uint8List bytes, Uint8List bytes,
@@ -27,209 +24,99 @@ class DocumentsCubit extends Cubit<DocumentsState> {
int? correspondent, int? correspondent,
List<int>? tags, List<int>? tags,
DateTime? createdAt, DateTime? createdAt,
bool propagateEventOnError = true,
}) async { }) async {
try { await documentRepository.create(
await documentRepository.create( bytes,
bytes, fileName,
fileName, title: title,
title: title, documentType: documentType,
documentType: documentType, correspondent: correspondent,
correspondent: correspondent, tags: tags,
tags: tags, createdAt: createdAt,
createdAt: createdAt, );
);
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
} else {
rethrow;
}
}
// documentRepository // documentRepository
// .waitForConsumptionFinished(fileName, title) // .waitForConsumptionFinished(fileName, title)
// .then((value) => onConsumptionFinished(value)); // .then((value) => onConsumptionFinished(value));
} }
Future<void> removeDocument( Future<void> removeDocument(DocumentModel document) async {
DocumentModel document, { await documentRepository.delete(document);
bool propagateEventOnError = true, return await reloadDocuments();
}) async {
try {
await documentRepository.delete(document);
return await reloadDocuments();
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
} else {
rethrow;
}
}
} }
Future<void> bulkRemoveDocuments(List<DocumentModel> documents, Future<void> bulkRemoveDocuments(List<DocumentModel> documents) async {
{bool propagateEventOnError = true}) async { await documentRepository.bulkDelete(documents);
try { return await reloadDocuments();
await documentRepository.bulkDelete(documents);
return await reloadDocuments();
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
} else {
rethrow;
}
}
} }
Future<void> updateDocument( Future<void> updateDocument(DocumentModel document) async {
DocumentModel document, { await documentRepository.update(document);
bool propagateEventOnError = true, await reloadDocuments();
}) async {
try {
await documentRepository.update(document);
await reloadDocuments();
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
} else {
rethrow;
}
}
} }
Future<void> loadDocuments({ Future<void> loadDocuments() async {
bool propagateEventOnError = true, final result = await documentRepository.find(state.filter);
}) async { emit(DocumentsState(
try { isLoaded: true,
final result = await documentRepository.find(state.filter); value: [...state.value, result],
emit(DocumentsState( filter: state.filter,
isLoaded: true, ));
value: [...state.value, result],
filter: state.filter,
));
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
} else {
rethrow;
}
}
} }
Future<void> reloadDocuments({ Future<void> reloadDocuments() async {
bool propagateEventOnError = true,
}) async {
if (state.currentPageNumber >= 5) { if (state.currentPageNumber >= 5) {
return _bulkReloadDocuments(); return _bulkReloadDocuments();
} }
var newPages = <PagedSearchResult>[]; var newPages = <PagedSearchResult>[];
try { for (final page in state.value) {
for (final page in state.value) { final result = await documentRepository
final result = await documentRepository .find(state.filter.copyWith(page: page.pageKey));
.find(state.filter.copyWith(page: page.pageKey)); newPages.add(result);
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({ Future<void> _bulkReloadDocuments() async {
bool propagateEventOnError = true, final result = await documentRepository
}) async { .find(state.filter.copyWith(page: 1, pageSize: state.documents.length));
try { emit(DocumentsState(isLoaded: true, value: [result], filter: state.filter));
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> loadMore({ Future<void> loadMore() async {
bool propagateEventOnError = true,
}) async {
if (state.isLastPageLoaded) { if (state.isLastPageLoaded) {
return; return;
} }
final newFilter = state.filter.copyWith(page: state.filter.page + 1); final newFilter = state.filter.copyWith(page: state.filter.page + 1);
try { final result = await documentRepository.find(newFilter);
final result = await documentRepository.find(newFilter); emit(DocumentsState(
emit(DocumentsState( isLoaded: true, value: [...state.value, result], filter: newFilter));
isLoaded: true, value: [...state.value, result], filter: newFilter));
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
errorCubit.add(error);
} else {
rethrow;
}
}
} }
Future<void> assignAsn( Future<void> assignAsn(DocumentModel document) async {
DocumentModel document, { if (document.archiveSerialNumber == null) {
bool propagateEventOnError = true, final int asn = await documentRepository.findNextAsn();
}) async { updateDocument(document.copyWith(archiveSerialNumber: asn));
try {
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. /// Update filter state and automatically reload documents. Always resets page to 1.
/// Use [DocumentsCubit.loadMore] to load more data. /// Use [DocumentsCubit.loadMore] to load more data.
Future<void> updateFilter( Future<void> updateFilter({
{final DocumentFilter filter = DocumentFilter.initial, final DocumentFilter filter = DocumentFilter.initial,
bool propagateEventOnError = true}) async { }) async {
try { final result = await documentRepository.find(filter.copyWith(page: 1));
final result = await documentRepository.find(filter.copyWith(page: 1)); emit(DocumentsState(filter: filter, value: [result], isLoaded: true));
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. /// Convenience method which allows to directly use [DocumentFilter.copyWith] on the current filter.
/// ///
Future<void> updateCurrentFilter( Future<void> updateCurrentFilter(
final DocumentFilter Function(DocumentFilter) transformFn, { final DocumentFilter Function(DocumentFilter) transformFn,
bool propagateEventOnError = true, ) async =>
}) async { updateFilter(filter: transformFn(state.filter));
try {
return updateFilter(filter: transformFn(state.filter));
} on ErrorMessage catch (error) {
if (propagateEventOnError) {
return errorCubit.add(error);
} else {
rethrow;
}
}
}
void toggleDocumentSelection(DocumentModel model) { void toggleDocumentSelection(DocumentModel model) {
if (state.selection.contains(model)) { if (state.selection.contains(model)) {

View File

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

View File

@@ -4,6 +4,13 @@ import 'package:paperless_mobile/features/documents/model/document.model.dart';
const pageRegex = r".*page=(\d+).*"; 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 { class PagedSearchResult<T> extends Equatable {
/// Total number of available items /// Total number of available items
final int count; final int count;
@@ -46,12 +53,14 @@ class PagedSearchResult<T> extends Equatable {
}); });
factory PagedSearchResult.fromJson( factory PagedSearchResult.fromJson(
Map<dynamic, dynamic> json, T Function(JSON) fromJson) { PagedSearchResultJsonSerializer<T> serializer) {
return PagedSearchResult( return PagedSearchResult(
count: json['count'], count: serializer.json['count'],
next: json['next'], next: serializer.json['next'],
previous: json['previous'], previous: serializer.json['previous'],
results: List<JSON>.from(json['results']).map<T>(fromJson).toList(), 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()), body: json.encode(doc.toJson()),
headers: {"Content-Type": "application/json"}).timeout(requestTimeout); headers: {"Content-Type": "application/json"}).timeout(requestTimeout);
if (response.statusCode == 200) { if (response.statusCode == 200) {
return DocumentModel.fromJson( return compute(
jsonDecode(utf8.decode(response.bodyBytes))); DocumentModel.fromJson,
jsonDecode(utf8.decode(response.bodyBytes)) as JSON,
);
} else { } else {
throw const ErrorMessage(ErrorCode.documentUpdateFailed); throw const ErrorMessage(ErrorCode.documentUpdateFailed);
} }
@@ -152,11 +154,13 @@ class DocumentRepositoryImpl implements DocumentRepository {
Uri.parse("/api/documents/?$filterParams"), Uri.parse("/api/documents/?$filterParams"),
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
final searchResult = PagedSearchResult.fromJson( return compute(
jsonDecode(utf8.decode(response.bodyBytes)), PagedSearchResult.fromJson,
DocumentModel.fromJson, PagedSearchResultJsonSerializer<DocumentModel>(
jsonDecode(utf8.decode(response.bodyBytes)),
DocumentModel.fromJson,
),
); );
return searchResult;
} else { } else {
throw const ErrorMessage(ErrorCode.documentLoadFailed); throw const ErrorMessage(ErrorCode.documentLoadFailed);
} }
@@ -261,8 +265,10 @@ class DocumentRepositoryImpl implements DocumentRepository {
Future<DocumentMetaData> getMetaData(DocumentModel document) async { Future<DocumentMetaData> getMetaData(DocumentModel document) async {
final response = await httpClient final response = await httpClient
.get(Uri.parse("/api/documents/${document.id}/metadata/")); .get(Uri.parse("/api/documents/${document.id}/metadata/"));
return DocumentMetaData.fromJson( return compute(
jsonDecode(utf8.decode(response.bodyBytes))); DocumentMetaData.fromJson,
jsonDecode(utf8.decode(response.bodyBytes)) as JSON,
);
} }
@override @override
@@ -280,10 +286,14 @@ class DocumentRepositoryImpl implements DocumentRepository {
final response = await httpClient final response = await httpClient
.get(Uri.parse("/api/documents/?more_like=$docId&pageSize=10")); .get(Uri.parse("/api/documents/?more_like=$docId&pageSize=10"));
if (response.statusCode == 200) { if (response.statusCode == 200) {
return PagedSearchResult<SimilarDocumentModel>.fromJson( return (await compute(
jsonDecode(utf8.decode(response.bodyBytes)), PagedSearchResult<SimilarDocumentModel>.fromJson,
SimilarDocumentModel.fromJson, PagedSearchResultJsonSerializer(
).results; jsonDecode(utf8.decode(response.bodyBytes)),
SimilarDocumentModel.fromJson,
),
))
.results;
} }
throw const ErrorMessage(ErrorCode.similarQueryError); throw const ErrorMessage(ErrorCode.similarQueryError);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -78,22 +78,32 @@ class SavedViewSelectionWidget extends StatelessWidget {
final newView = await Navigator.of(context).push<SavedView?>( final newView = await Navigator.of(context).push<SavedView?>(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => AddSavedViewPage( builder: (context) => AddSavedViewPage(
currentFilter: getIt<DocumentsCubit>().state.filter), currentFilter: getIt<DocumentsCubit>().state.filter,
),
), ),
); );
if (newView != null) { 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(
if (isSelected) { bool isSelected, BuildContext context, SavedView view) async {
BlocProvider.of<DocumentsCubit>(context) try {
.updateFilter(filter: view.toDocumentFilter()); if (isSelected) {
BlocProvider.of<SavedViewCubit>(context).selectView(view); BlocProvider.of<DocumentsCubit>(context)
} else { .updateFilter(filter: view.toDocumentFilter());
BlocProvider.of<DocumentsCubit>(context).updateFilter(); BlocProvider.of<SavedViewCubit>(context).selectView(view);
BlocProvider.of<SavedViewCubit>(context).selectView(null); } else {
BlocProvider.of<DocumentsCubit>(context).updateFilter();
BlocProvider.of<SavedViewCubit>(context).selectView(null);
}
} on ErrorMessage catch (error) {
showError(context, error);
} }
} }
@@ -105,7 +115,11 @@ class SavedViewSelectionWidget extends StatelessWidget {
) ?? ) ??
false; false;
if (delete) { if (delete) {
BlocProvider.of<SavedViewCubit>(context).remove(view); 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 { onPressed: () async {
setState(() => _isLoading = true); setState(() => _isLoading = true);
BlocProvider.of<DocumentsCubit>(context) try {
.updateFilter( await BlocProvider.of<DocumentsCubit>(context)
filter: state.filter .updateCurrentFilter(
.copyWith(sortOrder: state.filter.sortOrder.toggle())) (filter) => filter.copyWith(
.whenComplete(() => setState(() => _isLoading = false)); 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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.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/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/core/widgets/offline_banner.dart';
import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
@@ -74,10 +74,14 @@ class _HomePageState extends State<HomePage> {
} }
initializeLabelData(BuildContext context) { initializeLabelData(BuildContext context) {
BlocProvider.of<DocumentTypeCubit>(context).initialize(); try {
BlocProvider.of<CorrespondentCubit>(context).initialize(); BlocProvider.of<DocumentTypeCubit>(context).initialize();
BlocProvider.of<TagCubit>(context).initialize(); BlocProvider.of<CorrespondentCubit>(context).initialize();
BlocProvider.of<StoragePathCubit>(context).initialize(); BlocProvider.of<TagCubit>(context).initialize();
BlocProvider.of<SavedViewCubit>(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/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.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), leading: const Icon(Icons.logout),
title: Text(S.of(context).appDrawerLogoutLabel), title: Text(S.of(context).appDrawerLogoutLabel),
onTap: () { onTap: () {
// Clear all bloc data try {
BlocProvider.of<AuthenticationCubit>(context).logout(); BlocProvider.of<AuthenticationCubit>(context).logout();
getIt<DocumentsCubit>().reset(); getIt<DocumentsCubit>().reset();
getIt<CorrespondentCubit>().reset(); getIt<CorrespondentCubit>().reset();
getIt<DocumentTypeCubit>().reset(); getIt<DocumentTypeCubit>().reset();
getIt<TagCubit>().reset(); getIt<TagCubit>().reset();
getIt<DocumentScannerCubit>().reset(); getIt<DocumentScannerCubit>().reset();
getIt<GlobalErrorCubit>().reset(); } on ErrorMessage catch (error) {
showError(context, error);
}
}, },
), ),
const Divider(), const Divider(),

View File

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

View File

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

View File

@@ -1,10 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/di_initializer.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.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/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/bloc/correspondents_cubit.dart';
import 'package:paperless_mobile/features/labels/correspondent/model/correspondent.model.dart'; import 'package:paperless_mobile/features/labels/correspondent/model/correspondent.model.dart';
import 'package:paperless_mobile/util.dart';
class CorrespondentWidget extends StatelessWidget { class CorrespondentWidget extends StatelessWidget {
final int? correspondentId; final int? correspondentId;
@@ -44,17 +46,21 @@ class CorrespondentWidget extends StatelessWidget {
void _addCorrespondentToFilter(BuildContext context) { void _addCorrespondentToFilter(BuildContext context) {
final cubit = BlocProvider.of<DocumentsCubit>(context); final cubit = BlocProvider.of<DocumentsCubit>(context);
if (cubit.state.filter.correspondent.id == correspondentId) { try {
cubit.updateCurrentFilter( if (cubit.state.filter.correspondent.id == correspondentId) {
(filter) => cubit.updateCurrentFilter(
filter.copyWith(correspondent: const CorrespondentQuery.unset()), (filter) =>
); filter.copyWith(correspondent: const CorrespondentQuery.unset()),
} else { );
cubit.updateCurrentFilter( } else {
(filter) => filter.copyWith( cubit.updateCurrentFilter(
correspondent: CorrespondentQuery.fromId(correspondentId)), (filter) => filter.copyWith(
); correspondent: CorrespondentQuery.fromId(correspondentId)),
);
}
afterSelected?.call();
} on ErrorMessage catch (error) {
showError(context, error);
} }
afterSelected?.call();
} }
} }

View File

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

View File

@@ -1,9 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/document_type_query.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/bloc/document_type_cubit.dart';
import 'package:paperless_mobile/features/labels/document_type/model/document_type.model.dart'; import 'package:paperless_mobile/features/labels/document_type/model/document_type.model.dart';
import 'package:paperless_mobile/util.dart';
class DocumentTypeWidget extends StatelessWidget { class DocumentTypeWidget extends StatelessWidget {
final int? documentTypeId; final int? documentTypeId;
@@ -39,17 +41,21 @@ class DocumentTypeWidget extends StatelessWidget {
void _addDocumentTypeToFilter(BuildContext context) { void _addDocumentTypeToFilter(BuildContext context) {
final cubit = BlocProvider.of<DocumentsCubit>(context); final cubit = BlocProvider.of<DocumentsCubit>(context);
if (cubit.state.filter.documentType.id == documentTypeId) { try {
cubit.updateCurrentFilter( if (cubit.state.filter.documentType.id == documentTypeId) {
(filter) => cubit.updateCurrentFilter(
filter.copyWith(documentType: const DocumentTypeQuery.unset()), (filter) =>
); filter.copyWith(documentType: const DocumentTypeQuery.unset()),
} else { );
cubit.updateCurrentFilter( } else {
(filter) => filter.copyWith( cubit.updateCurrentFilter(
documentType: DocumentTypeQuery.fromId(documentTypeId)), (filter) => filter.copyWith(
); documentType: DocumentTypeQuery.fromId(documentTypeId)),
);
}
afterSelected?.call();
} on ErrorMessage catch (error) {
showError(context, error);
} }
afterSelected?.call();
} }
} }

View File

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

View File

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

View File

@@ -1,9 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/storage_path_query.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/bloc/storage_path_cubit.dart';
import 'package:paperless_mobile/features/labels/storage_path/model/storage_path.model.dart'; import 'package:paperless_mobile/features/labels/storage_path/model/storage_path.model.dart';
import 'package:paperless_mobile/util.dart';
class StoragePathWidget extends StatelessWidget { class StoragePathWidget extends StatelessWidget {
final int? pathId; final int? pathId;
@@ -43,17 +45,21 @@ class StoragePathWidget extends StatelessWidget {
void _addStoragePathToFilter(BuildContext context) { void _addStoragePathToFilter(BuildContext context) {
final cubit = BlocProvider.of<DocumentsCubit>(context); final cubit = BlocProvider.of<DocumentsCubit>(context);
if (cubit.state.filter.correspondent.id == pathId) { try {
cubit.updateCurrentFilter( if (cubit.state.filter.correspondent.id == pathId) {
(filter) => cubit.updateCurrentFilter(
filter.copyWith(storagePath: const StoragePathQuery.unset()), (filter) =>
); filter.copyWith(storagePath: const StoragePathQuery.unset()),
} else { );
cubit.updateCurrentFilter( } else {
(filter) => cubit.updateCurrentFilter(
filter.copyWith(storagePath: StoragePathQuery.fromId(pathId)), (filter) =>
); filter.copyWith(storagePath: StoragePathQuery.fromId(pathId)),
);
}
afterSelected?.call();
} on ErrorMessage catch (error) {
showError(context, error);
} }
afterSelected?.call();
} }
} }

View File

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

View File

@@ -43,18 +43,22 @@ class EditTagPage extends StatelessWidget {
} }
Future<void> _onDelete(Tag tag, BuildContext context) async { Future<void> _onDelete(Tag tag, BuildContext context) async {
await BlocProvider.of<TagCubit>(context).remove(tag); try {
final cubit = BlocProvider.of<DocumentsCubit>(context); await BlocProvider.of<TagCubit>(context).remove(tag);
final currentFilter = cubit.state.filter; final cubit = BlocProvider.of<DocumentsCubit>(context);
late DocumentFilter updatedFilter = currentFilter; final currentFilter = cubit.state.filter;
if (currentFilter.tags.ids.contains(tag.id)) { late DocumentFilter updatedFilter = currentFilter;
updatedFilter = currentFilter.copyWith( if (currentFilter.tags.ids.contains(tag.id)) {
tags: TagsQuery.fromIds( updatedFilter = currentFilter.copyWith(
currentFilter.tags.ids.where((tagId) => tagId != tag.id).toList(), tags: TagsQuery.fromIds(
), currentFilter.tags.ids.where((tagId) => tagId != tag.id).toList(),
); ),
);
}
cubit.updateFilter(filter: updatedFilter);
Navigator.pop(context);
} on ErrorMessage catch (error) {
showError(context, error);
} }
cubit.updateFilter(filter: updatedFilter);
Navigator.pop(context);
} }
} }

View File

@@ -38,22 +38,27 @@ class TagWidget extends StatelessWidget {
void _addTagToFilter(BuildContext context) { void _addTagToFilter(BuildContext context) {
final cubit = BlocProvider.of<DocumentsCubit>(context); final cubit = BlocProvider.of<DocumentsCubit>(context);
if (cubit.state.filter.tags.ids.contains(tag.id)) { try {
cubit.updateCurrentFilter( if (cubit.state.filter.tags.ids.contains(tag.id)) {
(filter) => filter.copyWith( cubit.updateCurrentFilter(
tags: TagsQuery.fromIds( (filter) => filter.copyWith(
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 { ),
cubit.updateCurrentFilter( );
(filter) => filter.copyWith( } else {
tags: TagsQuery.fromIds([...cubit.state.filter.tags.ids, tag.id!]), cubit.updateCurrentFilter(
), (filter) => filter.copyWith(
); tags: TagsQuery.fromIds([...cubit.state.filter.tags.ids, tag.id!]),
} ),
if (afterTagTapped != null) { );
afterTagTapped!(); }
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/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.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/core/type/types.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.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/document_type/model/matching_algorithm.dart';
import 'package:paperless_mobile/features/labels/model/label.model.dart'; import 'package:paperless_mobile/features/labels/model/label.model.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
class EditLabelPage<T extends Label> extends StatefulWidget { class EditLabelPage<T extends Label> extends StatefulWidget {
final T label; final T label;
@@ -144,6 +146,8 @@ class _EditLabelPageState<T extends Label> extends State<EditLabelPage<T>> {
Navigator.pop(context); Navigator.pop(context);
} on PaperlessValidationErrors catch (errorMessages) { } on PaperlessValidationErrors catch (errorMessages) {
setState(() => _errors = 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/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/bloc/label_bloc_provider.dart';
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.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/model/error_message.dart';
@@ -53,10 +52,9 @@ class LabelItem<T extends Label> extends StatelessWidget {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => LabelBlocProvider( builder: (context) => LabelBlocProvider(
child: BlocProvider( child: BlocProvider(
create: (context) => DocumentsCubit( create: (context) =>
getIt<DocumentRepository>(), DocumentsCubit(getIt<DocumentRepository>())
getIt<GlobalErrorCubit>()) ..updateFilter(filter: filter),
..updateFilter(filter: filter),
child: LinkedDocumentsPreview(filter: filter), child: LinkedDocumentsPreview(filter: filter),
), ),
), ),

View File

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

View File

@@ -26,10 +26,19 @@ class AuthenticationService {
required String password, required String password,
required String serverUrl, required String serverUrl,
}) async { }) async {
final response = await httpClient.post( late Response response;
Uri.parse("/api/token/"), try {
body: {"username": username, "password": password}, 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) { if (response.statusCode == 200) {
final data = jsonDecode(utf8.decode(response.bodyBytes)); final data = jsonDecode(utf8.decode(response.bodyBytes));
return data['token']; return data['token'];

View File

@@ -1,12 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.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/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.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/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/server_address_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/user_credentials_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/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
class LoginPage extends StatefulWidget { class LoginPage extends StatefulWidget {
const LoginPage({Key? key}) : super(key: key); const LoginPage({Key? key}) : super(key: key);
@@ -72,7 +74,8 @@ class _LoginPageState extends State<LoginPage> {
return ElevatedButton( return ElevatedButton(
style: ButtonStyle( style: ButtonStyle(
backgroundColor: MaterialStatePropertyAll( backgroundColor: MaterialStatePropertyAll(
Theme.of(context).colorScheme.primaryContainer), Theme.of(context).colorScheme.primaryContainer,
),
elevation: const MaterialStatePropertyAll(0), elevation: const MaterialStatePropertyAll(0),
), ),
onPressed: _login, onPressed: _login,
@@ -82,19 +85,25 @@ class _LoginPageState extends State<LoginPage> {
); );
} }
void _login() { void _login() async {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
if (_formKey.currentState?.saveAndValidate() ?? false) { if (_formKey.currentState?.saveAndValidate() ?? false) {
setState(() => _isLoginLoading = true); setState(() => _isLoginLoading = true);
final form = _formKey.currentState?.value; final form = _formKey.currentState!.value;
BlocProvider.of<AuthenticationCubit>(context) try {
.login( await BlocProvider.of<AuthenticationCubit>(context).login(
credentials: form?[UserCredentialsFormField.fkCredentials], credentials: form[UserCredentialsFormField.fkCredentials],
serverUrl: form?[ServerAddressFormField.fkServerAddress], serverUrl: form[ServerAddressFormField.fkServerAddress],
clientCertificate: clientCertificate:
form?[ClientCertificateFormField.fkClientCertificate], form[ClientCertificateFormField.fkClientCertificate],
) );
.whenComplete(() => setState(() => _isLoginLoading = false)); } 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); scans.removeAt(fileIndex);
emit(scans); emit(scans);
} catch (_) { } catch (_) {
addError(const ErrorMessage(ErrorCode.scanRemoveFailed)); throw const ErrorMessage(ErrorCode.scanRemoveFailed);
} }
} }
void reset() { void reset() {
for (final doc in state) { try {
doc.deleteSync(); for (final doc in state) {
if (kDebugMode) { doc.deleteSync();
log('[ScannerCubit]: Removed ${doc.path}'); if (kDebugMode) {
log('[ScannerCubit]: Removed ${doc.path}');
}
} }
imageCache.clear();
emit(initialState);
} catch (_) {
throw const ErrorMessage(ErrorCode.scanRemoveFailed);
} }
imageCache.clear();
emit(initialState);
} }
} }

View File

@@ -185,51 +185,32 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
} }
void _onSubmit() async { void _onSubmit() async {
_formKey.currentState?.save(); if (_formKey.currentState?.saveAndValidate() ?? false) {
if (_formKey.currentState?.validate() ?? false) {
try { try {
setState(() { setState(() => _isUploadLoading = true);
_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( await BlocProvider.of<DocumentsCubit>(context).addDocument(
widget.fileBytes, widget.fileBytes,
_formKey.currentState?.value[fkFileName], _formKey.currentState?.value[fkFileName],
onConsumptionFinished: (document) { onConsumptionFinished: _onConsumptionFinished,
ScaffoldMessenger.of(rootScaffoldKey.currentContext!).showSnackBar( title: title,
SnackBar( documentType: docType.id,
action: SnackBarAction( correspondent: correspondent.id,
onPressed: () { tags: tags.ids,
getIt<DocumentsCubit>().reloadDocuments(); createdAt: createdAt,
},
label: S
.of(context)
.documentUploadProcessingSuccessfulReloadActionText,
),
content:
Text(S.of(context).documentUploadProcessingSuccessfulText),
),
);
},
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(() { getIt<DocumentScannerCubit>().reset(); //TODO: Access via provider
_isUploadLoading = false;
});
getIt<DocumentScannerCubit>().reset();
Navigator.pop(context);
showSnackBar(context, S.of(context).documentUploadSuccessText); showSnackBar(context, S.of(context).documentUploadSuccessText);
Navigator.pop(context);
widget.afterUpload?.call(); widget.afterUpload?.call();
} on ErrorMessage catch (error) { } on ErrorMessage catch (error) {
showError(context, error); showError(context, error);
@@ -239,9 +220,28 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
showSnackBar(context, other.toString()); showSnackBar(context, other.toString());
} finally { } finally {
setState(() { 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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:mime/mime.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/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/model/error_message.dart';
import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/di_initializer.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/document_upload_page.dart';
import 'package:paperless_mobile/features/scan/view/widgets/grid_image_item_widget.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/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
import 'package:pdf/pdf.dart'; import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw; import 'package:pdf/widgets.dart' as pw;
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
@@ -32,15 +33,6 @@ class ScannerPage extends StatefulWidget {
class _ScannerPageState extends State<ScannerPage> class _ScannerPageState extends State<ScannerPage>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
static const _supportedExtensions = [
'pdf',
'png',
'tiff',
'gif',
'jpg',
'jpeg'
];
late final AnimationController _fabPulsingController; late final AnimationController _fabPulsingController;
late final Animation _animation; late final Animation _animation;
@@ -205,8 +197,14 @@ class _ScannerPageState extends State<ScannerPage>
itemBuilder: (context, index) { itemBuilder: (context, index) {
return GridImageItemWidget( return GridImageItemWidget(
file: scans[index], file: scans[index],
onDelete: () => BlocProvider.of<DocumentScannerCubit>(context) onDelete: () async {
.removeScan(index), try {
BlocProvider.of<DocumentScannerCubit>(context)
.removeScan(index);
} on ErrorMessage catch (error) {
showError(context, error);
}
},
index: index, index: index,
totalNumberOfFiles: scans.length, totalNumberOfFiles: scans.length,
); );
@@ -214,7 +212,11 @@ class _ScannerPageState extends State<ScannerPage>
} }
void _reset(BuildContext context) { void _reset(BuildContext context) {
BlocProvider.of<DocumentScannerCubit>(context).reset(); try {
BlocProvider.of<DocumentScannerCubit>(context).reset();
} on ErrorMessage catch (error) {
showError(context, error);
}
} }
Future<void> _requestCameraPermissions() async { Future<void> _requestCameraPermissions() async {
@@ -227,15 +229,14 @@ class _ScannerPageState extends State<ScannerPage>
void _onUploadFromFilesystem() async { void _onUploadFromFilesystem() async {
FilePickerResult? result = await FilePicker.platform.pickFiles( FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom, type: FileType.custom,
allowedExtensions: _supportedExtensions, allowedExtensions: supportedFileExtensions,
withData: true, withData: true,
); );
if (result?.files.single.path != null) { if (result?.files.single.path != null) {
File file = File(result!.files.single.path!); File file = File(result!.files.single.path!);
if (!_supportedExtensions.contains(file.path.split('.').last)) { if (!supportedFileExtensions.contains(file.path.split('.').last)) {
return getIt<GlobalErrorCubit>().add( //TODO: Show error message;
const ErrorMessage(ErrorCode.unsupportedFileFormat), return;
);
} }
final mimeType = lookupMimeType(file.path) ?? ''; final mimeType = lookupMimeType(file.path) ?? '';
late Uint8List fileBytes; late Uint8List fileBytes;

View File

@@ -187,5 +187,7 @@
"editLabelPageConfirmDeletionDialogTitle": "Löschen bestätigen", "editLabelPageConfirmDeletionDialogTitle": "Löschen bestätigen",
"editLabelPageDeletionDialogText": "Dieser Kennzeichner wird von Dokumenten referenziert. Durch das Löschen dieses Kennzeichners werden alle Referenzen entfernt. Fortfahren?", "editLabelPageDeletionDialogText": "Dieser Kennzeichner wird von Dokumenten referenziert. Durch das Löschen dieses Kennzeichners werden alle Referenzen entfernt. Fortfahren?",
"settingsPageStorageSettingsLabel": "Storage", "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", "editLabelPageConfirmDeletionDialogTitle": "Confirm deletion",
"editLabelPageDeletionDialogText": "This label contains references to other documents. By deleting this label, all references will be removed. Continue?", "editLabelPageDeletionDialogText": "This label contains references to other documents. By deleting this label, all references will be removed. Continue?",
"settingsPageStorageSettingsLabel": "Storage", "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_localizations/flutter_localizations.dart';
import 'package:flutter_native_splash/flutter_native_splash.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/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/bloc/label_bloc_provider.dart';
import 'package:paperless_mobile/core/global/asset_images.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/global/http_self_signed_certificate_override.dart';
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.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/service/file_service.dart';
import 'package:paperless_mobile/core/util.dart'; import 'package:paperless_mobile/core/util.dart';
import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/di_initializer.dart';
@@ -148,51 +149,46 @@ class AuthenticationWrapper extends StatefulWidget {
} }
class _AuthenticationWrapperState extends State<AuthenticationWrapper> { 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 @override
void initState() { void initState() {
super.initState(); super.initState();
// For sharing files coming from outside the app while the app is still opened // For sharing files coming from outside the app while the app is still opened
ReceiveSharingIntent.getMediaStream().listen((List<SharedMediaFile> value) { ReceiveSharingIntent.getMediaStream().listen(handleReceivedFiles);
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);
});
// For sharing files coming from outside the app while the app is closed // For sharing files coming from outside the app while the app is closed
ReceiveSharingIntent.getInitialMedia().then((List<SharedMediaFile> value) { ReceiveSharingIntent.getInitialMedia().then(handleReceivedFiles);
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(),
),
),
),
),
);
});
} }
@override @override
@@ -206,47 +202,37 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider.value( return SafeArea(
value: getIt<GlobalErrorCubit>(), top: true,
child: BlocListener<GlobalErrorCubit, GlobalErrorState>( left: false,
listener: (context, state) { right: false,
if (state.hasError) { bottom: false,
showSnackBar(context, translateError(context, state.error!.code)); child: BlocConsumer<AuthenticationCubit, AuthenticationState>(
listener: (context, authState) {
final bool showIntroSlider =
authState.isAuthenticated && !authState.wasLoginStored;
if (showIntroSlider) {
for (final img in AssetImages.values) {
img.load(context);
}
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ApplicationIntroSlideshow(),
fullscreenDialog: true,
),
);
}
},
builder: (context, authentication) {
if (authentication.isAuthenticated) {
return const LabelBlocProvider(
child: HomePage(),
);
} else {
return const LoginPage();
} }
}, },
child: SafeArea(
top: true,
left: false,
right: false,
bottom: false,
child: BlocConsumer<AuthenticationCubit, AuthenticationState>(
listener: (context, authState) {
final bool showIntroSlider =
authState.isAuthenticated && !authState.wasLoginStored;
if (showIntroSlider) {
for (final img in AssetImages.values) {
img.load(context);
}
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ApplicationIntroSlideshow(),
fullscreenDialog: true,
),
);
}
},
builder: (context, authentication) {
if (authentication.isAuthenticated) {
return const LabelBlocProvider(
child: HomePage(),
);
} else {
return const LoginPage();
}
},
),
),
), ),
); );
} }

View File

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