fix: Add custom fields, translations, add app logs to login routes

This commit is contained in:
Anton Stubenbord
2023-12-10 12:48:32 +01:00
parent 5e5e5d2df3
commit 9f6b95f506
102 changed files with 2399 additions and 1088 deletions

View File

@@ -0,0 +1,6 @@
enum LoadingStatus {
initial,
loading,
loaded,
error;
}

View File

@@ -0,0 +1,41 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/bloc/transient_error.dart';
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
import 'package:paperless_mobile/features/logging/data/logger.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/routing/navigation_keys.dart';
class MyBlocObserver extends BlocObserver {
@override
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
if (error is TransientError) {
_handleTransientError(bloc, error, stackTrace);
}
super.onError(bloc, error, stackTrace);
}
void _handleTransientError(
BlocBase bloc,
TransientError error,
StackTrace stackTrace,
) {
assert(rootNavigatorKey.currentContext != null);
final message = switch (error) {
TransientPaperlessApiError(code: var code) => translateError(
rootNavigatorKey.currentContext!,
code,
),
TransientMessageError(message: var message) => message,
};
final details = switch (error) {
TransientPaperlessApiError(details: var details) => details,
_ => null,
};
showSnackBar(
rootNavigatorKey.currentContext!,
message,
details: details,
);
}
}

View File

@@ -0,0 +1,16 @@
import 'package:paperless_api/paperless_api.dart';
sealed class TransientError extends Error {}
class TransientPaperlessApiError extends TransientError {
final ErrorCode code;
final String? details;
TransientPaperlessApiError({required this.code, this.details});
}
class TransientMessageError extends TransientError {
final String message;
TransientMessageError({required this.message});
}

View File

@@ -28,16 +28,31 @@ class DioHttpErrorInterceptor extends Interceptor {
type: DioExceptionType.badResponse,
),
);
} else if (data is String &&
data.contains("No required SSL certificate was sent")) {
handler.reject(
DioException(
requestOptions: err.requestOptions,
type: DioExceptionType.badResponse,
error:
const PaperlessApiException(ErrorCode.missingClientCertificate),
),
);
} else if (data is String) {
if (data.contains("No required SSL certificate was sent")) {
handler.reject(
DioException(
requestOptions: err.requestOptions,
type: DioExceptionType.badResponse,
error: const PaperlessApiException(
ErrorCode.missingClientCertificate),
),
);
} else {
handler.reject(
DioException(
requestOptions: err.requestOptions,
message: data,
error: PaperlessApiException(
ErrorCode.documentLoadFailed,
details: data,
),
response: err.response,
stackTrace: err.stackTrace,
type: DioExceptionType.badResponse,
),
);
}
} else {
handler.reject(err);
}

View File

@@ -1,192 +1,190 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository_state.dart';
import 'package:paperless_mobile/core/repository/persistent_repository.dart';
class LabelRepository extends PersistentRepository<LabelRepositoryState> {
class LabelRepository extends ChangeNotifier {
final PaperlessLabelsApi _api;
LabelRepository(this._api) : super(const LabelRepositoryState());
Map<int, Correspondent> correspondents = {};
Map<int, DocumentType> documentTypes = {};
Map<int, StoragePath> storagePaths = {};
Map<int, Tag> tags = {};
Future<void> initialize() async {
LabelRepository(this._api);
// Resets the repository to its initial state and loads all data from the API.
Future<void> initialize({
required bool loadCorrespondents,
required bool loadDocumentTypes,
required bool loadStoragePaths,
required bool loadTags,
}) async {
correspondents = {};
documentTypes = {};
storagePaths = {};
tags = {};
await Future.wait([
findAllCorrespondents(),
findAllDocumentTypes(),
findAllStoragePaths(),
findAllTags(),
if (loadCorrespondents) findAllCorrespondents(),
if (loadDocumentTypes) findAllDocumentTypes(),
if (loadStoragePaths) findAllStoragePaths(),
if (loadTags) findAllTags(),
]);
}
Future<Tag> createTag(Tag object) async {
final created = await _api.saveTag(object);
final updatedState = {...state.tags}
..putIfAbsent(created.id!, () => created);
emit(state.copyWith(tags: updatedState));
tags = {...tags, created.id!: created};
notifyListeners();
return created;
}
Future<int> deleteTag(Tag tag) async {
await _api.deleteTag(tag);
final updatedState = {...state.tags}..removeWhere((k, v) => k == tag.id);
emit(state.copyWith(tags: updatedState));
tags.remove(tag.id!);
notifyListeners();
return tag.id!;
}
Future<Tag?> findTag(int id) async {
final tag = await _api.getTag(id);
if (tag != null) {
final updatedState = {...state.tags}..[id] = tag;
emit(state.copyWith(tags: updatedState));
tags = {...tags, id: tag};
notifyListeners();
return tag;
}
return null;
}
Future<Iterable<Tag>> findAllTags([Iterable<int>? ids]) async {
final tags = await _api.getTags(ids);
final updatedState = {...state.tags}
..addEntries(tags.map((e) => MapEntry(e.id!, e)));
emit(state.copyWith(tags: updatedState));
return tags;
final data = await _api.getTags(ids);
tags = {for (var tag in data) tag.id!: tag};
notifyListeners();
return data;
}
Future<Tag> updateTag(Tag tag) async {
final updated = await _api.updateTag(tag);
final updatedState = {...state.tags}..update(updated.id!, (_) => updated);
emit(state.copyWith(tags: updatedState));
tags = {...tags, updated.id!: updated};
notifyListeners();
return updated;
}
Future<Correspondent> createCorrespondent(Correspondent correspondent) async {
final created = await _api.saveCorrespondent(correspondent);
final updatedState = {...state.correspondents}
..putIfAbsent(created.id!, () => created);
emit(state.copyWith(correspondents: updatedState));
correspondents = {...correspondents, created.id!: created};
notifyListeners();
return created;
}
Future<int> deleteCorrespondent(Correspondent correspondent) async {
await _api.deleteCorrespondent(correspondent);
final updatedState = {...state.correspondents}
..removeWhere((k, v) => k == correspondent.id);
emit(state.copyWith(correspondents: updatedState));
correspondents.remove(correspondent.id!);
notifyListeners();
return correspondent.id!;
}
Future<Correspondent?> findCorrespondent(int id) async {
final correspondent = await _api.getCorrespondent(id);
if (correspondent != null) {
final updatedState = {...state.correspondents}..[id] = correspondent;
emit(state.copyWith(correspondents: updatedState));
correspondents = {...correspondents, id: correspondent};
notifyListeners();
return correspondent;
}
return null;
}
Future<Iterable<Correspondent>> findAllCorrespondents(
[Iterable<int>? ids]) async {
final correspondents = await _api.getCorrespondents(ids);
final updatedState = {
...state.correspondents,
}..addAll({for (var element in correspondents) element.id!: element});
emit(state.copyWith(correspondents: updatedState));
return correspondents;
Future<Iterable<Correspondent>> findAllCorrespondents() async {
final data = await _api.getCorrespondents();
correspondents = {for (var element in data) element.id!: element};
notifyListeners();
return data;
}
Future<Correspondent> updateCorrespondent(Correspondent correspondent) async {
final updated = await _api.updateCorrespondent(correspondent);
final updatedState = {...state.correspondents}
..update(updated.id!, (_) => updated);
emit(state.copyWith(correspondents: updatedState));
correspondents = {...correspondents, updated.id!: updated};
notifyListeners();
return updated;
}
Future<DocumentType> createDocumentType(DocumentType documentType) async {
final created = await _api.saveDocumentType(documentType);
final updatedState = {...state.documentTypes}
..putIfAbsent(created.id!, () => created);
emit(state.copyWith(documentTypes: updatedState));
documentTypes = {...documentTypes, created.id!: created};
notifyListeners();
return created;
}
Future<int> deleteDocumentType(DocumentType documentType) async {
await _api.deleteDocumentType(documentType);
final updatedState = {...state.documentTypes}
..removeWhere((k, v) => k == documentType.id);
emit(state.copyWith(documentTypes: updatedState));
documentTypes.remove(documentType.id!);
notifyListeners();
return documentType.id!;
}
Future<DocumentType?> findDocumentType(int id) async {
final documentType = await _api.getDocumentType(id);
if (documentType != null) {
final updatedState = {...state.documentTypes}..[id] = documentType;
emit(state.copyWith(documentTypes: updatedState));
documentTypes = {...documentTypes, id: documentType};
notifyListeners();
return documentType;
}
return null;
}
Future<Iterable<DocumentType>> findAllDocumentTypes(
[Iterable<int>? ids]) async {
final documentTypes = await _api.getDocumentTypes(ids);
final updatedState = {...state.documentTypes}
..addEntries(documentTypes.map((e) => MapEntry(e.id!, e)));
emit(state.copyWith(documentTypes: updatedState));
Future<Iterable<DocumentType>> findAllDocumentTypes() async {
final documentTypes = await _api.getDocumentTypes();
this.documentTypes = {
for (var dt in documentTypes) dt.id!: dt,
};
notifyListeners();
return documentTypes;
}
Future<DocumentType> updateDocumentType(DocumentType documentType) async {
final updated = await _api.updateDocumentType(documentType);
final updatedState = {...state.documentTypes}
..update(updated.id!, (_) => updated);
emit(state.copyWith(documentTypes: updatedState));
documentTypes = {...documentTypes, updated.id!: updated};
notifyListeners();
return updated;
}
Future<StoragePath> createStoragePath(StoragePath storagePath) async {
final created = await _api.saveStoragePath(storagePath);
final updatedState = {...state.storagePaths}
..putIfAbsent(created.id!, () => created);
emit(state.copyWith(storagePaths: updatedState));
storagePaths = {...storagePaths, created.id!: created};
notifyListeners();
return created;
}
Future<int> deleteStoragePath(StoragePath storagePath) async {
await _api.deleteStoragePath(storagePath);
final updatedState = {...state.storagePaths}
..removeWhere((k, v) => k == storagePath.id);
emit(state.copyWith(storagePaths: updatedState));
storagePaths.remove(storagePath.id!);
notifyListeners();
return storagePath.id!;
}
Future<StoragePath?> findStoragePath(int id) async {
final storagePath = await _api.getStoragePath(id);
if (storagePath != null) {
final updatedState = {...state.storagePaths}..[id] = storagePath;
emit(state.copyWith(storagePaths: updatedState));
storagePaths = {...storagePaths, id: storagePath};
notifyListeners();
return storagePath;
}
return null;
}
Future<Iterable<StoragePath>> findAllStoragePaths(
[Iterable<int>? ids]) async {
final storagePaths = await _api.getStoragePaths(ids);
final updatedState = {...state.storagePaths}
..addEntries(storagePaths.map((e) => MapEntry(e.id!, e)));
emit(state.copyWith(storagePaths: updatedState));
Future<Iterable<StoragePath>> findAllStoragePaths() async {
final storagePaths = await _api.getStoragePaths();
this.storagePaths = {
for (var sp in storagePaths) sp.id!: sp,
};
notifyListeners();
return storagePaths;
}
Future<StoragePath> updateStoragePath(StoragePath storagePath) async {
final updated = await _api.updateStoragePath(storagePath);
final updatedState = {...state.storagePaths}
..update(updated.id!, (_) => updated);
emit(state.copyWith(storagePaths: updatedState));
storagePaths = {...storagePaths, updated.id!: updated};
notifyListeners();
return updated;
}

View File

@@ -1,18 +0,0 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
part 'label_repository_state.freezed.dart';
part 'label_repository_state.g.dart';
@freezed
class LabelRepositoryState with _$LabelRepositoryState {
const factory LabelRepositoryState({
@Default({}) Map<int, Correspondent> correspondents,
@Default({}) Map<int, DocumentType> documentTypes,
@Default({}) Map<int, Tag> tags,
@Default({}) Map<int, StoragePath> storagePaths,
}) = _LabelRepositoryState;
factory LabelRepositoryState.fromJson(Map<String, dynamic> json) =>
_$LabelRepositoryStateFromJson(json);
}

View File

@@ -1,95 +1,54 @@
import 'dart:async';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/cupertino.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/persistent_repository.dart';
part 'saved_view_repository_state.dart';
part 'saved_view_repository.g.dart';
part 'saved_view_repository.freezed.dart';
class SavedViewRepository
extends PersistentRepository<SavedViewRepositoryState> {
class SavedViewRepository extends ChangeNotifier {
final PaperlessSavedViewsApi _api;
final Completer _initialized = Completer();
Map<int, SavedView> savedViews = {};
SavedViewRepository(this._api)
: super(const SavedViewRepositoryState.initial());
SavedViewRepository(this._api);
Future<void> initialize() async {
try {
await findAll();
_initialized.complete();
} catch (e) {
_initialized.completeError(e);
emit(const SavedViewRepositoryState.error());
}
await findAll();
}
Future<SavedView> create(SavedView object) async {
await _initialized.future;
final created = await _api.save(object);
final updatedState = {...state.savedViews}
..putIfAbsent(created.id!, () => created);
emit(SavedViewRepositoryState.loaded(savedViews: updatedState));
savedViews = {...savedViews, created.id!: created};
notifyListeners();
return created;
}
Future<SavedView> update(SavedView object) async {
await _initialized.future;
final updated = await _api.update(object);
final updatedState = {...state.savedViews}..update(
updated.id!,
(_) => updated,
ifAbsent: () => updated,
);
emit(SavedViewRepositoryState.loaded(savedViews: updatedState));
savedViews = {...savedViews, updated.id!: updated};
notifyListeners();
return updated;
}
Future<int> delete(SavedView view) async {
await _initialized.future;
await _api.delete(view);
final updatedState = {...state.savedViews}..remove(view.id);
emit(SavedViewRepositoryState.loaded(savedViews: updatedState));
savedViews.remove(view.id!);
notifyListeners();
return view.id!;
}
Future<SavedView?> find(int id) async {
await _initialized.future;
final found = await _api.find(id);
if (found != null) {
final updatedState = {...state.savedViews}
..update(id, (_) => found, ifAbsent: () => found);
emit(SavedViewRepositoryState.loaded(savedViews: updatedState));
savedViews = {...savedViews, id: found};
notifyListeners();
}
return found;
}
Future<Iterable<SavedView>> findAll([Iterable<int>? ids]) async {
final found = await _api.findAll(ids);
final updatedState = {
...state.savedViews,
...{for (final view in found) view.id!: view},
savedViews = {
for (final view in found) view.id!: view,
};
emit(SavedViewRepositoryState.loaded(savedViews: updatedState));
notifyListeners();
return found;
}
// @override
// Future<void> clear() async {
// await _initialized.future;
// await super.clear();
// emit(const SavedViewRepositoryState.initial());
// }
// @override
// SavedViewRepositoryState? fromJson(Map<String, dynamic> json) {
// return SavedViewRepositoryState.fromJson(json);
// }
// @override
// Map<String, dynamic>? toJson(SavedViewRepositoryState state) {
// return state.toJson();
// }
}

View File

@@ -1,22 +0,0 @@
part of 'saved_view_repository.dart';
@freezed
class SavedViewRepositoryState with _$SavedViewRepositoryState {
const factory SavedViewRepositoryState.initial({
@Default({}) Map<int, SavedView> savedViews,
}) = _Initial;
const factory SavedViewRepositoryState.loading({
@Default({}) Map<int, SavedView> savedViews,
}) = _Loading;
const factory SavedViewRepositoryState.loaded({
@Default({}) Map<int, SavedView> savedViews,
}) = _Loaded;
const factory SavedViewRepositoryState.error({
@Default({}) Map<int, SavedView> savedViews,
}) = _Error;
factory SavedViewRepositoryState.fromJson(Map<String, dynamic> json) =>
_$SavedViewRepositoryStateFromJson(json);
}

View File

@@ -1,30 +1,45 @@
import 'package:equatable/equatable.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/persistent_repository.dart';
import 'package:paperless_mobile/features/logging/data/logger.dart';
part 'user_repository_state.dart';
/// Repository for new users (API v3, server version 1.14.2+)
class UserRepository extends PersistentRepository<UserRepositoryState> {
final PaperlessUserApiV3 _userApiV3;
final PaperlessUserApi _userApi;
UserRepository(this._userApiV3) : super(const UserRepositoryState());
UserRepository(this._userApi) : super(const UserRepositoryState());
Future<void> initialize() async {
await findAll();
}
Future<Iterable<UserModel>> findAll() async {
final users = await _userApiV3.findAll();
emit(state.copyWith(users: {for (var e in users) e.id: e}));
return users;
if (_userApi is PaperlessUserApiV3Impl) {
final users = await (_userApi as PaperlessUserApiV3Impl).findAll();
emit(state.copyWith(users: {for (var e in users) e.id: e}));
return users;
}
logger.fw(
"Tried to access API v3 features while using an older API version.",
className: 'UserRepository',
methodName: 'findAll',
);
return [];
}
Future<UserModel?> find(int id) async {
final user = await _userApiV3.find(id);
emit(state.copyWith(users: state.users..[id] = user));
return user;
if (_userApi is PaperlessUserApiV3Impl) {
final user = await (_userApi as PaperlessUserApiV3Impl).find(id);
emit(state.copyWith(users: state.users..[id] = user));
return user;
}
logger.fw(
"Tried to access API v3 features while using an older API version.",
className: 'UserRepository',
methodName: 'findAll',
);
return null;
}
// @override

View File

@@ -14,12 +14,12 @@ class FileService {
static FileService? _singleton;
late final Directory _logDirectory;
late final Directory _temporaryDirectory;
late final Directory _documentsDirectory;
late final Directory _downloadsDirectory;
late final Directory _uploadDirectory;
late final Directory _temporaryScansDirectory;
late Directory _logDirectory;
late Directory _temporaryDirectory;
late Directory _documentsDirectory;
late Directory _downloadsDirectory;
late Directory _uploadDirectory;
late Directory _temporaryScansDirectory;
Directory get logDirectory => _logDirectory;
Directory get temporaryDirectory => _temporaryDirectory;
@@ -186,14 +186,15 @@ class FileService {
}
Future<void> _initTemporaryDirectory() async {
_temporaryDirectory = await getTemporaryDirectory();
_temporaryDirectory =
await getTemporaryDirectory().then((value) => value.create());
}
Future<void> _initializeDocumentsDirectory() async {
if (Platform.isAndroid) {
final dirs =
await getExternalStorageDirectories(type: StorageDirectory.documents);
_documentsDirectory = dirs!.first;
_documentsDirectory = await dirs!.first.create(recursive: true);
return;
} else if (Platform.isIOS) {
final dir = await getApplicationDocumentsDirectory();
@@ -212,12 +213,12 @@ class FileService {
.then((directory) async =>
directory?.firstOrNull ??
await getApplicationDocumentsDirectory())
.then((directory) =>
Directory('${directory.path}/logs').create(recursive: true));
.then((directory) => Directory(p.join(directory.path, 'logs'))
.create(recursive: true));
return;
} else if (Platform.isIOS) {
_logDirectory = await getApplicationDocumentsDirectory().then(
(value) => Directory('${value.path}/logs').create(recursive: true));
_logDirectory = await getApplicationDocumentsDirectory().then((value) =>
Directory(p.join(value.path, 'logs')).create(recursive: true));
return;
}
throw UnsupportedError("Platform not supported.");
@@ -246,7 +247,7 @@ class FileService {
Future<void> _initUploadDirectory() async {
final dir = await getApplicationDocumentsDirectory()
.then((dir) => Directory('${dir.path}/upload'));
.then((dir) => Directory(p.join(dir.path, 'upload')));
_uploadDirectory = await dir.create(recursive: true);
}
@@ -265,3 +266,13 @@ enum PaperlessDirectoryType {
upload,
logs;
}
extension ClearDirectoryExtension on Directory {
Future<void> clear() async {
final streamedEntities = list();
final entities = await streamedEntities.toList();
await Future.wait([
for (var entity in entities) entity.delete(recursive: true),
]);
}
}

View File

@@ -76,5 +76,11 @@ String translateError(BuildContext context, ErrorCode code) {
ErrorCode.userNotFound => S.of(context)!.userNotFound,
ErrorCode.updateSavedViewError => S.of(context)!.couldNotUpdateSavedView,
ErrorCode.userAlreadyExists => S.of(context)!.userAlreadyExists,
ErrorCode.customFieldCreateFailed =>
'Could not create custom field, please try again.', //TODO: INTL
ErrorCode.customFieldLoadFailed =>
'Could not load custom field.', //TODO: INTL
ErrorCode.customFieldDeleteFailed =>
'Could not delete custom field, please try again.', //TODO: INTL
};
}

View File

@@ -0,0 +1,7 @@
bool isNotNull<T>(T element) {
return element != null;
}
bool isNull<T>(T element) {
return element == null;
}