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);
}
final addedItem = await save(item);
final newState = {...state};
newState.putIfAbsent(addedItem.id!, () => addedItem);
emit(newState);
return addedItem;
}
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);
}
final updatedItem = await update(item);
final newState = {...state};
newState[item.id!] = updatedItem;
emit(newState);
return updatedItem;
}
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);
}
final deletedId = await delete(item);
final newState = {...state};
newState.remove(deletedId);
emit(newState);
}
}

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);
}